Как в Android'е передать переменную из фрагмента в активность?

Рассказ о том, как в 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 по ссылкам выше.
Поделиться публикацией

Комментарии 26

    +3
    Вся статья о том, как в onAttach сохранить ссылку на контекст.
      +2
      Причем даже тут неверно. Надо не ссылку на контекст, а ссылку на интерфейс, чтобы каждом вызове не приводить к типу.
      0
      А зачем использовать в котлине as, если есть is, который ещё и производит каст? + игнорируется исключение — bad approach
        0
        в 2016, когда писал обычные приложения под Android, для подобных целей в компании охотно использовали готовое решение из коробки — EventBus от greenrobot
        github.com/greenrobot/EventBus

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

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

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

            После того, как прочухал и отстрада п.1 выше, в своих проектах на любых платформах стараюсь воздерживаться от дополнительных источников и приемников event, предпочитаю выстраивать архитектуру с более ясной инфраструктурой данных
              0
              1. Ужаснейший антипатерн потому что. Невозможно чисто по коду и за адекватное время отследить ни отправителей событий для конкретного получателя, ни получателей по конкретному отправителю. Мне в наследство прилетало два проекта с eventbus. Первый — целиком построен на ней и это адовый треш, понять, что откуда летит и кем принимается — не реально. Второй — eventbus используется всего в паре мест, но чтобы найти источник события нужно пользоваться поиском по всему проекту.
              2. Rx или LiveData.
          +1
          Если продолжить развивать эту тему, то следующей стадией этого Hello World будет одна ViewModel на активити и фрагмент с общей LiveData-переменной, значение которой в третьей версии приложения будет автоматом сохраняться в репозитории через SharedPreferences, которые в четвёртой версии могут трансформироваться до Room.
            0
            Почему бы просто не сделать так
            if (getActivity() != null && !getActivity().isFinishing()) {
                ((MainActivity) getActivity()).fragmentMail(counter);
            } 
            

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

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


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

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


                (activity as Postman).fragmentMail(counter)

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


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

                +1
                А зачем фрагмент расширен интерфейсом постман? зачем он нужен?
                  +2
                  а как же clean architecture и тп? пример из серии «как не нужно программировать».
                  А потом все дружно удивляются от куда у нас столько говнокодеров
                    0
                    Спасибо за подробную статью, как раз изучаю разработку на Андроиде на kotlin, и в сети явно не хватает таких вот подробных примеров.
                    0
                    1) Разве аргументы и бандлы отменили? Intent?
                    2) Presenter? ViewModel?
                    3) можно и так, но врядли твои коллеги одобрят+при перевороте значение потеряется
                      0
                      Джунам после такого надо больно по пальцам бить, чтобы больше так не делали.
                        0
                        Зачем фрагменту имплементировать Почтальона, если он так и так стучит к активности, которая имплементирует почтальона?
                          0
                          mikaakim, Вы правы, спасибо!
                            0
                            В котлин коде (да и в джава тоже) — зачем мы проверяем и берём активность, если нам нужен почтальон?
                            Т.е. сразу так и написать if ( context is Postman) this.postman = context
                            Всегда делал так. Это даст больше универсальности, т.к. postman может приходить, например, не только из контекста, но и передаваться сообщением или через save/restore или ещё десятком способов. Фильтры там нагородить можно будет.
                          0
                          Внезапно недавно обнаружил что можно сделать
                          SomeShit.kt:
                          object SomeShit
                          {
                          var SomeCounter: Int = 0
                          }
                          


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

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


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


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

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

                                0
                                Плюс еще надо будет нагородить костылей, чтобы сохранять состояние такого класса, иначе нет никаких гарантий, что значения останутся после того, как приложение ушло в бэкграунд.
                                0
                                По-хорошему, надо во фрагменте создать не поле activity, а поле postman. И создать метод
                                fun setPostman(postman: Postman) {
                                    this.postman = postman
                                }
                                

                                и из активности вызвать fragment.setPostman(this)

                                Не говорю, что это единственное и лучшее решение, но если речь о передаче данных через интерфейс, то стоит делать так, иначе джуны рискуют нарваться как минимум на критику ментора
                                  0

                                  Нет, как раз так делать не стоит. Жизненные циклы у фрагмента и у активити разные, ОС может пересоздать фрагмент, и ваш setPostman метод не будет вызван.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.