Содержание статьи
Грабли Activity
Большинство туториалов, демонстрирующих фишки Android-разработки, начинаются одинаково: неопытным разработчикам предлагают накидать все визуальные элементы прямо в XML-разметку главного Activity. Выглядит это примерно так:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
Такая проектировка входит в привычку, и проект заполняется новыми Activity со все более сложной разметкой. Как итог — даже минимально полезное приложение обрастает стеком Activity, сжирающим всю оперативную память, а в разработчика летят камни и двойки в Google Play.
Сегодня я хочу поделиться с тобой широко известным «секретом»: Activity совсем не предназначены для массового использования. Наоборот, в приложении это штучный инструмент, который идет в ход только в крайних случаях. Повсеместная генерация новых Activity создает серьезные проблемы, которые делают работу приложения непредсказуемой. И даже если на твоем устройстве все стабильно, в мире немыслимое количество Android-устройств, на большинстве из которых твое приложение будет падать.
А все потому, что ОС Android совершенно не обещает держать твои Activity живыми. Как ты помнишь, эти компоненты существуют независимо друг от друга, обладая особым жизненным циклом. Если Activity переходит в состояние onPause
, а происходит это довольно часто, он становится котейкой Шредингера: нельзя заранее знать, будет он жив или нет.
Используя дополнительные Activity для вывода меню и других мелочей, ты подвергаешь опасности все логические связи внутри приложения. Activity собираются в стек, и ОС может начать выгружать их по одному или группой. Когда пользователь захочет вернуться в предыдущее окно, Activity может уже быть уничтожен, и юзер выпадет из приложения.
Кроме проблем с сохранением логики, есть и рутина поддержки ООП-кода: плотно завязанные на Activity интерфейсы практически невозможно развивать дальше. Масштабирование, быструю замену одних элементов на другие, анимацию — все это будет очень тяжело реализовать в новых версиях приложения.
Благодаря моде на единую экосистему все мобильные приложения стали в целом очень похожи. Стоит только нащупать тропинку да немного потренироваться, и тогда качество начинает быстро расти. Следуя принципу «критикуя — предлагай», мы сейчас напишем приложение, реализующее универсальный пользовательский интерфейс. Это будет интересно не только с академической точки зрения — уверен, написанный сегодня код ты сможешь легко встроить в свои проекты. Итак, начнем!
Fragments
Чтобы работать с UI было проще и быстрее, Google создала фрагмент (Fragment) — класс — прослойку между Activity и визуальными составляющими программы. С одной стороны, это контейнер для любых View-объектов, которые могут быть показаны пользователю. С другой — продолжение Activity, от которого Fragment получает всю информацию об изменениях в жизненном цикле.
У фрагментов, как и у Activity, есть свой (правда, более оригинальный) жизненный цикл. К примеру, работать с UI сразу после создания фрагмента невозможно, нужно ждать загрузки всех элементов — после метода onCreate
выполнится метод onCreateView
, где и можно будет загрузить элементы.
public class FragmentOne extends Fragment {
...
public View onCreateView(...) {
View view =inflater.inflate(R.layout.fragment_one, container,false);
TextView textView = (TextView)view.findViewById(R.id.fo_text);
textView.setText("Hello, i'm first fragment!");
return view;
...
В фрагменте тоже можно переопределять любые методы, отслеживающие состояние окна. Так, если приложение уйдет в фон, в Activity выполнится onPause
, а затем метод с точно таким же названием выполнится здесь. Это может быть полезно — удобно для отключения от сторонних объектов, например привязанных сервисов (bound service).
FragmentTransaction
Зная, что работа с фрагментами будет насыщенной, Google заблаговременно создала для этого специальные инструменты. Классы FragmentManager
и FragmentTransaction
аккумулируют в себе все процессы: создание новых фрагментов и их удаление, передачу данных и так далее.
Объект FragmentManager
создавать не нужно, он уже есть в каждом Activity, нужно только получить на него ссылку. А все действия будут проходить через FragmentTransaction
, который затем самостоятельно передаст данные менеджеру фрагментов.
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
Хочу заметить, что классы, работающие с фрагментами, доступны в двух вариантах. Рекомендую использовать более новую версию — это те, у которых в пути импорта присутствует строчка android.support.v4. Это большая библиотека, созданная для организации обратной совместимости. Компания Google бережно относится к устройствам на всех версиях ОС, а библиотеки позволяют использовать новшества разработки даже при работе со старым API.
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.Fragment;...
FrameLayout
Часто данные в UI приходят динамически, в зависимости от происходящих событий. Чтобы было куда поместить картинку или текст, существует специальный контейнер — FrameLayout
. В этом и есть его основная задача — зарезервировать на экране место для любого объекта класса View, который можно будет подгрузить позднее. Фрагменты тоже живут в таких контейнерах.
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/frame_container"
...
/>
Добавить новый фрагмент в FrameLayout можно по-разному: в FragmentTransaction доступны схожие по функциональности методы замены (replace) и добавления (add).
transaction.replace(R.id.frame_container, fragmentObject);
Несмотря на внешнюю схожесть, нужно хорошо понимать, какого результата ты можешь добиться. Метод replace работает очень просто — добавит фрагмент и, если в контейнере раньше что-то было, удалит старые фрагменты.
transaction.add(R.id.frame_container, fragment);
Метод add
же создает стек из фрагментов, и каждый новый вызов помещает на верх стека новый экземпляр. Важно, что при появлении нового фрагмента старые объекты в стеке не получают никаких оповещений и работают так, как будто бы ничего не произошло. Это значит, что метод onPause для них не выполнится и они продолжат расходовать ресурсы устройства, хотя пользователь перестанет их видеть.
Такая растрата выделенной памяти бывает оправданна, например если хочется организовать предобработку данных: фрагмент может что-нибудь подгрузить, пока будет незаметен, а потом показать уже готовый результат.
Действий с фрагментами может быть несколько, они будут аккумулироваться до тех пор, пока не выполнится метод commit.
transaction.commit();
Смена фрагментов
Довольно часто нужно понять, какой именно фрагмент сейчас был показан пользователю. Самый очевидный способ — завести в Activity переменную, запоминающую номер фрагмента, выведенного на экран. Но с таким подходом фрагменты становятся жестко привязаны к экземпляру Activity, и, как только он будет уничтожен ОС, счетчик фрагментов будет утерян.
Метод onDestroy
, выгружающий Activity из памяти, выполняется не только когда приложение завершает работу, но и при перевороте экрана. Горячие головы на Stack Overflow рекомендуют на первый взгляд удобное решение этой проблемы — просто запретить ОС пересоздавать объект при повороте. Для этого нужно всего лишь подправить в манифесте описание Activity.
<activity
android:configChanges="keyboardHidden|orientation"
android:name=".MainActivity"
android:label="@string/app_name"
></activity>
Но этот подход работает ровно до тех пор, пока в приложении нет отдельного дизайна для портретной и альбомной ориентаций экрана: новый XML-файл подгрузится только вместе с созданием нового экземпляра Activity, в методе onCreate. Конечно, можно придумать костыли и на этот случай, но есть вариант проще.
Фрагментам можно назначать уникальные теги, по которым возможно вычислить тот, что сейчас видит пользователь. Теги задаются в тех же методах replace и add дополнительным параметром.
transaction.replace(R.id.frame_container, fragment, "Fragment_tag");
...
MyFragment fragment = (MyFragment)getSupportFragmentManager()
.findFragmentByTag(FRAGMENT_TAG);
Это удобно, поскольку в полноценном UI будет несколько фрагментов, и в зависимости от ситуации их нужно будет чередовать на экране.
if (fragmentA !=null) {
fragmentB = new FragmentTwo();
fragmentB.setArguments(bundle);
initFragment(null, fragmentB, FRAGMENT_B);
} else {
initFragment(null, new FragmentA(), FRAGMENT_A );
}
Portrait & Landscape
Раз уж всплыла тема особого UI для альбомной ориентации, посмотрим, как это реализовать с помощью фрагментов. Напомню, что файлы XML-разметки для разных типов экрана должны иметь одинаковое имя, но располагаться в папках с особыми именами. В частности, альбомную верстку нужно поместить в папку landscape-land.
Поскольку пространства стало больше, можно поместить рядом сразу несколько фрагментов. Это будет еще один, третий фрагмент, явно указанный в XML-коде.
<fragment android:name="com.localhost.grok.fragments.FragmentLandscape"
android:id="@+id/fragment3"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="@dimen/fab_margin"
/>
Он появится только при повороте экрана, но будет работать точно так же, как и любой другой, загруженный через контейнер FrameLayout.
Анимация
В мире Material Design важное место занимают плавные переходы между элементами UI. Если построить дизайн на основе фрагментов, организовать анимацию будет просто.
transaction.setCustomAnimations(android.R.anim.fade_in,
android.R.anim.fade_out);
В данном случае появление фрагмента будет сопровождаться эффектом постепенного проявления (fade_in). Если на экране будет какой-то другой фрагмент, то он так же плавно исчезнет (fade_out).
В Android уже есть заготовленные сценарии анимации, которых тебе наверняка хватит. Но можно создать и свой вариант, описав движения в привычном формате XML. Выглядеть это будет примерно так:
<set xmlns:android="...">
<alpha
android:fromAlpha="0.0" android:toAlpha="1.0"
android:duration="@android:integer/config_mediumAnimTime"
/>
</set>
«Оживлять» фрагменты намного проще, чем целые Activity. Запуск нового окна — трудоемкий процесс, который не так легко анимировать. К примеру, для новых экранов fade-эффекты практически не используются, так как они будут очень плохо выглядеть на слабых устройствах. А меняя один фрагмент другим, ты перерисовываешь только часть экрана, и, значит, ресурсов тратится меньше.
RetainedFragment
Ты уже знаешь, что многопоточная разработка требует повышенного внимания, иначе можно потерять все вычисления и осложнить жизнь сборщику мусора. Мы уже раньше разбирали, насколько опасным может быть пересоздание Activity, в случае с фрагментами ситуация та же самая: нужно сохранять связь между новыми компонентами и дополнительными потоками.
Для фрагментов сохранять связи легче — и это еще один повод отказаться от Activity. Достаточно только при создании фрагмента вызвать метод setRetainInstance.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
Теперь при повороте экрана ОС «сохранит» фрагмент, и все дополнительные потоки вернут результат именно в тот фрагмент, который видит пользователь. Кстати, благодаря реактивному программированию код стал гораздо проще.
Observable.just(genText()).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s->textView.setText(s));
Но вообще, это не совсем сохранение: в оригинале это называется Retain (помнить, удерживать), и смысл происходящего отличается от привычного Save.
Хотя RetainedFragment
и сохранит идентификаторы того фрагмента, который был до поворота экрана, его содержимое, скорее всего, будет потеряно. Данные, отображаемые во View-элементах, будут утрачены, поэтому о них нужно позаботиться самостоятельно.
Конечно, в некоторых случаях все не так печально. К объектам из базовых классов можно добавить специальный параметр:
android:freezesText="true"
Тогда, к примеру, EditText и TextView сохранят свои данные самостоятельно. Но это точно не сработает с самописными View-объектами, а может не сработать и с базовыми. К счастью, у нас есть печеньки и сериализация!
Bundle
Уверен, ты уже не раз встречал этот класс как удобный способ сериализации (сохранения в мире ООП) данных. Чтобы было что восстанавливать, нужно данные сначала сохранить — для этого в фрагментах доступен специальный метод, вызываемый перед выгрузкой фрагмента из памяти.
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("text", textView.getText().toString());
}
Как ты видишь, здесь система уже не только что-то сохраняет для себя, но и позволяет программисту добавить данные. При создании фрагмента они будут доступны в методе onViewStateRestored
или уже использованном нами onCreateView
.
public View onCreateView(... Bundle savedInstanceState) {
...
if (savedInstanceState!=null) {
textView.setText(savedInstanceState.getString("text", "Fragment B"));
}
...
Естественно, фрагменты можно снабжать и стартовым набором параметров, которые потом будут доступны точно таким же способом.
Bundle bundle new Bundle();
bundle.putString("url", "your_url");
myFragment.setArguments(bundle);
Взаимодействие
Приложение должно уметь перекидывать данные из компонента в компонент, и с этим у фрагментов все хорошо. Передавать данные между Activity и фрагментом возможно в любой момент времени, в том числе и во время их активной работы. Делается это по всем канонам ООП — сначала получить ссылку на объект, а затем спокойно вызывать его методы.
Поскольку Activity самостоятельно создает фрагмент, для доступа к его методам этого достаточно. Для обратной связи фрагменту нужно как-то узнать, какой именно Activity сейчас работает, — для таких целей создан метод onAttach
.
public void onAttach(Context context) {
super.onAttach(context);
activity = (MainActivity) context;
...
public void onDetach() {
super.onDetach();
activity=null;
}
Этот метод вызывается ОС при создании фрагмента и позволяет получить контекст приложения. Контекст дает возможность не только пользоваться всеми ресурсами приложения, но и добраться до сгенерировавшего вызов Activity. Получая доступ к контексту или Activity, нужно быть очень внимательным и удалять ссылки, когда эти объекты больше не нужны: если есть хотя бы одна ссылка, сборщик мусора оставит объект в памяти.
Опять же, в целях экономии Google просит разработчиков не связывать фрагменты между собой напрямую, а использовать Activity в качестве посредника. В противном случае велик риск запутаться в уничтоженных и еще живых элементах UI и перерасходовать выделенную устройством память.
Outro
Уверен, после этой статьи ты станешь с большим пиететом относиться к базовым компонентам ОС и реже их использовать. Фрагменты позволяют создавать удобный и гибкий UI, так что дизайнеры будут рады, если те станут основой интерфейсов. А в качестве примера для подражания всегда помни про приложение Google Play — во время его работы генерируется всего один Activity!
Обязательно скачай с нашего сайта исходники разобранного примера — полноценный листинг читается и запоминается легче. Если остались какие-то вопросы или предложения с замечаниями :), пиши мне. Удачи!