HOME > android > jetpack

Android Jetpack - ViewModel 소개 및 구현 방법

JSFollow13 Dec 2018

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은 소멸되지 않습니다.

jetpack viewmodel lifecycle

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초씩 시간이 증가합니다.

jetpack viewmodel lifecycle

이제 화면을 회전해보세요. 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으로 되돌아가지 않습니다.

jetpack viewmodel lifecycle

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의 데이터를 관찰할 수 있습니다.

참고