Как из фрагмента передать данные в активити
Перейти к содержимому

Как из фрагмента передать данные в активити

  • автор:

Как из фрагмента передать данные в активити

Одна activity может использовать несколько фрагментов, например, с одной стороны список, а с другой — детальное описание выбранного элемента списка. В такой конфигурации activity использует два фрагмента, которые между собой должны взаимодействовать. Рассмотрим базовые принципы взаимодействия фрагментов в приложении.

Создадим новый проект с пустой MainActivity. Далее создадим разметку layout для фрагментов. Пусть у нас в приложении будет два фрагмента. Добавим в папку res/layout новый xml-файл fragment_list.xml :

Здесь определен элемент ListView для вывода списка объектов.

И также добавим для другого фрагмента файл разметки fragment_detail.xml :

Оба фрагмента будут предельно простыми: один будет содержать список, а второй — текстовой поле. Логика приложения будет такова: при выборе элемента в списке в одном фрагменте выбранный элемент должен отобразиться в текством поле, которое находится во втором фрагменте.

Затем добавим в проект в одну папку с MainActivity собственно классы фрагментов. Добавим новый класс ListFragment со следующим содержимым:

package com.example.fragmentapp; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import androidx.fragment.app.Fragment; public class ListFragment extends Fragment < interface OnFragmentSendDataListener < void onSendData(String data); >private OnFragmentSendDataListener fragmentSendDataListener; String[] countries = < "Бразилия", "Аргентина", "Колумбия", "Чили", "Уругвай">; @Override public void onAttach(Context context) < super.onAttach(context); try < fragmentSendDataListener = (OnFragmentSendDataListener) context; >catch (ClassCastException e) < throw new ClassCastException(context.toString() + " должен реализовывать интерфейс OnFragmentInteractionListener"); >> @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) < View view = inflater.inflate(R.layout.fragment_list, container, false); // получаем элемент ListView ListView countriesList = view.findViewById(R.id.countriesList); // создаем адаптер ArrayAdapteradapter = new ArrayAdapter(getContext(), android.R.layout.simple_list_item_1, countries); // устанавливаем для списка адаптер countriesList.setAdapter(adapter); // добавляем для списка слушатель countriesList.setOnItemClickListener(new AdapterView.OnItemClickListener() < @Override public void onItemClick(AdapterViewparent, View v, int position, long id) < // получаем выбранный элемент String selectedItem = (String)parent.getItemAtPosition(position); // Посылаем данные Activity fragmentSendDataListener.onSendData(selectedItem); >>); return view; > >

Фрагменты не могут напрямую взаимодействовать между собой. Для этого надо обращаться к контексту, в качестве которого выступает класс Activity. Для обращения к activity, как правило, создается вложенный интерфейс. В данном случае он называется OnFragmentSendDataListener с одним методом.

interface OnFragmentSendDataListener < void onSendData(String data); >private OnFragmentSendDataListener fragmentSendDataListener;

Но чтобы взаимодействовать с другим фрагментом через activity, нам надо прикрепить текущий фрагмент к activity. Для этого в классе фрагмента определен метод onAttach(Context context) . В нем происходит установка объекта OnFragmentSendDataListener :

fragmentSendDataListener = (OnFragmentSendDataListener) context;

При обработке нажатия на элемент в списке мы можем отправить Activity данные о выбранном объекте:

String selectedItem = (String)parent.getItemAtPosition(position); fragmentSendDataListener.onSendData(selectedItem);

Таким образом, при выборе объекта в списке MainActivity получит выбранный объект.

Теперь определим класс для второго фрагмента. Назовем его DetailFragment :

package com.example.fragmentapp; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.fragment.app.Fragment; public class DetailFragment extends Fragment < @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) < return inflater.inflate(R.layout.fragment_detail, container, false); >// обновление текстового поля public void setSelectedItem(String selectedItem) < TextView view = getView().findViewById(R.id.detailsText); view.setText(selectedItem); >>

Задача этого фрагмента — вывод некоторой информации. Так как он не должен передавать никакую информацию другому фрагменту, здесь мы модем ограничиться только переопределением метода onCreateView() , который в качестве визуального интерфейса устанавливает разметку из файла fragment_detail.xml

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

В итоге получится следующая структура:

Создание фрагментов в Android

Теперь изменим файл разметки activity_main.xml :

С помощью двух элементов FragmentContainerView в MainActivity добавляются два выше определенных фрагмента.

И в конце изменим код MainActivity :

