본문 바로가기

Language/Kotlin

[Android] Fragment에서 데이터를 전달하는 3가지 방법.

728x90

최근 Single Activity Architecture (SAA)에 대해서 다시 한번 확인해보다, 해당 샘플 예제에서는 데이터를 전달하는 것에 대해서는 신경을 쓰지 않고 구현했다는 것을 알게 되었다.

 

기본적인 샘플이라 사용하는 방법에 대해서만 작성해 보았었는데,

이번에는 Fragment에서 데이터를 전달하는 3가지 방법에 대한 기본적인 방법에 대하여 글을 작성해보고자 한다.

 

해당 글에는 이전에 만들어두었던 SAA-Modular 예제를 사용하도록 하겠다.


Fragment에서 데이터를 전달하는 방법은 크게 3가지가 존재한다.

 

  1. Bundle을 사용한다.
  2. ViewModel을 사용한다.
  3. Safe-args를 사용한다.

 

이 3가지 방법을 사용하여 간단하게 데이터를 전달해보도록 하겠다.

 

처음으로,

Bundle을 사용하여 데이터를 전달하는 방법이다.

 

findNavController().navigate(moveFragment, bundleData)

 

Bundle을 사용하여 데이터를 전달하는 방법은 아주 간단하다.

 

Fragment 전환시에 navigate함수를 사용할 때, Bundle 데이터를 인자로 넣어주기만 하면 된다.

 

@MainThread
public open fun navigate(@IdRes resId: Int, args: Bundle?) {
    navigate(resId, args, null)
}

 

이처럼 args로 Bundle을 받아서 전달할 수 있으며,

 

 

navigate 함수를 확인해보면 더 다양한 방식으로 오버로딩되어 데이터를 전달할 수 있으니 필요에 따라 확인 후에 사용하면 될 것이다.

 

val sendData = Bundle().also {
    it.putString("destination", "MoveFragment")
    it.putString("other", "sample")
}

 

Bundle을 통해 다음과 같은 데이터를 전달한다고 하면,

 

val intentData = arguments?.getString("destination")

 

arguments.getString("key") 를 사용하여 전달받은 값을 가져와서 사용할 수 있다.

 

 

 

arguments는 Fragment에서 제공해주는 함수로, 번들로 전달받은 데이터가 있는 경우 반환해주기 위해 사용하는 함수이므로 참고하면 된다.

 

즉,

송신 측에서는 Bundle 타입으로 데이터를 전달하고,

수신 측에서는 arguement와 key 값을 사용하여 데이터를 가져와서 사용하면 된다.


다음으로,

ViewModel을 사용하여 데이터를 전달해보도록 하겠다.

 

우선, koin을 사용하여 viewModel에 대한 의존성 주입을 하고 있으며 필자는 main Module에 MainViewModel을 새로 생성하여 작업을 진행하였지만, Activity와 fragment에서 동일한 ViewMdoel을 사용해야 하는 경우에는 App Module을 확인해주어야 한다.

 

이 두개에서 다른 점은, Activity와 fragment에서 동시에 사용하기 위해서는 ViewModel에 대한 의존성 주입 타이밍을 잘 생각해야 한다.

이에 대한 문제 해결 방법은 다음 글을 확인하면 자세하게 작성해 두었으니 참고하길 바란다.

2022.07.08 - [Android/Architecture] - [Android] Single Activity Architecture (SAA) + Navigation - 이슈 해결

 

간단하게 말하자면, 이와 같은 상황에서 Fragment에서 진행되는 viewModel 의존성의 주입 타이밍은 

 

override fun onActivityCreated(savedInstanceState: Bundle?)

 

에서 진행하면 된다는 것이다.

 

Single Activity이기 때문에 main Module에는 별도의 activity가 존재하지 않아 onActivityCreated에서 viewModel에 대한 의존성을 주입하지 않아도 괜찮다.

따라서, 이와 같은 경우에는 viewModel에 대한 의존성을 주입하는 방법은 2가지가 존재한다.

 

var viewModel: MainViewModel? = null
viewModel = getViewModel()
private val viewModel: MainViewModel by viewModel()

 

