[Android翻译]在Jetpack Compose中导航

1,387 阅读18分钟

原文地址:medium.com/google-deve…

原文作者:jossiwolf.medium.com/

发布时间:2021年6月7日 - 14分钟阅读

如果你正在开发一个移动应用程序,你很可能需要某种形式的导航。要做好导航并不容易,因为有很多挑战:后栈处理、生命周期、状态保存和恢复以及深度链接只是其中的一部分。在这篇文章中,我们将探讨导航组件对Jetpack Compose的支持,并看一下它的内部结构。

image.png

我还能选择一张更老套的照片吗?也许不能。照片由Mick Haupt在Unsplash上拍摄。谢谢,Mick!

开始吧!

在开始之前,我们将添加一个对navigation-compose的依赖,这是导航组件支持Compose的工件。

implementation "androidx.navigation:navigation-compose:2.4.0-alpha02"

让我们跳进代码

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen()
		}
	}
}

@Composable
fun FeedScreen(...) { ... }

首先,我们使用rememberNavController方法创建并记忆一个NavController。rememberNavController返回一个NavHostController,它是NavController的一个子类,提供一些NavHost可以使用的额外API。在以后提到NavController和使用它的时候,我们将使用它作为NavController,因为我们自己不需要知道这些额外的API,它只是对NavHost很重要 。 我们把这个传给我们的NavHost composable。NavHost可组合式负责托管与NavBackStackEntry相关的NavDestination的内容(我们将在稍后查看细节!)。

我们传递给NavHost的lambda是我们导航图的构建者。在这里,我们可以访问NavGraphBuilder,并可以构建和声明我们的导航图。这就是我们声明我们的目的地和嵌套图的地方。如果你是从 "老 "导航库来的,一开始可能会觉得有点奇怪!这里不再有XML,甚至没有。没有XML了,甚至连导航图都没有。尽管Kotlin DSL已经存在了很长时间,但到目前为止,它已经被XML掩盖了。

这也意味着,在可预见的未来,我们不会有图形的视觉表现。XML导航图的渲染视图非常有用,所以让我们希望在某个时候,我们能在Compose上得到这样的效果!"。

声明一个可组合的目的地很容易:我们使用navigation-compose提供的可组合方法。

// From https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

它是NavGraphBuilder的一个扩展函数,本质上是NavGraphBuilder的addDestination方法的一个便利包装。在这里,一个特定于ComposeNavigator的NavDestination被创建。ComposeNavigator是负责处理后栈和可合成物导航的导航器

NavGraphBuilder是普通(非特定于组合)导航API的一部分,主要提供一个addDestination方法,将目的地添加到导航图中。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen()
		}
	}
}

@Composable
fun FeedScreen(...) { ... }

回过头来看我们的代码,我们给那个可组合函数传递了一条路线,告诉NavGraphBuilder我们以后想用什么路线来导航到这个目的地。如果你是从 "老 "导航库来的,路线大致相当于为一个目的地定义一个ID。从导航库的2.4.0版本开始,NavDestination的id会根据它的路线自动设置(并在每次更新路线时更新),所以不需要定义一个id。 可组合函数的最后一个参数是一个@Composable lambda,它将被设置为目的地的内容。当我们导航到这个目的地时,NavHost将承载这个可合成的。

在我们的@Composable lambda中,我们只是托管FeedScreen的可组合。虽然理论上你可以直接在导航图中声明你的所有内容,但请不要这样做。事情很快就会变得一团糟,而且清理工作也会很繁琐!

真棒!我们已经创建了我们的导航控制器。我们已经创建了我们的NavController,使用可组合的NavHost和可组合的NavGraphBuilder函数来创建一个可组合的目的地并将其添加到导航图中。让我们添加第二个目的地并进行导航!

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen()
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")
	}
}

@Composable
fun AdoptionScreen(...) { ... }

让我们来看看有什么变化。

首先,我们在图中添加了一个新的目的地,并添加了可组合的AdoptionScreen。为了在FeedScreen的按钮被点击时进行导航,我们在FeedScreen的参数中加入了NavController。最后,当按钮被点击时,我们调用NavController#navigate,输入我们想导航的目的地的路线。

插曲。引擎盖下

如果你不喜欢在引擎盖下工作的具体细节,请随意跳到下一节。这个插曲提供了一个高层次的概述,对你理解导航-构成很有用。

当我们调用navigate时,NavController会计算出它要做什么来让我们到达目的地。首先,它检查是否有一个与请求路线相关的目的地,以及它是否在导航图中。我们不希望导航到星际空间的深处

