Рассказ о том, как в Android'е передать информацию из фрагмента (Fragment) в активность (Activity). Информация будет полезной для новичков (джуниоров), осваивающих программирование для Android, и вряд ли будет интересной для миддлов и сеньоров.

Запускаем IDE (integrated development environment) Android Studio. Создаём новый проект: File -> New -> New Project. Выбираем «Empty Activity», жмём «Next».



Заполняем поля «Name», «Package name», «Save location».



IDE автоматически создаст два файла: «MainActivity.java» — в каталоге «java/[имя пакета]», «activity_main.xml» — в каталоге «res/layout».





Java-файл определяет, что приложение делает, xml – как оно выглядит. Делает же оно пока совсем мало, только «setContentView(R.layout.activity_main);». Эта строка указывает приложению при запуске использовать макет «activity_main.xml». И, поскольку макет содержит только один виджет типа «TextView» с текстом «Hello World!», то и выглядит наше приложение тоже весьма скромно.



В папке проекта создадим фрагмент с именем «Fragment1».





IDE создаст два файла: «Fragment1» и «fragment_fragment1.xml».





Откроем файл макета фрагмента и удалим ненужный нам виджет «TextView» с приветственной строкой.



Переключимся в режим дизайна и перетащим на макет кнопку (Button).



IDE создаст кнопку с идентификатором «button1».



Теперь отредактируем макет главной активности, т.е. файл «activity_main.xml»



Переместим текстовый виджет повыше и добавим в макет созданный нами фрагмент (для этого нужно перетащить элемент "<>" на макет, выбрать «Fragment1» и кликнуть «OK»).



В макете активности в настройках фрагмента установим layout_height=«wrap_content» и отредактируем на свой вкус его размещение. Также изменим идентификатор текстового поля на «textReport», а фрагмента — на «fragmentWithButton».



Запустим эмулятор (Shift+F10) и посмотрим, что получилось.



Приложение отображает надпись «Hello World!» и кнопку «BUTTON». Надпись выводится из активности, кнопка же принадлежит фрагменту. Кнопка нажимается, но никакого эффекта это пока не даёт. Попробуем запрограммировать надпись отображать количество нажатий кнопки. Для этого нам нужно будет передать сообщение о нажатии кнопки из фрагмента в активность.

Вначале научим фрагмент подсчитывать число нажатий кнопки. Откроем файл «Fragment1.java».



Добавим переменную «counter». В методе «onCreateView», который вызывается сразу после создания фрагмента, создадим «слушатель» кнопки. IDE потребует имплементировать View.OnClickListener — соглашайтесь (Alt + Enter). Создадим (переопределим) метод onClick, который будет увеличивать значение переменной «counter» при каждом клике по кнопке и выводить всплывающее сообщение.



Проверим в эмуляторе (снова Shift+F10), как это работает. Нажатие кнопки приводит к появлению в нижней части экрана приложения всплывающего сообщения «Количество нажатий кнопки: … ».



Отлично, идём дальше. Наша главная цель — передать информацию (в данном случае — число нажатий кнопки) из экземпляра фрагмента в экземпляр активности. Увы, жизненные циклы активностей и фрагментов организованы так, что Android (почти) не позволяет активности и фрагменту общаться напрямую, поэтому нам понадобится посредник-интерфейс. Назовём его «Postman» (почтальон). Интерфейс можно создавать как в отдельном файле, так и в файле с кодом фрагмента; мы выберем первый вариант. Наш интерфейс Postman будет содержать единственный абстрактный (без «тела») метод «fragmentMail».



Переменную «numberOfClicks» мы будем использовать как «конверт» для передачи сообщений от фрагмента в активность.

Откроем файл с кодом активности «MainActivity.java». Как мы помним, он выглядит так:



Имплементируем интерфейс «Postman» и добавим в активность метод интерфейса «fragmentMail», переопределив его (Override).



Теперь, как только активность «увидит» в переменной «numberOfClicks» новое значение, она выведет обновлённое сообщение в текстовом поле «textReport».

Но нам ведь ещё нужно «положить письмо в конверт», т.е. передать в переменную количество кликов по кнопке. А это мы делаем в коде фрагмента. Открываем файл «Fragment1.java».

Д?о?б?а?в?л?я?е?м? ?в? ?п?о?д?п?и?с?ь? ?к?л?а?с?с?а? ?и?м?п?л?е?м?е?н?т?а?ц?и?ю? ?и?н?т?е?р?ф?е?й?с?а? ?«?P?o?s?t?m?a?n?»?.? ?I?D?E? ?п?о?т?р?е?б?у?е?т? ?п?е?р?е?о?п?р?е?д?е?л?и?т?ь? ?м?е?т?о?д? ?и?н?т?е?р?ф?е?й?с?а? ?«?f?r?a?g?m?e?n?t?M?a?i?l?»?,? ?н?о? ?д?е?л?а?т?ь? ?в? ?н?ё?м? ?м?ы? ?н?и?ч?е?г?о? ?н?е? ?б?у?д?е?м?,? ?п?о?э?т?о?м?у? ?о?с?т?а?в?и?м? ?е?г?о? ?т?е?л?о? ?п?у?с?т?ы?м?. [Удалено, см. «Примечание 1 от 20.04.2019»]

Нам понадобится ссылка на экземпляр активности. Мы получим её при присоединении фрагмента к активности так:


В метод «onClick» (тот самый, который вызывается при нажатии кнопки нашего фрагмента) добавим обращение к интерфейсу из экземпляра активности.



Финальный код фрагмента после удаления (для компактности) комментариев выглядит так:


Теперь наш фрагмент считает количество нажатий кнопки, выводит их во всплывающем сообщении и затем с помощью интерфейса «Postman» передаёт значение переменной-счётчика в переменную numberOfClicks, служащую контейнером-конвертом для пересылки сообщения от фрагмента к активности. Активность, получая новое сообщение, тут же отображает его в своём текстовом поле-виджете с идентификатором «textReport». Цель достигнута!


P.S.: Смена языка программирования с Java на Kotlin позволяет существенно сократить код фрагмента:



P.P.S.: Скачать файлы проекта можно здесь: Java, Kotlin.

Примечание 1 от 20.04.2019:


Из кода фрагмента удалена имплементация интерфейса «Postman».
Фрагмент работает с интерфейсом через активность, в которой данный интерфейс уже имплементирован.
Спасибо пользователю mikaakim за комментарий.
Обновлённые файлы можно скачать с github по ссылкам выше.

