반응형
Android ViewModel과 LiveData 실전 구현 방법 (아키텍처 가이드)
Android 앱 아키텍처 가이드 시리즈의 두 번째 포스팅입니다. 이번 글에서는 ViewModel과 LiveData를 활용한 실전 예제를 통해 현대적인 Android 앱 아키텍처를 구현하는 방법을 상세히 알아보겠습니다. 사용자 프로필 화면을 구현하는 실제 예제를 통해 ViewModel, LiveData, SavedStateHandle의 사용법과 생명주기 관리를 익힐 수 있습니다. Google이 권장하는 Android 아키텍처 구성요소를 제대로 이해하고 활용하는 것이 현대적인 Android 개발의 핵심입니다.
목차
1. Android 아키텍처 구성요소 개요
2. ViewModel 구현과 SavedStateHandle
3. LiveData로 UI 업데이트 자동화
4. Fragment와 ViewModel 연결
5. 실전 프로필 화면 완성 코드
#1. Android 아키텍처 구성요소 개요
사용자 프로필 화면을 예제로 Android 아키텍처 구성요소들이 어떻게 협력하는지 알아보겠습니다.
1) 구현할 사용자 인터페이스 구조
UI는 UserProfileFragment와 관련 레이아웃 파일 user_profile_layout.xml로 구성됩니다.
(1) 필요한 데이터 요소
① 사용자 ID - 사용자의 식별자입니다. Fragment 인수를 사용하여 이 정보를 전달하는 것이 좋습니다
② 사용자 객체 - 사용자에 관한 세부정보를 보유하는 데이터 클래스입니다
Android OS에서 프로세스를 제거해도 이 정보가 유지되므로, 앱을 다시 시작할 때 ID를 사용할 수 있습니다.
. . . . .
2) 아키텍처 구성요소의 역할
(1) ViewModel의 역할
ViewModel 객체는 Fragment나 Activity 같은 특정 UI 구성요소에 대한 데이터를 제공하고 Model과 커뮤니케이션하기 위한 데이터 처리 비즈니스 로직을 포함합니다.
① 데이터를 로드하기 위해 다른 구성요소를 호출
② 사용자 요청을 전달하여 데이터를 수정
③ UI 구성요소에 관해 알지 못하므로 구성 변경의 영향을 받지 않음
④ 화면 회전 시에도 데이터가 유지됨
(2) LiveData의 역할
LiveData는 식별 가능한 데이터 홀더입니다. 앱의 다른 구성요소에서는 이 홀더를 사용하여 상호 간에 명시적이고 엄격한 종속성 경로를 만들지 않고도 객체 변경사항을 모니터링할 수 있습니다.
① Activity, Fragment, Service 같은 앱 구성요소의 수명 주기 상태를 고려
② 객체 유출과 과도한 메모리 소비를 방지하기 위한 정리 로직 포함
③ 생명주기를 인식하여 자동으로 구독 관리
. . . . .
3) 구현에 필요한 파일 구조
① user_profile.xml - 화면의 UI 레이아웃 정의
② UserProfileFragment - 데이터를 표시하는 UI 컨트롤러
③ UserProfileViewModel - Fragment에서 볼 수 있도록 데이터를 준비하고 사용자 상호작용에 반응하는 클래스
④ User - 사용자 정보를 담는 데이터 클래스
#2. ViewModel 구현과 SavedStateHandle
1) 기본 ViewModel 구조
먼저 기본적인 ViewModel 클래스를 작성해보겠습니다.
// UserProfileViewModel - 기본 구조
class UserProfileViewModel : ViewModel() {
val userId: String = TODO()
val user: User = TODO()
}
class UserProfileViewModel : ViewModel() {
val userId: String = TODO()
val user: User = TODO()
}
. . . . .
2) SavedStateHandle로 인수 전달하기
user를 가져오려면 ViewModel에서 Fragment 인수에 액세스해야 합니다. SavedState 모듈을 사용해 ViewModel에서 직접 인수를 읽을 수 있습니다.
(1) SavedStateHandle의 장점
SavedStateHandle을 사용하면 ViewModel에서 관련 Fragment 또는 Activity의 저장된 상태와 인수에 액세스할 수 있습니다.
① 프로세스가 종료되어도 데이터 유지
② Fragment 인수를 ViewModel에서 직접 접근 가능
③ 상태 저장 및 복원 자동화
(2) SavedStateHandle 구현 코드
// UserProfileViewModel - SavedStateHandle 사용
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// Fragment 인수에서 userId 가져오기
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
val user: User = TODO()
}
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// Fragment 인수에서 userId 가져오기
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
val user: User = TODO()
}
. . . . .
3) Fragment에서 ViewModel 초기화
// UserProfileFragment
class UserProfileFragment : Fragment() {
// ViewModel 초기화 (SavedStateHandle 지원)
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.user_profile_layout, container, false)
}
}
class UserProfileFragment : Fragment() {
// ViewModel 초기화 (SavedStateHandle 지원)
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.user_profile_layout, container, false)
}
}
#3. LiveData로 UI 업데이트 자동화
이제 user 객체가 확보되면 Fragment에 알려야 합니다. 여기에서 LiveData 아키텍처 구성요소가 사용됩니다.
1) ViewModel에 LiveData 적용
LiveData 구성요소를 앱에 통합하기 위해 UserProfileViewModel의 필드 유형을 LiveData<User>로 변경합니다.
// UserProfileViewModel - LiveData 적용
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
// LiveData로 변경 - 데이터 업데이트 시 자동으로 UI에 알림
val user: LiveData<User> = TODO()
}
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
// LiveData로 변경 - 데이터 업데이트 시 자동으로 UI에 알림
val user: LiveData<User> = TODO()
}
데이터가 업데이트되면 UserProfileFragment에 정보가 자동으로 전달됩니다. 또한 이 LiveData 필드는 수명 주기를 인식하기 때문에 더 이상 필요하지 않은 참조를 자동으로 정리합니다.
. . . . .
2) LiveData의 생명주기 인식 기능
(1) 주요 특징
① Activity나 Fragment가 활성 상태일 때만 업데이트 수신
② 생명주기가 DESTROYED 상태가 되면 자동으로 구독 해제
③ 메모리 누수 방지
④ 화면 회전 시에도 데이터 유지
(2) RxJava 사용자를 위한 참고사항
RxJava와 같은 라이브러리를 이미 사용하고 있다면 LiveData 대신 계속 사용해도 됩니다. 그러나 이러한 라이브러리와 방법을 사용하는 경우 앱의 수명 주기를 올바르게 처리해야 합니다.
① 관련 LifecycleOwner가 중지되면 데이터 스트림이 일시중지
② 관련 LifecycleOwner가 제거되면 스트림이 제거
③ android.arch.lifecycle:reactivestreams 아티팩트로 LiveData와 함께 사용 가능
#4. Fragment와 ViewModel 연결
1) Fragment에서 LiveData 관찰하기
이제 데이터를 관찰하고 UI를 업데이트하도록 UserProfileFragment를 수정합니다.
// UserProfileFragment - LiveData 관찰
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
// LiveData 관찰 - 데이터 변경 시 자동으로 콜백 호출
viewModel.user.observe(viewLifecycleOwner) { user ->
// UI 업데이트
updateUI(user)
}
}
private fun updateUI(user: User) {
// UI 구성요소 업데이트 로직
}
}
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
// LiveData 관찰 - 데이터 변경 시 자동으로 콜백 호출
viewModel.user.observe(viewLifecycleOwner) { user ->
// UI 업데이트
updateUI(user)
}
}
private fun updateUI(user: User) {
// UI 구성요소 업데이트 로직
}
}
사용자 프로필 데이터가 업데이트될 때마다 observe 콜백이 호출되고 UI가 자동으로 새로고침됩니다.
. . . . .
2) viewLifecycleOwner 사용의 중요성
Fragment에서는 viewLifecycleOwner를 사용하는 것이 중요합니다.
(1) viewLifecycleOwner vs this
① viewLifecycleOwner - Fragment의 뷰 생명주기를 따름 (권장)
② this - Fragment 자체의 생명주기를 따름
③ Fragment의 뷰는 Fragment보다 먼저 파괴될 수 있으므로 viewLifecycleOwner 사용 필수
#5. 실전 프로필 화면 완성 코드
1) 데이터 클래스 정의
// User 데이터 클래스
data class User(
val id: String,
val name: String,
val email: String,
val profileImageUrl: String
)
data class User(
val id: String,
val name: String,
val email: String,
val profileImageUrl: String
)
. . . . .
2) Repository 추가 (완성 버전)
// UserRepository - 데이터 소스 관리
class UserRepository {
fun getUser(userId: String): LiveData<User> {
// 실제로는 네트워크나 데이터베이스에서 데이터 가져오기
val liveData = MutableLiveData<User>()
// 예시 데이터
liveData.value = User(
id = userId,
name = "홍길동",
email = "hong@example.com",
profileImageUrl = "https://example.com/profile.jpg"
)
return liveData
}
}
class UserRepository {
fun getUser(userId: String): LiveData<User> {
// 실제로는 네트워크나 데이터베이스에서 데이터 가져오기
val liveData = MutableLiveData<User>()
// 예시 데이터
liveData.value = User(
id = userId,
name = "홍길동",
email = "hong@example.com",
profileImageUrl = "https://example.com/profile.jpg"
)
return liveData
}
}
. . . . .
3) 완성된 ViewModel
// UserProfileViewModel - 완성 버전
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val repository = UserRepository()
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
// Repository에서 LiveData 가져오기
val user: LiveData<User> = repository.getUser(userId)
}
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val repository = UserRepository()
val userId: String = savedStateHandle["uid"]
?: throw IllegalArgumentException("missing user id")
// Repository에서 LiveData 가져오기
val user: LiveData<User> = repository.getUser(userId)
}
. . . . .
4) 완성된 Fragment
// UserProfileFragment - 완성 버전
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
private var _binding: UserProfileLayoutBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = UserProfileLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// LiveData 관찰 및 UI 업데이트
viewModel.user.observe(viewLifecycleOwner) { user ->
binding.textViewName.text = user.name
binding.textViewEmail.text = user.email
// Glide로 프로필 이미지 로드
Glide.with(this)
.load(user.profileImageUrl)
.circleCrop()
.into(binding.imageViewProfile)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
)
private var _binding: UserProfileLayoutBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = UserProfileLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// LiveData 관찰 및 UI 업데이트
viewModel.user.observe(viewLifecycleOwner) { user ->
binding.textViewName.text = user.name
binding.textViewEmail.text = user.email
// Glide로 프로필 이미지 로드
Glide.with(this)
.load(user.profileImageUrl)
.circleCrop()
.into(binding.imageViewProfile)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
마무리
이번 글에서는 ViewModel, LiveData, SavedStateHandle을 활용한 실전 Android 아키텍처를 구현해보았습니다. 이러한 아키텍처 구성요소들은 Google이 권장하는 현대적인 Android 개발 방식의 핵심입니다.
핵심 요약
① ViewModel - UI 데이터를 관리하고 화면 회전 시에도 데이터 유지
② LiveData - 생명주기를 인식하여 UI를 자동으로 업데이트
③ SavedStateHandle - Fragment 인수와 저장된 상태에 안전하게 접근
④ viewLifecycleOwner - Fragment에서 LiveData 관찰 시 필수
⑤ Repository 패턴으로 데이터 소스 분리하여 관리
이러한 아키텍처 패턴을 사용하면 코드의 테스트 가능성이 높아지고, 유지보수가 쉬워지며, 메모리 누수를 방지할 수 있습니다. 실제 프로젝트에서는 Repository 패턴, 의존성 주입(Hilt/Dagger), 코루틴 등을 함께 사용하여 더욱 견고한 아키텍처를 구축할 수 있습니다.
긴 글 읽어주셔서 감사합니다.
끝.
끝.
반응형
'Development > Android' 카테고리의 다른 글
| [Android] Android Activity 오류 해결 및 성능 최적화 방법 (0) | 2020.04.08 |
|---|---|
| [Android] Android Picasso vs Glide 이미지 라이브러리 비교와 선택 기준 (0) | 2019.10.01 |
| [Android] Android App Architecture 완벽 가이드 - MVVM 패턴과 권장 아키텍처 설계 방법 (0) | 2019.09.23 |
| [Android] Android ProGuard 완벽 가이드 - 소스코드 난독화와 최적화 방법 (0) | 2019.09.22 |
| [Android] Android Button 텍스트 밑줄 추가하는 4가지 방법 (0) | 2019.09.22 |