Retain внутри, а снаружи ViewModel

    image

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

    Внимание


    TL;DR если жалко времени — мотайте вниз до вывода, мало что потеряете.

    Итак первое, на что можно обратить внимание — есть 2 разных пакета архитектурных компонентов с ViewModel, а именно:

    1) Старенький android.arch.lifecycle
    2) Новый androidx.lifecycle

    Спойлер: особой разницы между ними нет.

    Вся работа кроется за вызовом:

    ViewModelProviders.of(activity).get(MyViewModel::class.java)

    Начнем с метода of

        public static ViewModelProvider of(@NonNull FragmentActivity activity) {
            return of(activity, null);
        }
    	
        public static ViewModelProvider of(@NonNull FragmentActivity activity,
                @Nullable Factory factory) {
            Application application = checkApplication(activity);
            if (factory == null) {
                factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
            }
            return new ViewModelProvider(ViewModelStores.of(activity), factory);
        }

    checkApplication просто проверяет на null, а AndroidViewModelFactory является просто потоконебезопасным синглтоном который хранит у себя Application. Так что особого интереса они не представляют, самое интересное в методе ViewModelStores.of:

        public static ViewModelStore of(@NonNull FragmentActivity activity) {
            if (activity instanceof ViewModelStoreOwner) {
                return ((ViewModelStoreOwner) activity).getViewModelStore();
            }
            return holderFragmentFor(activity).getViewModelStore();
        }

    На первый взгляд выглядит довольно странно — зачем вообще проверка на наличие интерфейса ViewModelStoreOwner у FragmentActivity если он и так его имплементит? — Так было не всегда — до далекого февраля 2018 года, когда вышла версия Support library 27.1.0, FragmentActivity ни разу не имплементил ViewModelStoreOwner. При этом ViewModel вполне себе работала.

    Так что начнем со старого кейса — запускался метод holderFragmentFor:

        public static HolderFragment holderFragmentFor(FragmentActivity activity) {
            return sHolderFragmentManager.holderFragmentFor(activity);
        }

    Далее просто доставался или создавался новый holder фрагмент:

        HolderFragment holderFragmentFor(FragmentActivity activity) {
            FragmentManager fm = activity.getSupportFragmentManager();
            HolderFragment holder = findHolderFragment(fm);
            if (holder != null) {
                return holder;
            }
            holder = mNotCommittedActivityHolders.get(activity);
            if (holder != null) {
                return holder;
            }
    
            if (!mActivityCallbacksIsAdded) {
                mActivityCallbacksIsAdded = true;
                activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
            }
            holder = createHolderFragment(fm);
            mNotCommittedActivityHolders.put(activity, holder);
            return holder;
        }	

    Ну а сам HolderFragment конечно же retained

        public HolderFragment() {
            setRetainInstance(true);
        }

    Собственно в нем и хранится объект ViewModelStorе, который в свою очередь держит в себе пачку 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);
    		}
    	
    		public final void clear() {
    			for (ViewModel vm : mMap.values()) {
    				vm.onCleared();
    			}
    			mMap.clear();
    		}
    	}

    Возратимся назад к случаю, когда версия Support library 27.1.0 и выше. FragmentActivity уже реализует интерфейс ViewModelStoreOwner, то есть имплементит единственный метод getViewModelStore:

        public ViewModelStore getViewModelStore() {
            if (this.getApplication() == null) {
                throw new IllegalStateException("Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.");
            } else {
                if (this.mViewModelStore == null) {
                    FragmentActivity.NonConfigurationInstances nc = (FragmentActivity.NonConfigurationInstances)this.getLastNonConfigurationInstance();
                    if (nc != null) {
                        this.mViewModelStore = nc.viewModelStore;
                    }
    
                    if (this.mViewModelStore == null) {
                        this.mViewModelStore = new ViewModelStore();
                    }
                }
    
                return this.mViewModelStore;
            }
        }

    Здесь я немного упрощу — NonConfigurationInstances это объект с тем, что не должно зависеть от конфигурации (очевидно из названия), который лежит в Activity и проносится внутри ActivityClientRecord через ActivityThread во время пересоздания между onStop и onDestroy

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

    В Activity всегда был интересный метод onRetainNonConfigurationInstance. В классе Activity он по сути ничего не делал. Вообще:

        public Object onRetainNonConfigurationInstance() {
            return null;
        }

    Описание в документации при этом многобещающее:
    Called by the system, as part of destroying an activity due to a configuration change, when it is known that a new instance will immediately be created for the new configuration. You can return any object you like here, including the activity instance itself, which can later be retrieved by calling getLastNonConfigurationInstance() in the new activity instance.

    image

    То есть что туда не сунь — вылезет в getLastNonConfigurationInstance() после пересоздания Activity. Этим разработчики архитектурных компонентов и воспользовались. Из минусов — не работает до 4 андроида, там придется по старинке через retain фрагмент.

    Метод clear() у ViewModel вызывался крайне просто — в методе onDestroy FragmentActivity.

        protected void onDestroy() {
            super.onDestroy();
            if (this.mViewModelStore != null && !this.isChangingConfigurations()) {
                this.mViewModelStore.clear();
            }
    
            this.mFragments.dispatchDestroy();
        }
    

    На самом деле с Androidx почти все то же самое, разница лишь в том, что метод getViewModelStore() уже не во FragmentActivity, а в — ComponentActivity, от которого FragmentActivity наследуется в AndroidX. Изменился только вызов метода clear(), его вынесли из onDestroy в самостоятельный коллбэк, который создается в конструкторе ComponentActivity:

            getLifecycle().addObserver(new GenericLifecycleObserver() {
                @Override
                public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
                    if (event == Lifecycle.Event.ON_DESTROY) {
                        if (!isChangingConfigurations()) {
                            getViewModelStore().clear();
                        }
                    }
                }
            });

    Для протокола — во время создания статьи использовались:

    Support library 27.0.0, 28.0.0
    androidx.lifecycle:lifecycle-viewmodel:2.0.0
    androidx.lifecycle:lifecycle-extensions:2.0.0
    android.arch.lifecycle:extensions:1.1.1
    android.arch.lifecycle:viewmodel:1.1.1

    Выводы:


    — ViewModel действительно выживала пересоздание activity в retain фрагменте до Support library 27.1.0 появившейся в феврале 2018
    — C версии Support library 27.1.0 и дальше, а также в AndroidX ViewModel отправилась пережидать пересоздание Activity в FragmentActivity.NonConfigurationInstances (ComponentActivity.NonConfigurationInstances для AndroidX), по факту тем же механизмом, через который работают retain фрагменты, но создание лишнего фрагмента не требуется, все ViewModel отправляются «рядом» с retain фрагментами.
    — Механизм работы ViewModel почти не отличается в AndroidX и Support library
    — Если вам вдруг внезапно потребуется (да даже представить не могу зачем) протащить данные, которые должны жить пока живет Activity но при этом учитывать пересоздание — можно воспользоваться связкой onRetainNonConfigurationInstance()/getLastNonConfigurationInstance()
    — Что старое решение, что новое выглядят чем-то между документированным хаком и костылями
    • +11
    • 2,6k
    • 8
    Поделиться публикацией

    Похожие публикации

    Комментарии 8

      0
      Спасибо. А нет в запасе подобного разбора но в случае с ViewModelProviders.of(f: Fragment)? Сейчас посмотрел мельком — логика там какая то более запутанная и с наскока лично я ее не осилил, нужно будет отдельно поразбираться.
        0
        Не, думал о фрагментах написать, но на самом деле логика там абсолютно та же.
          0
          Надо значит перечитать сорцы, что то первая, быстрая попытка разобраться, не взлетела.
        0
        Из минусов — не работает до 4 андроида, там придется по старинке через retain фрагмент.

        getLastNonConfigurationInstance доступен с первой версии API

          0
          Верно, но есть нюанс — в документации есть такое:
          If you are targeting Build.VERSION_CODES.HONEYCOMB or later, consider instead using a Fragment with Fragment.setRetainInstance(boolean)

          И такое:
          This function is called purely as an optimization, and you must not rely on it being called.
            0
            Ну тут они советуют использовать родные ретейн фрагменты, которые появились начиная с HONEYCOMB (версия 3.0-3.2). Но если посмотреть исходники внимательно, то можно увидеть, что родные и саппорт фрагменты хранятся и передаются через NonConfigurationInstances, аналогично и лоадеры.
            А вот второе замечание интересное, что onRetainNonConfigurationInstance может в каки-то случаях не вызваться. Но что-то мне подсказывает на практике это никогда не случается. Иначе на ретейн фрагменты тоже нельзя полагаться.
              0
              Это они так пушили использовать фрагменты, так как они появились в 3 версии Android. Подобный ход они делают и сейчас, говоря что бы использовали ViewModel.
            0
            Спасибо за статью. Очень вовремя, потому что собирался искать почему MVVM от Google не такое золотое решение. А тут старый хороший ритейн задействован. Теперь задача сделать это удобное для использования без допольнительной библиотеки, поскольку LiveData это простой BehaviorSubject из Rx.
            И все же присоединяюсь к комментарью о Фрагментах. Каким именно способом можно это сделать там? Ведь создание мостов с Фрагментами которые будут отвечать за получение ViewModel из Фрагментов не очень удобно.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое