DialogFragment через изменения конфигурации и смерть процесса.

Nov 29 2022
Привет всем, Я уже давно использую среду, чтобы узнавать что-то новое, и сейчас настало время написать мою первую статью. Вероятно, начало многих других.

Всем привет,

Я уже давно использую среду, чтобы узнавать что-то новое, и сейчас настало время написать мою первую статью. Вероятно, начало многих других.

Введение

Почему я собираюсь говорить о 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

Примечание: я лично не знаком с этими ребятами, я просто слежу за ними, потому что они хороши и активны в Твиттере.