Комментарии (21)


  1. hardex
    19.04.2019 18:43
    +2

    Вся статья о том, как в onAttach сохранить ссылку на контекст.


    1. Beanut
      19.04.2019 19:23

      Причем даже тут неверно. Надо не ссылку на контекст, а ссылку на интерфейс, чтобы каждом вызове не приводить к типу.


  1. argamidon
    19.04.2019 19:13

    А зачем использовать в котлине as, если есть is, который ещё и производит каст? + игнорируется исключение — bad approach


  1. Zoolander
    19.04.2019 20:00

    в 2016, когда писал обычные приложения под Android, для подобных целей в компании охотно использовали готовое решение из коробки — EventBus от greenrobot
    github.com/greenrobot/EventBus


    1. dakatso
      20.04.2019 12:49

      В 2018, когда я поддерживал приложение, написанное с использованием EventBus от greenrobot, я очень сильно матерился на того, кто в 2016 это делал.


      1. Zoolander
        20.04.2019 15:50

        1. по какой именно причине? В своей практике я сталкивался с message race — когда два объекта взаимно уведомляли друг друга и порой вступали в цикл. Кроме того, если источников сообщений очень много — логику с ними становится тяжело разбирать, какое сообщение за кем вызывается и к чему это приводит.

        2. альтернативное решение из коробки для передачи сообщения от одной части к другой?

        я не адвокат greenrobot, но они в индустрии уже давно, уже в 2016 их многие библиотеки считались золотым стандартом.

        После того, как прочухал и отстрада п.1 выше, в своих проектах на любых платформах стараюсь воздерживаться от дополнительных источников и приемников event, предпочитаю выстраивать архитектуру с более ясной инфраструктурой данных


  1. Nexus7
    19.04.2019 20:56

    Если продолжить развивать эту тему, то следующей стадией этого Hello World будет одна ViewModel на активити и фрагмент с общей LiveData-переменной, значение которой в третьей версии приложения будет автоматом сохраняться в репозитории через SharedPreferences, которые в четвёртой версии могут трансформироваться до Room.


  1. androidovshchik
    19.04.2019 21:04

    Почему бы просто не сделать так

    if (getActivity() != null && !getActivity().isFinishing()) {
        ((MainActivity) getActivity()).fragmentMail(counter);
    } 
    

    В данном случае, контекст всегда будет у одной и той же активити, да и интерфейс никакой не нужен
    При необходимости можно еще добавить проверку на класс
    Все дела


    1. anegin
      20.04.2019 11:35
      -1

      В таком случае фрагмент будет жестко привязан к MainActivity, т.е. если использовать этот фрагмент в другой активити, то придется во фрагменте еще проверку и кастинг для другой активити добавлять. Не тестируемо, не расширяемо.


    1. ookami_kb
      21.04.2019 17:50

      Интерфейс – это, пожалуй, единственная здравая мысль в статье. Так что, если хочется простоты, то на котлине можно сделать так:


      (activity as? Postman)?.fragmentMail(counter)

      Или же сделать реализацию интерфейса обязательной, в зависимости от требований:


      (activity as Postman).fragmentMail(counter)

      Конечно, в этом случае, если активити-хозяин не реализует необходимый интерфейс, то приложение упадет.


      А вот проверку на isFinishing я бы во фрагменте не делал – это совсем не забота фрагмента следить за жизненным циклом активити. Если эта проверка нужна, то ее сама активити и должна делать.


  1. FGV
    19.04.2019 21:14
    +1

    А зачем фрагмент расширен интерфейсом постман? зачем он нужен?


  1. demonit
    19.04.2019 23:04
    +2

    а как же clean architecture и тп? пример из серии «как не нужно программировать».
    А потом все дружно удивляются от куда у нас столько говнокодеров


  1. Dekmabot
    20.04.2019 12:50

    Спасибо за подробную статью, как раз изучаю разработку на Андроиде на kotlin, и в сети явно не хватает таких вот подробных примеров.


    1. Nexus7
      20.04.2019 16:50

      Может быть не там ищите? antonioleiva.com/mvvm-vs-mvp


  1. sexol123
    20.04.2019 12:51

    1) Разве аргументы и бандлы отменили? Intent?
    2) Presenter? ViewModel?
    3) можно и так, но врядли твои коллеги одобрят+при перевороте значение потеряется


  1. yavfast
    20.04.2019 12:51

    Джунам после такого надо больно по пальцам бить, чтобы больше так не делали.


  1. mikaakim
    20.04.2019 15:44

    Зачем фрагменту имплементировать Почтальона, если он так и так стучит к активности, которая имплементирует почтальона?


    1. KotlinStudio Автор
      20.04.2019 21:32

      mikaakim, Вы правы, спасибо!


  1. nomadmoon
    21.04.2019 03:56

    Внезапно недавно обнаружил что можно сделать

    SomeShit.kt:
    object SomeShit
    {
    var SomeCounter: Int = 0
    }
    


    после чего в фрагменте можно писать в SomeShit.SomeCounter, а в активности читать оттуда и наоборот. Чувствую что есть какой то подвох, потому что слишком просто получается но не могу придумать какой именно.


    1. lgorSL
      21.04.2019 13:07

      На самом деле примерно так и надо (ну не именно так, а чуть хитрее). Я вообще не понимаю, почему большинство примеров начинаются с хранения состояния в активити и попыток его сохранить/передать при повороте экрана, сворачивании приложения и переходе на другую активити.
      Всей этой боли можно избежать, если сделать иначе: хранить состояние отдельно от активити в более стабильном месте: например, в статическом поле, в поле application, а что-то долговременное вообще сохранять в preferences.


      Есть некоторые моменты, связанные с многопоточностью и activity lifecycle (оно может создаваться заново, поэтому ссылку на него хранить нельзя, но при этом его надо как-то уведомлять об изменения состояния). В androidx появились стандартные классы, которые делают это адекватным образом:


      1. Класс для хранения состояния, который может уведомлять подписавшихся, причем состояние можно изменять и из UI потока, и из другого.
      2. У activity появился lifecycleOwner, благодаря чему активити при завершении работы будет отписано от уведомлений.
        Я не использовал этот подход для чего-то сложного, но на простых примерах очень понравилось — код намного проще и короче.


    1. ookami_kb
      21.04.2019 17:44

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