안드로이드 개발을 하다보면 여러개 View의 데이터를 서로 공유해야하는 상황이 꼭 발생한다. 예를 들면 회원가입 페이지에서 여러개의 화면에서 사용자의 정보를 입력받을 때, 각 화면에서는 하나의 정보(전화번호, 이름)을 받아도 이전의 화면에서 받았던 정보를 저장해야 한다.
안드로이드에서는 다양한 방법으로 각 화면간의 데이터가 공유가 가능하다. 몇가지 방법을 예시와 함께 소개하며, 편의를 위해 가장 많이 사용하는 Fragment를 기준으로 작성하였다. 아래의 방식 외에도 추가적으로 공유하는 방법이 존재할 수 있으며, 장점과 단점을 파악하여 알맞는 방법을 선택하는 것이 중요하다.
1. Navigation Argument
첫번째 방법은 Navigation Component의 Safe Args를 이용하거나 Bundle을 이용하는 방법이다. 해당 방법의 장점은 가장 쉽고 간편하게 서로의 데이터를 전달하여 공유할 수 있으며, 또한 SavedStateHandle을 지원하여, LMK의 메모리 정리와 같은 이유로 데이터가 소멸 된 후 다시 해당 화면을 켜도, SavedStateHandle을 통해 이전에 전달했던 데이터를 다시 사용하여 앱의 안정성에 있어서 장점이 있다.
그러나 해당 방법은 복잡한 데이터를 전달할 때는 적합하지 않다. 기본적인 자료형이 아닐 때에는 따로 Parcelable을 적용해야 하기 때문이다. 또한, 여러개의 화면에서 데이터를 공유하기 위해서도 적합하지 않다. 예를 들어 5개의 화면끼리 데이터를 공유한다고 가정할 때에는 5개 다 서로 데이터를 주고 받는 것을 구현해야 하기 때문에, 매우 귀찮은 상황이 발생한다.
Navigation Argument를 활용한 예제
@AndroidEntryPoint
class WriteReviewFragment : Fragment(){
private fun initViews(){
binding.backButton.setOnClickListener {
val direction = WriteReviewFragmentDirections
.actionWriteReviewFragmentToReviewListFragment(args.drinkId) // 인자 전달
findNavController().navigate(direction)
}
}
@AndroidEntryPoint
class ReviewListFragment : Fragment() {
private val args : ReviewListFragmentArgs by navArgs()
binding.reviewPostButton.setOnClickListener {
viewModel.postReview(args.drinkId)
}
}
해당 예제는 Navigation Componet 중 Safe Args를 활용한 예제이다. 화면 전환을 할 때단순히 argument를 넣어주는 것 만으로 데이터를 전달할 수 있다.
2. SharedViewModel
두번째 방법은 각 화면간 공유하는 ViewModel을 두는 것이다. SharedViewModel은 앞서서 많은 수의 화면의 데이터 공유에서 하나의 ViewModel에 접근하면 되기에, 상대적으로 많은 화면에서 데이터 공유에서 간편하며 즉각적인 UiState Update가 발생한다는 장점이 있다.
acitivityViewModel을 활용한 예제
SharedViewModel을 구현하는 데에는 두가지 방법이 있다. 첫 번째 방법은 Activity를 기준으로 두고 Fragment에서 해당 Activity의 ViewModelOwner를 통해 ViewModel을 사용하는 방법이다. Fragment에 관계 없이 Activity의 LifeCycle을 따르기 때문에 Activity가 살아있을 때 까지 죽지 않는다는 단점이 존재한다. 기능단위의 여러개의 Activity를 사용할 때에 적합한 방식이다.
@HiltViewModel
class AuthViewModel @Inject constructor(
private val authRepository: AuthRepository
) : ViewModel() {
// 사용자 input
val email = MutableLiveData<String>("")
val password = MutableLiveData<String>("")
val name = MutableLiveData<String>("")
}
@AndroidEntryPoint
class SignUpEmailFragment : Fragment() {
private val viewModel by activityViewModels<AuthViewModel>()
}
@AndroidEntryPoint
class SignUpNameFragment : Fragment() {
private val viewModel by activityViewModels<AuthViewModel>()
}
ParentFragmentViewModel을 활용한 방법
SharedViewModel을 구현하기 위한, 두번째 방법은 ParentFragment를 두고, ChildFragment간 데이터를 공유하는 방법이다. Activity의 LifeCycle에 관계없이, ParentFragment의 LifeCycle을 따르는 SharedViewModel을 사용한다. ViewPager, TapLayout, Navigation Drawer와 같이 Fragment안 하위 Fragment를 가지고 있고 서로 데이터를 공유하는 상황에서 사용이 가능하다.
class ListFragment: Fragment() {
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
// Update the list UI
}
}
}
class ChildFragment: Fragment() {
private val viewModel: ListViewModel by viewModels({requireParentFragment()})
...
}
SharedViewModel + SaveStateHandle
SharedViewModel은 ViewModel의 UiState를 활용하는 방법이기 때문에, SavedStateHandle을 따로 자동으로 지원하지 않는다. LMK와 같은 이유로 데이터가 소멸될 때를 방지하기 위해, SavedStateHandle을 따로 구현하여 복원하는 작업을 거쳐야 한다.
@HiltViewModel
class GameViewModel @Inject constructor(
private val stateHandler: SavedStateHandle
): ViewModel(){
// saveStateHandler 를 사용한 score 값 저장
private val _score = stateHandler.getMutableStateFlow("score", 0)
val score: StateFlow<Int>
get() = _score.asStateFlow()
}
class SavableMutableStateFlow<T>(
private val savedStateHandle: SavedStateHandle,
private val key : String,
initialValue: T
){
private val state: StateFlow<T> = savedStateHandle.getStateFlow(key, initialValue)
var value : T
get() = state.value
set(value){
savedStateHandle[key] = value
}
fun asStateFlow(): StateFlow<T> = state
}
fun <T> SavedStateHandle.getMutableStateFlow(key: String, initialValue: T): SavableMutableStateFlow<T> =
SavableMutableStateFlow(this, key, initialValue)
3. Persistent Storage
세번째 방법은 Persistent Storage를 두는 것이다.
여러개의 View에서 Persistent Storage에 접근하여 데이터를 읽어오고, 파괴될 때 새로운 데이터를 쓰는 것으로 데이터를 공유할 수 있다. 해당 작업은 전반적인 Model Class와 Storage를 따로 생성해야 하며, 기본적으로 I/O 작업에 기반하여 비용과 시간이 많이 든다는 단점이 있다.
Persistent Storage 예시
class BeJuRyuDatastoreImpl @Inject constructor(
@ApplicationContext context: Context
) : BeJuRyuDatastore{
private val storeDelegate by lazy {
if (BuildConfig.DEBUG) context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
else EncryptedSharedPreferences.create(
FILE_NAME,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
override var accessToken: String
set(value) = storeDelegate.edit { putString("ACCESS_TOKEN", value) }
get() = storeDelegate.getString("ACCESS_TOKEN", "") ?: ""
override var refreshToken: String
set(value) = storeDelegate.edit { putString("REFRESH_TOKEN", value) }
get() = storeDelegate.getString("REFRESH_TOKEN", "") ?: ""
override var userId: Long
set(value) = storeDelegate.edit { putLong("USER_ID", value) }
get() = storeDelegate.getLong("USER_ID", -1)
override var nickName: String
set(value) = storeDelegate.edit{ putString("NICK_NAME", value) }
get() = storeDelegate.getString("NICK_NAME", "") ?: ""
override fun clear() {
storeDelegate.edit { clear() }
}
}
Persistent Storage는 Authetication을 위한 Token을 저장하고 사용하거나, 로그인 시 사용자 정보를 저장하고, 필요한 화면에서 사용하는 것과 같이 하나의 액티비티 이상으로 보다 넓은 범위에서 사용할 때 적합한 방식이다.
결론
데이터 공유 방식은 매번 프로젝트를 진행할 때 가장 고민이 많이되는 것들 중 하나인 것 같다. 이번에 포스팅을 하면서, 각 방식에 적합한 상황과 사용 방법을 알게되었다. 이 밖에도 여러가지 방식이 있을 수 있으며, 어떠한 방식을 채택할지는 필요한 상황에 맞게 선택하면 좋을 것 같다.
참고 자료
https://developer.android.com/guide/fragments/communicate?hl=ko
프래그먼트와 통신 | Android 개발자 | Android Developers
프래그먼트와 통신 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 프래그먼트를 재사용하려면 자체 레이아웃과 동작을 정의하는 완전히 독립된 구성요소
developer.android.com