注意:当用路线导航时,这在内部被视为一个深度链接。这也意味着,如果你使用Navigation Kotlin DSL并且用路由而不是id注册目的地,你就可以免费获得深度链接

在NavController确定我们要导航的目的地存在后,NavController会查看所有的导航选项(是否应该弹出后端栈?目的地应该单顶启动吗?),如果目的地还不在后堆栈上(也就是说,当我们之前没有导航到它或者弹出它时),就创建一个NavBackStackEntry,如果目的地在后堆栈上,就检索后堆栈条目。当我们有下面的流程时,可能就是这种情况。

image.png

带有 "Feed "起始目的地和 "Adoption "目的地的导航流程图。首先,我们从 "Feed "导航到 "Adoption "目的地,然后再导航回 "Feed"。

为我们的 "Feed "起始目的地创建了一个后栈条目并添加到后栈中。当我们导航到 "收养 "屏幕时,为收养屏幕创建了一个后堆栈条目。当我们导航到 "Feed "时,我们已经有了 "Feed "的后堆栈条目在后堆栈中。就像我们在Android开发中知道的Fragments、Activities或其他组件一样,NavBackStackEntry也有一个生命周期,这样,一个back stack entry可以在不活动的情况下留在back stack上,也就是说,因为它不在back stack的顶部。

为了导航,NavController会查看请求的NavDestination的Navigator。在我们的例子中,那是ComposeNavigator,因为我们要导航到一个可组合的目的地。NavController用请求的目的地和导航选项调用导航器的navigate函数,执行导航器的导航逻辑,如果需要的话,将后栈条目添加到后栈中。最后,该条目也被添加到NavController的后栈中。一个导航器只有它知道如何处理的后堆栈条目(从这个导航器的目的地创建的条目),而NavController维护整个图形的后堆栈。你可以把NavController看作是 "大老板",把导航器看作是 "小老板"。 另外,导航器(更准确地说,是导航器的NavigatorState)将后堆栈条目移动到RESUMED状态,因为它现在已经可以被显示了。NavigatorState也将前一个条目的状态移动到CREATED状态,表明它不再是活动的了。

同时,可组合的NavHost查看ComposeNavigator的后堆栈,它现在有添加或更新的NavBackStackEntry。它用更新的后堆栈条目列表重新组合,并将每个后堆栈条目的目标内容(我们之前传入的@Composable lambda)排放到组合中。简单地说,它调用了我们传递给NavGraphBuilder中可合成函数的@Composable lambda。

然后,我们就到达了我们的目的地!🎉。

回到我们的代码

好了,我们的小插曲结束了,让我们回到我们的代码中来吧 在我们的例子中,我们在导航时没有指定任何导航选项--我们只是用路线调用导航。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen()
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")
	}
}

@Composable
fun AdoptionScreen(...) { ... }

默认情况下,之前的目的地(我们的FeedScreen目的地)将被保留在后面的栈中。当我们想从AdoptionScreen回去的时候,我们只需要把那个目的地从后面的堆栈里弹出来。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen(navController)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")
	}
}

@Composable
fun AdoptionScreen(navController: NavController) { 
	Button(onClick = { navController.popBackStack("adopt", inclusive = true) }) {
		Text("Click me to go back!")
	}
}

我们不定义我们想去的地方,而是告诉NavController,采纳路线上的目的地是最上面的目的地,应该通过设置包容性为真来弹出后栈。注意,这是popBackStack的默认行为,所以我们也可以直接调用navController.popBackStack()而不需要任何参数。这样,我们可以从任何地方导航到采用的路线,并且总是回到我们来时的地方。另外,我们也可以在这里使用navController.navigateUp()。它试图在导航层次结构中向上导航。在大多数情况下,这意味着从后面的堆栈中弹出当前条目,但如果应用程序是通过深度链接打开的,使用navigateUp可以确保你回到你来的地方,例如另一个应用程序。

就这样,我们有了最基本的导航形式。让我们把代码清理一下吧!

清理

你可以想象,在我们想要导航的地方重复我们的路线并不是一个非常可扩展的方法。我们希望能够重新使用这些路由。这将防止我们因打错字而引入错误,并在我们想改变导航逻辑时帮助我们。对此有几种方法,即在一个对象(或多个对象)中把所有路线定义为常量。

object AppDestinations {
	const val Feed = "feed"
	const val Adopt = "adopt"
}