이전의 다른 예제에서 볼 수 있듯이 주입할 수 있고, app Module에서 주입한 것처럼 원하는 타이밍에 의존성을 주입시킬 수 있다.

 

여기서 각 ChildFragment에서도 각각 MainViewModel에 대한 의존성 주입을 한 후에 사용해주어야 하는데, viewModel에 있는 데이터를 사용할 때 호출 순서를 고려하여 사용해야 한다.

 

Activity와 Fragment에서 viewModel을 함께 사용할 때 의존성 주입에 따른 타이밍 문제가 발생한 것처럼,

같은 Fragment에서 viewModel을 사용할 때 부모 fragment와 자식 fragment의 호출 순서를 확인하고 viewModel을 사용해야 의도한 바와 동일한 동작을 수행하게 된다.

 

 

2개의 Fragment의 onCreateView, onViewCreated, onActivityCreated에 로그를 찍어 보았고, onCreateView에서는 이전에 했던 것과 마찬가지로 layout을 세팅하기 전과 후에 1,2라는 숫자로 나누어서 찍어보았다.

 

 

위와 같은 순서로 호출이 되고 있기 때문에, 필자가 사용한 것처럼 MainFragment에서 ViewModel에서 사용할 값의 초기 값을 설정하고, 해당 값을 토대로 ChildFragment인 HomeFragment에서 작업을 수행하게 된다면 onActivityCreated에서 작업을 수행시키는 것이 안전하게 원하는 결과를 보일 수 있을 것이다.

 

만약, HomeFragment에서 onCreateView에서 viewModel에 있는 함수를 호출하게 된다면 다음과 같은 순서로 로그가 찍히게 된다.

 

 

ChildFragment에서도 ViewModel에 대한 의존성 주입을 하고 있기 때문에, HomeFragment에서 viewModel을 더 먼저 호출하게 되어 위처럼 Data가 원하는 것과 다르게 나오게 된다.

 

viewModel을 사용하여 데이터를 저장하고, 가져오는 것에는 단순하게 호출하면 되는 부분이기 때문에 넘어가도록 하겠다.


마지막으로,

Safe-args를 사용하여 데이터를 전달해보도록 하자.

 

해당 방법은 AAC Navigation를 사용할 때만 사용할 수 있는 방법으로, Gradle에 추가적인 작업을 해주어야 한다.

 

