Jetpack Compose Navigation 是导航的好设计吗?

Is Jetpack Compose Navigation good design for navigating?

以下代码来自官方示例project.

有两个分支,mainend

我发现 Code mainCode end 使用不同的导航方式。

代码main简单明了,在其他项目中,它导航基础状态就像来自project.

的代码A

代码结束使用NavHostController来导航,但是我们使用Jetpack Compose好像不需要再使用Navigation对吧?

主要代码

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        Scaffold(
          ...
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                currentScreen.content(
                    onScreenChange = { screen ->
                        currentScreen = RallyScreen.valueOf(screen)
                    }
                )
            }
        }
    }
}

enum class RallyScreen(
    val icon: ImageVector,
    val body: @Composable ((String) -> Unit) -> Unit
) {
    Overview(
        icon = Icons.Filled.PieChart,
        body = { OverviewBody() }
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
        body = { AccountsBody(UserData.accounts) }
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
        body = { BillsBody(UserData.bills) }
    );

    @Composable
    fun content(onScreenChange: (String) -> Unit) {
        body(onScreenChange)
    }
}

代码结束

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.name,
        modifier = modifier
    ) {
        composable(Overview.name) {
            OverviewBody(
              ...
            )
        }
        composable(Accounts.name) {
          ...
        }
        composable(Bills.name) {
          ...
        }       
    }
}



enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        fun fromRoute(route: String?): RallyScreen =
            when (route?.substringBefore("/")) {
                Accounts.name -> Accounts
                Bills.name -> Bills
                Overview.name -> Overview
                null -> Overview
                else -> throw IllegalArgumentException("Route $route is not recognized.")
            }
    }

代码A

fun CraneHomeContent(
   ...
) {
    val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()

    val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
    var tabSelected by remember { mutableStateOf(CraneScreen.Fly) }

    BackdropScaffold(
        ...
        frontLayerContent = {
            when (tabSelected) {
                CraneScreen.Fly -> {
                  ...
                }
                CraneScreen.Sleep -> {
                   ...
                }
                CraneScreen.Eat -> {
                   ...
                }
            }
        }
    )
}

我从早期的 alpha 阶段就开始使用 Compose,但很快就对 Google 试图提供一种更现代的方法来导航单个 activity 应用程序的蹩脚尝试感到失望。当您考虑 Android 的基于视图的系统完全被 Compose 使用的声明方法所取代时,您不得不认真地想知道为什么他们会坚持使用不允许您从一个屏幕传递对象的导航控制器给另一个。还有一个问题是,在从一个屏幕过渡到另一个屏幕时添加动画是事后才想到的。有一个插件支持动画转换。

但 Compose 最糟糕的地方可能是它无法处理设备配置更改。在旧的基于视图的系统下,您在 xml 文件中定义了布局,并将它们放置在文件夹名称中具有限定符的资源文件夹中,这将有助于 Android 根据诸如屏幕之类的内容选择正确的布局密度、方向、屏幕尺寸等。使用 Compose 时 window。最终 Google 确实添加了 API 来处理需要根据屏幕尺寸和密度选择的可组合项。但最终,您最终会在可组合项中编写此决策逻辑,并且您的代码开始看起来像意大利面条。 Google 的 Android 团队在选择将 UI 布局与确定选择哪个布局的逻辑混合时完全忘记了最基本的“关注点分离”。设计可组合项是一回事,如何选择它们又是另一回事。不要把两者混为一谈。当您集成这些 API 时,您的可组合项的可重用性会越来越差。 Android 的原始开发人员(他们不是来自 Google)非常了解让操作系统根据设备配置的更改来管理布局选择。

我选择创建自己的框架,以几乎与基于视图的系统工作方式相同的方式处理导航和管理可组合项。它还解决了无法将对象从一个屏幕传递到另一个屏幕的问题。有兴趣的可以去看看:

https://github.com/JohannBlake/Jetmagic

因此,要回答您关于 Compose Navigation 是否适合导航的问题,我不得不说不。我已经使用它并发现它严重缺乏。如果您只想成为另一个 运行-of-the-mill-Android- 开发人员,请使用 Compose Navigation。但是,如果您想规划自己的道路并将自己从 Google 糟糕的设计实践中解放出来,那么请使用 Jetmagic 或者更好,创建您自己的导航框架。没那么难。

回答您的问题:Code Main 不是正确的导航方式。

这是入门示例。在那种情况下,您只需隐藏和显示 Composables 就是这样。

为什么使用导航更好?

  1. Lifecylce 得到了更好的处理。想象一下,您想要启动一个 cameraX 可组合屏幕,然后您想要 return 回到您的初始可组合。导航组件将处理并释放资源。

  2. 可组合项已添加到返回堆栈。因此,当您按下后退键时,它会自动 return 回到上一个屏幕可组合项。

  3. 你得到了这个漂亮的动画,你不会立即看到下一个屏幕。

我相信还有其他要点,但这些是我现在想到的一些....

这取决于您要实施的具体用例。一般来说,是的,任何类型的导航库都更适合导航。它的字面意思是名称。大多数导航库(包括导航组件)都提供以下基本要素:

  1. 存储整个导航历史(后栈)和后栈中每个条目的保存状态(rememberSaveable)。同一个 destination 可能在 backstack 中出现多次,但它们将被存储为不同的 backstack entries。这允许为它们中的每一个存储单独的保存状态。

  2. 为它们中的每一个提供单独的范围内的生命周期、ViewModelStore 和 SavedStateRegistry。这可能对 allocating/cleaning 在目标可见或在后台堆栈中时加载资源很有用。

像您的 Code mainCode A 那样在目的地之间进行简单的切换可能是一个有效的选择,但它缺少上述功能。您的所有目的地都使用父可组合项的相同生命周期、ViewModelStore 和 SavedStateRegistry。此外,rememberSaveable 在目的地之间切换时不会保存状态。在某些情况下可能没问题,但你应该意识到其中的区别。

关于 Navigation Component for Compose 是否适合导航的问题?这很好,但也有缺陷和并发症。您可以阅读 CommonsWare 关于该主题的一篇好文章:Navigating in Compose: Criteria

考虑到官方库的种种怪异,我什至制作了自己的导航库:Compose Navigation Reimagined