ViewModel 개요   Android Jetpack의 구성요소

ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.

Android 프레임워크는 활동과 프래그먼트 같은 UI 컨트롤러의 수명 주기를 관리합니다. 프레임워크는 특정 사용자 작업이나 완전히 통제할 수 없는 기기 이벤트에 대한 응답으로 UI 컨트롤러를 제거하거나 다시 만들도록 결정할 수 있습니다.

시스템에서 UI 컨트롤러를 제거하거나 다시 만드는 경우, 컨트롤러에 저장된 모든 일시적인 UI 관련 데이터가 삭제됩니다. 예를 들면 앱 활동 중 하나에 사용자 목록이 포함되어 있는데, 구성이 변경되어 활동이 다시 생성되면 새 활동이 사용자 목록을 다시 가져와야 합니다. 데이터가 단순한 경우 활동은 onSaveInstanceState() 메서드를 사용하여 onCreate()의 번들에서 데이터를 복원할 수 있습니다. 하지만 이 접근 방법은 직렬화했다가 다시 역직렬화할 수 있는 소량의 데이터에만 적합하며 사용자 목록이나 비트맵과 같은 대용량일 가능성이 높은 데이터에는 적합하지 않습니다.

다른 문제는 UI 컨트롤러가 반환하는 데 시간이 걸릴 수 있는 비동기 호출을 자주 해야 한다는 점입니다. UI 컨트롤러는 비동기 호출을 관리해야 하며, 메모리 누수 가능성을 방지하기 위해 호출이 소멸된 후 시스템에서 호출을 정리해야 합니다. 관리에는 많은 유지관리가 필요하며, 구성 변경 시 객체가 다시 생성되는 경우 객체가 이미 수행된 호출을 다시 호출해야 할 수 있으므로 리소스 낭비가 발생합니다.

활동 및 프래그먼트와 같은 UI 컨트롤러의 목적은 기본적으로 UI 데이터를 표시하거나, 사용자 작업에 반응하거나, 권한 요청과 같은 운영체제 커뮤니케이션을 처리하는 것입니다. 또한 UI 컨트롤러에 데이터베이스나 네트워크로부터 데이터를 로드하도록 하면 클래스가 팽창됩니다. UI 컨트롤러에 과도한 책임을 할당하면 다른 클래스로 작업이 위임되지 않고, 하나의 클래스에서 앱의 모든 작업을 처리하려고 할 수 있습니다. 또한 이런 방법으로 UI 컨트롤러에 과도한 책임을 할당하면 테스트가 훨씬 더 어려워집니다.

UI 컨트롤러 로직에서 뷰 데이터 소유권을 분리하는 방법이 훨씬 쉽고 효율적입니다.

ViewModel 구현

아키텍처 구성요소는 UI의 데이터 준비를 담당하는 UI 컨트롤러에 ViewModel 도우미 클래스를 제공합니다. ViewModel 객체는 구성이 변경되는 동안 자동으로 보관되므로, 이러한 객체가 보유한 데이터는 다음 활동 또는 프래그먼트 인스턴스에서 즉시 사용할 수 있습니다. 예를 들어 앱에서 사용자 목록을 표시해야 한다면 다음 샘플 코드에 설명된 대로 사용자 목록을 확보하여 활동이나 프래그먼트 대신 ViewModel에 보관하도록 책임을 할당해야 합니다.

class MyViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

그 후에 다음과 같이 활동에서 목록에 액세스할 수 있습니다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

활동이 다시 생성되면 첫 번째 활동에서 생성된 동일한 MyViewModel 인스턴스를 받습니다. 소유자 활동이 완료되면 프레임워크는 리소스를 정리할 수 있도록 ViewModel 객체의 onCleared() 메서드를 호출합니다.

ViewModel 객체는 뷰 또는 LifecycleOwners의 특정 인스턴스화보다 오래 지속되도록 설계되었습니다. 이러한 설계는 뷰 및 Lifecycle 객체에 관해 모르기 때문에 개발자는 ViewModel을 다루는 테스트를 더 쉽게 작성할 수 있습니다. ViewModel 객체에는 LiveData 객체와 같은 LifecycleObservers가 포함될 수 있습니다. 그러나 ViewModel 객체는 LiveData 객체와 같이 수명 주기를 인식하는 Observable의 변경사항을 관찰해서는 안 됩니다. 예를 들어 ViewModel은 시스템 서비스를 찾는 데 Application 컨텍스트가 필요하면 AndroidViewModel 클래스를 확장하고 생성자에 Application을 받는 생성자를 포함할 수 있습니다. Application 클래스가 Context를 확장하기 때문입니다.

