推荐看的文章:https://juejin.cn/post/7568873939033260082
Navigation3
整体模型
1 2 3 4 5 6 7 8 9 10 11
| backStack ↓ entryProvider ↓ NavEntry ↓ entryDecorators ↓ SceneStrategy / Scene ↓ 最终显示的 UI
|
开发者维护一个backStack,Navigation根据backStack里面的key生成NavEntry,再把NavEntry组织成当前UI。
backStack——导航栈
backStack通常就是一个列表:
1
| val backStack = remember { mutableStateListOf<Any>(Home) }
|
表示当前导航的一个历史,比如:[Home, Detail, Sheet]通常里面的最后一个元素代表当前栈顶页面或当前的覆盖层。
需要注意的是,导航栈里面放的也不是UI,而是导航Key,比如:
1 2 3
| data object Home data class Detail(val id: String) data object Settings
|
然后添加时:
1
| backStack.add(Detail(id = "1001"))
|
我们可以发现添加的时候也没有添加UI,而是一个导航key,代表目的地以及其参数,真正的UI是通过entryProvider生成的。
entryProvider
entryProvider是key到NavEntry的映射:
1 2 3 4 5 6 7 8 9
| entryProvider { entry<Home> { HomeScreen() }
entry<Detail> { key -> DetailScreen(id = key.id) } }
|
Navigation3 会根据 entryProvider 创建对应的 NavEntry,以便这些NavEntry参与后续流程。
NavEntry
NavEntry非常重要,这是一个被Navigation3管理的条目,包含了对应的key,metdata以及页面内容。
NavEntry 不是最终的 UI 容器,它只是导航条目。
而最终UI怎么展示还是要看Scene。
需要注意的是,在Nav2中,NavBackStackEntry本身就是一个ViewmodelStoreOwner,所以你在页面里面直接获取viewModel(),是可以直接拿到当前作用域的ViewModel的。但在Navigation3中,NavEntry却不是ViewModelStoreOwner,如果你希望每个 NavEntry 都有自己独立的 ViewModel 作用域,需要额外使用:
1
| rememberViewModelStoreNavEntryDecorator()
|
这个装饰器会给每个 NavEntry 关联对应的 ViewModelStoreOwner。
NavEntry除了可以提供页面内容,还可以携带metadata。metadata的作用是让后面的SceneStrategy能否识别这个entry是否需要特殊处理。
可以拓展Navigation3,比如我使用metadata实现了BottomSheetSceneStrategy。
NavDisplay
如果说 backStack 是导航状态,entryProvider 是映射规则,NavEntry 是导航条目,那么 NavDisplay 就是最终把这些东西组合起来并显示的 Composable。
典型写法类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| NavDisplay( backStack = backStack, entryDecorators = listOf( rememberViewModelStoreNavEntryDecorator(), rememberSaveableStateHolderNavEntryDecorator(), ), entryProvider = entryProvider { entry<Home> { HomeScreen() }
entry<Detail> { key -> DetailScreen(id = key.id) } } )
|
NavDisplay有默认的返回方法onBack():
1 2 3
| if (backStack is MutableList<T>) { backStack.removeLastOrNull() }
|
默认会移除最后一个元素。
NavEntryDecorator
Navigation3 里一个非常重要的设计是 NavEntryDecorator。
它的作用可以理解成:
在 NavEntry 的页面内容外面包一层能力。
Navigation3 没有把所有能力都硬塞进 NavEntry 本身,而是通过装饰器按需添加。
最常见的两个装饰器是:
1 2
| rememberViewModelStoreNavEntryDecorator() rememberSaveableStateHolderNavEntryDecorator()
|
Scene
Scene就是真正描述UI的地方。
比如普通的单栈页面:[Home, Detail],最终通常都是显示Detail,但是可能有覆盖场景,所以说Scene关注的是显示和如何显示Entry,以及哪些是底层/覆盖内容。
SceneStrategy
计算当前Scene的策略。SceneStrategy 处理的是整个 entries 列表,而不是单个 entry。即,对整个entries列表判断当前应该生成普通Scene,BottomSheet的Scene还是Dialog Scene。
sceneStrategies是按顺序执行的,可以理解为一条链:
1 2 3 4 5 6 7
| for (strategy in sceneStrategies) { val scene = strategy.calculateScene(entries)
if (scene != null) { return scene } }
|
因为策略是按顺序执行的,所以顺序会影响结果。
如果一个普通单栈策略放在最前面,并且它总是可以返回 Scene,那么后面的特殊策略就没有机会执行。
因此通常应该把更特殊的策略放前面,把兜底策略放后面。比如BottomSheet/Dialog的Scene就应该放最前面,兜底策略(普通单栈策略)放最后。
每次 backStack 发生变化,Navigation3 都会拿新的 entries 重新跑一遍策略链。sceneStrategies也不会消失。
所以 Scene 是动态计算出来的结果,不是一次绑定永久生效。
最后放一下掌邮的BottomSheetSceneStrategy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
| package com.cyxbs.components.view.ui
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavMetadataKey import androidx.navigation3.runtime.metadata import androidx.navigation3.scene.Scene import androidx.navigation3.scene.SceneStrategy import androidx.navigation3.scene.SceneStrategyScope import androidx.navigation3.runtime.get import androidx.navigation3.scene.OverlayScene
class BottomSheetSceneStrategy : SceneStrategy<Any> {
override fun SceneStrategyScope<Any>.calculateScene(entries: List<NavEntry<Any>>): Scene<Any>? { val entry = entries.lastOrNull() ?: return null val properties = entry.metadata[BottomSheetKey] ?: return null return BottomSheetScene( key = entry.contentKey, previousEntries = entries.dropLast(1), overlaidEntries = entries.dropLast(1), entry = entry, properties = properties, onBack = onBack, ) }
companion object {
data class Properties(
val stateProvider: @Composable () -> BottomSheetState?, val peekHeight: Dp = 0.dp, val expandOnShow: Boolean = false, val dismissOnBackPress: Boolean = true, val dismissOnClickOutside: Boolean = false, val scrimColor: Color = Color.Transparent, val modifier: Modifier = Modifier.navigationBarsPadding(),
val popOnHide: Boolean = true, )
object BottomSheetKey : NavMetadataKey<Properties>
fun bottomSheet( properties: Properties ): Map<String, Any> = metadata { put(BottomSheetKey, properties) }
}
}
private class BottomSheetScene( override val key: Any, override val previousEntries: List<NavEntry<Any>>, override val overlaidEntries: List<NavEntry<Any>>, private val entry: NavEntry<Any>, private val properties: BottomSheetSceneStrategy.Companion.Properties, private val onBack: () -> Unit, ) : OverlayScene<Any> {
override val entries: List<NavEntry<Any>> = listOf(entry)
private lateinit var bottomSheetState: BottomSheetState
override val content: @Composable (() -> Unit) = {
val state = properties.stateProvider() if (state != null) { bottomSheetState = state
BottomSheetCompose( bottomSheetState = state, modifier = properties.modifier, peekHeight = properties.peekHeight, dismissOnBackPress = properties.dismissOnBackPress, dismissOnClickOutside = properties.dismissOnClickOutside, scrimColor = properties.scrimColor ) { CompositionLocalProvider(LocalBottomSheetScope provides this) { entry.Content() } }
if (properties.expandOnShow) { LaunchedEffect(state) { state.expand() } }
if (properties.popOnHide) { LaunchedEffect(state) { var hasShown = false state.stateFlow.collect { value -> if (value != BottomSheetValueState.Hide) { hasShown = true } else if (hasShown) { onBack() } } } } } }
override suspend fun onRemove() { if (::bottomSheetState.isInitialized) { bottomSheetState.hide() } }
override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false
other as BottomSheetScene
return key == other.key && previousEntries == other.previousEntries && overlaidEntries == other.overlaidEntries && entry == other.entry && properties == other.properties }
override fun hashCode(): Int { return key.hashCode() * 31 + previousEntries.hashCode() * 31 + overlaidEntries.hashCode() * 31 + entry.hashCode() * 31 + properties.hashCode() * 31 }
override fun toString(): String { return "BottomSheetScene(key=$key, entry=$entry, previousEntries=$previousEntries, " + "overlaidEntries=$overlaidEntries, properties=$properties)" }
}
|