Хабрахабр

Нестыдные вопросы про жизненный цикл

Некоторыми из них я и хочу с вами поделиться. Каждый разработчик сталкивался с вопросами про жизненный цикл 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) ... }

Можно, например, подписаться на ActivityLifecycleCallbacks, на onStop удалять (removeView) из текущего активити, на onStart добавлять в следующее открываемое (addView).

А в чём отличие между этими двумя вариантами с точки зрения порядка вызова методов жизненного цикла? 3. Фрагмент можно добавить через add и через replace. В чём преимущества каждого из них?

Решение

Это значит, что на этом месте в контейнере заменится его вью, следовательно, у текущего фрагмента будет вызвано onDestroyView, а при возврате назад будет снова вызван onCreateView. Даже если вы добавляете фрагмент через replace, то это не значит, что он полностью заменяется.

Приходится детачить все контроллеры и классы, связанные с UI именно в onDestroyView. Это довольно сильно меняет правила игры. Нужно чётко разделять получение данных, необходимых фрагменту, и заполнение вью (списков и т.д.), так как заполнение и разрушение вью будет происходить намного чаще, чем получение данных (чтение каких-то данных из БД).

К тому же стоит учитывать, что если в onViewStateRestored пришёл null, то это значит, что не нужно ничего восстанавливать, а не сбрасываться до дефолтного состояния. Также появляются нюансы с восстановлениям состояния: например, onSaveInstanceState иногда приходит после onDestroyView.

Также намного удобнее с replace управлять панелью инструментов, так как в onCreateView можно её переинфлейтить. Если говорить про удобства между add и replace, то replace экономнее по памяти, если у вас глубокая навигация (у нас глубина навигации юзера — один из продуктовых KPI). Из плюсов add: меньше проблем с жизненным циклом, при возврате назад не пересоздаются вью и не нужно ничего заново заполнять.

С одним из подобных сервисов взаимодействует активити (только один активити). 4. Иногда всё ещё приходится работать напрямую с сервисами и даже с bind-сервисами. При повороте экрана наш активити разрушается, и мы обязаны отбайндится от этого сервиса. Он коннектится к сервису и передаёт в него данные. Как сделать так, чтобы при повороте сервис оставался жить? Но если нет ни одного соединения, то сервис разрушается, и после поворота bind будет к совершенно другому сервису.

Решение

На ум приходит только нечто подобное: Если вы знаете красивое решение, то напишите в комментариях.

@Override protected void onDestroy() { super.onDestroy(); ThreadsUtils.postOnUiThread(new Runnable() { @Override public void run() { unbindService(mConnection); } }); }

Раньше каждый экран приложения был отдельным активити, сейчас навигация работает на фрагментах. 5. Недавно мы переделали навигацию внутри нашего приложения на Single Activity (с помощью одной из доступных библиотек). Как можно вернуться к фрагменту в середине стека? Проблема возврата к активити в середине стека решалась intent-флагами.

Решение

Cicerone делает внутри себя нечто подобное: Да, решения из коробки FragmentManager не предоставляет.

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

В некоторых фрагментах мы использовали Inner-фрагменты. 6. Также недавно мы избавились от такого неэффективного и сложного компонента, как ViewPager, потому что логика взаимодействия с ним очень сложна, а поведение фрагментов непрогнозируемо в определённых кейсах. Что будет при использовании фрагментов внутри элементов RecycleView?

Решение

Фрагмент без проблем добавится и будет отображаться. В общем случае не будет ничего плохого. Реализация на ViewPager управляет жизненным циклом фрагментов посредством setUserVisibleHint, а RecycleView делает всё в лоб, не думая про фактическую видимость и доступность фрагментов. Единственное, с чем мы столкнулись, — это нестыковки с его жизненным циклом.

В случае с фрагментами это реализовывалось силами фреймворка: в нужных местах мы просто переопределяли onSaveInstanceState и сохраняли в Bundle все необходимые данные. 7. Всё по той же причине перехода с ViewPager мы столкнулись с проблемой восстановления состояния. Что делать в случае с RecycleView и его ViewHolder? При пересоздании ViewPager все фрагменты восстанавливались силами FragmentManager и возвращали свое состояние.

Решение

Или логика сохранения состояния должна быть снаружи, а список — это просто отображение. «Надо писать всё в базу и каждый раз читать из неё», — скажете вы. Но в нашем случае каждый элемент списка — это сложный экран со своей логикой. В идеальном мире так и есть. Поэтому пришлось изобрести свой велосипед в стиле «сделаем такую же логику, как во 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); }
}

При пересоздании холдеров мы достаем сохранённый Bundle и на onBindViewHolder передаём найденные состояния внутрь холдеров: На Fragment.onSaveInstanceState мы считываем состояние нужных нам холдеров и кладём их в Bundle.

8. Чем нам это грозит?

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

Решение

В том же 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; }

возвращается всегда именно первый найденный элемент, и на разных уровнях вызова findViewById это могут быть разные объекты. Стоит быть внимательнее, если у нас в иерархии есть элементы с одинаковыми id, т.к.

Как найти виновного? 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() } }

При определённых условиях нам нужно пересоздать набор этих зависимостей (например, по клику запустить какой-то эксперимент с другим UI). 10. Предположим, у нас есть активити и набор зависимостей на нём. Как нам это реализовать?

Решение

Но на деле всё очень просто — у активити есть метод recreate. Конечно, можно повозиться с флагами и сделать это каким-то «костыльным» перезапуском активити через запуск интента.

Однако некоторые из них хорошо демонстрируют, как человек умеет рассуждать и предлагать свои решения. Скорее всего, большая часть этих знаний вам и не пригодится, так как к каждому из них приходишь не от хорошей жизни. Если у вас есть интересные задачи, которые вам предлагали решить на собеседованиях, или вы сами их ставите, напишите их в комментариях — интересно будет обсудить! Мы используем подобные вопросы на собеседованиях.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть