欢迎大家对于本站的访问 - AsterCasc
其实笔者在写这篇文章的时候,KMP
已经有实验性的导航解决方案了,官方文档compose-navigation-routing中有介绍,而且使用起来也比较简单,可以参考我构建的的样例的这个分支
但是目前版本由于是实验性的,不支持深层链接,而且返回手势只有安卓支持,甚至这些都不是最重要的,最大问题在于:笔者在使用这个导航的时候发现,官方导航组件在安卓环境下,有小概率会触发错误的过渡效果,原因不明,即使不设置任何动画效果,也有小几率触发,而且这个问题只在安卓条件下会出现,所以目前版本的建议还是使用比较成熟的外部导航组件
喜欢直接看代码的小伙伴可以直接参考我的构建样例实现
我们这里使用voyager实现多平台的导航,对于官方的实验性导航方案,我们是可以将参数直接传入子组件内,完成remember
的量的屏幕刷新逻辑的,尤其是在状态提升即State hoisting
的时候。但是对于voyager
而言,我们使用传入的方式并不美观,而且状态量多了之后,会导致上层代码过分臃肿,所以这个部分我们改用ScreenModel来实现,这个概念和安卓的ViewModel
概念相似,而且对于变量的注入,我们可以手写,也可以使用轻量级的Kotlin
依赖注入工具Koin
故而我们的整体实现逻辑为:在最外部使用Voyager
的标签导航,将首页的几个页面设置为单例对象,这里使用Kotlin
的object
实现,然后对于每个页面内的深层导航,直接使用导航嵌套完成。在嵌套导航中,同样设置嵌套页面的单例,通过继承Screen
的方式,将原本的页面组件至于新的页面的Content()
函数中。但是这里由于Content()
方法不接受参数,原本需要传递的数据无法传递到原本页面组件中,这里我们不使用data class
传入参数的方式实现,而是隔离到底,使用Koin
进行对象注入
这样说比较抽象,下面我们一步步实现,注意,我这里可能默认小伙伴们了解Jetpack Compose
的相关内容,如果对于Jetpack Compose
不了解的小伙伴可以先从Android Basics with Compose简单了解一下
Koin
实现依赖注入和导航没有直接的关系,只是方便对于对象状态进行统一管理。对于Spring
比较了解的小伙伴肯定知道,Spring
的核心是控制反转和面向切面,而对于控制反转实现的方式就是通过依赖注入让类对象之间解耦,这里Koin
也是基于相同的观念设计,完成对于对象状态操作
我们需要在公共模块中添加依赖:
kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.koin.android)
}
commonMain.dependencies {
api(libs.koin.core)
api(libs.koin.compose)
}
}
}
koin = "3.5.6"
koinCompose = "1.1.5"
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
依赖添加完成后,我们需要在公共模块中添加需要注入的对象类,比如我们需要注入的类为MainScreenModel
,即需要设置初始化模块:
fun commonModule() = module {
single<MainScreenModel> {
MainScreenModel()
}
}
再写入初始化Koin
流程:
class KoinInit {
fun init(appDeclaration: KoinAppDeclaration = {}): Koin {
return startKoin {
modules(
listOf(
commonModule(),
),
)
appDeclaration()
}.koin
}
}
最后分别在各个平台进行初始化调用,对于安卓平台示例:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
KoinInit().init {
androidLogger()
androidContext(thisContext)
modules()
}
//...
}
桌面平台示例:
lateinit var koin: Koin
fun main() {
koin = KoinInit().init()
koin.loadModules(
listOf(),
)
//...
}
后续如果需要注入,只需要直接在组件上标记需要注入的对象类,即可注入完成,比如:
@Composable
fun MainSettingsScreen(
mainModel: MainScreenModel = koinInject(),
) {
//...
}
如上所言,我们在最外层使用TabNavigation
,示例代码如下:
TabNavigator(
tab = HomeTab,
tabDisposable = {
TabDisposable(
navigator = it,
tabs = listOf(
HomeTab, ArticlesTab, MusicsTab, VideosTab, ChatTab, SettingTab
)
)
},
) { _ ->
Scaffold(
topBar = {
MainAppBar()
},
bottomBar = {
MainAppNavigationBar()
},
content = { padding ->
BoxWithConstraints(
modifier = Modifier.padding(padding).fillMaxSize()
) {
//...
CurrentTab()
//...
}
} )
}
对于MainAppNavigationBar
而言,核心代码如下:
@Composable
fun MainAppNavigationBar() {
val tabNavigator = LocalTabNavigator.current
val isSelected = tabNavigator.current == nav.tab
NavigationBarItem(
modifier = Modifier.padding(0.dp)
.clip(RoundedCornerShape(100.dp)),
colors = NavigationBarItemDefaults.colors().copy(
unselectedIconColor = MaterialTheme.colorScheme.unselectedColor,
),
icon = {
//...
},
selected = isSelected,
onClick = { tabNavigator.current = nav.tab },
)
}
我们这里可以通过LocalTabNavigator
直接获取导航当前页,也就是为什么我们在前面TabNavigator
中没有使用TabNavigatorContent
给的传入参数的原因,此时可以使用tabNavigator.current::set
来进行标签导航
在使用Compose Navigation
原生导航,或者不使用专门的导航包,我们的组件一般是这样的:
@Composable
fun MainMusicsScreen(
//param1
//param2
//param3
//param4
) {
//..
}
这里我们将所有参数放到Koin
管理的类对象中,以播放状态为例,当前音乐播放状态类示例如下:
@Stable
class MusicPlayerState {
var isPlaying by mutableStateOf(false)
internal set
var isBuffering by mutableStateOf(false)
var currentTime by mutableStateOf(0.0)
var totalDuration by mutableStateOf(0.0)
var currentIndex by mutableStateOf(-1)
var playModel: MusicPlayModel = MusicPlayModel.ORDER
fun toBack() {
isPlaying = false
isBuffering = false
currentTime = 0.0
totalDuration = 0.0
}
}
我们创建MusicScreenModel
用于管理所有和音乐相关的对象,其中增加音乐播放状态对象:
class MusicScreenModel : ScreenModel {
private val _playerState = MutableStateFlow(MusicPlayerState())
val playerState = _playerState.asStateFlow()
}
并将MusicScreenModel
和上文MainScreenModel
一样放入Koin
需要管理的依赖中,此时我们就可以在MainMusicsScreen
中直接注入MusicScreenModel
了:
fun MainMusicsScreen(
screenModel: MusicScreenModel = koinInject(),
) {
val playList = screenModel.musicPlayList.collectAsState().value
val currentTime = screenModel.playerState.collectAsState().value.currentTime
val totalDuration = screenModel.playerState.collectAsState().value.totalDuration
val isPlaying = screenModel.playerState.collectAsState().value.isPlaying
}
届时我们的参数就可以直接在screenModel
中获取,而不需要像之前那样传入非常多参数来维护子组件和父组件之间的状态交互了
.collectAsState()
这个函数在这里非常关键,如果不加这个函数,在状态改变时候子组件不会刷新,只有在增加这个函数,状态改变才会触发重组,这个部分是在内部使用协程来启动对Flow
的收集,并不断更新State
对象的值来实现的,小伙伴可以在Jetpack Compose
教程的ViewModel
相关部分看到更详细的解释。但是过于频繁地触发重组对于运行性能也是一种冲击,小伙伴们需要根据具体需求觉得是否携带这个函数
我们需要将刚才改造后的子组件转换为单例对象,添加到标签导航中,只要增加一次包装即可:
object MainMusicsScreen : Screen {
private fun readResolve(): Any = MainMusicsScreen
@Composable
override fun Content() {
MainMusicsScreen()
}
}
此时我们已经有了MainMusicsScreen
单例对象,再以TabNavigation
的标准构建方式,构建标签单例即可:
object MusicsTab : Tab {
private fun readResolve(): Any = MusicsTab
override val options: TabOptions
@Composable
get() {
val title = stringResource(MainNavigationEnum.MUSICS.title)
val icon = rememberVectorPainter(MainNavigationEnum.MUSICS.icon)
val index = MainNavigationEnum.MUSICS.ordinal.toUShort()
return remember {
TabOptions(
index = index,
title = title,
icon = icon,
)
}
}
@Composable
override fun Content() {
Navigator(screen = MainMusicsScreen)
}
}
至此,标签导航加嵌套导航就基本完成了,如果你的标签导航页页面上没有需要嵌套的导航处理,那么直接写成:
object MusicsTab : Tab {
//...
@Composable
override fun Content() {
MainMusicsScreen()
}
}
即可,object MainMusicsScreen : Screen {}
都不需要构建。如果使用嵌套导航,那么在子组件中可使用val navigator = LocalNavigator.currentOrThrow
获取当前当前导航,并且使用push(item: Item)
、pop()
相关函数完成具体导航逻辑
Tomoyo
Compose Navigation
Voyager
Koin
欢迎大家对于本站的访问 - AsterCasc