package com.example.fragmentapp; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends AppCompatActivity implements ListFragment.OnFragmentSendDataListener < @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); >@Override public void onSendData(String selectedItem) < DetailFragment fragment = (DetailFragment) getSupportFragmentManager() .findFragmentById(R.id.detailFragment); if (fragment != null) fragment.setSelectedItem(selectedItem); >>

Для взаимодействия фрагмента ListFragment c другим фрагментом через MainActivity надо, чтобы эта activity реализовывала интерфейс OnFragmentSendDataListener. Для этого реализуем метод onSendData() , который получает фрагмент DetailFragment и вызывает у него метод setSelectedItem()

В итоге получится, что при выборе в списке во фрагменте ListFragment будет срабатывать слушатель списка и в частности его метод onItemClick(AdapterView parent, View v, int position, long id) , который вызовет метод fragmentSendDataListener.onSendData(selectedItem); . fragmentSendDataListener устанавливается как MainActivity, поэтому при этом будет вызван метод setSelectedItem у фрагмента DetailFragment. Таким образом, произойдет взаимодействие между двумя фрагментами.

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

Фрагменты в activity в Android и Java

Однако пока фрагменты одинаково выводятся в одной activity как в альбомной, так и в портретной ориентации вне зависимости от устройства. Поэтому оптимизируем приложение.

Фундаментальные проблемы Android

Очевидно, у большинства объектов есть зависимости, которые должны быть переданы через конструктор. Исключениями из правила являются синглтоны: либо у них нет зависимостей, либо они не должны быть синглтонами.

Все компоненты Android-приложений — активити, фрагменты , сервисы — по умолчанию создаются через no-arg конструктор (данные можно передать лишь посредством extras/arguments). Как только компонент создан, он оторван от внешнего мира и нежизнеспособен: уже позже вызываются setArguments , attachBaseContext/onAttach , onCreate и т. п.. Соответственно, такие объекты, подобно плохим синглтонам, вынуждены разрешать зависимости самостоятельно: лезть в Application или static и забирать нужные объекты. Более-менее исправлено у Activity и Service в SDK 28 и у бэкпорта Fragment.

Наиболее приемлемым костылём может показаться использование Application , но он один на всё приложение вместе с библиотеками, поэтому Google рекомендует использовать статики/синглтоны; нужно знать, что во время бэкапа Application будет предательски подменён, а также, после рестарта процесса все классы загрузятся заново и данные, хранившиеся в памяти, пропадут.

Необходимость иметь no-arg-конструкторы становится феерической помехой повторному использованию кода: при добавлении новой функциональности она не делегируется объектам, которые передаются в конструктор, а реализуется прямо внутри фрагмента (активити, презентера, вьюмодели) в if-else/switch-case/when. Впоследствии такой класс становится типичным customizable object, подчиняющимся десяткам условий — а такой код крайне сложно поддерживать.

Ложку дёгтя добавляет жизненный цикл. Предположение, что Activity/Fragment — обычный Java-объект, оказывается немного неверно: после пересоздания ту же задачу выполняет уже другой экземпляр, которому достаётся Bundle с сохранённым состоянием предшественника. Люди, не знающие Java, часто неверно интерпретируют пересоздание как «GC вычистил мне все ссылки!»

Иерархии типов

Нужно постоянно наследовать классы. Если какая-нибудь библиотека, например, хочет предоставить свои подтипы фрагментов, приходится учитывать, что есть нативные фрагменты ( android.app.Fragment ), их старый бэкпорт ( android.support.v4.app.Fragment ) и новый бэкпорт ( androidx.fragment.app.Fragment ), а также несколько особых случаев: DialogFragment , BottomSheetDialogFragment и т. п..

У бэкпортированных и нативных фрагментов одинаковый интерфейс (множество публичных методов), но они не реализуют один interface , следовательно, для поддержки обеих иерархий классов нужно создавать два экземпляра одного кода, которые различаются только импортами. (Благодаря AndroidX эта проблема теперь есть и у тех, кто наследует андроидные View.)

Чтобы поселить фактический сервис (просто объект, представляющий определённую функциональность) в Bound Service и передать его в Activity , нужно унаследовать как минимум Service и Binder (а также реализовать ServiceConnection ). Пример заворачивания объекта в сервис

Как работать с асинхронностью?
AsyncTask

Невероятно странный интерфейс: метод execute() принимает дженерик, но задачу можно исполнить лишь один раз, поэтому проще передать параметр в конструктор; второй дженерик определяет тип промежуточных данных — он обычно не используется, поэтому имеет смысл завести для редкого случая отдельный класс — скажем, ProgressAsyncTask ; execute() и onPostExecute() принимают vararg, но в большинстве случаев передаётся ровно один объект; нельзя создавать и запускать AsyncTask из фона — onPreExecute() вызывается прямо из execute() , а в старых версиях статический инициализатор вызывает new Handler(без аргументов) , тем самым привязываясь к текущему Looper (если он есть); ничего не знает о жизненном цикле. В Android 11 считается устаревшим с бредовым обоснованием.

