본문 바로가기
Android Studio

[Android 앱개발 숙련] 프래그먼트의 데이터 전달 (# 추가하기)

by 젼젼39 2024. 4. 11.

1. Activity -> Fragment

    : 프래그먼트의 인스턴스를 생성하고 newInstance 메소드를 통해 데이터를 전달함

    - Bundle 객체를 사용해 데이터를 프래그먼트의 인자 (arguments)로 설정하고, 이 인자를 프래그먼트가 받아 사용함

1) MainActivity.kt (보내는 코드)

    - Fragment1Btn 클릭 리스너 안에서 FirstFragment의 인스턴스를 생성하고,
        newInstance 메소드에 데이터를 전달해 프래그먼트를 설정(set)함

    - Fragment2Btn에 대해서도 동일한 방법으로 SecondFragment에 데이터를 전달함

//변경한 코드. 프래그먼트의 데이터 전달, 보내는 부분
        binding.run {
            fragment1Btn.setOnClickListener {
                // [1] Activity -> FirstFragment
                val dataToSend = "Hello First Fragment! \n From Activity"
                //FragmentBtn1 클릭 리스너 안에서 FirstFragment의 인스턴스를 생성하고,
                //  newInstance 메소드에 데이터를 설정해 생성된 프래그먼트에 전달함
                val fragment = FirstFragment.newInstance(dataToSend)
                //생성한 프래그먼트를 setFragment 안에 넣어서 프래그먼트를 설정, 화면에 표시함
                setFragment(fragment)
            }
            fragment2Btn.setOnClickListener {
                // [1] Activity -> SecondFragment
                val dataToSend = "Hello Second Fragment!\n From Activity"
                val fragment = SecondFragment.newInstance(dataToSend)
                setFragment(fragment)
            }
        }

        //프로그램이 시작될 때 FirstFragment를 뿌리라고 코드 작성함
        setFragment(FirstFragment())

 

 

2) FirstFragment.kt (받는 코드)

    - newInstance 메소드에서 전달받은 데이터를 Bundle에 담고, 프래그먼트의 인자로 설정

    - onViewCreated에서는 인자로 받은 데이터를 텍스트 뷰에 설정함

//newInstance() 메서드는 프래그먼트가 생성될 때 데이터를 설정하고,
// onCreate() 메서드는 프래그먼트가 초기화(onCreate)될 때 데이터를 읽어와 param1에 집어넣고,
//  onViewCreated에서 해당 param1을 사용해 텍스트 바꿈
1. view: 이 매개변수는 프래그먼트의 뷰(View) 객체임
    즉, 프래그먼트가 화면에 표시될 때 이 메서드가 호출되며, 이때 해당 프래그먼트의 뷰가 전달됨
    이 뷰를 사용하여 프래그먼트 내의 UI 요소들을 조작하거나 초기화할 수 있음

2. savedInstanceState: 이 매개변수는 이전에 저장된 프래그먼트의 상태를 나타내는 Bundle 객체임
    액티비티의 `onCreate()` 메서드에서의 `savedInstanceState`와 유사한 역할을 함
    이전 상태가 저장되어 있을 경우, 이를 사용하여 프래그먼트가 다시 생성되었을 때 이전 상태를 복원할 수 있음.
    주로 화면 회전 또는 액티비티가 재생성될 때 이전 상태를 복원하는 데 사용됨

`onViewCreated()` 메서드는 프래그먼트의 뷰가 생성되고 초기화된 후에 호출되며,
    이 메서드 내에서는 프래그먼트의 UI 요소들을 초기화하거나 데이터를 표시하는 작업 등을 수행할 수 있음.
private var param1: String? = null

