Android KMP 快速入门2 - 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实现了跨平台开发;

  1. expect一般在commonApp里面定义,这是整个项目的通用逻辑部分,它相当于一个抽象类;
  2. 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框架的整体逻辑架构图,下面对逻辑进行简要陈述

  1. startKoin用于初始化整个Koin框架,在这里需要定义配置以及注册对应的modules模块
  2. 每一个module都管理者多个repository以及viewmodel
  3. 下图中sharedModule模块注册了DbRepo单例,并同时注册了DbVM
  4. DbRepo相当于MVC的Service层,用于仓储等底层逻辑的操作,他有一个对应的实现类DbRepoImpl
  5. DbVM相当于MVC的controller层,用于调用Service层内包装好的逻辑方法
  6. 所以最终我们的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)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zhillery

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值