Очистка Android ViewModel вручную?

Со ссылкой на android.arch.lifecycle.ViewModel учебный класс.

ViewModel ограничено жизненным циклом компонента пользовательского интерфейса, к которому он относится, поэтому в Fragmentприложение, которое будет частью жизненного цикла фрагмента. Это хорошая вещь.


В некоторых случаях каждый хочет поделиться ViewModel экземпляр между несколькими фрагментами. В частности, меня интересует случай, когда многие экраны относятся к одним и тем же базовым данным.

(Документы предлагают аналогичный подход, когда несколько связанных фрагментов отображаются на одном экране, но это можно обойти, используя один фрагмент хоста в соответствии с ответом ниже.)

Это обсуждается в официальной документации ViewModel:

ViewModels также можно использовать в качестве уровня связи между различными фрагментами действия. Каждый фрагмент может получить ViewModel, используя тот же ключ через свою активность. Это позволяет осуществлять связь между фрагментами в разобщенном виде, так что им никогда не нужно напрямую общаться с другим фрагментом.

Другими словами, для обмена информацией между фрагментами, которые представляют разные экраны, ViewModel должны быть ограничены до Activity жизненный цикл (и в соответствии с документацией Android это также может быть использовано в других общих экземплярах).


Теперь в новом шаблоне навигации Jetpack рекомендуется использовать архитектуру "Один вид деятельности / Много фрагментов". Это означает, что действие длится все время использования приложения.

то есть любой поделился ViewModel случаи, которые ограничены Activity жизненный цикл никогда не будет очищен - память остается в постоянном использовании.

В целях сохранения памяти и использования всего лишь необходимого количества ресурсов в любой момент времени было бы неплохо иметь возможность очищать общие ViewModel случаи, когда больше не требуется.


Как можно вручную очистить ViewModel из этого ViewModelStore или держатель фрагмента?

14 ответов

Решение

Если вы проверите здесь код, вы обнаружите, что вы можете получить ViewModelStore из ViewModelStoreOwner а также Fragment, FragmentActivity например, реализует этот интерфейс.

Су оттуда можно просто позвонить viewModelStore.clear(), который в документации говорит:

 /**
 *  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();
}

Быстрое решение без использования Navigation Component библиотека:

getActivity().getViewModelStore().clear();

Это решит эту проблему без включения Navigation Componentбиблиотека. Это также простая одна строка кода. Это очистит теViewModels которые разделены между Fragments через Activity

Как сказали OP и Archie, Google дал нам возможность ограничить ViewModel навигационными графиками. Я добавлю здесь, как это сделать, если вы уже используете компонент навигации.

Вы можете выбрать все фрагменты, которые необходимо сгруппировать внутри навигационного графа, и right-click->move to nested graph->new graph

теперь это переместит выбранные фрагменты во вложенный граф внутри основного навигационного графа следующим образом:

<navigation app:startDestination="@id/homeFragment" ...>
    <fragment android:id="@+id/homeFragment" .../>
    <fragment android:id="@+id/productListFragment" .../>
    <fragment android:id="@+id/productFragment" .../>
    <fragment android:id="@+id/bargainFragment" .../>

    <navigation 
        android:id="@+id/checkout_graph" 
        app:startDestination="@id/cartFragment">

        <fragment android:id="@+id/orderSummaryFragment".../>
        <fragment android:id="@+id/addressFragment" .../>
        <fragment android:id="@+id/paymentFragment" .../>
        <fragment android:id="@+id/cartFragment" .../>

    </navigation>

</navigation>

Теперь внутри фрагментов, когда вы инициализируете модель просмотра, сделайте это

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

если вам нужно передать фабрику модели просмотра (может быть, для внедрения модели просмотра), вы можете сделать это следующим образом:

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }

Убедитесь, что это R.id.checkout_graph и нет R.navigation.checkout_graph

По какой-то причине создание навигационного графа и использование includeвложить его в основной навигационный граф у меня не получалось. Наверное, это ошибка.

Источник: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e

Спасибо OP и @Archie за то, что указали мне правильное направление.

Я думаю, что у меня есть лучшее решение.

Как заявил @Nagy Robi, вы можете очистить ViewModel по телефону viewModelStore.clear(), Проблема в том, что он очистит ВСЕ модель представления, ограниченную этим ViewModelStore, Другими словами, вы не будете иметь контроль над которым ViewModel очистить.

Но в соответствии с @mikehc здесь. Мы могли бы создать свой собственный ViewModelStore вместо. Это позволит нам детально контролировать, в какой области видимости должен существовать ViewModel.

Примечание: я не видел, чтобы кто-то делал такой подход, но я надеюсь, что он правильный. Это будет действительно хороший способ управления областями в приложении с одним действием.

Пожалуйста, дайте несколько отзывов об этом подходе. Все будет оценено.

Если вы не хотите ViewModel быть ориентированным на Activity жизненный цикл, вы можете включить его в жизненный цикл родительского фрагмента. Так что если вы хотите поделиться экземпляром ViewModel с несколькими фрагментами на экране вы можете расположить фрагменты так, чтобы все они имели общий родительский фрагмент. Таким образом, когда вы создаете ViewModel Вы можете просто сделать это:

CommonViewModel viewModel = ViewModelProviders.of(getParentFragment()).class(CommonViewModel.class);

Надеюсь, это поможет!

Я нашел простой и довольно элегантный способ справиться с этой проблемой. Уловка состоит в том, чтобы использовать DummyViewModel и ключ модели.

Код работает, потому что AndroidX проверяет тип класса модели в get(). Если он не совпадает, он создает новую ViewModel, используя текущую ViewModelProvider.Factory.

public class MyActivity extends AppCompatActivity {
    private static final String KEY_MY_MODEL = "model";

    void clearMyViewModel() {
        new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).
            .get(KEY_MY_MODEL, DummyViewModel.class);
    }

    MyViewModel getMyViewModel() {
        return new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication()).
            .get(KEY_MY_MODEL, MyViewModel.class);
    }

    static class DummyViewModel extends ViewModel {
        //Intentionally blank
    }
}   

Похоже, что в последней версии компонентов архитектуры она уже решена.

ViewModelProvider имеет следующий конструктор:

    /**
 * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
 * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
 *
 * @param owner   a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
 *                retain {@code ViewModels}
 * @param factory a {@code Factory} which will be used to instantiate
 *                new {@code ViewModels}
 */
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
}