companion object {
        @JvmStatic    
        fun newInstance(param1: String) =
            // [1] Activity -> FirstFragment
            // FirstFragment의 새 인스턴스를 생성
            //새로운 프래그먼트 인스턴스를 생성할 때, 해당 프래그먼트에 전달할 데이터를 bundle에 설정함
            FirstFragment().apply {
                //전달받은 데이터를 bundle에 담음
                //새 bundle을 생성하고, 이를 사용해 프래그먼트에 전달할 데이터를 설정
                arguments = Bundle().apply {
                    //프래그먼트의 인자로  설정함... argument에 저장되는데... 위에 onCreate 코드 확인!
                    //param1 에는 Main에서 보낸 string이 들어있음.
                    //Bundle에 데이터 추가, "ARG_PARAM1" 상수를 키로 사용
                    putString(ARG_PARAM1, param1)
                    //putString(ARG_PARAM2, param2)
                }
            }          
        //newInstance() 메서드는 프래그먼트가 생성될 때 데이터를 설정하고,
        // onCreate() 메서드는 프래그먼트가 초기화(onCreate)될 때 데이터를 읽어와 param1에 집어넣고,
        //  onViewCreated에서 해당 param1을 사용해 텍스트 바꿈
    }

    // [1] Activity -> FirstFragment
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //인자로 받은 데이터를 텍스트 뷰에 저장
        // view가 create 될 때 param1을 텍스트에 넣어줌
        binding.tvFrag1Text.text = param1

    }

    //ARG_PARAM1 을 사용하려면
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val receivedData = arguments?.getString(ARG_PARAM1)
        binding.tvFrag1Text.text = receivedData
    }

 

// 실행 순서대로는

1. 버튼 클릭 이벤트 처리:
    - 사용자가 `fragment1Btn` 또는 `fragment2Btn` 중 하나를 클릭하면 해당 버튼에 지정된 클릭 리스너가 실행됨

2. 프래그먼트 인스턴스 생성:
    - 클릭된 버튼에 따라 다른 데이터를 생성함
    - 클릭된 버튼이 `fragment1Btn`이면 "Hello First Fragment! \n From Activity" 문자열을, 
            `fragment2Btn`이면 "Hello Second Fragment!\n From Activity" 문자열을 생성함
    - 각각의 데이터를 가지고 `FirstFragment.newInstance(dataToSend)`또는 `SecondFragment.newInstance(dataToSend)`를
            호출하여 새로운 프래그먼트 인스턴스를 생성함

3. 프래그먼트에 데이터 전달:
    - `newInstance` 메서드는 새로운 프래그먼트 인스턴스를 생성하고, 
            생성된 프래그먼트의 인자로 전달할 데이터를 Bundle에 담아 설정함.

4. 프래그먼트 화면에 표시:
    - 생성된 프래그먼트를 화면에 표시하기 위해 `setFragment(fragment)` 메서드를 호출함
    - 이렇게 하면 해당 프래그먼트가 화면에 나타남

5. 프래그먼트에서 데이터 활용:
    - 생성된 프래그먼트는 화면에 나타나면서 `onViewCreated` 메서드가 호출됨
    - 이 메서드 내부에서는 프래그먼트에 전달된 데이터를 활용하여 UI를 업데이트할 수 있음
    - 코드에서는 `param1` 변수에 전달된 데이터가 설정되고, 이를 TextView에 표시함

이렇게 하면 액티비티에서 프래그먼트로 데이터를 전달하고, 해당 프래그먼트에서 이를 활용하여 화면에 표시할 수 있음

 

// 내가 사용한 코드들

 - first랑 secondFragment 에 binding 코드 추가...

- 프래그먼트들의 onCreateView 부분 코드 변경

    -> 이전 코드 : 프래그먼트의 레이아웃 XML 파일을 사용하여 UI를 생성함
        inflater.inflate() 메서드를 사용하여 프래그먼트의 XML 레이아웃 파일(R.layout.fragment_second)을 인플레이트하고,
        그 결과물인 View를 반환함

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_second, container, false)
    }

    -> 이후 코드 :  뷰 바인딩을 사용하여 프래그먼트의 UI를 생성함.
        먼저 FragmentSecondBinding.inflate() 메서드를 사용하여 데이터 바인딩 객체를 생성하고,
            이를 사용하여 프래그먼트의 XML 레이아웃 파일을 인플레이트함.
        그 후에는 바인딩 객체의 root 속성을 반환하여 프래그먼트의 최상위 View를 반환함

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSecondBinding.inflate(inflater,container,false)
        return binding.root
    }

    => 주요 차이점은 두 번째 코드에서 뷰 바인딩을 사용하여 프래그먼트의 레이아웃을 인플레이트하고,
        해당 바인딩 객체를 통해 UI 요소에 쉽게 접근할 수 있다는 점임 

        첫 번째 방법에서는 뷰 바인딩 사용하지 않았으니 해당 프래그먼트의 레이아웃을 직접적으로 조작하기 어려움.
        -> 프래그먼트의 UI 요소를 변경하거나 조작하기 위해서는
            프래그먼트의 onCreateView() 메서드에서 반환된 View 객체를 통해(findV~) 해당 UI 요소를 찾고 조작해야함

 

