DialogFragment via les modifications de configuration et la mort du processus.

Nov 29 2022
Bonjour à tous, Cela fait longtemps que j'utilise le médium pour continuer à apprendre de nouvelles choses et il est maintenant temps pour moi d'écrire mon premier article. Probablement le début de bien d'autres.

Salut tout le monde,

Cela fait longtemps que j'utilise le médium pour continuer à apprendre de nouvelles choses et il est maintenant temps pour moi d'écrire mon premier article. Probablement le début de bien d'autres.

Introduction

Pourquoi suis-je sur le point de parler de DialogFragment lié aux changements de configuration et à la mort des processus ? Parce que nous nous soucions de l'expérience utilisateur et que nous n'aimons pas les rapports de plantage ou les comportements étranges en cas de problème.

Cet article concerne DialogFragment mais la réflexion est la même pour n'importe quel Fragment.

Pour effectuer des changements de configuration

Un moyen simple d'effectuer des "modifications de configuration" dans votre application consiste à basculer entre les modes clair/sombre. En bref, les fragments/activités seront recréés avec des arguments enregistrés et saveInstanceState mais les ViewModels existeront toujours. Lors d'une mort de processus, les modèles de vue, les activités et les fragments seront tous recréés.

Provoquer la mort d'un processus

- Option développeur (astuce) : Options développeur -> Ne pas conserver les activités -> activer.
- Option en situation réelle : exécutez un tas d'applications gourmandes ou créez votre propre application qui consommera beaucoup de mémoire (en utilisant une boucle par exemple) jusqu'à ce que l'appareil estime que les applications précédemment ouvertes devraient être libérées.

Chaque fois que vous reviendrez sur une application qui a été libérée, elle sera relancée avec des arguments /sauvedInstanceState save. Celles-ci devraient être la seule source de données de vérité dont vous devriez vraiment vous soucier.

C'est toujours une bonne pratique de tester tous les écrans de votre application en utilisant les changements de configuration / la mort du processus pour s'assurer que tout fonctionne comme prévu. Si vous avez des plantages ou des comportements étranges à ce sujet, ne l'ignorez pas car c'est "rare"...
De plus, pour certains appareils qui atteignent moins de 15% d'autonomie, l'appareil passera automatiquement en mode sombre pour économiser la batterie ce qui signifie qu'un utilisateur peut effectuer des actions aléatoires dans votre application et que votre application peut soudainement être recréée à cause de cela.

Eh bien, j'ai introduit de mauvaises nouvelles, ne laissez pas l'article supplies :(

Si je prends le temps d'expliquer ces problématiques, c'est parce qu'il y a beaucoup d'articles brillants ici qui expliquent comment écrire du beau code en kotlin en utilisant DSL avec des paramètres de fonction envoyés directement dans un dialogue.
Je ne suis pas nouveau dans le développement Android et malgré le fait que Kotlin soit un langage génial, je l'adore à coup sûr, certains articles devraient s'occuper des changements de configuration ou de la mort du processus, sinon, ils ne servent pas les développeurs juniors.

Les choses se disent

1°) Un fragment (et bien sûr DialogFragment) doit toujours avoir un constructeur vide. Essayez de passer en mode sombre/clair alors qu'un fragment a un constructeur avec des paramètres ; ça ne finit pas bien.

De plus, lorsque vous naviguez vers un DialogFragment, ne passez pas de fonctions à celui-ci via des fonctions définies dans votre DialogFragment qui l'enregistre en variable locale car lors d'un changement de configuration, le fragment sera restauré mais sans que ces fonctions soient appelées ; vos variables locales ne seront donc pas initialisées. Seuls les constructeurs vides et les fonctions remplacées (comme onCreate, onCreateDialog, etc.) de DialogFragment seront appelés. Les arguments et saveInstanceState ne devraient être que votre préoccupation.

2°) Ne passez pas de fonctions en argument à votre fragment via un bundle. Une fonction n'est pas parcelable/sérialisable, l'application plantera lors des changements de configuration.

Passer des fonctions à n'importe quel fragment est une très mauvaise pratique ; De plus, cela peut entraîner des fuites de mémoire, alors ne le faites pas. Je sais, c'est facile de dire : ' Hé, si tu cliques sur succès, va sur l'écran X ' avec un lambda. Je l'ai fait aussi au début du développement d'Android mais croyez-moi, vous ne voulez pas livrer un produit instable à votre utilisateur en production.