Loader

Полны багов, API монструозен. Считаются устаревшими в Android 9.

ThreadPoolExecutor

Неплохой, хоть и старомодный, инструмент из Java, который, естественно, ничего не знает о жизненном цикле компонентов. С небольшими усилиями FutureTask можно переоборудовать, чтобы получать результат в колбэк.

Стороннее

Kotlinx.coroutines или монструозную RxJava несложно адаптировать для работы с Android (в частности, прерывать текущие задачи в onDestroyView или onDestroy ), но их нужно приносить с собой, т. к. фреймворк их не содержит. Да и в большинстве случаев это из пушки по воробьям.

Работа с JSON

Я бы не стал использовать этот формат добровольно, но к этому принуждают многие публичные API.

  • org.json (AST), который есть в Android, хорош лишь в качестве академической реализации.
  • android.util.JsonReader (streaming) — копия com.google.gson.stream.JsonReader , немного оптимизированная для Android. Вообще, стриминг — достаточно низкоуровневый принцип, поверх которого обычно пишут более человечную реализацию. Но при этом JsonReader недостаточно низкоуровневый: он читает символы из Reader и возвращает строки в виде String , то есть доступа к нижележащему InputStream/byte[] он не имеет, что мешает выполнять некоторые оптимизации. Присутствует попытка убирать дубликаты строк с помощью libcore.internal.StringPool , но этот пул не умеет разрешать коллизии, поэтому после считывания нескольких сотен строк в пуле оседает от силы 30.
  • Практические реализации — Gson, Jackson, Moshi, Klaxon — нужно приносить с собой.
HTTP

HttpURLConnection — это вообще издевательство. Конечно же, большинство использует OkHttp/Retrofit, пока Google пытается делать припарки библиотеке Volley.

Управление зависимостями

Вес приложений усугубляется полнейшим отсутствием пакетного менеджера. В некоторых дистрибутивах Linux есть apt-get , apt или aptitude , на Mac OS можно установить brew . В Java-мире есть системы сборки Gradle, Maven и множество других. Все вышеперечисленные умеют скачивать пакеты, их зависимости, зависимости их зависимостей и т. д.. В Android нет ничего подобного — Gradle скачивает всё на многострадальный компьютер разработчика, ProGuard/R8 давится десятками библиотек, dx/D8 конвертирует их в Android-совместимый байт-код, программист загружает в маркет, пользователь — из маркета, рантайм верифицирует, загружает, компилирует все эти классы, собирает о них статистическую информацию для наиболее оптимального выполнения (profile-driven compilation). Всё это происходит снова и снова, даже если множество приложений используют одни и те же зависимости одинаковых версий, в том числе для таких крупных библиотек как AppCompat/Support/AndroidX/Desing/Material, Google Mobile Services, RxJava, ExoPlayer, Realm, FFMpeg. Последние две содержат нативный (машинный) код, что заставляет разработчиков собирать по несколько APK для разных архитектур (ABI splits) либо App Bundle (отдай гуглу свою подпись, это же так безопасно!).

Технически несложно создать и использовать менеджер пакетов, для этого не нужны ни особые разрешения, ни привилегии системных приложений; ирония в том, что такое приложение должно будет нарушить правила Маркета, и, соответственно, никогда не станет популярным и востребованным.

Context

Контекст — это god object. View , например, нужен не контекст, а тема и ресурсы. А registerReceiver мог бы быть методом не Context , а Application (для локального броадкаста) и, допустим, AndroidSystem (выдуманный класс) для броадкаста по всей системе.

Особенно доставляет, что контексты бывают разные — приложения, сервиса, активити — и с ними нужно обходиться по-разному.

non-configuration instance

Retain-фрагменты (deprecated) переживают смену конфигурации. Слышали что-нибудь о retain-фрагментах без View ? Это костыль, который позволяет хранить объекты в таком фрагменте. В Activity есть похожий механизм — non-configuration instance (custom non-configuration instance у AppCompatActivity ). Отсюда вопрос: почему нельзя сделать Activity живучей, как retain-фрагмент, и почему у фрагмента, наоборот, нет non-configuration instance?

Ресурсы

