DialogFragment por meio de alterações de configuração e morte do processo.

Nov 29 2022
Olá a todos, Já faz muito tempo que estou usando o medium para continuar aprendendo coisas novas e agora é a hora de escrever meu primeiro artigo. Provavelmente o início de muitos outros.

Oi pessoal,

Já faz muito tempo que estou usando o medium para continuar aprendendo coisas novas e agora é a hora de escrever meu primeiro artigo. Provavelmente o início de muitos outros.

Introdução

Por que estou prestes a falar sobre DialogFragment relacionado a alterações de configuração e morte do processo? Porque nos preocupamos com a experiência do usuário e não gostamos de relatórios de falhas ou comportamentos estranhos quando algo dá errado.

Este artigo é sobre DialogFragment mas a reflexão é a mesma para qualquer Fragment.

Para realizar alterações de configuração

Uma maneira fácil de realizar “alterações de configuração” em seu aplicativo é alternar entre o modo claro/escuro. Resumindo, Fragments/Activities serão recriados com argumentos salvos e saveInstanceState, mas ViewModels ainda existirão. Na morte de um processo, ViewModels, Activities e Fragments serão todos recriados.

Provocar um processo de morte

- Opção do desenvolvedor (maneira fácil): Opções do desenvolvedor -> Não manter atividades -> ativar.
- Opção de situação real: execute um monte de aplicativos gananciosos ou crie seu próprio aplicativo que consumirá muitas memórias (usando loop, por exemplo) até que o dispositivo estime que os aplicativos abertos anteriores devem ser liberados.

Cada vez que você voltar para qualquer aplicativo que foi liberado, ele será reiniciado com argumentos/salvoInstanceState salvos. Esses devem ser a única fonte de dados verdadeiros com os quais você realmente deve se preocupar.

É sempre uma boa prática testar todas as telas do seu aplicativo usando alterações de configuração / morte do processo para garantir que tudo funcione conforme o esperado. Se você tiver travamentos ou comportamentos estranhos sobre isso, não ignore porque é 'raro'...
Além disso, para alguns dispositivos que atingem menos de 15% da duração da bateria, o dispositivo mudará automaticamente para o modo escuro para economizar bateria o que significa que um usuário pode executar algumas ações aleatórias em seu aplicativo e seu aplicativo pode ser recriado repentinamente por causa disso.

Bem, eu apresentei algumas más notícias, não deixe os fundamentos do artigo :(

Se eu levar algum tempo para explicar essas problemáticas, é porque há muitos artigos brilhantes aqui que explicam como escrever um código bonito em kotlin usando DSL com parâmetros de função enviados diretamente para um diálogo.
Não sou novo no desenvolvimento android e apesar do fato de Kotlin ser uma linguagem incrível, eu adoro com certeza, alguns artigos deveriam cuidar de alterações de configuração ou morte do processo, caso contrário, eles não servem para desenvolvedores juniores.

coisas sendo ditas

1°) Um fragmento (e claro DialogFragment) deve sempre ter um construtor vazio. Tente alternar para o modo escuro/claro enquanto um fragmento tiver um construtor com parâmetros; não acaba bem.

Além disso, ao navegar para um DialogFragment, não passe funções para este através de funções definidas em seu DialogFragment que o salva em variável local, pois ao alterar a configuração, o fragmento será restaurado, mas sem essas funções chamadas; então suas variáveis ​​locais não serão inicializadas. Somente construtores vazios e funções substituídas (como onCreate, onCreateDialog etc.) de DialogFragment serão chamados. Argumentos e saveInstanceState devem ser apenas sua preocupação.

2°) Não passe funções como argumento para seu fragmento através de um bundle. Uma função não é parcelavel/serializável, o aplicativo falhará com as alterações de configuração.

Passar funções para qualquer fragmento é uma prática muito ruim; Além disso, pode levar a vazamentos de memória, então não faça isso. Eu sei, é fácil dizer: ' Ei, se você clicar em sucesso, vá para a tela X ' com um lambda. Eu também fiz isso no início do desenvolvimento do Android, mas acredite, você não quer entregar um produto instável para o usuário em produção.

