DialogFragment через изменения конфигурации и смерть процесса.
Всем привет,
Я уже давно использую среду, чтобы узнавать что-то новое, и сейчас настало время написать мою первую статью. Вероятно, начало многих других.
Введение
Почему я собираюсь говорить о DialogFragment, связанном с изменениями конфигурации и смертью процесса? Потому что мы заботимся о пользовательском опыте и нам не нравятся отчеты о сбоях или странное поведение, когда что-то идет не так.
Эта статья о DialogFragment, но отражение одинаково для любого фрагмента.
Для внесения изменений в конфигурацию
Простой способ выполнить «изменение конфигурации» в вашем приложении — переключиться между светлым и темным режимом. Короче говоря, фрагменты/действия будут воссозданы с сохраненными аргументами и saveInstanceState, но ViewModels все еще будут существовать. В случае смерти процесса ViewModels, Activity и Fragments будут созданы заново.
Спровоцировать смерть процесса
- Вариант разработчика (простой способ): Параметры разработчика -> Не сохранять действия -> включить.
- Вариант реальной ситуации: запустить кучу жадных приложений или создать собственное приложение, которое будет потреблять много памяти (например, с использованием цикла), пока устройство не оценит, что предыдущие открытые приложения должны быть освобождены.
Каждый раз, когда вы вернетесь к любому бесплатному приложению, оно будет перезапущено с сохраненными аргументами / saveInstanceState. Тезисы должны быть единственным источником истинных данных, о которых вы должны действительно заботиться.
Хорошей практикой всегда является тестирование всех экранов в вашем приложении с использованием изменений конфигурации/отключения процесса, чтобы убедиться, что все работает должным образом. Если у вас есть сбои или странное поведение по этому поводу, не игнорируйте это, потому что это «редко» ...
Более того, для некоторых устройств, которые достигают менее 15% времени автономной работы, устройство автоматически переключается в темный режим для экономии заряда батареи. это означает, что пользователь может выполнять некоторые случайные действия в вашем приложении, и из-за этого ваше приложение может быть внезапно воссоздано.
Что ж, я представил плохие новости, пожалуйста, не покидайте статью :(
Если мне потребуется время, чтобы объяснить эти проблемы, это потому, что здесь есть много блестящих статей, в которых объясняется, как написать красивый код на kotlin с использованием DSL с параметрами функции, отправляемыми непосредственно в диалог.
Я не новичок в Android-разработке, и, несмотря на то, что Kotlin — отличный язык, он мне определенно нравится, некоторые статьи должны быть посвящены изменениям конфигурации или смерти процесса, иначе они не служат младшим разработчикам.
Что говорят
1°) Фрагмент (и, конечно, DialogFragment) всегда должен иметь пустой конструктор. Попробуйте переключиться в темный/светлый режим, пока у фрагмента есть конструктор с параметрами; это плохо кончается.
Кроме того, при переходе к DialogFragment не передавайте функции этому через функции, определенные в вашем DialogFragment, которые сохраняют его в локальной переменной, потому что при изменении конфигурации фрагмент будет восстановлен, но без вызова этих функций; поэтому ваши локальные переменные будут неинициализированы. Будут вызываться только пустой конструктор и переопределенные функции (такие как onCreate, onCreateDialog и т. д.) из DialogFragment. Аргументы и saveInstanceState должны быть только вашей заботой.
2°) Не передавайте функции в качестве аргумента вашему фрагменту через пакет. Функция не может быть разделена/сериализована, приложение аварийно завершает работу при изменении конфигурации.
Передача функций любому фрагменту — очень плохая практика; Более того, это может привести к утечке памяти, поэтому не делайте этого. Я знаю, легко сказать: « Эй, если вы нажмете на успех, перейдите на экран X » с лямбдой. Я тоже так делал в начале разработки Android, но поверьте мне, вы не хотите предоставлять нестабильный продукт своему пользователю в производственной среде.
3°) Использование общей ViewModel? Конечно нет ! Во-первых, из-за смерти процесса, а затем из-за того, что вы не хотите иметь головную боль при отладке, почему он не работает в каком-то случае. Общие модели просмотра — наиболее подверженная ошибкам концепция. Это может быть полезно в некоторых случаях, но в большинстве случаев не используйте его.
Решение
Выполните запрос к вашему DialogFragment, дождитесь результата, а затем выполните что-то в зависимости от полученного результата.
Простой вариант использования:
Вы хотите получить ответ от вашего пользователя для одобрения конкретного действия. Действие «ОК» означает, что вы перейдете к следующему экрану с конкретными данными, связанными с пользовательским вводом.
В нашем простом случае действия пользователя могут быть: «ОК», «Отмена» (2 кнопки пользовательского интерфейса) и «Отклонить» (значок возврата пользовательского интерфейса или нажатие системы).
Но для простоты мы будем считать, что «Отклонить» обрабатывается как действие «Отмена». Таким образом, взаимодействие с пользователем ограничено двумя типами взаимодействия: «ОК» или «Отмена».
Мы потенциально можем запросить несколько раз один и тот же тип DialogFragment в одном или нескольких фрагментах в зависимости от многих контекстов. Вот почему мы предоставим 'requestKey', который в большинстве случаев уникален для конкретного фрагмента. Единственное, что нам нужно, это ответ на то, что мы просили: «ОК» или «Отмена».
Идея состоит в том, чтобы отправить «requestKey» в DialogFragment и дождаться действия пользователя: «ОК» или «Отмена» для этого «requestKey».
Существует много способов связи между вашим фрагментом диалога и фрагментом, выполняющим запрос.
Я лично использую:
- setFragmentResult from DialogFragment
- setFragmentResultListener в onCreate() из вашего фрагмента, выполняющего запрос
Функция setFragmentResult имеет два параметра: «requestKey: String» и «result: Bundle». Эта функция распространяет данные через parentFragmentManager, что означает, что любой фрагмент, который запрашивает тот же ключ requestKey, может перехватить результат.
Ниже приведен некоторый «упрощенный» код с использованием расширений функций (таким образом, мы фокусируемся только на том, что действительно важно, и мы можем легко понять, кто что вызывает. На практике, конечно, копирование/вставка функции тела существующей функции фрагмента/диалогового фрагмента):
enum class UserAction {
OK, CANCEL
}
/*******************
* DIALOG FRAGMENT *
*******************/
/**
* In your DialogFragment, the contract mentions that REQUEST_KEY_ARG_KEY is a mandatory arg.
* You will notice that it works for Fragment too, not only DialogFragment
* In our case, a device back press or UI back press will invoke onDismiss(), just override it and call this function
* @param userAction 'Yes' or 'Cancel'
*/
fun DialogFragment.deliverUserAction(userAction: UserAction) {
setFragmentResult(requireArguments().getString(REQUEST_KEY_ARG_KEY)!!,
bundleOf(USER_ACTION_RESULT_KEY to userAction,
REQUEST_DATA_BUNDLE_KEY to requireArguments().getBundle(REQUEST_DATA_BUNDLE_KEY)))
}
/**
* Defined in public in your DialogFragment
*/
const val REQUEST_KEY_ARG_KEY = "request_key" // Mandatory in arg
const val REQUEST_DATA_BUNDLE_KEY = "request_data" // Optional in arg and result, it is just a way to forward contextual data
const val USER_ACTION_RESULT_KEY = "user_action" // Mandatory in result
/***********************************
* FRAGMENT REQUESTING USER ACTION *
***********************************/
/**
* Listen to a user action
* Of cource, use it in real override onCreate function
*/
fun Fragment.fakeOverrideOnCreate() {
setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { _, bundle: Bundle ->
// Called once in Lifecycle.State.ON_START
val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
when (userAction) {
UserAction.OK -> {
TODO("Do something with ${requestData} if you need it to navigate to next screen")
}
UserAction.CANCEL -> TODO()
}
}
}
/**
* Request a user action
* @see fakeOverrideOnCreate directly related to it
* @param requestKey requestKey is mandatory
* @param requestData optional, some data you may need to forward to dialog fragment and get back in setFragmentResultListener
*/
fun Fragment.navigateToMyDialogFragmentX(requestKey: String, requestData: Bundle?) {
bundleOf(
REQUEST_KEY_ARG_KEY to requestKey,
REQUEST_DATA_BUNDLE_KEY to requestData
)
// etc.
// depending on if you are using Navigation component or not
}
fun Fragment.calledAnywhereInFragmentRequestingUserAction1() {
navigateToMyDialogFragmentX(requestKey = REQUEST_KEY_TO_GO_TO_SCREEN_B,
requestData = bundleOf("input1" to "Here", "input2" to "we", "input3" to "go!"))
}
/**
* Defined in private in your fragment requesting user action
*/
private const val REQUEST_KEY_TO_GO_TO_SCREEN_B = "request_key_to_go_to_screen_B"
Ниже приведены некоторые улучшения с использованием потоков
/***************************************
* YOURFRAGMENT REQUESTING USER ACTION *
***************************************/
private val _userActionResultMutableStateFlow: MutableStateFlow<Pair<String, Bundle>?> = MutableStateFlow(null)
private val userActionResultStateFlow: StateFlow<Pair<String, Bundle>?> get() = _userActionResultMutableStateFlow
/**
* Listen to a user action
* Of cource, use it in real override onCreate function
*/
fun Fragment.fakeOverrideOnCreate() {
setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { requestKey, bundle: Bundle ->
_userActionResultMutableStateFlow.value = requestKey to bundle
}
}
/**
* Listen to a user action and consume it only when fragment is the currentBackStackEntry
* Of cource, use it in real override onViewCreated function
*/
fun Fragment.fakeOverrideOnViewCreated() {
viewLifecycleOwner.lifecycleScope.launch {
combine((findNavController().currentBackStackEntryFlow), userActionResultStateFlow) { currentBackStackEntry, userActionResult ->
if (currentBackStackEntry.destination.id in R.id.YOURFRAGMENT_DEFINED_IN_NAV_GRAPH) {
userActionResult
} else {
null
}
}.filterNotNull().collect { (requestKey, bundle) ->
consumeUserActionResult(requestKey, bundle)
_userActionResultMutableStateFlow.value = null // don't forget to notify that user action has been consumed
}
}
}
/**
* consume user action result
*/
fun consumeUserActionResult(requestKey: String,
bundle: Bundle) {
when (requestKey) {
REQUEST_KEY_TO_GO_TO_SCREEN_B -> {
val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
when (userAction) {
UserAction.OK -> {
TODO("Do something with ${requestData} if you need it to navigate to next screen")
}
UserAction.CANCEL -> TODO()
}
}
}
}
Вы заметите, что много кода не так много. Более того, вы можете подумать, что вам нужно будет дублировать этот шаблонный код на каждом фрагменте, используя ваш фрагмент диалога. Да и нет. Делегат — это мощный инструмент, который позволит вам написать этот код только один раз. Дайте мне знать, если вы заинтересованы в добавлении такого улучшения в эту статью или нет.
Если вы что-то узнали или уже знакомы, но чувствуете, что другие должны знать об этом, смело хлопайте :).
Мне нравится делиться знаниями, и если вы что-то не понимаете или я пишу что-то не так, не стесняйтесь комментировать ниже. Я отвечу на него и внесу изменения в случае ошибок.
Просто для обмена, несколько интересных парней, с которыми я рекомендую вам учиться:
Джейк Уортон
— Твиттер:https://twitter.com/JakeWharton
- Середина:https://medium.com/@techyourchance
Василий Зуканов
- Твиттер:https://twitter.com/VasiliyZukanov
- Середина :https://medium.com/@techyourchance
Кристоф Бейлс
— Твиттер:https://twitter.com/BladeCoder
- Середина:https://medium.com/@bladecoder
Габор Варади
— Твиттер:https://twitter.com/Zhuinden
- Середина:https://medium.com/@zhuinden
Примечание: я лично не знаком с этими ребятами, я просто слежу за ними, потому что они хороши и активны в Твиттере.