这就是我们最初使用的方法,但我更喜欢 Chris Banes 使用密封类的实现。它更容易阅读,一般来说也更容易维护。

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("adopt")
}

Kotlin 1.4和1.5对密封类的宽松规则允许这些定义的干净分离,使密封类在这里非常合适。

让我们去更新我们之前的代码来使用它吧

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("adopt")
}
@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(navController)
		}
		composable(route = Screen.Adopt) {
			AdoptionScreen(navController)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate(Screen.Adopt) }) {
		Text("Click me to adopt!")
	}
}

@Composable
fun AdoptionScreen(navController: NavController) { 
	Button(onClick = { navController.popBackStack(Screen.Adopt, inclusive = true) }) {
		Text("Click me to go back!")
	}
}

参数

当导航到一个目的地时,我们经常要传递一个ID或其他参数,以加载特定的数据。假设我们的FeedScreen现在有一个待收养的可爱小狗的列表,我们想在点击它时显示特定小狗的收养页面。

@Composable
fun FeedScreen(navController: NavController) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { navController.navigate(Screen.Adopt) }
			)
		}
	}
}

@Composable
private fun DogCard(dog: Dog, onClick: (dog: Dog) -> Unit) { ... }

我们的Screen.Adopt目的地还不知道如何处理参数,所以让我们跳回到我们的路由,先添加一个参数。

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("dog/{dogId}/adopt")
}

参数是用大括号定义的,大括号里有参数的名称。我们以后将使用这个名字来检索参数。大括号将其注册为占位符,当路由被创建时,这就是参数的预期位置。

当然,路由的格式完全取决于你,但遵循RESTful URL设计是有意义的。把路由想象成屏幕的标识符。它应该是唯一的、清晰的、容易理解的。

对于需要的参数,将参数定义为路径参数。对于我们的Adopt路由,我们总是要求dogId的存在,所以我们把它定义为路径参数。如果我们想提供可选的参数,我们将使用查询参数的语法:adoption?dogId={dogId}。

回到我们的导航图构建器,compose函数的@Composable lambda需要一个参数。目的地的NavBackStackEntry。在其他重要信息中,NavBackStackEntry保存着从正在被导航的路线中提取的参数。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(navController)
		}
		composable(route = Screen.Adopt) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(navController, dogId)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) { ... }


@Composable
fun AdoptionScreen(navController: NavController, dogId: String) { ... }

从后堆栈条目中,我们可以提取我们在路由中作为参数声明的dogId。请注意,该条目的参数是空的,但我们可以肯定,这个数据在这里,因为它是我们定义的路由的一部分。当导航时,请求的路线必须与目的地的路线或其模式完全匹配。由于我们的dogId参数是路由的一部分,我们不能陷入这个参数丢失的棘手局面。不过,考虑处理这种情况仍然是一个很好的做法,不要用一个简单的非空断言就把它扫地出门。

现在我们有了导航目的地的设置,我们可以在feed中把参数添加到我们的导航调用中了!要做到这一点,我们必须修改我们的参数。要做到这一点,我们必须修改我们要导航的路线。我们之前使用Screen.Adopt作为路由,但后来更新了这个路由为模板,所以我们不能在这里添加我们的参数。相反,我们可以用所需的参数创建一个createRoute函数,它将建立路线。完全归功于 Chris Banes 这个主意的功劳。

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("{dogId}/adopt") {
		fun createRoute(dogId: String) = "$dogId/adopt"
	}
}
@Composable
fun FeedScreen(navController: NavController) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { selectedDog -> 
					navController.navigate(Screen.Adopt.createRoute(selectedDog.id)
				}
			)
		}
	}
}

你可以在官方的Android开发者文档中找到更多关于用参数导航、可选参数和除字符串以外的参数类型的信息。

将我们的导航调用放在同一地点

酷,一切看起来都很好,而且运作良好,对吗?嗯......在最初的几个屏幕上。随着我们的应用程序变得越来越复杂,我们会想从多个地方导航到一个目的地。我们最终会把NavController传递给至少每个屏幕级别的composable。这在composable和NavController之间产生了依赖关系,使得测试和创建@Previews的难度加大。导航-组合的测试指南也说明了这一点。

image.png

developer.android.com/jetpack/com…