종속 항목이 있는 ViewModel 만들기

종속 항목 삽입의 권장사항에 따라 ViewModel은 종속 항목을 생성자의 매개변수로 사용할 수 있습니다. 이는 대부분 도메인 또는 데이터 레이어의 유형입니다. 프레임워크에서 ViewModel을 제공하기 때문에 ViewModel의 인스턴스를 만들려면 특수한 메커니즘이 필요합니다. 이 메커니즘이 ViewModelProvider.Factory 인터페이스입니다. 이 인터페이스의 구현만 올바른 범위에서 ViewModel을 인스턴스화할 수 있습니다.

ViewModel 클래스가 생성자의 종속 항목을 수신하는 경우 ViewModelProvider.Factory 인터페이스를 구현하는 팩토리를 제공합니다. ViewModel의 새 인스턴스를 제공하도록 create(Class<T>, CreationExtras) 함수를 재정의합니다.

CreationExtras를 사용하면 ViewModel을 인스턴스화하는 데 유용한 관련 정보에 액세스할 수 있습니다. Extras에서 액세스할 수 있는 키 목록은 다음과 같습니다.

기능
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY KeyViewModelProvider.get()에 전달한 맞춤 키에 대한 액세스 권한을 제공합니다.
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY Application 클래스의 인스턴스에 대한 액세스 권한을 제공합니다.
SavedStateHandleSupport.DEFAULT_ARGS_KEY SavedStateHandle을 구성하는 데 사용해야 하는 인수의 Bundle에 대한 액세스 권한을 제공합니다.
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY ViewModel을 구성하는 데 사용되는 SavedStateRegistryOwner에 대한 액세스 권한을 제공합니다.
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY ViewModel을 구성하는 데 사용되는 ViewModelStoreOwner에 대한 액세스 권한을 제공합니다.

SavedStateHandle의 새 인스턴스를 만들려면 CreationExtras.createSavedStateHandle().createSavedStateHandle()) 함수를 사용하여 ViewModel에 전달합니다.

다음은 범위가 Application 클래스 및 SavedStateHandle로 범위가 지정된 저장소를 종속 항목으로 사용하는 ViewModel의 인스턴스를 제공하는 방법을 보여 주는 예입니다.

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras

class MyViewModel(
    private val myRepository: MyRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // ViewModel logic
    // ...

    // Define ViewModel factory in a companion object
    companion object {

        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel> create(
                modelClass: Class<T>,
                extras: CreationExtras
            ): T {
                // Get the Application object from extras
                val application = checkNotNull(extras[APPLICATION_KEY])
                // Create a SavedStateHandle for this ViewModel from extras
                val savedStateHandle = extras.createSavedStateHandle()

                return MyViewModel(
                    (application as MyApplication).myRepository,
                    savedStateHandle
                ) as T
            }
        }
    }
}

import static androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle;
import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;

import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.viewmodel.ViewModelInitializer;

public class MyViewModel extends ViewModel {

    public MyViewModel(
        MyRepository myRepository,
        SavedStateHandle savedStateHandle
    ) { /* Init ViewModel here */ }

    static final ViewModelInitializer<MyViewModel> initializer = new ViewModelInitializer<>(
        MyViewModel.class,
        creationExtras -> {
            MyApplication app = (MyApplication) creationExtras.get(APPLICATION_KEY);
            assert app != null;
            SavedStateHandle savedStateHandle = createSavedStateHandle(creationExtras);

            return new MyViewModel(app.getMyRepository(), savedStateHandle);
        }
    );
}

그런 다음 ViewModel의 인스턴스를 가져올 때 이 팩토리를 사용할 수 있습니다.

import androidx.activity.viewModels

class MyActivity : AppCompatActivity() {

    private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }

    // Rest of Activity code
}

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;

public class MyActivity extends AppCompatActivity {

    MyViewModel myViewModel = new ViewModelProvider(
        this,
        ViewModelProvider.Factory.from(MyViewModel.initializer)
    ).get(MyViewModel.class);

    // Rest of Activity code
}

Compose

import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun MyScreen(
    modifier: Modifier = Modifier,
    viewModel: MyViewModel = viewModel(factory = MyViewModel.Factory)
) {
    // ...
}

또는 ViewModel 팩토리 DSL을 사용하여 더 자연스러운 Kotlin API를 사용하는 팩토리를 만듭니다.

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory

