Проблема
С выходом android 3.0 (Api Level 11) в android появились фрагменты, и так уж у разработчиков google получилось, что для них поддерживается только один backstack. Не всегда дизайнеры и заказчики хотят учитывать эту особенность. А иногда просто хотят полную копию уже существующего ios приложения.
Допустим нам нужно сделать подобие Tab Bar на android, в том числе с сохранением истории в каждой вкладке. Но у нас один backstack, и что же нам делать? Задача кажется невозможной.
Исследование
«Если не спросить, никогда не узнаешь. Если знаешь, нужно лишь спросить.»
С одной стороны это противоречит официальному guide (смотри Behavior), в котором однозначно написано, что навигация через низ должна сбрасывать состояние.
Но кого это волнует, когда речь идёт об удобстве пользователя? Положа руку на сердце вы признаете, что так, как рекомендуется — удобнее?
И как же всё-таки сделать, что казалось бы невозможно из-за ограничения платформы? На ум приходят несколько вариантов один сложнее другого, а поиск в google выдаёт и того больше ужасных костыльных решений.
Но зачем гадать, если можно подсмотреть как сделано у Instagram? К слову сказать, Instagram так работал не всегда, были времена, когда дизайн Instagram был с вкладками, а история при переключении сбрасывалась.
Декомпилируем apk Instagram при помощи apktool и смотрим что там. Главное activity приложения — com.instagram.android.activity.MainTabActivity, смотрим от чего она унаследована, — от класса com/instagram/base/activity/tabactivity/a, который в свою очередь унаследован от android/app/ActivityGroup. Дальше можно не копать.
Такие классы, как ActivityGroup, TabActivity, LocalActivityManager — deprecated с 13 Api Level, тоесть почти сразу, как появились фрагменты. На developer.android.com для этих классов написано следующее:
This class was deprecated in API level 13.
Use the new Fragment and FragmentManager APIs instead; these are also available on older platforms through the Android compatibility package.
Все знают, что использовать deprecated в новой разработке нехорошо. Все бросились писать на фрагментах и классы были преданы забвению.
Решение?
Пожалуй, это единственное рабочее решение. Оно работает «из коробки», никаких костылей (deprecated ведь не считается). Лично я просто забыл про LocalActivityManager, хотя начал разрабатывать под android ещё в те времена, когда телефонов с 8 Api Level было больше, чем остальных, но они активно вытеснялись.
Везде настолько упорно утверждают, что фрагменты наше всё, а тенденция разработки single activity application столь непреложна, что те, кто присоединился к разработке на android после 2011, скорее всего просто ничего и не слышали о LocalActivityManager.
Это простое решение, глупо им не воспользоваться. В каждой вкладке у нас будет своя activity со своим жизненным циклом, а главное своим backstack'ом!
Немного кода
Использовать TabHost просто. Если знать, что искать, можно найти много древних туториалов, как им пользоваться. Интернет помнит.
Layout для нашего главного activity:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TabHost
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/bottom_bar"
android:layout_below="@+id/top">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TabWidget
android:id="@android:id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"></TabWidget>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
</LinearLayout>
</TabHost>
</FrameLayout>
Собственно код:
//Можно было бы унаследоваться от TabActivity (который тоже deprecated),
//Но зачем нам два deprecated класса, если можно обойтись одним?
public class MainActivity extends android.app.ActivityGroup {
TabHost mTabHost;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setup(getLocalActivityManager());
TabHost.TabSpec tabSpec;
tabSpec = mTabHost.newTabSpec("tag1");
tabSpec.setIndicator("Вкладка 1");//use getString
//TabActivity должна быть в AndroidManifest
tabSpec.setContent(new Intent(this, TabActivity.class));
mTabHost.addTab(tabSpec);
tabSpec = mTabHost.newTabSpec("tag2");
tabSpec.setIndicator("Вкладка 2");//use getString
//TabActivity должна быть в AndroidManifest
tabSpec.setContent(new Intent(this, TabActivity.class));
mTabHost.addTab(tabSpec);
}
}
P.S.
Очень жаль, что приходится использовать deprecated классы, но пока google не сделает другого решения — это единственный адекватный вариант. Можно построить удобную навигацию с одним backstack'ом, есть другие разумные ограничения платформы, которые необходимо учитывать и которые обоснованны, но в данном случае кажется, что google попросту упустил такую возможность при проектировании Fragment API.
Так уж получается, что в этом моменте android оказался уж точно не круче iphone…
Комментарии (13)
xkor
26.12.2016 12:27А вы про вложенные фрагменты не слышали? Делаете для каждого таба корневой фрагмент и все под экраны этого таба складируется как "подфрагменты" этого фрагмента, соответственно их бэкстек хранится во фрагмент менеджере коневого фрагмента и сохраняется вместе с состоянием корневого фрагмента. Надеюсь как сохранять состояние фрагментов при переключении табов вы в курсе. Собственно это вроде уже довольно стандартное решение вашей задачи.
Есть правда мнение что вложенные фрагменты это нереальное зло и приводит к невообразимому числу подводных камней, костылей и багов. Но лично у мня за два года их использования проблемы с ними возникали не часто и каких то критических или нерешаемых проблем не было. Хотя бывает что с ними приходится хорошенько подумать и покурить сорци андройда для лучшего понимания их жизненного цикла. Но в андройде так имхо почти везде...Alexey_Bespaly
26.12.2016 12:45Есть ссылка на реальное, работающее приложение, где реализовано поведение через вложенные фрагменты, где фрагменты отображают что-то чуть сложнее просто текста?
xkor
26.12.2016 13:00+1Есть даже пяток:
https://play.google.com/store/apps/details?id=com.touchin.redmond
https://play.google.com/store/apps/details?id=com.riotrus.guilds
https://play.google.com/store/apps/details?id=com.nascar.nascarmobile
https://play.google.com/store/apps/details?id=com.vzw.indycar
https://play.google.com/store/apps/details?id=com.dfbpokal.mobile
В первых двух правда уже не очень помню на сколько повсеместно используются вложенные фрагменты, возможно всего на паре экранов, но вот на следующих двух вложенные фрагменты просто таки по всюду, и даже там где и не нужно.., но уж в таком виде нам оно нам досталось на допиливание.Alexey_Bespaly
26.12.2016 13:05То, что вложенные фрагменты работают — я не сомневаюсь, но хорошо они работают до тех пор, пока не попадают в backstack. В каком из 5 приложений искать такой случай?
xkor
26.12.2016 13:20Ну если вы про случай описанный в статье. То есть с переключением между несколькими бэк стеками, то боюсь такого кейса нет ни в одном из приложений. Ну а просто вложенные фрагменты внутри какого либо экрана который может попасть в бэк стек точно есть в 4м (com.vzw.indycar), какие то конкретные экраны правда боюсь назвать так сходу не смогу.
Alexey_Bespaly
26.12.2016 14:00Надеюсь как сохранять состояние фрагментов при переключении табов вы в курсе.
Расскажите мне.
Допустим мы имеем 2 таба. Мы переключились из первого таба (в котором у нас RecyclerView в котором в свою очередь ViewPager) во второй. Как нам сохранить состояние первого фрагмента не держа его в памяти?
saveFragmentInstanceState не предлагать, — всё-равно не работает.xkor
26.12.2016 23:59saveFragmentInstanceState вполне себе работает, пользовался им несколько раз, никаких багов не наблюдал, правда последний раз это было с год назад. Проблемы скорее могут возникнуть с сохранением состояния RecyclerView и особенно ViewPager, точнее с восстановлением этих состояний. На сколько помню у обоих этих контролов позиция нормально восстанавливается только если восстанавливать её не сразу в onCreate/onViewCreated или onViewStateRestored а с небольшой задержкой через метод post. А учитывая что ViewPager в ячейке RecyclerView, надо его состояние через адаптер прокидывать и уже там в bindViewHolder восстанавливать, опять же возможно с отложенным через post вызовом setCurrentItem. Но это всё к вложенным фрагментам отношения не имеет, без вложенностей и даже вообще без фрагментов придется городить всё те же костыли(
Можно ещё заюзать FragmentStatePagerAdapter для табов что бы самому не писать сохранение/восстановление состояний, но он работает через тот же saveFragmentInstanceState, так что это уже просто вопрос удобства.Alexey_Bespaly
27.12.2016 10:22+1RecyclerView и ViewPager прекрасно возвращаются из бекстека ровно в том состоянии, в котором и были туда положены.
и даже вообще без фрагментов придется городить всё те же костыли
А вообще без фрагментов это как?) При возврате по стеку activity уж точно восстанавливается предыдущее состояние в прежнем виде без всяких костылей.
klim76
Прошу прощения за «нубский»вопрос, но все же: Чем грозит использование «устаревших» методов в приложении?
Alexey_Bespaly
Не гарантируется работа, остановоенна поддержка. Выйдет android 8 и окажется, что на нем это не работает.
Netscape
Начиная с android 7 вложенные активити в TabActivity не получают onConfigurationChanged() при повороте, например.