除了测试之外,这也使得改变我们的导航逻辑更加困难。如果我们从5个不同的地方导航到我们的采用屏幕,每个地方都调用navController.navigate ,如果我们想增加一个目的地或以其他方式改变导航逻辑,如弹出后端堆栈,我们最好的情况是恼人的,最坏的情况是很难更新这个目的地的参数,而且会产生bug。这个神奇的短语是 "导航调用的共同位置"--我们要确保我们所有的导航调用都在一个地方,而不是分散在30个不同的组合体中。

更新我们的代码,而不是把NavController传递给FeedScreen和AdoptionScreen,我们可以把它们改成接受一个lambda。

@Composable
fun AdoptionScreen(dogId: String, navigateUp: () -> Unit) {
	Button(onClick = navigateUp) {
		Text("Click me to go back!")
	}	
}
@Composable
fun FeedScreen(showAdoptionPage: (dogId: String) -> Unit) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { dog -> showAdoptionPage(dog.id) }
			)
		}
	}
}

之后,我们更新我们的CuteDogPicturesApp来代替传递这个lambda。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(Screen.Adopt.createRoute(dogId))
				}
			)
		}
		composable(route = Screen.Adopt) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId, 
				navigateUp = { navController.popBackStack(Screen.Adopt, inclusive = true) }
			)
		}
	}
}

这样,我们已经将我们的可组合性与实际的导航依赖关系解耦,可以很容易地伪造这种行为,并在以后很容易地重构它。如果我们愿意,我们甚至可以将实际的导航调用提取到一个本地函数中。由于不同的目的地可能需要不同的导航逻辑(当你从屏幕B导航到C时,你可能想弹出后堆栈,但当从A导航到C时就不需要了),我建议暂时不要进行这种(很可能)不成熟的优化。通过导航调用的共同定位,如果你想在以后提取更多的东西,你已经有了一个好的起点。

嵌套导航图

使用嵌套的导航图,我们可以对一组目的地进行分组并将其模块化。如果你以前使用过nav组件,你可能会知道<navigation> XML标签,它可以用来声明一个嵌套的导航图。通过导航DSL,有一个导航扩展函数,类似于navigation-compose提供的可组合扩展。这个导航扩展功能是由通用的(非组合特定的)导航工件提供的。

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Dog: Screen("dog/{dogId}") 
}

sealed class DogScreen(val route: String) {
  	object Adopt: DogScreen("dog/{dogId}/adopt") {
		fun createRoute(dogId: String) = "dog/$dogId/adopt"
	}
  	object ContactDetails: DogScreen("dog/{dogId}/contactDetails") {
		fun createRoute(dogId: String) = "dog/$dogId/contactDetails"
	}
}
@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(DogScreen.Adopt.createRoute(dogId))
				}
			)
		}
		navigation(route = Screen.Adopt, startDestination = DogScreen.Adopt) {
		    composable(route = DogScreen.Adopt) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		    }
		    composable(route = DogScreen.ContactDetails) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		    }
		}
		// imagine 30 more of these in here!
	}
}

为了声明一个嵌套图,我们用这个嵌套图的路线来调用导航方法,这样它就可以被导航到,也可以设置起始目的地。我们还引入了一个DogScreen密封类,代表这个图中的路线。Screen密封类现在代表顶级目的地,而嵌套的目的地被定义在它们自己的密封类中。这使得代码更容易阅读和维护,因为更多的目的地被添加。我们还可以把DogScreen类移到它自己的文件中,以实现更多的分离

提取导航图

随着你的应用程序的增长,你的导航图也会增长。最终,你会需要嵌套的导航,并最终得到一个非常长的导航图定义,很难阅读和维护。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(Screen.Adopt.createRoute(dogId))
				}
			)
		}
		navigation(route = Screen.Dog, startDestination = DogScreen.Adopt) {
		    composable(route = DogScreen.Adopt) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		    }
		    composable(route = DogScreen.ContactDetails) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		    }
		}
		// imagine 30 more of these in here!
	}
}

由于可组合和导航函数只是NavGraphBuilder的扩展,我们也可以使用扩展函数来分解我们的导航图。

@Composable
fun CuteDogPicturesApp() {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
	        addFeedGraph(navController)
		addDogGraph(navController)
		// imagine 30 more of these in here - that's better, right?
	}
}

private fun NavGraphBuilder.addFeedGraph(navController: NavController) {
    composable(route = Screen.Feed) {
	    FeedScreen(
		    showAdoptionPage = { dogId ->
			    navController.navigate(Screen.Adopt.createRoute(dogId))
			}
		)
	}
}

