Kotlin Compose Multiplatform下导航解决方案

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

其实笔者在写这篇文章的时候,KMP已经有实验性的导航解决方案了,官方文档compose-navigation-routing中有介绍,而且使用起来也比较简单,可以参考我构建的的样例的这个分支

但是目前版本由于是实验性的,不支持深层链接,而且返回手势只有安卓支持,甚至这些都不是最重要的,最大问题在于:笔者在使用这个导航的时候发现,官方导航组件在安卓环境下,有小概率会触发错误的过渡效果,原因不明,即使不设置任何动画效果,也有小几率触发,而且这个问题只在安卓条件下会出现,所以目前版本的建议还是使用比较成熟的外部导航组件

实现

喜欢直接看代码的小伙伴可以直接参考我的构建样例实现

我们这里使用voyager实现多平台的导航,对于官方的实验性导航方案,我们是可以将参数直接传入子组件内,完成remember的量的屏幕刷新逻辑的,尤其是在状态提升即State hoisting的时候。但是对于voyager而言,我们使用传入的方式并不美观,而且状态量多了之后,会导致上层代码过分臃肿,所以这个部分我们改用ScreenModel来实现,这个概念和安卓的ViewModel概念相似,而且对于变量的注入,我们可以手写,也可以使用轻量级的Kotlin依赖注入工具Koin

故而我们的整体实现逻辑为:在最外部使用Voyager的标签导航,将首页的几个页面设置为单例对象,这里使用Kotlinobject实现,然后对于每个页面内的深层导航,直接使用导航嵌套完成。在嵌套导航中,同样设置嵌套页面的单例,通过继承Screen的方式,将原本的页面组件至于新的页面的Content()函数中。但是这里由于Content()方法不接受参数,原本需要传递的数据无法传递到原本页面组件中,这里我们不使用data class传入参数的方式实现,而是隔离到底,使用Koin进行对象注入

这样说比较抽象,下面我们一步步实现,注意,我这里可能默认小伙伴们了解Jetpack Compose的相关内容,如果对于Jetpack Compose不了解的小伙伴可以先从Android Basics with Compose简单了解一下

Koin

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(),  
) {
	//...
}

Voyager

如上所言,我们在最外层使用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

你可能感兴趣的:(kotlin,开发语言,android,multiplatform,compose,多平台,KMP)