3°) Usando ViewModel compartilhado ? De jeito nenhum ! Primeiro, por causa da morte do processo e depois porque você não quer ter dor de cabeça durante a depuração porque não está funcionando em alguns casos. Viewmodels compartilhados é o conceito mais propenso a erros. Pode ser útil em alguns casos, mas no máximo, não o use.

Solução

Execute uma solicitação ao seu DialogFragment, aguarde um resultado e execute algo dependendo do resultado recebido.

Um caso de uso simples:

Você deseja receber uma resposta do seu usuário para aprovar uma ação específica. A ação 'Ok' implica que você navegará para uma próxima tela com dados específicos relacionados às entradas do usuário.
Em nosso caso de uso simples, a ação do usuário pode ser: 'Ok', 'Cancelar' (2 botões da interface do usuário) e 'Dispensar' (ícone de retorno da interface do usuário ou pressionamento de retorno do sistema).
Mas, para simplificar, consideraremos que 'Dispensar' é tratado como uma ação 'Cancelar'. Portanto, a interação do usuário é limitada a dois tipos de interação: 'Ok' ou 'Cancelar'.

Podemos potencialmente solicitar várias vezes um mesmo tipo de DialogFragment em um ou vários fragmentos, dependendo de muitos contextos. É por isso que forneceremos um 'requestKey' que é, na maioria dos casos, exclusivo para um fragmento específico. A única coisa que queremos é uma resposta sobre o que pedimos: 'Ok' ou 'Cancelar'.

A ideia é emitir um 'requestKey' para um DialogFragment e aguardar uma ação do usuário: 'Ok' ou 'Cancel' para este 'requestKey'.

Há muitas maneiras de se comunicar entre seu fragmento de diálogo e o fragmento que executa a solicitação.

Eu pessoalmente estou usando:

- setFragmentResult de DialogFragment
- setFragmentResultListener em onCreate() de seu Fragment realizando uma solicitação

A função setFragmentResult tem dois parâmetros: 'requestKey: String' e 'result: Bundle'. Essa função propaga dados por meio de parentFragmentManager, o que significa que qualquer fragmento que esteja solicitando com a mesma requestKey é suscetível a capturar o resultado.

Abaixo estão alguns códigos 'simplificados' usando extensões de função (dessa forma, focamos apenas no que realmente importa e podemos entender facilmente quem chama o quê. Na prática, é claro, copie/cole a função do corpo da função existente de fragmento/fragmento de diálogo):

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"

Abaixo estão algumas melhorias usando fluxos

/***************************************
 * 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()
            }
        }
    }
}

Você notará que há muito código para pouco. Além disso, você pode pensar que precisará duplicar esse código clichê em cada fragmento usando seu fragmento de diálogo. Sim e não. Delegate é uma ferramenta poderosa e permitirá que você escreva este código apenas uma vez. Deixe-me saber se você está interessado em tal melhoria para adicionar neste artigo ou não.

Se você aprender algo ou se já estiver familiarizado, mas sentir que outras pessoas deveriam saber sobre isso, sinta-se à vontade para bater palmas :).

Gosto de compartilhar conhecimento e se você não entendeu algo ou se escrevi algo errado, não hesite em comentar abaixo. Eu responderei e farei alterações em caso de erros.

Apenas para compartilhar, alguns caras interessantes com os quais recomendo que você aprenda:

Jake Wharton
- Twitter:https://twitter.com/JakeWharton
- Médio:https://medium.com/@techyourchance
Vasiliy Zukanov
- Twitter:https://twitter.com/VasiliyZukanov
- Médio:https://medium.com/@techyourchance
Christophe Beyls
- Twitter:https://twitter.com/BladeCoder
- Médio:https://medium.com/@bladecoder
Gabor Varadi
- Twitter:https://twitter.com/Zhuinden
- Médio:https://medium.com/@zhuinden

Nota: Eu não conheço esses caras pessoalmente, apenas os sigo porque eles são bons e estão ativos no Twitter.