class MyViewModel(
    private val myRepository: MyRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // ViewModel logic

    // Define ViewModel factory in a companion object
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val savedStateHandle = createSavedStateHandle()
                val myRepository = (this[APPLICATION_KEY] as MyApplication).myRepository
                MyViewModel(
                    myRepository = myRepository,
                    savedStateHandle = savedStateHandle
                )
            }
        }
    }
}

2.5.0 이전의 ViewModel 버전에 대한 팩토리

2.5.0 이전의 ViewModel 버전을 사용하는 경우 ViewModelProvider.Factory를 확장하고 create(Class<T>) 함수를 구현하는 클래스의 하위 집합에서 팩토리를 제공해야 합니다. ViewModel에 필요한 종속 항목에 따라 다음에서 다른 클래스를 확장해야 합니다.

Application 또는 SavedStateHandle이 필요하지 않으면 ViewModelProvider.Factory에서 확장합니다.

다음 예에서는 저장소 및 SavedStateHandle 유형을 종속 항목으로 사용하는 ViewModel에 대해 AbstractSavedStateViewModelFactory를 사용합니다.

class MyViewModel(
    private val myRepository: MyRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // ViewModel logic ...

    // Define ViewModel factory in a companion object
    companion object {
        fun provideFactory(
            myRepository: MyRepository,
            owner: SavedStateRegistryOwner,
            defaultArgs: Bundle? = null,
        ): AbstractSavedStateViewModelFactory =
            object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(
                    key: String,
                    modelClass: Class<T>,
                    handle: SavedStateHandle
                ): T {
                    return MyViewModel(myRepository, handle) as T
                }
            }
    }
}

import androidx.annotation.NonNull;
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;

public class MyViewModel extends ViewModel {
    public MyViewModel(
        MyRepository myRepository,
        SavedStateHandle savedStateHandle
    ) { /* Init ViewModel here */ }
}

public class MyViewModelFactory extends AbstractSavedStateViewModelFactory {

    private final MyRepository myRepository;

    public MyViewModelFactory(
        MyRepository myRepository
    ) {
        this.myRepository = myRepository;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    protected <T extends ViewModel> T create(
        @NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle
    ) {
        return (T) new MyViewModel(myRepository, handle);
    }
}

그런 다음 팩토리를 사용하여 ViewModel을 가져올 수 있습니다.

import androidx.activity.viewModels

class MyActivity : AppCompatActivity() {

    private val viewModel: MyViewModel by viewModels {
        MyViewModel.provideFactory((application as MyApplication).myRepository, this)
    }

    // Rest of Activity code
}

public class MyActivity extends AppCompatActivity {

    MyViewModel myViewModel = new ViewModelProvider(
        this,
        new MyViewModelFactory(((MyApplication) getApplication()).getMyRepository())
    ).get(MyViewModel.class);

    // Rest of Activity code
}

Compose

import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun MyScreen(
    modifier: Modifier = Modifier,
    viewModel: MyViewModel = viewModel(
        factory = MyViewModel.provideFactory(
            (LocalContext.current.applicationContext as MyApplication).myRepository,
            owner = LocalSavedStateRegistryOwner.current
        )
    )
) {
    // ...
}

ViewModel의 수명 주기

ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelProvider에 전달되는 Lifecycle로 지정됩니다. ViewModel은 범위가 지정된 Lifecycle이 영구적으로 경과될 때까지, 즉 활동에서는 활동이 끝날 때까지, 그리고 프래그먼트에서는 프래그먼트가 분리될 때까지 메모리에 남아 있습니다.

그림 1에서는 활동이 회전을 거친 다음 끝날 때까지 활동의 다양한 수명 주기 상태를 보여줍니다. 또한 관련 활동 수명 주기 옆에 ViewModel의 전체 기간도 보여줍니다. 이 특정 다이어그램에서는 활동의 상태를 보여줍니다. 동일한 기본 상태가 프래그먼트의 수명 주기에 적용됩니다.

활동 상태 변경에 따른 ViewModel의 수명 주기를 나타내는 그림

일반적으로 시스템에서 활동 객체의 onCreate() 메서드를 처음 호출할 때 ViewModel을 요청합니다. 시스템은 활동 기간 내내(예: 기기 화면이 회전될 때) onCreate() 메서드를 여러 번 호출할 수 있습니다. ViewModel이 처음 요청되었을 때부터 활동이 끝나고 폐기될 때까지 ViewModel은 존재합니다.

프래그먼트 간 데이터 공유

활동에 포함된 둘 이상의 프래그먼트는 서로 커뮤니케이션해야 한다는 것이 일반적입니다. 사용자가 목록에서 항목을 선택하는 프래그먼트와 선택된 항목의 콘텐츠를 표시하는 또 다른 프래그먼트가 있는 split-view(list-detail) 프래그먼트의 일반적인 사례를 가정해 보세요. 두 프래그먼트가 모두 인터페이스 설명을 정의해야 하고 소유자 활동이 두 프래그먼트를 함께 결합해야 하므로 이 사례는 간단히 처리할 수 있는 작업이 아닙니다. 또한 두 프래그먼트는 모두 다른 프래그먼트가 아직 생성되지 않았거나 표시되지 않은 시나리오도 처리해야 합니다.

이러한 일반적인 고충은 ViewModel 객체를 사용하면 해결할 수 있습니다. 이러한 프래그먼트는 다음 샘플 코드와 같이 이 커뮤니케이션을 처리하기 위한 활동 범위를 사용하여 ViewModel을 공유할 수 있습니다.

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData()