2. Fragment -> Fragment

    : 첫 번째 프래그먼트에서 두 번째 프래그먼트의 newInstance 메소드를 사용해 인스턴스를 생성하고, 데이터를 전달

1) FirstFragment.kt (보내는 코드)

    - 버튼 클릭 시 SecondFragment 의 새 인스턴스 생성, newInstance 메소드를 사용해 데이터 전달
        프래그먼트 트랜잭션을 통해 두 번째 프래그먼트를 시작

// [1] Activity -> FirstFragment
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //인자로 받은 데이터를 텍스트 뷰에 저장
        // view가 create 될 때 param1을 텍스트에 넣어줌
        binding.tvFrag1Text.text = param1

        // [2] Fragment -> Fragment
        binding.btnGofrag2.setOnClickListener {
            val dataToSend = "Hello Fragment2! \n From Fragment1"
            //버튼 클릭 시 SecondFragment 의 새 인스턴스를 생성하고,
            //  newInstance 메소드를 사용해 데이터를 전달한 후
            val fragment2 = SecondFragment.newInstance(dataToSend)

            //프래그먼트 트랜잭션을 통해 두 번째 프래그먼트를 시작함
            // 프래그먼트에서 프래그먼트로!
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.frameLayout, fragment2)
                .addToBackStack(null)
                .commit()
        }
    }

 

2) SecondFragment.kt (받는 코드)

    - newInstance 메소드로 전달받은 데이터를 Bundle에 담고,
        onCreate 또는 onViewCreated에서 Bundle로부터 데이터를 추출해 사용

private const val ARG_PARAM1 = "param1"

class SecondFragment : Fragment() {

    private var _binding: FragmentSecondBinding? = null
    private val binding get() = _binding!!

    // TODO: Rename and change types of parameters
    private var param1: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            //param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSecondBinding.inflate(inflater, container, false)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment SecondFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String) =
            // [1] Activity -> FirstFragment
            SecondFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    //putString(ARG_PARAM2, param2)
                }
            }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // [2] Fragment -> Fragment
        binding.tvFrag2Text.text = param1
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // Binding 객체 해제
        _binding = null
    }

}

 

3. Fragment -> Activity

    : 콜백 인터페이스를 정의하고, 해당 인터페이스를 액티비티가 구현하도록 해야 함

    - 프래그먼트는 이 인터페이스를 사용해 액티비티에 데이터를 전달

    // 인터페이스 만들 때 다른 파일들에서 우클릭 > new > interface 해서 만들어도됨

1) SecondFragment.kt (보내는 코드)

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
//private const val ARG_PARAM2 = "param2"

// [3] SecondFragment -> Activity
interface FragmentDataListener {
    fun onDataReceived(data: String)
}

