这里写目录标题
- 代码仓库
- KMP 框架
- 基本框架
- actual&expect
- Koin 依赖注入管理
代码仓库
本小节代码已经上传到gitee,请自行查看:
点击访问仓库
KMP 框架
基本框架
源码集合 | 描述 | 存放内容示例 |
---|---|---|
androidMain | 针对 Android 平台的代码 | 使用 Android SDK、Android 特定的 API 和 UI 组件 |
desktopMain | 针对桌面平台(Windows、macOS、Linux)的代码 | 使用 JavaFX、Swing 或其他桌面 GUI 框架的代码 |
commonMain | 跨平台的用户界面代码,使用 Compose Multiplatform 框架 | 定义可在 Android、iOS、桌面等多个平台上共享的 UI 组件 |
nativeMain | 原生代码,直接访问底层系统 API 或使用特定于平台的库的代码 | 使用 POSIX 接口、平台特定的库或服务 |
actual&expect
KMP使用两个关键词actual和expect实现了跨平台开发;
- expect一般在commonApp里面定义,这是整个项目的通用逻辑部分,它相当于一个抽象类;
- actual一般在desktopApp或androidApp里面定义,它是对expect定义的函数或者变量的具体实现,这样就可以实现了每一个平台都有不同的各自对应的处理函数逻辑;
例如,先在commonApp下创建一个文件 BatteryManager.kt
该文件定义了获取当前设备电量剩余多少的方法;
expect class BatteryManager {
fun getBatteryLevel(): Int
}
之后就到各个平台的实现层进行该方法的具体逻辑实现;
比如我在androidApp层实现了该方法,同样的,你需要在对应的位置创建一样名称的文件,用来对该expect定义的类进行相应的actual实现(你可以使用AndroidStudio的自动创建功能实现这一步骤)
/**
* 实现电池管理功能的类
*
* @param context 应用程序上下文,用于获取电池信息
*/
actual class BatteryManager(
private val context: Context
) {
/**
* 获取电池电量百分比
*
* 通过广播接收器获取电池状态,并计算电池电量百分比
*
* @return 电池电量百分比(0-100)
*/
actual fun getBatteryLevel(): Int {
// 创建一个意图过滤器,用于匹配电池状态改变的广播
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
// 使用广播接收器接收电池状态的广播,这里不需要创建一个具体的接收器对象
val batteryStatus = context.registerReceiver(null, intentFilter)
// 从广播意图中获取电池电量级别,如果没有提供则默认为-1
val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
// 从广播意图中获取电池电量的缩放值,如果没有提供则默认为-1
val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
// 计算并返回电池电量百分比
return (level / scale.toFloat() * 100).roundToInt()
}
}
actual class BatteryManager {
actual fun getBatteryLevel(): Int {
val systemInfo = SystemInfo()
val batteryLevel = systemInfo.hardware.powerSources.firstOrNull()
return batteryLevel?.remainingCapacityPercent?.times(100)?.roundToInt() ?: -1
}
}
Koin 依赖注入管理
此部分使用koin框架实现基本的依赖注入管理,采用MVVM架构
参考视频:Full Guide to Dependency Injection With Koin for Compose Multiplatform - KMP for Beginners (youtube.com)
下图展示了下面案例使用Koin框架的整体逻辑架构图,下面对逻辑进行简要陈述
- startKoin用于初始化整个Koin框架,在这里需要定义配置以及注册对应的modules模块
- 每一个module都管理者多个repository以及viewmodel
- 下图中sharedModule模块注册了DbRepo单例,并同时注册了DbVM
- DbRepo相当于MVC的Service层,用于仓储等底层逻辑的操作,他有一个对应的实现类DbRepoImpl
- DbVM相当于MVC的controller层,用于调用Service层内包装好的逻辑方法
- 所以最终我们的App视图实际上是自动注入DbVM后,通过该viewmodel调用对应的方法来执行对应的结果的(具体的代码思路可以参考Springboot的DI思想,这里不做过多阐述)
commonApp层
在KMP开发过程中,所有逻辑都必须先从commonApp层定义,然后再扩展到各个平台实现层来完善具体代码;
首先导入koin依赖,因为我们这边使用DSL进行依赖管理,所以需要先在libs.version.toml文件定义好对应依赖的版本以及名称;
[versions]
koin = "3.6.0-Beta4"
koinComposeMultiplatform = "1.2.0-Beta4"
navigationCompose = "2.8.0-alpha02"
lifecycleViewModel = "2.8.2"
[libraries]
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" }
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
然后再build.gradle.kts里面为各个层导入对应的依赖(只添加我在下面添加的依赖,其他的不用管)
sourceSets {
val desktopMain by getting
androidMain.dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
}
commonMain.dependencies {
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.navigation.compose)
}
desktopMain.dependencies {
}
}
这是commonApp层的所有代码,请遵照上方给出的逻辑架构图进行理解!!!
初始化koin
/**
* 初始化Koin依赖注入框架
*
* 该函数用于在应用启动时设置Koin依赖注入框架的配置,并开始定义注入模块
* 它允许传递一个自定义配置函数,以便于在不同项目中进行特定的配置调整
*
* @param config 一个可选的自定义配置函数,用于在启动Koin时进行额外配置
* 该函数接收一个KoinApplicationBuilder作为参数,通过它可以自定义Koin的配置
*/
fun initKoin(config: KoinAppDeclaration? = null) {
// 启动Koin,并在其配置过程中注入自定义配置(如果提供)以及定义好的注入模块
startKoin {
// 如果提供了自定义配置函数,则执行该函数,允许开发者对Koin进行特定的配置定制
config?.invoke(this)
// 注册应用所需的注入模块,这些模块包含应用中所有需要进行依赖注入的类和它们的创建逻辑
modules(sharedModule, platformModule)
}
}
定义两个Modules
expect val platformModule: Module
val sharedModule = module {
singleOf(::DbRepoImpl).bind<DbRepo>()
viewModelOf(::DbVM)
}
定义全局唯一的客户端实例,用来标注koin的唯一性
expect class DbClient
定义viewmodel
class DbVM(
private val repository: DbRepo
) : ViewModel() {
fun getHelloWorldString(): String {
return repository.helloWorld()
}
}
定义仓储接口DbRepo及其对应的实现类DbRepoImpl
interface DbRepo {
fun helloWorld(): String
}
class DbRepoImpl(
private val dbClient: DbClient
) : DbRepo {
override fun helloWorld(): String {
return "Hello World!"
}
}
最后在app文件内调用DbVM,来显示简单的字段,嘻嘻
@OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
MaterialTheme {
KoinContext {
NavHost(
navController = rememberNavController(),
startDestination = "home"
) {
composable(route = "home") {
val viewModel = koinViewModel<DbVM>()
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = viewModel.getHelloWorldString()
)
}
}
}
}
}
}
androidmanifest.xml 配置文件设置
在该配置文件内,添加字段 android:name
,他表示Koin的程序入口点,我们待会在androidApp层需要编写一个名称完全一致的方法,并在该方法内调用initKoin,来实现koin框架的初始化,以便在整个App最终启动完毕前就完成了DI;
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
androidApp层
首先需要处理程序入口点的问题
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin {
androidContext(this@MyApplication)
}
}
}
初始化全局实例
actual class DbClient(
private val context: Context
)
我们之前定义的platformModule模块直接用于存储DbClient的单例对象
actual val platformModule = module {
singleOf(::DbClient)
}