private fun NavGraphBuilder.addDogGraph(navController: NavController) {
    	navigation(route = Screen.Dog, startDestination = Screen.Dog.Adopt) {
		composable(route = Screen.Dog.Adopt) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		}
		composable(route = Screen.Dog.ContactDetails) { backStackEntry ->
			val dogId = backStackEntry.arguments?.getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		}
	}
}

随着我们的导航图的增长,这些也可以托管在他们各自的文件中,使事情更容易处理。如果你的应用程序是模块化的,这也使你能够在一个模块内封装该模块的导航和路线。对于我们的例子,这意味着暴露了dog模块的addFeedGraph扩展函数。为了在这个嵌套图之外进行导航,addFeedGraph也会接受一个lambda来导航到采用屏幕。

如果你对模块化感兴趣,我强烈推荐 Joe Birch 的文章中关于 Compose 中的模块化导航!

来自现实世界的教训

Snapp Mobile,我们目前正在与一个技术先进的客户合作,他要求我们在一个绿地项目中部署Jetpack Compose。我们使用Compose和navigation-compose已经有几个月了,从我们的经验来看,遵循上述最佳实践是最重要的。

在阅读官方关于将可组合物与NavController解耦的建议之前,我们将其传递给我们所有的屏幕级可组合物,并在可组合物和导航库之间建立了紧密的耦合。从使用导航库的Fragments(对不起,Jake)和Activities开始,我们习惯于在Fragments中调用findNavController,因为我们之前没有任何抽象的方法。一开始传递NavController是 "自然 "的方式,但随着代码库和导航图的增长,很明显,这导致了难以改变的混乱的代码--我们仍在努力消除这种损害。

如果你刚刚开始使用navigation-compose,请遵循这个最佳实践。如果你已经使用了一段时间,我建议开始考虑如何尽快重构这段代码,以减少你必须更新的东西的数量。Chris Banes 最近重构了他的TiVi应用程序的导航,将导航呼叫放在一起,如果你想寻找灵感的话。

正确地定义路线和导航图并将它们分割开来是另一个重要的要点。正如前面所指出的,我们一开始就在(嵌套的)对象中定义了我们的路由,这变得非常难以管理,而且在以后的重构中也不是最愉快的事情。在最新的Kotlin版本中,利用密封的类和它们宽松的规则,对保持事物的概览非常重要。考虑到一个有40多个导航目的地的应用程序,在一个地方定义所有的路线最终会形成一个相当大的、难以阅读和不可维护的文件。

但是过渡期呢?

从Navigation 2.4.0-alpha02开始,还不支持可组合目的地之间的转换。我们在当前的项目中还不需要它(还没有),但是如果目的地之间的转换是一个需求,那么在你的项目中决定使用导航合成时,最好记住这点。然而,组合动画和导航团队正在努力解决这个问题,我们应该在导航2.4.0稳定后看到一些东西出来。同时,我建议跟踪这个问题

除了这三个问题,虽然我们偶尔会遇到一两个bug,但导航-构图工作得非常好,我们正在使用它和它的Hilt集成,并且很高兴。就像所有的新事物一样,我们仍然在摸索新的最佳实践的过程中,但对我们目前的方法感到满意。导航2.4.0还支持多个后栈,并修复了大量的其他错误,所以看到团队积极致力于所要求的功能是非常好的!

资源

你已经走到了最后! 祝贺你! 尽管有些内容是相似的,但请看看官方的Android开发者文档中关于导航-合成的内容。Chris在Tivi中的拉动请求是一个很好的例子,可以让我们从糟糕的导航模式中迁移出来。如果你想获得一些灵感,Tivi资源库本身也是一个很好的参考,可以在真实世界的应用中使用navigation-compose实现导航。

官方的导航样本也是一个很好的起点,但请记住,它们只是:样本而已。他们不一定遵循所有的最佳实践(比如不传递你的NavController),所以请谨慎对待,不要盲目地复制它们。

除了官方资源外,还有一些很酷的社区项目,用于在 Compose 中进行导航。虽然我们没有在生产中使用这些项目,但我听到其他人说他们喜欢这些库。这绝对值得去看看 Zsolt Kocsi 的 compos-router、Zach Klippenstein 的 compos-backstack 和 Arkadii Ivanov 的Decompose库。

你使用过navigation-compose吗?我很想听听你的实际经验!

感谢 Volodymyr Galandzij, 马克-迪克森, ashdavies ™ 和 Ian Lake 为他们可爱的建议和评论!


www.deepl.com 翻译