ViewModel

sumi Yoo·2022년 10월 15일
0

ViewModel

UI 관련 데이터를 수명 주기를 의식한 방식으로 저장하고 관리하도록 설계되었다. 이 ViewModel클래스를 사용하면 화면 회전과 같은 구성 변경 후에도 데이터를 유지할 수 있다.

안드로이드 프레임워크는 액티비티나 프래그먼트의 수명 주기를 관리합니다.

시스템이 UI 컨트롤러를 파괴하거나 다시 생성하면 저장한 일시적인 UI 관련 데이터가 손실됩니다. (화면 회전을 하면 입력했던 정보들이 사라지는 등)

간단한 데이터의 경우, 이 작업은 onSaveInstanceState() 메서드를 사용하여 onCreate()의 번들에서 데이터를 복원할 수 있지만, 이 접근 방식은 사용자 목록이나 비트맵과 같은 잠재적으로 많은 양의 데이터에 적합하지 않고 직렬화 후 역직렬화할 수 있는 소량의 데이터에만 적합하다.

또 다른 문제는 UI 컨트롤러가 반환하는 데 시간이 걸릴 수 있는 비동기식 호출을 자주 수행해야 한다는 것이다. (서버에서 데이터를 가져오는 등) UI 컨트롤러는 이러한 호출을 관리하고 잠재적인 메모리 누수를 방지하기 위해 파괴된 후 시스템이 이를 정리하도록 해야 한다. 이 관리에는 많은 유지 관리가 필요하며 구성 변경을 위해 개체를 다시 만드는 경우 개체가 이미 만든 호출을 다시 발행해야 할 수 있으므로 리소스 낭비이다. (화면이 Destroy 될때마다 메모리를 해제해야 하고 Create 될때마다 다시 생성해야 한다.)

액티비티 및 프래그먼트와 같은 UI 컨트롤러는 주로 UI 데이터를 표시하고, 사용자 작업에 반응하거나, 권한 요청과 같은 운영 체제 통신을 처리하기 위한 것이다.

UI 컨트롤러에 과도한 책임을 할당하면 테스트가 훨씬 더 어려워진다. 여기서 말하는 과도한 책임이란 네트워크에서 데이터를 로드하는 작업까지도 UI 컨트롤러에 책임을 가하는 것이다.

UI 컨트롤러 로직에서 뷰 데이터 소유권을 분리하는 것이 더 쉽고 효율적이다.

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.
    }
}

그런 다음 다음과 같이 액티비티에서 목록에 액세스할 수 있다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // 시스템이 액티비티의 onCreate() 메서드를 처음 호출할 때 ViewModel을 만듭니다.
        // 다시 작성된 액티비티는 첫 번째 액티비티에 의해 작성된 것과 동일한 MyViewModel 인스턴스를 수신합니다.

        // 'by viewModels()' Kotlin 속성 대리자 사용
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

뷰모델은 view, 수명 주기 또는 activity context에 대한 참조를 포함할 수 있는 클래스를 참조해서는 안 됩니다.

ViewModel 개체는 뷰 또는 라이프사이클 소유자의 특정 인스턴스보다 오래 지속되도록 설계되었다. 또한 View Model은 View 및 Lifecycle 객체를 모르기 때문에 View Model을 보다 쉽게 다룰 수 있는 테스트를 작성할 수 있다.

ViewModel 개체에는 LiveData 개체와 같은 LifecycleObserver가 포함될 수 있다.

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

ViewModel의 수명 주기

ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelStoreOwner가 ViewModelProvider에게 전달한 수명 주기로 지정된다. ViewModel은 ViewModelStoreOwner가 영구적으로 삭제될 때까지 메모리에 남아 있다.

  • Navigation 항목의 경우 백 스택에서 제거되었을 때.
  • fragment가 분리 되었을 때
  • activity가 끝났을 때

ViewModel APIs

ViewModelProvider.get()

ViewModelProvider.get() 메서드를 사용하면 ViewModelStoreOwner로 범위가 지정된 ViewModel의 인스턴스를 가져올 수 있습니다.

public class MyActivity extends AppCompatActivity {

    // The ViewModel is scoped to `this` Activity
    MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
}

public class MyFragment extends Fragment {

    // The ViewModel is scoped to `this` Fragment
    MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
}

kotlin:

class MyActivity : AppCompatActivity() {

    // ViewModel API available in activity.activity-ktx
    // The ViewModel is scoped to `this` Activity
    val viewModel: MyViewModel by viewModels()
}

class MyFragment : Fragment() {

    // ViewModel API available in fragment.fragment-ktx
    // The ViewModel is scoped to `this` Fragment
    val viewModel: MyViewModel by viewModels()
}

프래그먼트 간 데이터 공유

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
        })
    }
}

Context Injection

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
            }
        }
    }
}

그런 다음 ViewModel의 인스턴스를 검색할 때 이 팩토리를 사용할 수 있다. 공유도 가능하다.

import androidx.activity.viewModels

class MyActivity : AppCompatActivity() {

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

    // Rest of Activity code
}

ViewModel은 언제 해제가 될까?

ViewModel이 액티비티나 프래그먼트의 라이프 사이클보다 길기 때문에 상태를 유지하거나 ViewModelScope를 통해 쉽게 코루틴을 사용할 수 있다. 그렇담 어떻게 인지를 하고 해제가 되는 걸까?

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

Closeable의 close()가 불려야 해제가 된다.

public abstract class ViewModel {
    // Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
    @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();
    @Nullable
    private final Set<Closeable> mCloseables = new LinkedHashSet<>();
    private volatile boolean mCleared = false;

ViewModelScope를 Map으로 관리하게 된다. (setTagIfAbsent())

  <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
        if (mCleared) {
            // It is possible that we'll call close() multiple times on the same object, but
            // Closeable interface requires close method to be idempotent:
            // "if the stream is already closed then invoking this method has no effect." (c)
            closeWithRuntimeException(result);
        }
        return result;
    }

이 clear()는 어떻게 불려지게 되는걸까?

액티비티나 프래그먼트가 모두 종료가 되어야 불리기 때문에 ComponentActivity 코드를 확인해 보면

getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // Clear out the available context
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStore
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });

라이프 사이클을 확인해서 ON_DESTROY일 때 getViewModelStore().clear()가 호출되는 것을 볼 수 있다. ViewModelStore는 말 그대로 ViewModel이 저장되는 곳이다.

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

ViewModelStore 코드를 살펴보면, 갖고있는 ViewModel의 clear()를 호출하는 것을 볼 수 있다.

 final void clear() {
        mCleared = true;
        // Since clear() is final, this method is still called on mock objects
        // and in those cases, mBagOfTags is null. It'll always be empty though
        // because setTagIfAbsent and getTag are not final so we can skip
        // clearing it
        if (mBagOfTags != null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        // We need the same null check here
        if (mCloseables != null) {
            synchronized (mCloseables) {
                for (Closeable closeable : mCloseables) {
                    closeWithRuntimeException(closeable);
                }
            }
        }
        onCleared();
    }

closeWithRuntimeException(closeable) 내부 코드를 보면

 private static void closeWithRuntimeException(Object obj) {
        if (obj instanceof Closeable) {
            try {
                ((Closeable) obj).close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

Closeable의 close()를 호출하는 것을 볼 수 있다.

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

CloseableCoroutineScope은 Closeable을 상속받기 때문에 close()가 호출되어 coroutineContext.cancel()가 실행된다.

이렇게 우리가 job 해제를 고려하지 않고 viewmodelscope를 통해 쉽게 코루틴을 사용할 수 있는 것이다.

0개의 댓글

관련 채용 정보