3°) Utiliser le ViewModel partagé ? Sûrement pas ! D'abord, à cause de la mort du processus, puis parce que vous ne voulez pas avoir mal à la tête lors du débogage pour savoir pourquoi cela ne fonctionne pas dans certains cas. Les modèles de vue partagés sont le concept le plus sujet aux erreurs. Cela peut être utile dans quelques cas mais au maximum, ne l'utilisez pas.

La solution

Effectuez une requête sur votre DialogFragment, attendez un résultat, puis effectuez quelque chose en fonction du résultat reçu.

Un cas d'utilisation simple :

Vous souhaitez recevoir une réponse de votre utilisateur pour approuver une action spécifique. L'action 'Ok' implique que vous naviguez vers un écran suivant avec des données spécifiques liées aux entrées de l'utilisateur.
Dans notre cas d'utilisation simple, l'action de l'utilisateur pourrait être : 'Ok', 'Annuler' (2 boutons de l'interface utilisateur) et 'Rejeter' (icône de retour de l'interface utilisateur ou pression arrière du système).
Mais par simplicité, nous considérerons que 'Ignorer' est traité comme une action 'Annuler'. Ainsi, l'interaction de l'utilisateur est limitée à deux types d'interaction : 'Ok' ou 'Annuler'.

On peut potentiellement demander plusieurs fois un même type de DialogFragment dans un ou plusieurs fragment(s) selon de nombreux contextes. C'est pourquoi nous fournirons une "requestKey" qui est, dans la plupart des cas, unique à un fragment spécifique. La seule chose que nous voulons est une réponse à ce que nous avons demandé : 'Ok' ou 'Annuler'.

L'idée est d'émettre une 'requestKey' vers un DialogFragment et d'attendre une action de l'utilisateur : 'Ok' ou 'Cancel' pour cette 'requestKey'.

Il existe de nombreuses façons de communiquer entre votre fragment de dialogue et le fragment exécutant la requête.

J'utilise personnellement :

- setFragmentResult de DialogFragment
- setFragmentResultListener dans onCreate() de votre Fragment effectuant une requête

La fonction setFragmentResult a deux paramètres : 'requestKey: String' et 'result: Bundle'. Cette fonction propage les données via parentFragmentManager, ce qui signifie que tout fragment qui demande avec la même requestKey est susceptible d'attraper le résultat.

Vous trouverez ci-dessous du code "simplifié" en utilisant des extensions de fonction (de cette façon, nous nous concentrons uniquement sur ce qui compte vraiment et nous pouvons facilement comprendre qui appelle quoi. En pratique, bien sûr, copiez/collez la fonction body de la fonction fragment/dialogfragment existante) :

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"

Voici quelques améliorations à l'aide de flux

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

Vous remarquerez qu'il y a beaucoup de code pour pas tellement. De plus, vous pouvez penser que vous devrez dupliquer ces codes passe-partout sur chaque fragment en utilisant votre fragment de dialogue. Oui et non. Delegate est un outil puissant et vous permettra d'écrire ce code une seule fois. Faites-moi savoir si vous êtes intéressé par une telle amélioration à ajouter dans cet article ou non.

Si vous apprenez quelque chose ou si vous êtes déjà familier mais pensez que d'autres devraient le savoir, n'hésitez pas à applaudir :).

J'aime partager des connaissances et si vous ne comprenez pas quelque chose ou si j'écris quelque chose de mal, n'hésitez pas à commenter ci-dessous. J'y répondrai et j'apporterai des modifications en cas d'erreurs.

Juste pour partager, quelques mecs intéressants avec qui je vous recommande d'apprendre :

Jake Wharton
- Gazouillement:https://twitter.com/JakeWharton
- Moyen:https://medium.com/@techyourchance
Vassili Zoukanov
- Twitter :https://twitter.com/VasiliyZukanov
- Moyen :https://medium.com/@techyourchance
Christophe Beyls
- Twitter:https://twitter.com/BladeCoder
- Moyen:https://medium.com/@bladecoder
Gabor Varadi
- Twitter:https://twitter.com/Zhuinden
- Moyen:https://medium.com/@zhuinden

Note : Je ne connais pas personnellement ces gars, je les suis juste parce qu'ils sont bons et qu'ils sont actifs sur Twitter.