DialogFragment a través de cambios de configuración y muerte del proceso.
Hola a todos,
Ha pasado mucho tiempo que estoy usando medium para seguir aprendiendo cosas nuevas y ahora es el momento de escribir mi primer artículo. Probablemente el comienzo de muchos otros.
Introducción
¿Por qué estoy a punto de hablar sobre DialogFragment relacionado con los cambios de configuración y la muerte del proceso? Porque nos preocupamos por la experiencia del usuario y no nos gustan los informes de fallas o el comportamiento extraño cuando algo sale mal.
Este artículo trata sobre DialogFragment pero la reflexión es la misma para cualquier Fragment.
Para realizar cambios de configuración
Una manera fácil de realizar "cambios de configuración" en su aplicación es cambiar entre el modo claro/oscuro. En resumen, los fragmentos/actividades se volverán a crear con argumentos guardados y estado de instancia guardado, pero los modelos de vista seguirán existiendo. En un proceso de muerte, se recrearán ViewModels, Activities y Fragments.
Provocar un proceso de muerte.
- Opción de desarrollador (manera fácil): Opciones de desarrollador -> No mantener actividades -> activar.
- Opción de situación real: ejecute un montón de aplicaciones codiciosas o cree su propia aplicación que consumirá una gran cantidad de memoria (usando el bucle, por ejemplo) hasta que el dispositivo estime que las aplicaciones abiertas anteriores deberían liberarse.
Cada vez que regrese a cualquier aplicación que se haya liberado, se reiniciará con los argumentos/guardadoInstanceState guardado. Estos deberían ser la única fuente de datos verídicos que realmente debería importar.
Siempre es una buena práctica probar todas las pantallas de su aplicación usando cambios de configuración/muerte del proceso para asegurarse de que todo funcione como se espera. Si tiene bloqueos o comportamientos extraños al respecto, no lo ignore porque es 'raro'...
Además, para algunos dispositivos que alcanzan menos del 15% de la duración de la batería, el dispositivo cambiará automáticamente al modo oscuro para ahorrar batería. lo que significa que un usuario podría realizar algunas acciones aleatorias en su aplicación y su aplicación puede volver a crearse repentinamente debido a eso.
Bueno, introduje una mala noticia, no abandonen el artículo por favor :(
Si me tomo el tiempo para explicar estas problemáticas, es porque hay muchos artículos brillantes aquí que explican cómo escribir un código hermoso en Kotlin usando DSL con parámetros de función enviados directamente a un cuadro de diálogo.
No soy nuevo en el desarrollo de Android y, a pesar del hecho de que Kotlin es un lenguaje increíble, me encanta, algunos artículos deberían ocuparse de los cambios de configuración o la muerte del proceso; de lo contrario, no sirven a los desarrolladores junior.
cosas que se dicen
1°) Un fragmento (y por supuesto DialogFragment) siempre debe tener un constructor vacío. Intente cambiar al modo oscuro/claro mientras un fragmento tiene un constructor con parámetros; no termina bien.
Además, cuando navegue a un DialogFragment, no le pase funciones a este a través de funciones definidas en su DialogFragment, que lo guarda en una variable local porque al cambiar la configuración, el fragmento se restaurará pero sin llamar a estas funciones; por lo que sus variables locales no se inicializarán. Solo se llamará al constructor vacío y las funciones anuladas (como onCreate, onCreateDialog, etc.) de DialogFragment. Los argumentos y el estado de instancia guardado solo deberían ser su preocupación.
2°) No pase funciones como argumento a su fragmento a través de un paquete. Una función no se puede empaquetar/serializar, la aplicación fallará en los cambios de configuración.
Pasar funciones a cualquier fragmento es una muy mala práctica; Además, puede provocar pérdidas de memoria, así que no hagas eso. Lo sé, es fácil decir: ' Oye, si haces clic en éxito, ve a la pantalla X ' con una lambda. También lo hice al comienzo del desarrollo de Android, pero créanme, no desea entregar un producto inestable a su usuario en producción.
3°) ¿Usando ViewModel compartido? ¡Diablos no! Primero, debido a la muerte del proceso y luego porque no quiere tener un dolor de cabeza durante la depuración de por qué no funciona en algunos casos. Los modelos de vista compartidos son el concepto más propenso a errores. Puede ser útil en algunos casos, pero en la mayoría, no lo use.
Solución
Realice una solicitud a su DialogFragment, espere un resultado y luego realice algo según el resultado recibido.
Un caso de uso simple:
Desea recibir una respuesta de su usuario para aprobar una acción específica. La acción 'Ok' implica que navegará a la siguiente pantalla con datos específicos relacionados con las entradas del usuario.
En nuestro caso de uso simple, la acción del usuario podría ser: 'Aceptar', 'Cancelar' (2 botones de la IU) y 'Descartar' (icono de retroceso de la IU o presión del sistema hacia atrás).
Pero por simplicidad, consideraremos que 'Descartar' se maneja como una acción de 'Cancelar'. Por lo tanto, la interacción del usuario se limita a dos tipos de interacción: 'Aceptar' o 'Cancelar'.
Potencialmente podemos solicitar varias veces un mismo tipo de DialogFragment en uno o varios fragmentos dependiendo de muchos contextos. Es por eso que proporcionaremos una 'clave de solicitud' que, en la mayoría de los casos, es única para un fragmento específico. Lo único que queremos es una respuesta a lo que solicitamos: 'Ok' o 'Cancelar'.
La idea es emitir una 'requestKey' a un DialogFragment y esperar una acción del usuario: 'Ok' o 'Cancel' para esta 'requestKey'.
Hay muchas formas de comunicarse entre su fragmento de diálogo y el fragmento que realiza la solicitud.
Personalmente estoy usando:
- setFragmentResult de DialogFragment
- setFragmentResultListener en onCreate() desde su Fragmento realizando una solicitud
La función setFragmentResult tiene dos parámetros: 'requestKey: String' y 'result: Bundle'. Esta función propaga datos a través de parentFragmentManager, lo que significa que cualquier fragmento que solicite con la misma clave de solicitud es susceptible de captar el resultado.
A continuación se muestra un código 'simplificado' mediante el uso de extensiones de función (de esa manera, solo nos enfocamos en lo que realmente importa y podemos entender fácilmente quién llama a qué. En la práctica, por supuesto, la función copiar/pegar cuerpo de la función fragmento/fragmento de diálogo existente):
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"
A continuación se muestran algunas mejoras utilizando flujos
/***************************************
* 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()
}
}
}
}
Notarás que hay mucho código para no tanto. Además, puede pensar que necesitará duplicar este código repetitivo en cada fragmento usando su fragmento de diálogo. Si y no. Delegate es una herramienta poderosa y te permitirá escribir este código solo una vez. Avíseme si está interesado en una mejora de este tipo para agregar en este artículo o no.
Si aprende algo o si ya está familiarizado pero siente que otros deberían saber sobre esto, no dude en aplaudir :).
Me gusta compartir conocimientos y si no entiendes algo o si escribo algo mal, no dudes en comentar abajo. La responderé y haré cambios en caso de errores.
Solo por compartir, algunos chicos interesantes con los que te recomiendo aprender:
Jake Wharton
-Twitter:https://twitter.com/JakeWharton
- Medio:https://medium.com/@techyourchance
Vasiliy Zukanov
- Twitter :https://twitter.com/VasiliyZukanov
- Medio :https://medium.com/@techyourchance
Christophe Beyls
-Twitter:https://twitter.com/BladeCoder
- Medio:https://medium.com/@bladecoder
Gabor Varadi
- Twitter:https://twitter.com/Zhuinden
- Medio:https://medium.com/@zhuinden
Nota: No conozco personalmente a estos muchachos, solo los sigo porque son buenos y están activos en Twitter.