У Android собственный механизм для доступа к ресурсам. Стандартные для Java ресурсы из classpath работают, но медленно и расточительно. Та же проблема затрагивает механизм ServiceLoader , т. к. META-INF/services — тоже ресурсы classpath.

XML — основной и официальный способ описания ресурсов — от вёрстки, меню, переводов и графики до конфигураций, примитивов и строк. Он хорошо подходит для случая, когда ОС/лончеру/шторке нужно достать ресурсы, не пробуждая приложение, но мешает всяческим попыткам написать лаконично, гибко и без дублирования. Присутствует (неудавшаяся) попытка исправить XML статической типизацией.

Каждый ресурс формально находится в определённом пакете (package) — изолированном пространстве имён. На практике таковых всего два — android и пакет текущего приложения; Android Gradle Plugin сливает ресурсы изо всех AAR-библиотек в пакет приложения, провоцируя конфликты имён, а также применяет страшные костыли для перегенерации R.class .

AttributeSet — это интерфейс, но реализовывать его бесполезно: obtainStyledAttributes кастит его к XmlBlock.Parser — а это package-private класс. По сути, AttributeSet играет роль маркер-интерфейса, что есть антипаттерн.

Сама абстракция Drawable кажется мне очень удачной, а разнообразие коробочных реализаций радует глаз. Но атрибуты темы не работают на четвёрках, названия XML-тегов ( selector , shape ) отличаются от имён классов ( StateListDrawable , GradientDrawable ), возможность использовать в XML свои классы доступна аж с SDK 24, а Drawable paddings влияют на View paddings по-разному, в зависимости от версии Android.

Кстати, Drawable парсит содержимое своего тега в произвольной форме: . LayoutInflater же этого не позволяет: внутри вьюгруппы могут находиться только вью, а родитель парсит только LayoutParams из тегов своих детей. Вследствие этого образовалось несколько недоразумений:

  • у ConstraintLayout.LayoutParams 15 разных свойств вида layout_constraintEdge_toEdgeOf . Хотелось бы вместо этого иметь всего два тега вида и, соответственно, ;
  • com.google.android.material.tabs.TabItem — эдакая транзинтая вьюшка, потому что не-вьюшку LayoutInflater не признает. Существует только для того, чтобы спарсить четыре атрибута (три собственных и стандартный contentDescription) и доставить их до TabLayout . При этом использует суперконструктор View(Context, AttributeSet) , который парсит все стандартные (в SDK 31 это 96 штук) атрибуты вью;
  • onFinishInflate() . Если бы LayoutInflater позволял вьюшкам самостоятельно парсить своё содержимое, каждая вьюгруппа к концу своего конструктора уже собрала бы всех своих детей (а стектрейсы ошибок инфлейта были бы более внятными). Но нет, это же андроид, у нас добрая традиция инициализировать объекты не до конца, а потом вставлять разные костыли.

В векторных картинках можно задавать контурам цвета из темы (например, ?colorPrimary ). Но темы нельзя создавать из кода! (Выход: берём androidx.core.graphics.PathParser голыми руками и рисуем векторные контуры, как вздумается!)

Нет никакой гарантии, что указанный ресурс вообще существует. Он может иметь значение только для определённых квалификаторов или не иметь его вовсе ( ).

shrinkResources удаляет неиспользуемые ресурсы, но как-то неуверенно ; оставшиеся ресурсы не переименовываются .

Remote views нельзя создать из кода, только XML. Что вносит множество ограничений.

LayoutParams можно создать только из одного XML-тэга. с layout_gravity , в котором находится с layout_margin , который содержит вью с layout_width и layout_height — нельзя, получайте непереиспользуемый код.

В XML-вёрстку нельзя передавать параметры, будь это хоть LayoutInflater#inflate , хоть или (а вот в любом шаблонизаторе такая возможность есть).

В XML-вёрстке нельзя поставить точку останова (breakpoint), что делает отладку ошибок вроде Binary XML file line #0: Error inflating class . чрезвычайно увлекательной. LayoutInflater работает рекурсивно, поэтому многие стектрейсы не вмещаются и обрезаются: 86 more.

findViewById — нечто абсолютно противоположное лаконичности и типобезопасности. Количество различных костылей, с этим связанных, превышает все мыслимые пределы. Нет механизма, который помешал бы использовать вью разных типов в вариантах одной вёрстки для разных конфигураций и получать трудноуловимые рантаймовые падения. Можно написать свой класс View , у которого будут type-параметры (дженерики) — тогда каст в findViewById станет unchecked. Так как сам каст находится внутри findViewById , компилятор никак об этом не предупредит.