Что, в случае фрагмента, будет использовать ViewModelStore с ограниченным объемом.

androidx.fragment.app.Fragment#getViewModelStore

    /**
 * Returns the {@link ViewModelStore} associated with this Fragment
 * <p>
 * Overriding this method is no longer supported and this method will be made
 * <code>final</code> in a future version of Fragment.
 *
 * @return a {@code ViewModelStore}
 * @throws IllegalStateException if called before the Fragment is attached i.e., before
 * onAttach().
 */
@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (mFragmentManager == null) {
        throw new IllegalStateException("Can't access ViewModels from detached fragment");
    }
    return mFragmentManager.getViewModelStore(this);
}

androidx.fragment.app.FragmentManagerViewModel#getViewModelStore

    @NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
    ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
    if (viewModelStore == null) {
        viewModelStore = new ViewModelStore();
        mViewModelStores.put(f.mWho, viewModelStore);
    }
    return viewModelStore;
}

Я просто пишу библиотеку для решения этой проблемы: scoped-vm, не стесняйтесь проверить, и я буду очень признателен за любые отзывы. Под капотом он использует упомянутый подход @Archie - он поддерживает отдельный ViewModelStore для каждой области. Но он идет на один шаг дальше и очищает сам ViewModelStore, как только последний фрагмент, который запросил viewmodel из этой области, разрушается.

Я должен сказать, что в настоящее время все управление моделью представления (и эта библиотека в частности) затронуто серьезной ошибкой с backstack, надеюсь, это будет исправлено.

Резюме:

  • Если вы заботитесь о ViewModel.onCleared() не вызывая, лучший способ (на данный момент), чтобы очистить его самостоятельно. Из-за этой ошибки у вас нет гарантии, что модель представления fragment когда-нибудь будет очищен.
  • Если вы просто беспокоитесь о утечки ViewModel - не волнуйтесь, они будут собирать мусор, как и любые другие объекты, на которые нет ссылок. Не стесняйтесь использовать мою библиотеку для детальной оценки, если это соответствует вашим потребностям.

В моем случае большинство вещей, которые я наблюдаю, связаны с Views, поэтому мне не нужно очищать его, если View уничтожается (но не Fragment).

В случае, если мне нужны такие вещи, как LiveData это приводит меня к другому Fragment (или это происходит только один раз), я создаю "потребляющего наблюдателя".

Это можно сделать, расширив MutableLiveData<T>:

fun <T> MutableLiveData<T>.observeConsuming(viewLifecycleOwner: LifecycleOwner, function: (T) -> Unit) {
    observe(viewLifecycleOwner, Observer<T> {
        function(it ?: return@Observer)
        value = null
    })
}

