[안드로이드] 『프래그먼트 통신(데이터 전달)』

SHY(code poet)·2024년 3월 29일
1
post-thumbnail

Fragment는 그 자체로 호스트 뷰인 Activity에 종속되기에, Activity와의 관계성을 고려하여 데이터를 전달해야 한다. 이에 따르면, 다음과 같은 가짓수들이 있을 수 있다.
1. 액티비티 👉 프래그먼트
2. 프래그먼트 👉 액티비티
3. 프래그먼트 👉 프래그먼트

1. 액티비티 👉 프래그먼트 : Bundle객체를 arguments로 설정

① 우선, Activity와 Fragment_A를 각각 하나씩 만들었다고 가정하고, Activity 실행시, Fragment를 즉시 띄우도록 코드를 짜보자.

class MainActivity : appCompatActivity() {
	override funn onCreat(savedInstanceState: Bundle?) {
    .
    .
    .
    setFragment(Fragment_A()) 
    }
    // 트랜잭션 함수 만들고, 프래그먼트 띄우기
    fun setFragment(fragment : Fragment) {
    	val transaction = supporFragmentManager.beginTransaction() 
        transaction.replce(R.id.frameLayout, fragment)
        transaction.commit()

② 이제 여기서, 프래그먼트를 띄움과 동시에 데이터도 함께 보내보자

class MainActivity : appCompatActivity() {
	override fun onCreat(savedInstanceState: Bundle?) {
    .
    .
    .
    // setFragment(Fragment_A()) 
       setDataAtFragment(Fragment_A(), "☆")
    }
    
    // 프래그먼트에 액티비티의 데이터 전달 
    fun setDataAtFragment(fragment: Fragment,:String) {
    	val bundle = Bundle()
        bundle.putString("●",)
        fragment.arguments = bundle
        setFragment(fragment)
    
    
    // 트랜잭션 함수 만들고, 프래그먼트 띄우기
    fun setFragment(fragment : Fragment) {
    	val transaction = supporFragmentManager.beginTransaction() 
        transaction.replce(R.id.frameLayout, fragment)
        transaction.commit()

설명
Bundle()이 무엇인지에 대해서는 아래의 글을 참고하면 좋다.
Bundle이란 무엇인가
Parcel도 함께 이해하고 있으면 👍
parcel과 parcelable
ⓑ 안드로이드는 프래그먼트 생성과 동시에 데이터를 전달하거나 받을 수 있는 'arguments'를 제공한다. 이 argument는 Bundle 객체로 데이터를 전달할 수 있고, Bundle 객체는 데이터를 key와 value의 쌍으로 전달한다
ⓒ 보다시피, 우리는 데이터를 Fragment의 argument에 넣었다.

③ 액티비티에서 데이터를 전달했으니, 이제 프래그먼트가 데이터를 받도록 해보자.

class Fragment_A : Fragment() {
 private var title: String? = null
 
 override fun onCreate(savedInstanceState: Bundle?) {
 	super.onCreate(savedInstanceState)
    arguments?.let {= it.getString("●")
        }
        Log.d("Fragment_A", "${}")
    }
 }

※ 다른 예제

2. 프래그먼트 👉 액티비티: 액티비티 內 함수 호출

① 일단 Activity에 함수 하나를 만들어 보자

class main() {
	fun onCreat() {
    	.
        .
        .
    }
    fun receiveData(who:String) {
    	Log.d.("Main", "나는 ${who}입니다.")
        }
       }
    	

② Fragment에서 이 함수를 호출할 때, 데이터를 데이터를 인자로 던져주면 끝이다.

class MainActivity : appCompatActivity() {
	override fun onCreat(savedInstanceState: Bundle?) {
    super,onViewCreated(view, savedInstanceState)
    .
    .
    .
    //프래그먼트 內 버튼을 눌렀을 때
    btn.setOnclickListner {
    val mActivity = activity as MainActivity
    mActivity.receiveData("프래그먼트A")
    }
  }
}
    

✨심화✨
위의 예시는 단순히 하나의 Fragment에서 하나의 Activity로 데이터를 전달하는 것에 대해서만 다루었다. 그러나, Activity가 2개라면 어떻게 통신을 해야할까?

Activity a안에 Fragment가 있고, 이 Fragment와 새로운 Activity b가 통신하는 상황이라고 하자.
액티비티와 프래그먼트가 통신할 수 있는 방법은 크게 두 가지가있다.
① 액티비티 a를 거치지 않는다.
액티비티 a를 거치지 않고 프래그먼트에서 직접 인텐트를 액티비티 b로 startActivityForResult를 이용해 넘겨준다면, 액티비티b에서 받아 처리 후 setResult로 프래그먼트로 직접 다시 돌아온다.
이렇게 되면 프래그먼트가 속한 액티비티를 거치지 않고 바로 다른 액티비티와 통신이 가능하다.
② 액티비티 a를 거친다.
프래그먼트에서 메소드로 데이터를 액티비티 a로 전달 후 액티비티 a에서 인텐트를 액티비티 b로 startActivityForResult를 이용해 넘겨준다면, 액티비티 b에서 setResult를 하면 프래그먼트가 아닌 액티비티 a로 돌아온다.
그럼 전달받은 데이터를 다시 액티비티a에서 메소드로 프래그먼트에 넘겨주면 프래그먼트와 액티비티b의 통신이 이루어진다.
👍메모리 부족과 같은 현상으로 Activiy가 재생성되었을 때 Activity 가 데이터를 가지고 있어야 Fragment 로 데이터를 넘길 수 있기 때문에 방법2를 더 추천한다.

3. 프래그먼트 👉 프래그먼트

① 프래그먼트 內 함수 호출

이 부분은 생략하도록 하겠다. 프래그먼트 👉 액티비티와 같은 방식으로 하되, 액티비티를 프래그먼트로 교체해서 코드를 짜주면 된다.

② Bundle: FragmentManager에 Bundle로 Data를 담아 전달: arguments 이용

ⓐ 송신 Fragment class

//PassBundleFragment는 본인이 전달하고자 하는 Fragment class
 val bundle = Bundle()
 bundle.putString("key", "value")
 
val passBundleBFragment = PassBundleBFragment()
passBundleBFragment.arguments = bundle parentFragmentManager.beginTransaction()
	.replace(R.id.fragment_container_bundle, PassBundleFragment())
	.commit()

ⓑ 수신 Fragment class

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
	var result = arguments?.getString("key")
	
    	return inflater.inflate(R.layout.fragment_pass_b, container, false)
    }

이제부터 본격적으로 구글 공식문서에서 제시하는 방법들을 정리해보고자 한다.
**********************************
사실, 지금까지는 액티비티와 프래그먼트 그리고 프래그먼트와 프래그먼트가 서로 간에 '직접' 통신하는 방법만을 제시했다. 그러나, '독립성 유지'를 위해 직접 통신보다는 '통신채널'을 사용해 데이터를 공유하는 방식으로 통신을 해야한다.
(FragmentManager에 관해서는 다음의 글을 참고 Fragment와 FragmentManager)
👇
프래그먼트 라이브러리는 ViewModel 또는 Fragment Result API라는 두 가지 통신 옵션을 제공한다. 권장되는 옵션은 사용 사례에 따라 다르다.
영구적인 데이터를 사용한다면 ViewModel을 사용해야 하고
일회성의 결과로 Bundle에 배치할 수 있는 데이터라면 Fragment Result API(Activity Result API와 유사)를 사용하는 것이 좋다.

③ Fragment간 공통의 ViewModel로 전달: HostActivity의 ViewModel

하나의 Activity에서 container로 여러 Fragment를 이동하는 경우,
공통의 메모리 데이터를 이용하여 서로의 데이터를 전달하는 방법.

👉
Activity를 공유하는 여러 Fragment들은 Activity의 메모리를 공유할 수 있고 AAC(Android Architecture Components)의 ViewModel은 Activity의 lifecycle보다 오래 살아있는 것이 보장되기 때문에 안전하게 공통의 Activity의 ViewModel을 사용하여 데이터 전달이 가능하다.
안드로이드 AAC

  • Activity에 Fragment들이 같은 데이터를 바라볼 수 있게 ViewModel 객체를 만들고 그 객체에 데이터를 전달하여 다수의 프래그먼트끼리 데이터를 공유하거나 호스트 액티비티와 프래그먼트간에 데이터를 공유할 수 있는 것이다. (ViewModel 객체는 UI 데이터를 저장하고 관리한다.)

① 우선, ViewModel 클래스를 작성해보자

class ItemViewModel : ViewModel() {
    private val mutableSelectedItem = MutableLiveData<Item>()
    val selectedItem: LiveData<Item> get() = mutableSelectedItem

    fun selectItem(item: Item) {
        mutableSelectedItem.value = item
    }
}
  • 데이터는 MutableLiveData 타입인 mutableSelectedItem에 저장된다. 그리고 이 값은 selectedItem에 의해 접근할 수 있다.

② 이제 액티비티와 프래그먼트 각각에 ViewModel을 생성해보자.

// 액티비티 코드
class ViewModelPassActivity : AppCompatActivity() {
    val viewModel : PassingViewModel by viewModels()
}

// 프래그먼트A 코드
class PassAFragment : Fragment() {
    val viewModel : PassingViewModel by activityViewModels()
}

// 프래그먼트B 코드
class PassBFragment : Fragment() {
    val viewModel: PassingViewModel by activityViewModels()
}

✨심화: 액티비티 👉 프래그먼트
다음은 프래그먼트에서는 데이터를 저장하고 액티비티에서는 그 데이터를 접근하도록 하는 예제 코드이다.

// 액티비티 코드
class MainActivity : AppCompatActivity() {
	//  ViewModel을 획득
    private val viewModel: ItemViewModel by viewModels() 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.selectedItem.observe(this, Observer { item ->
            // 획득한 아이템에 대한 액션 지정
            Toast.makeText(this, "item : $item", Toast.LENGTH_SHORT).show()
        })
    }
}

// 프래그먼트 코드
class ListFragment : Fragment() {
    // fragment-ktx의 activityViewModels()를 사용하여 호스트 엑티비티의 ViewModel 획득
    private val viewModel: ItemViewModel by activityViewModels()
    // 아이템이 클릭되면 호출
    fun onItemClicked(item: Item) {
        // ViewModel의 selectItem을 호출하여 MutableLiveData.setValue 호출
        viewModel.selectItem(item)
    }
}
  • 만약 동일한 액티비티에 있는 프래그먼트끼리 데이터를 공유한다면 위와 똑같이 액티비티의 ViewModel을 사용하면 되고, 상위 프래그먼트-하위 프래그먼트의 관계에서 데이터를 공유한다면 가진다면 상위 프래그먼트를 ViewModel의 scope로 사용하면 된다.

🤔고민해보기: IPC기법(Inter-Process Communication, 프로세스 간 통신)
물론 실제 안드로이드 개발에서는 구글에서 권장하는 방법을 사용해야겠지만,
IPC기법을 고려한다면 더 많은 종류의 Data전달 방법이 떠오를 수 있다.
[종류]
File 사용
Message Queue
Shared Memory (ViewModel을 이용한 데이터 전달 방법과 유사)
Pipe
...

④ Fragment Result API를 사용하여 Data 전달: FragmentManager

FragmentManger란 앱의 fragment들을 더하고, 삭제하고, 교체하고, 백스택에 더하는 활동 등을 책임을 지는 class 입니다.
(FragmentManager is the class responsible for performing actions on your app's fragments, such as adding, removing, or replacing them, and adding them to the back stack.// 공식문서)

앞서 언급한 바와 같이, Fragment Result API는 프래그먼트 간에 또는 호스트 액티비티와 프래그먼트간에 일회성 값을 전달하는 경우 주로 사용한다.
예를 들어 QR 코드를 읽고 이전 프래그먼트로 데이터를 다시 전달하는 프래그먼트가 있을 수 있을 수 있다.

  • 이 API는 Fragment 1.3.0-alpha04부터 추가된 기능으로 FragmentManager는 FragmentResultOwner 인터페이스를 구현하고 있다.

👉 FragmentResultOwner는 프래그먼트 사이에 전달하는 데이터를 관리하는 클래스이다. 이 인터페이스에는 위와 같은 메소드들이 있는데, 이 메소드를 사용하여 리스너를 등록하고 데이터를 받아오면 된다. 따라서 이를 구현하는 FragmentManager는 프래그먼트 결과(데이터)의 중앙 저장소 역할을 수행하는 것이다.

의존성 추가하기
우선 Android KTX의 Fragment KTX를 추가한다.
(Android KTX는 코틀린의 확장 기능을 안드로이드 Jetpack과 안드로이드 라이브러리에 적용한 것이다.)

//(AppModule)
dependencies {
	...
    implementation "androidx.fragment:fragment-ktx:1.4.0"
}

구조 파악하기 (위의 사진 참조)

  • 프래그먼트 B에서 프래그먼트 A로 데이터를 전달한다고 가정하자.
  • 데이터를 받는 프래그먼트 A는 FragmentManager에 setFragmentResultListener("requestKey") 메소드를 사용하여 주어진 requestKey로 리스너를 설정한다.
  • 데이터를 전송하는 프래그먼트 B는 setFragmentResult("requestKey", Bundle) 메소드를 사용하여 주어진 requestKey로 데이터를 저장한다. 그러면 데이터가 FragmentManager를 통해서 동일한 requestKey로 리스너를 등록한 프래그먼트 A로 전달된다.

이제 이를 바탕으로 하여 송신 fragment와 수신 fragment의 코드를 각각 짜보자.

송신 Fragment
데이터를 전송하는 프래그먼트인 프래그먼트 B는 requestKey를 사용하여 동일한 FragmentManager에 데이터를 설정한다. setFragmentResult()를 사용하여 데이터를 설정할 수 있다.

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact
    // fragment-ktx의 코틀린 확장을 사용
    // setFragmentResult 메소드에 key와 Bundle을 사용해 데이터 전송
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

수신 Fragment

  • 결과를 수신하는 프래그먼트인 프래그먼트 A에서 결과 리스너를 설정한다. 아래의 코드와 같이 프래그먼트 A의 FragmentManager에서 setFragmentResultListener()를 호출하면 된다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // fragment-ktx의 코틀린 확장을 사용
    // 주어진 requestKey를 사용하여 FragmentResultListener를 설정
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // 데이터를 수신하고 생명주기 STARTED 상태가 되면 호출되는 콜백 메소드
        val result = bundle.getString("bundleKey")
        
        // Do something with the result
    }
}

※다른 예제

✨심화: 상위 프래그먼트 👉 하위 프래그먼트

하위 프래그먼트의 데이터를 상위 프래그먼트로 전달하려면, 단지 상위 프래그먼트에서 setFragmentResultListener()를 호출할 때 getParentFragmentManager() 대신 getChildFragmentManager()를 사용하면 된다.

데이터를 전달하는 하위 프래그먼트

button.setOnClickListener {
    val result = "result"
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

데이터를 전달받는 상위 프래그먼트

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // child Fragment Manager에 리스너 등록
    childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("bundleKey")
        // Do something with the result
    }
}

⑤ Jetpack의 Navigation에서 제공하는 safe-args로 전달

이 내용은 아래의 벨로그 글과 깃허브 예제코드를 참고하라.
프래그먼트간 데이터 전달 방법들
FragmentDataPassExample

📜Referance
1. Fragment와 FragmentManager
2. FragmentManager?
3. 코틀린으로 안드로이드 프래그먼트 데이터 전달하기
4. 프래그먼트간 데이터 전달 방법들
5. Fragment-Activity 데이터 전달
6. 액티비티와 프래그먼트의 통신
7. Fragment Result API

profile
그것을 이해하고자 하기 때문에 결국은 그것을 견디어내게 된다.

0개의 댓글