Есть альтернативный вариант записи вьюшек в XML: . Незаменимо, когда вью — вложенный класс, но слишком непривычно, поэтому на практике не используется. Стоит ли упоминать, что name обрабатывается отдельным, специально обученным кодом, и никакие @ссылки и ?атрибуты не поддерживаются?

Для ресурсов генерируются провязки в Java-код — R.java . Но для вариантов этого не сделали — это обычные инты, для которых соответствующие enum ы или @IntDef ы можно написать руками.

Идеологически TypedArray — это массив TypedValue . Но функциональность у них разная: TypedArray предоставляет человеческий интерфейс из методов getText, getBoolean, getInt, getFloat, getColor, . , а TypedValue ничего из этого не умеет. Зато у TypedValue есть поле changingConfigurations , а вот TypedArray#getChangingConfigurations возвращает одно общее значение для всех элементов (за O(n)) и доступно только для 21+. В итоге приходится использовать TypedArray даже для единственного атрибута и прибегать к помощи TypedValue , даже когда имеется «человеческий» TypedArray .

changingConfigurations у классов ColorStateList, Theme, TypedArray, Drawable, TypedValue расскажут, при смене каких конфигураций ресурс инвалидируется. Казалось бы, чтобы обновлять ресурсы вовремя, нужно придержать айдишники всех ресурсов, которые могут инвалидироваться в течение жизни данного компонента (т. е. (component.configChanges & resource.configChanges) != 0 ) и переопределить onConfigurationChanged данного Activity или View , где и можно перезагрузить протухшие ресурсы. Но метод Activity#getChangingConfigurations предназначен для другого и возвращает осмысленное значение, только когда активити уничтожается, а у вью вообще нет подобного метода. Чтобы узнать, какие конфигурации обрабатывает текущая активити, нужно спросить ActivityInfo у PackageManager . Хотя нужный объект ActivityInfo уже есть у Activity . В приватном поле.

По умолчанию все ресурсы библиотеки считаются публичными. Как только появляются ресурсы, явно отмеченные, как публичные, все остальные становятся приватными. Android Lint отмечает использование «приватных» ресурсов как warning. Не хватает возможностей:

  • писать модификаторы доступа «на месте», а не в отдельном файле;
  • генерировать настоящую ошибку компиляции при использовании чужих приватных ресурсов;
  • кроме private in module иметь private in file, например, для идентификаторов, которые используются для позиционирования в RelativeLayout ;
  • писать документацию к ресурсам, по аналогии с javadoc и KDoc.

Есть Resources#getString(@StringRes int) для получения строки, Resources#getString(@StringRes int id, Object. formatArgs) для строки с подстановками, Resources#getText(@StringRes int id) для текста с форматированием, но нет метода для обработки пересечения последних двух случаев: Resources#getText(int, Object. formatArgs) . Хотя это максимально универсальный вариант, который перекрывает функциональность всех трёх существующих методов.

В строковых ресурсах используется собственный механизм экранирования. С \@ и \? всё понятно, а вот вместо   придётся писать \u00A0 . Зачем нужно экранировать одинарные кавычки — для меня загадка. Ещё большей загадкой остаётся вопрос, что же мешало явно отделить @ и ? от литералов: либо , либо литерал . Всё равно смешивать ссылки и литералы нельзя: @string/close форточку .

В итоге терпимо использовать для переводов (если написать свой getText(int, Object. formatArgs) ), а растровые и векторные картинки нормально чувствуют себя в drawable-*dpi. Всё остальное настолько убого, негибко и многословно, что оказывается довольно бесполезным, т. к. напрашивается на перенос в код.

Parcel

Писать собственную сериализацию — это всегда весело и задорно. Parcel очень напоминает DataInput и DataOutput из JDK, а Parcelable — это подобие Externalizable , но реализациями этих интерфейсов они не являются, что заставляет писать платформозависимый код.

Parcelable.Creator нужно держать в статическом поле с именем CREATOR , чтобы фреймворк мог найти его рефлекшеном. При этом, не найдя CREATOR , фреймворк пойдёт искать его у суперклассов, откладывая возникновение ошибок десериализации на потом.

Parcel нельзя долгосрочно хранить, потому что формат может быть изменён при обновлении ОС. Но тогда такой сериализации прямой путь в мусорку, потому что почти всегда объкеты в каком-то виде пересылаются или хранятся, а поддерживать более одного формата сериализации — затея сомнительная.

Асинхронные события и фрагменты

У фрагментов может случиться state loss. Очевидно, выполнение транзакций с фрагментами после сохранения состояния — это ошибка. Но фреймворк никак не поможет её найти — в асинхронных стектрейсах вообще не будет вашего кода.