    fun select(item: Item) {
        selected.value = item
    }
}

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer { item ->
            // Update the UI
        })
    }
}

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData getSelected() {
        return selected;
    }
}

public class ListFragment extends Fragment {
    private SharedViewModel model;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), item -> {
           // Update the UI.
        });
    }
}

두 프래그먼트는 모두 자신이 포함된 활동을 가져옵니다. 그러면 각 프래그먼트는 ViewModelProvider를 가져올 때 이 활동으로 범위가 지정된 동일한 SharedViewModel 인스턴스를 받습니다.

이 접근 방법에는 다음과 같은 이점이 있습니다.

  • 활동은 아무것도 할 필요가 없고 이 커뮤니케이션에 관해 어떤 것도 알 필요가 없습니다.
  • 프래그먼트는 SharedViewModel 계약 외에 서로 알 필요가 없습니다. 프래그먼트 중 하나가 사라져도 다른 프래그먼트는 계속 평소대로 작동합니다.
  • 각 프래그먼트는 자체 수명 주기가 있으며, 다른 프래그먼트 수명 주기의 영향을 받지 않습니다. 한 프래그먼트가 다른 프래그먼트를 대체해도, UI는 아무 문제 없이 계속 작동합니다.

ViewModel로 로더 대체하기

CursorLoader와 같은 로더 클래스는 앱 UI의 데이터와 데이터베이스 간의 동기화를 유지하는 데 자주 사용됩니다. ViewModel을 몇 가지 클래스와 함께 사용하여 로더를 대체할 수 있습니다. ViewModel을 사용하면 UI 컨트롤러가 데이터 로드 작업에서 분리됩니다. 즉, 클래스 간에 강력한 참조가 적어집니다.

일반적인 로더 사용 방법 중 하나로, 앱이 CursorLoader를 사용하여 데이터베이스의 내용을 관찰할 수 있습니다. 데이터베이스에서 값이 변경되면 로더가 자동으로 데이터 새로고침을 트리거하고 UI를 업데이트합니다.

그림 2. 로더로 데이터 로드하기

ViewModelRoomLiveData와 함께 작업하여 로더를 대체합니다. ViewModel은 기기 설정이 변경되어도 데이터가 유지되도록 보장합니다. 데이터베이스가 변경되면 Room에서 LiveData에 변경을 알리고, 알림을 받은 LiveData는 수정된 데이터로 UI를 업데이트합니다.

그림 3. ViewModel로 데이터 로드하기

ViewModel과 함께 코루틴 사용

ViewModel에는 Kotlin 코루틴 지원이 포함됩니다. 자세한 내용은 Android 아키텍처 구성요소와 함께 Kotlin 코루틴 사용을 참고하세요.

추가 정보

데이터가 더 복잡해지면 데이터 로드만을 위한 별도의 클래스를 사용하는 것이 좋습니다. ViewModel의 목적은 UI 컨트롤러의 데이터를 캡슐화하여 구성이 변경되어도 데이터를 유지하는 것입니다. 구성 변경 시 데이터를 로드, 유지 및 관리하는 방법에 관한 자세한 내용은 UI 상태 저장을 참고하세요.

Android 앱 아키텍처 가이드에서는 이러한 함수를 처리하는 저장소 클래스 빌드를 제안합니다.

추가 리소스

ViewModel 클래스에 관한 자세한 내용은 다음 리소스를 참고하세요.

샘플

Codelab

블로그

동영상