Architecture de navigation Jetpack Compose avec ViewModels | par Tom Seifert | Août 2021
Comment cela fonctionnait dans les temps anciens
Au commencement il y avait l̵i̵g̵h̵t̵ startActivity(AnyActivity::class.java)
chaque fois que nous voulions afficher un nouvel écran, vous utiliseriez alternativement des fragments avec le FragmentManager
et FragmentTransactions
, gérez le(s) backstack(s), pensez à utiliser le ChildFragmentManager
, aussi, chaque fois que vous en avez besoin, rappelez-vous qu’il existe un « ancien » FragmentManager
c’est un SupportFragmentManager
que vous pourriez mélanger etc. Google a décidé que c’était nul et a développé le composant de navigation, nous donnant des graphiques de navigation et un NavController
qui a toute la puissance des temps anciens combinés.
Les bases
Suivons le tutoriel sur le composant de navigation Compose, utilisez la version Compose du nouveau NavController
, et rapidement nous avons quelque chose comme ceci :
class HomeActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyTheme {
Scaffold {
NavigationComponent(navController)
}
}
}
}
}
@Composable
fun NavigationComponent(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable("details") {
DetailScreen()
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
Button(onClick = { navController.navigate("detail") }) {
Text(text = "Go to detail")
}
}
@Composable
fun DetailScreen() {
Text(text = "Detail")
}
Nous avons créé un simple NavHost
avec deux itinéraires, home et detail, où l’écran d’accueil a un bouton pour accéder à l’écran de détails, chacun composé d’un simple champ de texte.
Présentation des ViewModels
Lorsque vous atteignez un certain point, vous souhaiterez introduire une logique plus complexe, et cela se fait généralement avec ViewModels
du package d’architecture Android. Ceci est expliqué en détail ici. Heureusement, ils ont également expliqué comment connecter cela au composant de navigation précédent, qui est décrit ici.
Créons un ViewModel
suivez les tutoriels jusqu’à notre écran de détails et obtenez le texte à afficher à partir de là, tout en le fournissant à partir de notre composant de navigation :
@Composable
fun NavigationComponent(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable("details") {
DetailScreen(viewModel())
}
}
}@Composable
fun DetailScreen(viewModel: DetailViewModel) {
Text(text = viewModel.getDetailText())
}
class DetailViewModel : ViewModel() {
fun getDetailText(): String {
// some imaginary backend call
return "Detail"
}
}
Contrairement aux « anciens temps », où l’on retrouve une ViewModel
au sein d’une activité ou d’un fragment et il était assez évident quand ViewModel.onCleared()
a été appelé, puisqu’il était lié au cycle de vie de l’activité/du fragment, quand s’appelle-t-il maintenant ?
Que vous utilisiez ou non viewModel()
ou avec Hilt hiltViewModel()
pour récupérer votre ViewModel
, les deux appelleront onCleared()
quand le NavHost
termine la transition vers un itinéraire différent. Donc, chaque fois que vous naviguez vers un autre Composable
, il sera nettoyé. Ceci est obtenu en définissant un DisposableEffect
dans l’itinéraire de navigation lorsque le NavHost
est créé et vous pouvez imiter le comportement même si vous n’utilisez pas la bibliothèque de navigation et devez effacer le ViewModel
toi-même.
Mais maintenant mon @Preview est cassé
Avec l’introduction de ViewModels
notre écran de détails a maintenant besoin d’un DetailViewModel
exemple comme entrée. Donc si vous définissez un Preview
n’importe où, c’est cassé maintenant.
@Preview
@Composable
fun DetailScreenPreview() {
DetailScreen(viewModel = ??)
}
Après avoir lu ce numéro, j’ai trouvé cette conversation sur Slack avec Jim Sproch de Google :
La meilleure pratique consiste probablement à éviter de faire référence aux ViewModels AAC dans vos fonctions combinables.
Ah… alors, super, oublions tous les exemples que nous lisons dans les tutoriels et refactorisons nos fonctions composables :
@Composable
fun NavigationComponent(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable("details") {
val viewModel = viewModel<DetailViewModel>()
DetailScreen(viewModel::getDetailText)
}
}
}@Composable
fun DetailScreen(textProvider: () -> String) {
Text(text = textProvider())
}
@Preview
@Composable
fun DetailScreenPreview() {
DetailScreen { "Sample text" }
}
L’idée est que vos fonctions combinables n’acceptent que les entrées de bas niveau comme les lambdas, LiveData
ou une Flow
(dont vous pourriez avoir besoin si vous voulez travailler avec un état). En fait, cela nous permet aussi désormais de visualiser facilement différents textes 🎉.
D’accord, cool, mais comment afficher l’écran d’accueil ?
Rappelez-vous notre écran de démarrage que nous avons créé exactement comme indiqué dans le didacticiel de navigation et passé le NavController
afin que nous puissions accéder à l’écran des détails ?
@Composable
fun HomeScreen(navController: NavController) {
Button(onClick = { navController.navigate("detail") }) {
Text(text = "Go to detail")
}
}
Pour créer un aperçu de cela, nous aurions besoin de fournir une valeur pour NavController
évidemment. Vous n’avez pas de version simulée à portée de main, dites-vous..? Comment le connecter maintenant au cas où ViewModel
demande de naviguer vers un autre écran ?
ma recommandation est de changer tout logique de navigation en dehors de leurs fonctions combinables. Ma suggestion est de créer une couche intermédiaire pour la navigation :
class Navigator {private val _sharedFlow =
MutableSharedFlow<NavTarget>(extraBufferCapacity = 1)
val sharedFlow = _sharedFlow.asSharedFlow()
fun navigateTo(navTarget: NavTarget) {
_sharedFlow.tryEmit(navTarget)
}
enum class NavTarget(val label: String) {
Home("home"),
Detail("detail")
}
}
au lieu de kotlin SharedFlow
bien sûr, vous êtes libre d’utiliser ce que vous voulez. Transmettez la référence singleton à votre ViewModels
et chaque fois que vous voulez naviguer vers un autre écran, appelez simplement le navigateTo()
Occupation.
La dernière étape consiste à naviguer vers un écran différent, ce qui sera fait au sein de notre composition. NavigationComponent
fonction depuis le début :
@Composable
fun NavigationComponent(
navController: NavHostController,
navigator: Navigator
) {
LaunchedEffect("navigation") {
navigator.sharedFlow.onEach {
navController.navigate(it.label)
}.launchIn(this)
}NavHost(
navController = navController,
startDestination = NavTarget.Home.label
) {
...
}
}
Avec LaunchedEffect
nous avons créé un CoroutineScope
qui démarre dès que notre composant combinable est créé et s’annule dès que la composition est supprimée. En conséquence, chaque fois que Navigator.navigateTo()
est appelé, cet extrait écoute et effectue la transition proprement dite.
Merci d’avoir lu
Commentaires
Laisser un commentaire