Даже если транзакция выполняется в ответ на действие пользователя (например, непосредственно в OnClickListener ), Activity вполне может быть на паузе в этот момент. Банальное нажатие кнопки «назад» может привести к падению, приложению при этом вообще не обязательно использовать фрагменты.

Если пользователь сделает два быстрых клика, они оба вполне могут быть обработаны, даже если предполагается, что клик должен увести пользователя с текущего экрана. ButterKnife содержит костыль, который избавляет от множественных кликов.

Обратная совместимость

В javadoc не проставлены @since . (На сайте они откуда-то всё же берутся.)

APK, собранные с использованием gradle plugin 3 и build-tools 26, ломают лончер и файловый менеджер Cyanogenmod.

В AppCompat 26 сломали target у фрагментов в тех случаях, которые не описаны (и никогда не были описаны) в документации как недопустимые. Почему-то FragmentManager оказался не в силах пробежать по родительским менеджерам и найти общего менеджера двух произвольных фрагментов.

В AndroidX таргеты устарели, им на смену пришёл новый механизм. Давайте сравнивать:

Возможность \ механизм setTargetFragment(…, RC) setFragmentResultListener(RK, …)
Различать, от кого пришёл результат onActivityResult(getTargetRC(), …) а как хочешь
Фрагменты из разных менеджеров уже нельзя нельзя
Консистентность с Activity Result болимень отнюдь
Область видимости requestCode/Key вызывающий фрагмент менеджер
Момент доставки когда попало STARTED

Итого: изменения ради изменений, стало ещё более неудобно, а requestKey могут теперь конфликтовать с оными у соседних фрагментов. А ведь всё, чего не хватало — метода setResult() , который перебросит результат, когда вызывающий фрагмент оттает.

Слабые контракты

Intent может содержать Action (строка), Uri, Extras (Bundle, т. е. Map ). Для популярных extras есть заранее заготовленные ключи, например, Intent.EXTRA_EMAIL . Но нет типов. Так, можно попытаться открыть почтовый клиент, передав в качестве темы письма картинку, или открыть браузер, не передавая адреса веб-страницы. Чтение интента в видимой всей системе активити — поле непаханное для крэшей. Некоторые популярные приложения валились у меня на глазах при попытке поделиться картинкой. И дело не только в кривизне рук разработчиков, но и в изначальной проблемности такого механизма.

Кроме как в Extras, Bundle используется ещё и в аргументах фрагмента ( Fragment.setArguments ). Как всегда, нет гарантий, что по нужному ключу передан объект нужного типа.

Положим, у вас есть фрагмент, который принимает какой-то объект (выдумаем, например, ParcelUser ) через аргументы. И вы осознаёте, что не нужно передавать объект целиком, достаточно передать идентификатор пользователя ( ParcelUuid ). Счастливого рефакторинга!

Профилирование

Взамен Android Monitor сделали полурабочий Android Profiler. Вот тред о том, как снова нормально смотреть хип-дампы.

Взамен Android Device Monitor не сделали ничего. Благо, его не удалили по-настоящему, а просто убрали из меню в IDE.

Единственный профайлер, способный предоставить полезную информацию, — это стороннее решение. Method tracing бесполезен, т. к. выбрасывает скомпилированный (и даже интринсифицированный!) код и использует интерпретатор. Systrace помогает найти медленные места, но очень приблизительно, т. к. фреймворковые методы инструментировать нельзя.

Баги
  • Fragment#isRemoving работает вдвойненеправильно.
  • DialogFragment неправильно работает c retainInstance.
  • SparseArray не проверяет границы переданного индекса при чтении.

Ясное дело, баги есть в любом софте. Только здесь серьёзные преграды для разработки почему-то не считаются проблемой.

Урок 15. Передача данных между экранами – пунктами назначения. Android Navigation. Bundle vs Safe Args

Продолжаем серию уроков по разработке android-приложений в Android Studio на языке Kotlin.

На прошлом уроке мы выполняли навигацию по условию, авторизован пользователь или нет.

  1. На этом уроке
  2. Передача данных между экранами
  3. Bundle или Safe Args?
  4. Создаем проект
  5. Создаем граф навигации
  6. Добавляем пункты назначения – фрагменты
  7. Создаем action
  8. Добавим аргумент
  9. Подготовим макеты фрагментов
  10. Передаем данные через Bundle
  11. Принимаем данные через Bundle
  12. Тестирование приложения
  13. Подключение в проект Safe Args
  14. Передача данных через Safe Args
  15. Тестирование приложения
  16. Исходный код

