Android

Architecture de navigation Jetpack Compose avec ViewModels | par Tom Seifert | Août 2021

Le 7 septembre 2021 - 5 minutes de lecture

Comment cela fonctionnait dans les temps anciens

Les bases

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

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é

@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 ?

@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

Votre commentaire sera révisé par les administrateurs si besoin.