JetPack 또는 AAC(Android Architecture Component)의 ViewModel는 View에서 사용되는 데이터를 우리가 쉽게 관리하는데 도와줍니다. Jetpack ViewModel은 MVVM 패턴에서 우리가 알고있는 ViewModel과 좀 다릅니다.
JetPack ViewModel은 안드로이드의 액티비티 생명주기에 분리를 시켜, 액티비티가 재실행되도 데이터가 소멸되지 않도록 합니다. 또한 액티비티가 파괴(destroy)되면 ViewModel의 자원도 자연히 소멸됩니다.
예를들어 화면이 로테이션 되면, 액티비티가 재실행됩니다. 이런 경우 액티비티에서 사용하는 자원을 버리고 다시 불러오게 됩니다. 데이터 크기가 작으면 어렵지 않겠지만, 코드를 더 작성하는 것은 귀찮은 일이고, 리소스를 소모하게 됩니다.
ViewModel 객체는 액티비티의 Lifecycle 상태가 종료(FINISH)될 때 까지 소멸되지 않습니다. 즉, 화면이 Rotation되어 액티비티가 재실행된다고 해도 ViewModel이 갖고 있는 데이터는 소멸되지 않으며, 다시 로딩할 필요가 없습니다.
아래 그림은 액티비티와 ViewModel의 Lifecycle을 보여줍니다. 액티비티의 상태가 Finished
가 되었을 때 ViewModel은 소멸됩니다.
Rotation으로 재실행되는 경우에도 ViewModel은 소멸되지 않습니다.
Android codelabs에 ViewModel의 예제가 있습니다. 이 예제를 살펴보면서 ViewModel을 어떻게 사용하고 동작되는지 알아보겠습니다.
이 예제에서는 기존에 방식대로 앱을 구현했을 때 발생하는 문제점을 확인하고, 기존 코드를 ViewModel로 해결하는 순서로 진행됩니다.
문제가 있는 앱
이 앱의 MainActivity가 보여주는 UI는 Chronometer가 전부입니다. 이 객체는 1초마다 1초씩 증가하는 시간을 출력해줍니다.
<Chronometer
android:id="@+id/chronometer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/hello_textview"
android:layout_centerHorizontal="true" />
액티비티 코드는 chronometer.start()
를 호출하여 시간이 증가하도록 하였습니다.
class ChronoActivity2 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chrono_2)
chronometer.start()
}
}
앱을 실행해보면 아래 화면처럼 1초씩 시간이 증가합니다.
이제 화면을 회전해보세요. Rotation되면서 Configuration Change
가 발생하였고 액티비티가 재실행됩니다.
데이터는 따로 저장하지 않기 때문에 0초부터 다시 증가하기 시작합니다.
문제점은 0초로 다시 돌아가는 것입니다. 만약 회전을 해도 시간이 0초로 돌아가지 않도록 하려면 어떻게 해야 할까요?
Configuration Change
가 발생했을 때 액티비티가 재실행되지 않도록 설정하거나, onPause
에 시간을 잠시 저장했다가 재실행 후 onResume
에 다시 읽어서 chronometer
에 전달해주는 방법이 있습니다.
ViewModel로 문제 해결
뷰모델을 사용한다면 액티비티의 Lifecycle에서 자유롭기 때문에, 로테이션이 되도 데이터가 소멸되지 않습니다.
ViewModel을 사용한다면 onResume
, onPause
에 시간을 임시로 저장하는 것을 구현할 필요가 없습니다.
먼저 뷰모델을 구현해야 합니다. ViewModel
를 상속받는 ChronometerViewModel 객체를 정의하였습니다.
import androidx.lifecycle.ViewModel
class ChronometerViewModel : ViewModel() {
private var startTime: Long = 0
fun getStartTime(): Long = startTime
fun setStartTime(startTime: Long) {
this.startTime = startTime
}
}
액티비티의 코드는 아래와 같습니다.
ViewModelProviders라는 helper 객체를 이용하여 ViewModel의 객체를 생성하고 startTime
을 가져와 chronometer
에 전달해주었습니다.
만약 액티비티가 재실행된다고 해도 ViewModel은 소멸되지 않고 이전에 생성한 것을 사용하기 때문에 startTime
은 처음에 설정된 값을 갖고 있습니다.
class ChronoActivity2 : AppCompatActivity() {
companion object {
const val TAG = "ChronoActivity2"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chrono_2)
// The ViewModelStore provides a new ViewModel or one previously created.
val chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel::class.java)
if (chronometerViewModel.getStartTime() <= 0) {
// If the start date is not defined, it's a new ViewModel so set it.
val startTime = SystemClock.elapsedRealtime()
chronometerViewModel.setStartTime(startTime)
chronometer.base = startTime
Log.d(TAG, "Use new created time")
} else {
chronometer.base = chronometerViewModel.getStartTime()
Log.d(TAG, "Use saved time")
}
chronometer.start()
}
}
회전을 해도 ViewModel을 통해 지금까지 저장된 시간을 가져오기 때문에 0으로 되돌아가지 않습니다.
ViewModel 생성 코드 분석
위에서 사용한 것처럼 ViewModel은 아래와 같은 코드로 생성합니다. 이 코드가 두번 이상 호출되는 경우 처음 호출될 때 생성된 값이 반환됩니다. Singleton 클래스와 유사합니다.
ViewModelProviders.of(this).get(ChronometerViewModel::class.java)
ViewModelProviders.of
의 코드를 보면 내부적으로 AndroidViewModelFactory를 사용한다고 하고 ViewModelProvider를 리턴해줍니다.
/**
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
* {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
* <p>
* It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param fragment a fragment, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
return of(fragment, null);
}
ViewModelProvider.get
의 코드를 보면 mViewModelStore
에 객체가 존재하면 이 객체를 리턴해주고, 그렇지 않으면 새로 만들어줍니다.
자세히 보면 저장된 객체가 ViewModel의 객체인지 확인하는 코드도 있습니다.
/**
* Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
* an activity), associated with this {@code ViewModelProvider}.
* <p>
* The created ViewModel is associated with the given scope and will be retained
* as long as the scope is alive (e.g. if it is an activity, until it is
* finished or process is killed).
*
* @param key The key to use to identify the ViewModel.
* @param modelClass The class of the ViewModel to create an instance of it if it is not
* present.
* @param <T> The type parameter for the ViewModel.
* @return A ViewModel that is an instance of the given type {@code T}.
*/
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
viewModel = mFactory.create(key, modelClass);
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
AndroidViewModel
앞에서 사용한 ViewModel
객체에 Context를 인자로 전달하거나, Activity, Fragment, View 객체나 Context를 ViewModel에 저장하면 안됩니다.
액티비티가 종료되어 자원이 소멸될 때, ViewModel의 자원도 자연스럽게 소멸되어야 하는데요. 만약 내부에 Context를 참조하고 있다면 GC로도 데이터를 정리할 수 없게 됩니다.
만약 ViewModel에서 Context나 Activity 객체를 사용하고 싶다면 ViewModel을 사용하지 말고 AndroidViewModel
를 사용해야 합니다.
class ChronometerViewModel(application: Application) : AndroidViewModel(application) {
}
정리
ViewModel에 대해서 간단히 알아보았습니다. MVVM의 ViewModel과 다르며, 액티비티의 Lifecycle과 데이터 유지라는 부분에 초점을 맞추었습니다. 액티비티와 데이터(비지니스 모델)를 분리할 수 있다는 장점이 있으며, 액티비티는 Rx Observable, LiveData 등을 이용하여 ViewModel의 데이터를 관찰할 수 있습니다.
참고
- 샘플 코드는 GitHub에 있습니다(step2, step2_solution 참고)
- Android Lifecycles - CodeLabs
- Jetpack ViewModel - Android developer
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력