На этом уроке

На этом уроке рассмотрим возможности передачи данных между экранами – пунктами назначения навигации в андроид-приложении. Создадим приложение с двумя экранами. На первом экране будет поле для ввода имени и кнопка отправки, а на втором экране будет отображаться приветствие с именем, которое мы указали. Таким образом, мы передадим данные (имя) с первого экрана на второй.

Передача данных между экранами

В процессе работы приложения часто возникает необходимость передавать данные между экранами. В уроке №5 мы рассматривали возможность передачи данных между активити с помощью интента. Для передачи данных между фрагментами такой способ не подходит, поскольку часто фрагменты отображаются в одном и том же активити.

Bundle или Safe Args?

В этом уроке мы рассмотрим два способа передачи данных между фрагментами: традиционный – с помощью наборов данных Bundle и типобезопасный – при помощи безопасных аргументов SafeArgs. Первый способ относительно прост – создаем набор данных «ключ-значение» типа Bundle и передаем через action в первом фрагменте, и извлекаем во втором фрагменте.
Второй способ потребует немного больше кода. На первый взгляд он может показаться сложнее, поскольку используется кодогенерация – среда разработки создает необходимые классы вместо вас. Но мы попробуем разобраться и вы увидите, что ничего особо сложного там нет. По сути, SafeArgs – просто обертка над Bundle. Тем не менее, разработчики настоятельно рекомендуют применять именно SafeArgs, как типобезопасный способ передачи данных между фрагментами в процессе навигации.

Создаем проект

Откройте среду разработки Android Studio и создайте новый проект с использованием шаблона Empty Activity.

Создаем граф навигации

Далее перейдите в папку res и создайте в ней папку navigation. Внутри папки navigation создайте Navigation Resource File с именем nav_graph.xml и корневым элементом .

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

Добавляем пункты назначения – фрагменты

Добавьте новые пункты назначения. Для этого:

  1. В окне редактора дизайна нажмите кнопку «New destination»
  2. Выберите «Create new destination»
  3. Далее в окне добавления фрагмента выберите Fragment (Blank):

Создайте таким образом два фрагмента:

  • FragmentOne
  • FragmentTwo

Чтобы фрагменты из графа навигации отображались на экране, не забудьте добавить в макет главного активити activity_main.xml компонент fragment — хост навигации вместо TextView:

Создаем action

Вернемся в nav_graph.xml и добавим action для перехода. Можно просто соединить фрагменты стрелкой на экране редактора дизайна, и в коде графа навигации добавится секция :

Добавим аргумент

Для передачи данных нужно использовать элемент , добавим его в граф навигации. Для этого выделите второй фрагмент в редакторе дизайна и справа в панели атрибутов на вкладке Arguments нажмите плюс. Укажите имя аргумента, например, MyArg и значение по умолчанию. Тип можно не указывать. Я передам в качестве дефолтного значения текст «Hello, Android!». Значение по умолчанию будет отображаться, если мы ничего не передадим из первого фрагмента во второй.

Подготовим макеты фрагментов

Измените макет разметки первого фрагмента fragment_one.xml таким образом:

Здесь мы оставили текстовое поле, которое теперь отображает текст «Ваше имя?». Ниже TextView добавлен EditText для возможности вписать туда имя, и кнопка отправки ImageButton, значок для которой был скачан с сайта https://material.io/resources/icons/
Вы также можете скачать его оттуда, либо взять из исходников этого проекта по ссылке в конце текстовой версии урока.

Введенный на первом экране текст будем передавать во второй фрагмент для отображения в текстовом поле. Откройте макет второго фрагмента fragment_two.xml и сделайте так, чтобы элемент TextView был посредине. Для этого можно заменить корневой FrameLayout, например, на ConstraintLayout. Также добавьте идентификатор для TextView.

Передаем данные через Bundle

В классе FragmentOne переопределим метод onViewCreated и напишем в нем такой код:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) < super.onViewCreated(view, savedInstanceState) val imgButton : ImageButton = view.findViewById(R.id.imgButton) val editText : EditText = view.findViewById(R.id.editText) val bundle = Bundle() imgButton.setOnClickListener < val name = editText.text val hello = "Привет, $name" bundle.putString("MyArg", hello) findNavController().navigate(R.id.fragmentTwo, bundle) >>

Обратите внимание – переменные кнопки и поля для ввода текста мы инициализируем через метод findViewById, как мы это делали на первых уроках. Этот способ требует больше повторяющегося кода в отличие от использования ViewBinding и DataBinding для привязки элементов разметки, однако многие программисты пользуются именно этим способом, как самым простым и надежным.
Объявим также переменную для набора данных Bundle.

