Создать хороший UI сложно, особенно если у тебя еще не так много опыта в этой области. Поэтому вот тебе быстрый тест на знание вопроса: если ты привык, что для нового окна обязательно нужен Activity или вместо плавной анимации в свеженаписанной программе почему-то происходят конвульсии, — эта статья для тебя 🙂
 

Грабли 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 получает всю информацию об изменениях в жизненном цикле.

Рис. 1. Жизненный цикл фрагмента (с) Google
Рис. 1. Жизненный цикл фрагмента (с) Google

У фрагментов, как и у 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();
Рис. 2. Сменяемые фрагменты
Рис. 2. Сменяемые фрагменты
 

Смена фрагментов

Довольно часто нужно понять, какой именно фрагмент сейчас был показан пользователю. Самый очевидный способ — завести в 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.

Рис. 3. Дополняем UI третьим фрагментом
Рис. 3. Дополняем UI третьим фрагментом
 

Анимация

В мире 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!

Обязательно скачай с нашего сайта исходники разобранного примера — полноценный листинг читается и запоминается легче. Если остались какие-то вопросы или предложения с замечаниями :), пиши мне. Удачи!

11 комментарий

  1. Аватар

    alexmaddys

    21.12.2016 в 17:52

    а где скачать исходники разобранного примера?

  2. Аватар

    baragoz

    22.12.2016 в 10:11

    А вот при Джобсе в айфоне с плавностью проблем не было, а андроидофилы до сих пор напрягаются(

  3. Аватар

    craigslist4

    23.12.2016 в 06:09

    Q1: Скажем, у меня бизнесс апп из 10 «страниц», включая логин/регистрацию. Мне тогда делать один активити и 10 фрагментов, или есть какой-то критерий, когда всё-таки нужен «свой» активити, помимо главного?

    Q2: Где же можно скачать обещанные исходники примера?

    • Аватар

      Groff

      11.01.2017 в 12:07

      Ну вообще не сильно имеет смысл вообще отказываться от Activity. Нужно смотреть на сложность дизайнов экрана и логики. Если вы используете ViewPager с Fragments внутри фрагмента, то Вам, нужно использовать getChildFragmentManager() и тогда при смерте нашего фрагмента могут умереть и его дочерние фрагменты.

      Если у вас флоу регистрации и логинизации то советовал бы вам разбить его на фрагменты и поместить его в пределах одного активити, это максимально логично. Отказываться от Activity ради того что бы сделать всё на фрагментах не лучшая практика. Попробуйте проанализировать и разделить все фрагменты на «логические модули» и уже для каждого такого модуля создавать своё Activity.

      Ваша программа должна быть спроектирована так что бы ваша Активити ничего не знала про ваш Преимущество фрагментов в том что если у вас есть экран регистрации и допустим экран редактирования профиля и они одинаковы, вы можете создать один фрагмент для этого скрина и переиспользовать его в разных активити просто передав Bundle с нужно информацией в newInstance()

  4. Аватар

    Андрей Пахомов

    23.12.2016 в 13:58

    Всем привет!
    Исходники почему-то не залились, скоро исправим 🙂
    пока их можно тут взять
    http://www.filedropper.com/fragmentstar

    комментарий по вопросу:
    Нужно смотреть, сколько у приложения «точек входа», т.е. сколько есть вариантов запуска твоего приложения. Это может быть клик по иконке, интент или еще что-то. На каждую точку входа в приложение есть смысл выделять отдельное активити, так как отлавливать причину запуска активити не очень удобно.
    А если оно будет запускаться только по клику на иконку, то можно и одним активити обойтись.

  5. Аватар

    CookieWithJam

    04.02.2017 в 08:48

    Отличная статья (собственно и все предыдущие тоже:))! Спасибо за Ваш труд!

  6. Аватар

    aj_al_vas

    25.04.2019 в 12:28

    Статья хорошая, но обещанного примера который «читается и запоминается легче» так и нет.

  7. Аватар

    user357x

    23.06.2020 в 20:24

    Где скачать исходники?

Оставить мнение