buildscript {
    ext.nav_version = "2.5.1"

    repositories {
        google()
    }
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

 

우선, Project 범위의 Gradle에 이처럼 safe-args에 대한 classPath를 추가해주도록 하자.

Gradle 7.2 버전으로 올라감에 따라 classpath를 사용하지 않고 Plugin으로 추가하는 방식으로 변경이 되었는데, 해당 부분에 대한 plugin을 어떻게 추가해야 하는지 찾지를 못하여서 이처럼 기존 방식을 통해 추가해주었다.

 

plugins {
    ...
    id 'androidx.navigation.safeargs'
}

 

그리고 safe-args를 사용하는 module 범위의 gradle에 위와 같은 plugin을 모두 추가해주도록 한다.

필자 같은 경우, app 모듈에서 main 모듈로 데이터를 던질 것이고, navigation 모듈도 별도로 있기 때문에 이 3가지 모듈에 위와 같은 plugin을 추가해주었다.

 

다음으로는,

 

data class User(val id: Int, val name: String)

 

간단하게 전달한 데이터 클래스를 domain 계층에 만들고,

navigation을 관장하는 nav_graph.xml 파일에 전달할 argument를 작성해주도록 하자.

 

<argument
    android:name="UserData"
    app:argType="com.example.domain.model.User[]" />

 

필자는 User에 대한 리스트를 전달하고자 이처럼 추가를 해주었다.

그리고 위와 같은 부분은 전달할 곳에서 선언하는 것이 아닌, 전달받을 곳에서 선언해주어야 한다.

즉, App모듈의 IntroFragment에서 Main모듈의 MainFragment로 전달한다고 할 때, mainFragment에 선언을 해주어야 한다.

 

이렇게 선언해주었으면, 데이터를 만들어서 전달해주도록 하자.

 

val userData = ArrayList<User>()
userData.add(User(0,"1"))
userData.add(User(1,"2"))

// Use Safe-args.
val action = IntroFragmentDirections.actionIntroToMain(userData.toTypedArray())
findNavController().navigate(action)

 

 

여기서 하단의 action을 확인해보자.

IntroFragmentDirections는 정상적으로 plugin을 추가했다면 사용할 수 있을 것이고, actionIntroToMain이라는 함수는 nav_graph.xml에 선언했던 introFragment에서 mainFragment로 이동할 수 있도록 선언해둔 action의 id 값을 사용한 것이다.

 

 

findNavController().navigate(action)

 

그리고 Bundle에서 그랬던 것처럼 navigate를 사용하여 해당 action을 넣어 호출하도록 한다.

 

 

이처럼 action 값 안에는 동작에 대한 값과, 전달할 값이 있기 때문에 그것을 사용하여 fragment를 변경하는 것이다.

 

이렇게 데이터를 전달했으면, MainFragment에서 데이터를 받아 사용해보도록 하자.

 

val args: MainFragmentArgs by navArgs()
val userData = args.userData.toCollection(ArrayList())

 

MainFragmentArgs는 nav_graph.xml에 선언한 fragment 태그의 id 값 뒤에 Args를 붙인 것으로 설정한 ID 값 뒤에 Args를 붙여서 사용하면 된다.

args.userData.toCollection(~) 부분에서 userData는 위에 argument 태그를 추가할 때 넣었던 name 값을 사용한 것이고,

argType을 배열로 넣었기 때문에 ArrayList()를 사용하여 값을 저장하도록 하였다.

 

이처럼 선언을 하고, 빌드를 해보면 오류가 발생하게 된다.

 

error: incompatible types: User[] cannot be converted to Parcelable[]

 

Domain 계층에 선언했던 데이터 클래스인 User 클래스를 Parcelable로 변환할 수 없다는 것이다.

이 말은 즉, safe-args를 사용하여 데이터를 전달할 때 직렬화 작업을 통해 byte로 데이터를 만들어서 전달한다는 것이다.

 

따라서, data class인 User 클래스에 Parcelable을 상속받고 이하의 필요한 함수들을 작성해주면 된다.

 

data class User(val id: Int, val name: String) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readInt(),
        parcel.readString().toString())

    override fun describeContents(): Int {
        return 0
    }

    override fun writeToParcel(p0: Parcel, p1: Int) {
        p0.writeInt(id)
        p0.writeString(name)
    }

    companion object CREATOR : Parcelable.Creator<User> {
        override fun createFromParcel(parcel: Parcel): User {
            return User(parcel)
        }

        override fun newArray(size: Int): Array<User?> {
            return arrayOfNulls(size)
        }
    }
}

 

이처럼 필요한 부분을 작성한 후, 다시 빌드를 해보면 정상적으로 데이터가 전달되어 값을 확인할 수 있다.


Fragment 끼리 데이터를 공유하는 3가지 방법에 대하여 간단하게 작성해 보았다.

AAC Navigation에서 safe-args를 제공하는 이유가 분명하지만, 사용하는 것에는 ViewModel과 Bundle을 사용하는 것이 가장 간편한 것 같다.

 

그리고 아무래도 viewModel을 사용하는 것이 사용하는 방법의 차이도 없고, 의존성 주입과 데이터를 컨트롤하는 타이밍만 신경 쓰면 되기 때문에 손쉽게 원하는 동작을 구현할 수 있는 안정성 높은 방법이라고 생각된다.

물론, 아직 Fragment에 대하여 많은 부분을 사용하지 않았기 때문에 그렇게 느낄 수도 있다고 생각은 되지만 말이다.

 

데이터를 전달하는 것 이외에도 추가적으로 적용할 부분을 찾는다면, 해당 샘플 예제를 사용하여 좀 더 개선해보도록 하겠다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/SAA-Modular

 

GitHub - HeeGyeong/SAA-Modular

Contribute to HeeGyeong/SAA-Modular development by creating an account on GitHub.

github.com

 

728x90