Далее присваиваем слушатель кнопке, и по ее нажатию производим следующие действия:

  • Инициализируем переменную name и сохраняем в нее текст из поля ввода;
  • Переменная hello уже содержит текст и добавляет в него текст переменной name;
  • В переменную bundle передаем переменную hello с ключем «MyArg»
  • Обращаемся к контроллеру навигации и реализуем переход к FragmentTwo с передачей идентификатора макета фрагмента и bundle в метод navigate контроллера.

Принимаем данные через Bundle

В коде класса FragmentTwo также переопределяем метод onViewCreated и пишем в него код:

override fun onViewCreated(view: View, savedInstanceState: Bundle?)

Здесь мы инициализируем TextView и переменную text. В эту переменную мы сохраняем значение из переданного Bundle, обращаясь к нему через его геттер arguments, и получаем строку с ключом «MyArg». Далее передаем ее для отображения в текстовое поле.

Тестирование приложения

Запустите приложения на эмуляторе или смартфоне. На первом экране с полем ввода и кнопкой введите имя и нажмите кнопку отправки. Откроется второй экран, где будет отображен текст приветствия с введенным именем.

Подключение в проект Safe Args

Несмотря на простоту этого способа, официальные разработчики все же рекомендуют выполнять передачу данных между фрагментами посредством Safe Args, так как этот способ является типобезопасным.

Чтобы добавить поддержку Safe Args в проект, в файле build.gradle верхнего уровня пропишите:

buildscript < repositories < google() >dependencies < def nav_version = "2.3.3" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" >>

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

Если ваш проект на языке Java, или смешанный, на Java и Kotlin, добавьте эту строку в файл build.gradle модуля app вашего приложения:

apply plugin: "androidx.navigation.safeargs"

А если проект только на Kotlin, то добавьте эту:

apply plugin: "androidx.navigation.safeargs.kotlin"

Также вы должны для поддержки AndroidX указать android.useAndroidX=true в файле gradle.properties .

Передача данных через Safe Args

Теперь изменим код фрагментов.

Метод onViewCreated класса FragmentOne:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) < super.onViewCreated(view, savedInstanceState) val imgButton : ImageButton = view.findViewById(R.id.imgButton) val editText : EditText = view.findViewById(R.id.editText) //val bundle = Bundle() imgButton.setOnClickListener < val name = editText.text val hello = "Привет, $name" /*bundle.putString("MyArg", hello) findNavController().navigate(R.id.fragmentTwo, bundle)*/ val action = FragmentOneDirections.actionFragmentOneToFragmentTwo(hello) findNavController().navigate(action) >>

Поскольку мы теперь не работаем с Bundle напрямую, в методе onViewCreated закомментируем строку объявления его переменной.

Далее в слушателе нажатия кнопки вместо сохранения аргумента в bundle и передачи его контроллеру, будем использовать сгенерированный средой разработки класс FragmentOneDirections. В его функцию actionFragmentOneToFragmentTwo передаем строковую переменную hello и затем это все передаем методу navigate контроллера навигации.

А если мы откроем класс FragmentOneDirections, то увидим, что внутри он использует все тот же Bundle.

Теперь изменим код метода onViewCreated класса FragmentTwo:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) < super.onViewCreated(view, savedInstanceState) val tv :TextView = view.findViewById(R.id.tvFragmentTwo) //val text = arguments?.getString("MyArg") val args: FragmentTwoArgs by navArgs() val text = args.MyArg tv.text = text >

Вместо получения строки из Bundle инициализируем переменную класса FragmentTwoArgs, который хранит данные, переданные нами из первого фрагмента. Мы можем извлечь данные, обращаясь к свойству экземпляра класса по имени аргумента, которое мы указали при его создании в графе навигации. Здесь мы передаем его в текстовое поле.

Если открыть FragmentTwoArgs, мы увидим, что это дата-класс с публичной переменной, имя которой совпадает с именем нашего аргумента. Эта переменная получает свое значение посредством Bundle, а также имеет значение по умолчанию – это фраза «Hello, Android!» – та самая, которую мы указали как значение по умолчанию аргумента в графе навигации.

Оба класса – FragmentTwoArgs и FragmentOneDirections – сгенерированы средой разработки, их можно найти в папке build модуля app нашего проекта.

Тестирование приложения

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

Если в коде второго фрагмента ничего не передать в переменную action, то на втором экране будет отображаться значение аргумента по умолчанию.

Исходный код

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

Как в 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 по ссылкам выше.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *