Pull to refresh

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

Reading time5 min
Views11K
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()
— Что старое решение, что новое выглядят чем-то между документированным хаком и костылями
Tags:
Hubs:
+11
Comments8

Articles

Change theme settings