/**
 * A simple [Fragment] subclass.
 * Use the [SecondFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class SecondFragment : Fragment() {

    private var _binding: FragmentSecondBinding? = null
    private val binding get() = _binding!!

    // TODO: Rename and change types of parameters
    private var param1: String? = null
    //private var param2: String? = null


    // [3] SecondFragment -> Activity
    // 해당 인터페이스 타입의 리스너를 만들었음
    private var listener: FragmentDataListener? = null

    // [3] SecondFragment -> Activity
    // MainActivity와 SecondFragent를 연결해야 하니까... context 사용
    override fun onAttach(context: Context) {
        super.onAttach(context)

        // 프래그먼트가 액티비티에 붙을 때 액티비티가 이 인터페이스를 구현했는지 확인
        // context는 MainActivity에서 들어온 것

        //context를 검사해서 MainActivity 안에 FragmentDataListener가 구현되어있는지를 체크
        if (context is FragmentDataListener) {
            //이 리스너를 통해 호출하게 됨
            listener = context
        } else {
            throw RuntimeException("$context must implement FragmentDataListener")
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            //param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSecondBinding.inflate(inflater, container, false)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment SecondFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String) =
            // [1] Activity -> FirstFragment
            SecondFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    //putString(ARG_PARAM2, param2)
                }
            }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // [2] Fragment -> Fragment
        binding.tvFrag2Text.text = param1

        // [3] SecondFragment -> Activity
        binding.btnSendActivity.setOnClickListener {
            val dataToSend = "Hello from SecondFragment!"
            //데이터를 액티비티에 전달
            //listener는 위에서 FragmentDataListener 타입으로 만들어놓고 MainActivity의 context가 할당된 상태.
            //  인터페이스의 onDataReceived 를 호출함, 그걸 상속받고 있는 메인에서 토스트가 뜸
            listener?.onDataReceived(dataToSend)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // Binding 객체 해제
        _binding = null

        // [3] SecondFragment -> Activity
        listener = null
    }
}

 

2) MainActivity.kt (받는 코드)

    - FragmentDataListener 인터페이스 구현, onDataReceived 메소드를 오버라이드해 프래그먼트로부터 데이터 받기

    - 데이터 받으면 토스트 띄우기

// [3] SecondFragment -> Activity (FragmentDataListener 인터페이스 구현.. 아래줄)
class MainActivity : AppCompatActivity(), FragmentDataListener {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //변경한 코드. 프래그먼트의 데이터 전달, 보내는 부분
        binding.run {
            fragment1Btn.setOnClickListener {
                // [1] Activity -> FirstFragment
                val dataToSend = "Hello First Fragment! \n From Activity"
                //FragmentBtn1 클릭 리스너 안에서 FirstFragment의 인스턴스를 생성하고,
                //  newInstance 메소드에 데이터를 설정해 생성된 프래그먼트에 전달함
                val fragment = FirstFragment.newInstance(dataToSend)
                //생성한 프래그먼트를 setFragment 안에 넣어서 프래그먼트를 설정, 화면에 표시함
                setFragment(fragment)
            }
            fragment2Btn.setOnClickListener {
                // [1] Activity -> SecondFragment
                val dataToSend = "Hello Second Fragment!\n From Activity"
                val fragment = SecondFragment.newInstance(dataToSend)
                setFragment(fragment)
            }
        }

        //프로그램이 시작될 때 FirstFragment를 뿌리라고 코드 작성함
        setFragment(FirstFragment())
    }

    private fun setFragment(frag: Fragment) {
        //동적
        //setFragment가 불리면 전달받은 프래그먼트로 전환하는 것
        supportFragmentManager.commit {
            //frameLayout에 전달받은 프래그먼트를 뿌릴 것임
            replace(R.id.frameLayout, frag)
            setReorderingAllowed(true)
            addToBackStack("")
        }
    }

    // [3] SecondFragment -> Activity
    //MainActivity에서 해당 인터페이스 상속받고, 그 안에 구현체도 있어야 함...
    // FragmentDataListener 리스너를 걸어두고, 그 리스너를 통해 onDataReceived가 발생하면
    //   그 안으로 스트링 데이터가 들어올거고, 그걸 토스트로 띄울 것임. (onDataReceived를 override 해서 구현체 만들었음)
    override fun onDataReceived(data: String) {
        // Fragment에서 받은 데이터를 처리
        Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
    }
}

    - MainActivity에서 해당 인터페이스 상속받고, 그 안에 구현체도 있어야 함...
        FragmentDataListener 리스너를 걸어두고, 그 리스너를 통해
            onDataReceived가 발생하면 그 안으로 스트링 데이터가 들어올거고,
                그걸 토스트로 띄울 것임. (onDataReceived를 override 해서 구현체 만들었음)