Нестыдные вопросы про жизненный цикл
Каждый разработчик сталкивался с вопросами про жизненный цикл Activity: что такое bind-сервис, как сохранить состояние интерфейса при повороте экрана и чем Fragment отличается от Activity.
У нас в FunCorp (см. источник) накопился список вопросов на похожие темы, но с определёнными нюансами. Некоторыми из них я и хочу с вами поделиться.
1. Все знают, что если открыть второе активити поверх первого и повернуть экран, то цепочка вызовов жизненного цикла будет выглядеть следующим образом:
Открытие Activity
FirstActivity: onPause SecondActivity: onCreate SecondActivity: onStart SecondActivity: onResume FirstActivity: onSaveInstanceState FirstActivity: onStop
Поворот
SecondActivity: onPause SecondActivity: onSaveInstanceState SecondActivity: onStop SecondActivity: onCreate SecondActivity: onStart SecondActivity: onRestoreInstanceState SecondActivity: onResume
Возврат назад
SecondActivity: onPause FirstActivity: onCreate FirstActivity: onStart FirstActivity: onRestoreInstanceState SecondActivity: onStop
А что будет в случае, если второе активити прозрачное?
Решение
В случае с прозрачным верхним активити с точки зрения логики всё немного отличается. Именно потому, что оно прозрачное, после поворота необходимо восстановить содержимое и того активити, которое находится непосредственно под ним. Поэтому порядок вызовов будет немного отличаться:
Открытие activity
FirstActivity: onPause SecondActivity: onCreate SecondActivity: onStart SecondActivity: onResume
Поворот
SecondActivity: onPause SecondActivity: onSaveInstanceState SecondActivity: onStop SecondActivity: onCreate SecondActivity: onStart SecondActivity: onRestoreInstanceState SecondActivity: onResume FirstActivity: onSaveInstanceState FirstActivity: onStop FirstActivity: onCreate FirstActivity: onStart FirstActivity: onRestoreInstanceState FirstActivity: onResume FirstActivity: onPause
2. Ни одно приложение не обходится без динамического добавления вью, но иногда приходится перемещать одну и ту же вью между разными экранами. Можно ли один и тот же объект добавить одновременно в две разных активити? Что будет, если я создам её с контекстом Application и захочу добавлять одновременно в различные активити?
Зачем это нужно?
Существуют «не очень приятные» библиотеки, которые внутри кастомных вью держат важную бизнес-логику, и пересоздание этих вью в рамках каждого нового активити является плохим решением, т.к. хочется иметь один набор данных.
Решение
Ничего не мешает создать вью с контекстом Application. Она просто применит дефолтные стили, не относящиеся к какому-либо активити. Также без проблем можно перемещать эту вью между разными активити, но нужно следить, чтобы она была добавлена только в одного родителя.
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { ... if (child.getParent() != null) { throw new IllegalStateException("The specified child already has a parent. " + "You must call removeView() on the child's parent first."); } ... }
Можно, например, подписаться на ActivityLifecycleCallbacks, на onStop удалять (removeView) из текущего активити, на onStart добавлять в следующее открываемое (addView).
3. Фрагмент можно добавить через add и через replace. А в чём отличие между этими двумя вариантами с точки зрения порядка вызова методов жизненного цикла? В чём преимущества каждого из них?
Решение
Даже если вы добавляете фрагмент через replace, то это не значит, что он полностью заменяется. Это значит, что на этом месте в контейнере заменится его вью, следовательно, у текущего фрагмента будет вызвано onDestroyView, а при возврате назад будет снова вызван onCreateView.
Это довольно сильно меняет правила игры. Приходится детачить все контроллеры и классы, связанные с UI, именно в onDestroyView. Нужно чётко разделять получение данных, необходимых фрагменту, и заполнение вью (списков и т.д.), так как заполнение и разрушение вью будет происходить намного чаще, чем получение данных (чтение каких-то данных из БД).
Также появляются нюансы с восстановлением состояния: например, onSaveInstanceState иногда приходит после onDestroyView. К тому же стоит учитывать, что если в onViewStateRestored пришёл null, то это значит, что не нужно ничего восстанавливать, а не сбрасываться до дефолтного состояния.
Если говорить про удобства между add и replace, то replace экономнее по памяти, если у вас глубокая навигация (у нас глубина навигации юзера — один из продуктовых KPI). Также намного удобнее с replace управлять панелью инструментов, так как в onCreateView можно её переинфлейтить. Из плюсов add: меньше проблем с жизненным циклом, при возврате назад не пересоздаются вью и не нужно ничего заново заполнять.
4. Иногда всё ещё приходится работать напрямую с сервисами и даже с bind-сервисами. С одним из подобных сервисов взаимодействует активити (только одна активити). Она коннектится к сервису и передаёт в него данные. При повороте экрана наша активити разрушается, и мы обязаны отбайндится от этого сервиса. Но если нет ни одного соединения, то сервис разрушается и после поворота bind будет к совершенно другому сервису. Как сделать так, чтобы при повороте сервис оставался жить?
Решение
Если вы знаете красивое решение, то напишите в комментариях. На ум приходит только нечто подобное:
@Override protected void onDestroy() { super.onDestroy(); ThreadsUtils.postOnUiThread(new Runnable() { @Override public void run() { unbindService(mConnection); } }); }
5. Недавно мы переделали навигацию внутри нашего приложения на Single Activity (с помощью одной из доступных библиотек). Раньше каждый экран приложения был отдельным активити, сейчас навигация работает на фрагментах. Проблема возврата к активити в середине стека решалась intent-флагами. Как можно вернуться к фрагменту в середине стека?
Решение
Да, решения из коробки FragmentManager не предоставляет. Cicerone делает внутри себя нечто подобное:
protected void backTo(BackTo command) { String key = command.getScreenKey(); if (key == null) { backToRoot(); } else { int index = localStackCopy.indexOf(key); int size = localStackCopy.size(); if (index != -1) { for (int i = 1; i < size - index; i++) { localStackCopy.pop(); } fragmentManager.popBackStack(key, 0); } else { backToUnexisting(command.getScreenKey()); } } }
6. Также недавно мы избавились от такого неэффективного и сложного компонента, как ViewPager, потому что логика взаимодействия с ним очень сложна, а поведение фрагментов непрогнозируемо в определённых кейсах. В некоторых фрагментах мы использовали Inner-фрагменты. Что будет при использовании фрагментов внутри элементов RecycleView?
Решение
В общем случае не будет ничего плохого. Фрагмент без проблем добавится и будет отображаться. Единственное, с чем мы столкнулись, — это нестыковки с его жизненным циклом. Реализация на ViewPager управляет жизненным циклом фрагментов посредством setUserVisibleHint, а RecycleView делает всё в лоб, не думая про фактическую видимость и доступность фрагментов.
7. Всё по той же причине перехода с ViewPager мы столкнулись с проблемой восстановления состояния. В случае с фрагментами это реализовывалось силами фреймворка: в нужных местах мы просто переопределяли onSaveInstanceState и сохраняли в Bundle все необходимые данные. При пересоздании ViewPager все фрагменты восстанавливались силами FragmentManager и возвращали свое состояние. Что делать в случае с RecycleView и его ViewHolder?
Решение
«Надо писать всё в базу и каждый раз читать из неё», — скажете вы. Или логика сохранения состояния должна быть снаружи, а список — это просто отображение. В идеальном мире так и есть. Но в нашем случае каждый элемент списка — это сложный экран со своей логикой. Поэтому пришлось изобрести свой велосипед в стиле «сделаем такую же логику, как во ViewPager и фрагменте»:
public class RecycleViewGalleryAdapter extends RecyclerView.Adapter<GalleryItemViewHolder> implements GalleryAdapter { private static final String RV_STATE_KEY = "RV_STATE"; @Nullable private Bundle mSavedState; @Override public void onBindViewHolder(GalleryItemViewHolder holder, int position) { if (holder.isAttached()) { holder.detach(); } holder.attach(createArgs(position, getItemViewType(position))); restoreItemState(holder); } @Override public void saveState(Bundle bundle) { Bundle adapterState = new Bundle(); saveItemsState(adapterState); bundle.putBundle(RV_STATE_KEY, adapterState); } @Override public void restoreState(@Nullable Bundle bundle) { if (bundle == null) return; mSavedState = bundle.getBundle(RV_STATE_KEY); } private void restoreItemState(GalleryItemViewHolder holder) { if (mSavedState == null) { holder.restoreState(null); return; } String stateKey = String.valueOf(holder.getGalleryItemId()); Bundle state = mSavedState.getBundle(stateKey); if (state == null) { holder.restoreState(null); mSavedState = null; return; } holder.restoreState(state); mSavedState.remove(stateKey); } private void saveItemsState(Bundle outState) { GalleryItemHolder holder = getCurrentGalleryViewItem(); saveItemState(outState, (GalleryItemViewHolder) holder); } private void saveItemState(Bundle bundle, GalleryItemViewHolder holder) { Bundle itemState = new Bundle(); holder.saveState(itemState); bundle.putBundle(String.valueOf(holder.getGalleryItemId()), itemState); } }
На Fragment.onSaveInstanceState мы считываем состояние нужных нам холдеров и кладём их в Bundle. При пересоздании холдеров мы достаем сохранённый Bundle и на onBindViewHolder передаём найденные состояния внутрь холдеров:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); ViewGroup root = findViewById(R.id.default_id); ViewGroup view1 = new LinearLayout(this); view1.setId(R.id.default_id); root.addView(view1); ViewGroup view2 = new FrameLayout(this); view2.setId(R.id.default_id); view1.addView(view2); ViewGroup view3 = new RelativeLayout(this); view3.setId(R.id.default_id); view2.addView(view3); }
8. Чем нам это грозит?
Решение
На самом деле, ничего плохого в этом нет. В том же RecycleView хранятся списки из элементов с одинаковыми id. Однако всё-таки есть небольшой нюанс:
@Override protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return (T) v; } } } return null; }
Стоит быть внимательнее, если у нас в иерархии есть элементы с одинаковыми id, т.к. возвращается всегда именно первый найденный элемент, и на разных уровнях вызова findViewById это могут быть разные объекты.
9. Вы падаете с TooLargeTransaction при повороте экрана (да, здесь по-прежнему косвенно виноват наш ViewPager). Как найти виновного?
Решение
Всё довольно просто: повесить ActivityLifecycleCallbacks на Application, ловить все onActivitySaveInstanceState и парсить всё, что лежит внутри Bundle. Там же можно достать и состояние всех вью и всех фрагментов внутри этого активити. Ниже пример, как мы достаём состояние фрагментов из Bundle:
/** * Tries to find saved [FragmentState] in bundle using 'android:support:fragments' key. */ fun Bundle.getFragmentsStateList(): List<FragmentBundle>? { try { val fragmentManagerState: FragmentManagerState? = getParcelable("android:support:fragments") val active = fragmentManagerState?.mActive ?: return emptyList() return active.filter { it.mSavedFragmentState != null }.map { fragmentState -> FragmentBundle(fragmentState.mClassName, fragmentState.mSavedFragmentState) } } catch (throwable: Throwable) { Assert.fail(throwable) return null } } fun init() { application.registerActivityLifecycleCallbacks( object : SimpleActivityLifecycleCallback() { override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { super.onActivitySaveInstanceState(activity, outState) outState?.let { ThreadsUtils.runOnMainThread { trackActivitySaveState(activity, outState) } } } }) } @MainThread private fun trackActivitySaveState(activity: Activity, outState: Bundle) { val sizeInBytes = outState.getSizeInBytes() val fragmentsInfos = outState.getFragmentsStateList()?.map { mapFragmentsSaveInstanceSaveInfo(it) } ... }
Далее мы просто вычисляем размер Bundle и логируем его:
fun Bundle.getSizeInBytes(): Int { val parcel = Parcel.obtain() return try { parcel.writeValue(this) parcel.dataSize() } finally { parcel.recycle() } }
10. Предположим, у нас есть активити и набор зависимостей на нём. При определённых условиях нам нужно пересоздать набор этих зависимостей (например, по клику запустить какой-то эксперимент с другим UI). Как нам это реализовать?
Решение
Конечно, можно повозиться с флагами и сделать это каким-то «костыльным» перезапуском активити через запуск интента. Но на деле всё очень просто — у активити есть метод recreate.
Скорее всего, большая часть этих знаний вам и не пригодится, так как к каждому из них приходишь не от хорошей жизни. Однако некоторые из них хорошо демонстрируют, как человек умеет рассуждать и предлагать свои решения. Мы используем подобные вопросы на собеседованиях. Если у вас есть интересные задачи, которые вам предлагали решить на собеседованиях, или вы сами их ставите, напишите их в комментариях — интересно будет обсудить!
Источник: Нестыдные вопросы про жизненный цикл