и как только это будет замечено, оно исчезнет из LiveData.

Теперь вы можете назвать это так:

viewModel.navigation.observeConsuming(viewLifecycleOwner) { 
    startActivity(Intent(this, LoginActivity::class.java))
}

Как было указано, невозможно очистить отдельную ViewModel из ViewModelStore с помощью API компонентов архитектуры. Одним из возможных решений этой проблемы является наличие хранилищ для каждой модели просмотра, которые можно безопасно очистить при необходимости:

class MainActivity : AppCompatActivity() {

val individualModelStores = HashMap<KClass<out ViewModel>, ViewModelStore>()

inline fun <reified VIEWMODEL : ViewModel> getSharedViewModel(): VIEWMODEL {
    val factory = object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            //Put your existing ViewModel instantiation code here,
            //e.g., dependency injection or a factory you're using
            //For the simplicity of example let's assume
            //that your ViewModel doesn't take any arguments
            return modelClass.newInstance()
        }
    }

    val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}

    val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}

inline fun <reified VIEWMODEL : ViewModel> getIndividualViewModelStore(): ViewModelStore {
    val viewModelKey = VIEWMODEL::class
    var viewModelStore = individualModelStores[viewModelKey]
    return if (viewModelStore != null) {
        viewModelStore
    } else {
        viewModelStore = ViewModelStore()
        individualModelStores[viewModelKey] = viewModelStore
        return viewModelStore
    }
}

inline fun <reified VIEWMODEL : ViewModel> clearIndividualViewModelStore() {
    val viewModelKey = VIEWMODEL::class
    individualModelStores[viewModelKey]?.clear()
    individualModelStores.remove(viewModelKey)
}

}

Использовать getSharedViewModel() чтобы получить экземпляр ViewModel, привязанный к жизненному циклу Activity:

val yourViewModel : YourViewModel = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)

Позже, когда придет время избавиться от общей ViewModel, используйте clearIndividualViewModelStore<>():

(requireActivity() as MainActivity).clearIndividualViewModelStore<YourViewModel>()

В некоторых случаях вам нужно очистить ViewModel как можно скорее, если она больше не нужна (например, если она содержит некоторые конфиденциальные данные пользователя, такие как имя пользователя или пароль). Вот способ регистрации состоянияindividualModelStores при каждом переключении фрагмента, чтобы помочь вам отслеживать общие ViewModels:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (BuildConfig.DEBUG) {
        navController.addOnDestinationChangedListener { _, _, _ ->
            if (individualModelStores.isNotEmpty()) {
                val tag = this@MainActivity.javaClass.simpleName
                Log.w(
                        tag,
                        "Don't forget to clear the shared ViewModelStores if they are not needed anymore."
                )
                Log.w(
                        tag,
                        "Currently there are ${individualModelStores.keys.size} ViewModelStores bound to ${this@MainActivity.javaClass.simpleName}:"
                )
                for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
                    Log.w(
                            tag,
                            "${index + 1}) $viewModelClass\n"
                    )
                }
            }
        }
    }
}

Я получаю данные модели представления из наблюдения. Затем очистите андроидviewmodel

      viewModel.getLiveData().observe(this, response -> {
      // get data here
      requireActivity().getViewModelStore().clear(); // clearing viewModel data
});

Если у вас есть общий доступ к ViewModel и вы хотите очистить их все, и они основаны на одном действии, добавьте следующую строку кода, и все готово.

      getActivity().getViewModelStore().clear();

Насколько я знаю, вы не можете удалить объект ViewModel вручную с помощью программы, но вы можете очистить данные, хранящиеся в нем, в этом случае вы должны вызвать Oncleared() вручную для этого:

  1. Переопределить Oncleared() метод в этом классе, который расширен от ViewModel учебный класс
  2. В этом методе вы можете очистить данные, сделав пустым поле, в котором вы храните данные.
  3. Вызовите этот метод, если хотите полностью очистить данные.

Обычно вы не очищаете ViewModel вручную, потому что он обрабатывается автоматически. Если вы чувствуете необходимость очистить вашу ViewModel вручную, вы, вероятно, делаете слишком много в этой ViewModel...

Нет ничего плохого в использовании нескольких моделей. Первый может быть ограничен областью действия, а другой - фрагментом.

Старайтесь использовать Viewmodel с областью действия только для тех вещей, которыми нужно делиться. И поместите как можно больше вещей в модель фрагмента с областью видимости. Модель представления фрагмента будет очищена при уничтожении фрагмента. Уменьшение общей памяти.

Другие вопросы по тегам