使用 koin 作为 Android 注入工具,真香

9,384 阅读5分钟

使用 koin 作为 Android 注入工具,真香

koin 为 Android 提供了简单易用的 API 接口,让你简单轻松地接入 koin 框架。

[koin 在 Android 中的 gradle 配置]

mp.weixin.qq.com/s/bscC7mO4O…

1.Application 类中 startKoin

从您的类中,您可以使用该函数并注入 Android 上下文,如下所示:

Application startKoin androidContext
class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // Log Koin into Android logger
            androidLogger()
            // Reference Android context
            androidContext(this@MainApplication)
            // Load modules
            modules(myAppModules)
        }

    }
}

如果您需要从另一个 Android 类启动 Koin,您可以使用该函数为您的 Android 实例提供如下:startKoin Context

startKoin {
    //inject Android context
    androidContext(/* your android context */)
    // ...
}

2. 额外配置

从您的 Koin 配置(在块代码中),您还可以配置 Koin 的多个部分。startKoin { }

2.1 Koin Logging for Android

koin 提供了 log 的 Android 实现。

startKoin {
    // use Android logger - Level.INFO by default
    androidLogger()
    // ...
}

2.2 加载属性

您可以在文件中使用 Koin 属性来存储键/值:assets/koin.properties

startKoin {
    // ...
    // use properties from assets/koin.properties
    androidFileProperties()

}

3. Android 中注入对象实例

3.1 为 Android 类做准备

koin 提供了KoinComponents 扩展,Android 组件都具有这种扩展,这些组件包括 Activity Fragment Service ComponentCallbacks

您可以通过如下方式访问 Kotlin 扩展:

by inject()- 来自 Koin 容器的延迟计算实例

get() - 从 Koin 容器中获取实例

我们可以将一个属性声明为惰性注入:

module {
    // definition of Presenter
    factory { Presenter() }
}
class DetailActivity : AppCompatActivity() {

    // Lazy inject Presenter
    override val presenter : Presenter by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        //...
    }
}

或者我们可以直接得到一个实例:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Retrieve a Presenter instance
    val presenter : Presenter = get()
}

注意:如果你的类没有扩展,只需添加 KoinComponent 接口,如果你需要或来自另一个类的实例。inject() get()

3.2 Android Context 使用

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            //inject Android context
            androidContext(this@MainApplication)
            // ...
        }

    }
}

在你的定义中,下面的函数允许你在 Koin 模块中获取实例,以帮助你简单地编写需要实例的表达式。androidContext() androidApplication() Context Application

val appModule = module {

    // create a Presenter instance with injection of R.string.mystring resources from Android
    factory {
        MyPresenter(androidContext().resources.getString(R.string.mystring))
    }
}

4. 用于 Android 的 DSL 构造函数

4.1 DSL 构造函数

Koin 现在提供了一种新的 DSL 关键字,允许您直接面向类构造函数,并避免在 lambda 表达式中键入您的定义。

对于 Android,这意味着以下新的构造函数 DSL 关键字:

viewModelOf()- 相当于viewModel { }

fragmentOf()- 相当于fragment { }

workerOf()- 相当于worker { }

注意:请务必在类名之前使用,以定位类构造函数::

4.2 Android DSL 函数示例

给定一个具有以下组件的 Android 应用程序:

// A simple service
class SimpleServiceImpl() : SimpleService

// a Presenter, using SimpleService and can receive "id" injected param
class FactoryPresenter(val id: String, val service: SimpleService)

// a ViewModel that can receive "id" injected param, use SimpleService and get SavedStateHandle
class SimpleViewModel(val id: String, val service: SimpleService, val handle: SavedStateHandle) : ViewModel()

// a scoped Session, that can received link to the MyActivity (from scope)
class Session(val activity: MyActivity)

// a Worker, using SimpleService and getting Context & WorkerParameters
class SimpleWorker(
    private val simpleService: SimpleService,
    appContext: Context,
    private val params: WorkerParameters
) : CoroutineWorker(appContext, params)

我们可以这样声明它们:

module {
    singleOf(::SimpleServiceImpl){ bind<SimpleService>() }

    factoryOf(::FactoryPresenter)

    viewModelOf(::SimpleViewModel)

    scope<MyActivity>(){
        scopedOf(::Session)
    }

    workerOf(::SimpleWorker)
}

5. Android 中的 koin 多模块使用

通过使用 Koin,您可以描述模块中的定义。在本节中,我们将了解如何声明,组织和链接模块。

5.1 koin 多模块

组件不必位于同一模块中。模块是帮助您组织定义的逻辑空间,并且可以依赖于其他定义 模块。定义是惰性的,然后仅在组件请求它时才解析。

让我们举个例子,链接的组件位于单独的模块中:

// ComponentB <- ComponentA
class ComponentA()
class ComponentB(val componentA : ComponentA)

val moduleA = module {
    // Singleton ComponentA
    single { ComponentA() }
}

val moduleB = module {
    // Singleton ComponentB with linked instance ComponentA
    single { ComponentB(get()) }
}

我们只需要在启动 Koin 容器时声明已使用模块的列表:

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // ...

            // Load modules
            modules(moduleA, moduleB)
        }

    }
}

5.2 模块包含

类中提供了一个新函数,它允许您通过以有组织和结构化的方式包含其他模块来组合模块includes() Module

新模块有 2 个突出特点:

将大型模块拆分为更小、更集中的模块。

在模块化项目中,它允许您更精细地控制模块可见性(请参阅下面的示例)。

它是如何工作的?让我们采用一些模块,我们将模块包含在:parentModule

// `:feature` module
val childModule1 = module {
    /* Other definitions here. */
}
val childModule2 = module {
    /* Other definitions here. */
}
val parentModule = module {
    includes(childModule1, childModule2)
}

// `:app` module
startKoin { modules(parentModule) }

请注意,我们不需要显式设置所有模块:通过包含,声明的所有模块将自动加载。

parentModule includes childModule1 childModule2 parentModule childModule1 childModule2

信息:模块加载现在经过优化,可以展平所有模块图,并避免重复的模块定义。

最后,您可以包含多个嵌套或重复的模块,Koin 将扁平化所有包含的模块,删除重复项:

// :feature module
val dataModule = module {
    /* Other definitions here. */
}
val domainModule = module {
    /* Other definitions here. */
}
val featureModule1 = module {
    includes(domainModule, dataModule)
}
val featureModule2 = module {
    includes(domainModule, dataModule)
}
// :app module
class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // ...

            // Load modules
             modules(featureModule1, featureModule2)
        }

    }
}

请注意,所有模块将只包含一次:dataModule domainModule featureModule1 featureModule2

5.3 Android ViewModel 和 Navigation

Gradle 模块引入了一个新的 DSL 关键字,该关键字作为补充,以帮助声明 ViewModel 组件并将其绑定到 Android 组件生命周期。关键字也可用允许您使用其构造函数声明 ViewModel。koin-android viewModel singlefactory viewModelOf

val appModule = module {

    // ViewModel for Detail View
    viewModel { DetailViewModel(get(), get()) }

    // or directly with constructor
    viewModelOf(::DetailViewModel)
}

声明的组件必须至少扩展类。您可以指定如何注入类的构造函数 并使用该函数注入依赖项。android.arch.lifecycle.ViewModel get()

注意:关键字有助于声明 ViewModel 的工厂实例。此实例将由内部 ViewModelFactory 处理,并在需要时重新附加 ViewModel 实例。它还将允许注入参数。viewModel viewModelOf

5.4 注入 ViewModel

在 Android 组件中使用 viewModel ,Activity Fragment Service

by viewModel()- 惰性委托属性,用于将视图模型注入到属性中

getViewModel()- 直接获取视图模型实例

class DetailActivity : AppCompatActivity() {

    // Lazy inject ViewModel
    val detailViewModel: DetailViewModel by viewModel()
}

5.5 Activity 共享 ViewModel

一个 ViewModel 实例可以在 Fragment 及其主 Activity 之间共享。

要在使用中注入共享视图模型,请执行以下操作:Fragment

by activityViewModel()- 惰性委托属性,用于将共享 viewModel 实例注入到属性中

get ActivityViewModel()- 直接获取共享 viewModel 实例

只需声明一次视图模型:

val weatherAppModule = module {

    // WeatherViewModel declaration for Weather View components
    viewModel { WeatherViewModel(get(), get()) }
}

注意:viewModel 的限定符将作为 viewModel 的标记处理

并在 Activity 和 Fragment 中重复使用它:

class WeatherActivity : AppCompatActivity() {

    /*
     * Declare WeatherViewModel with Koin and allow constructor dependency injection
     */
    private val weatherViewModel by viewModel<WeatherViewModel>()
}

class WeatherHeaderFragment : Fragment() {

    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by activityViewModel<WeatherViewModel>()
}

class WeatherListFragment : Fragment() {

    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by activityViewModel<WeatherViewModel>()
}

5.6 将参数传递给构造函数

向 viewModel 传入参数,示例代码如下:

模块中

val appModule = module {

    // ViewModel for Detail View with id as parameter injection
    viewModel { parameters -> DetailViewModel(id = parameters.get(), get(), get()) }
    // ViewModel for Detail View with id as parameter injection, resolved from graph
    viewModel { DetailViewModel(get(), get(), get()) }
    // or Constructor DSL
    viewModelOf(::DetailViewModel)
}

依赖注入点传入参数

class DetailActivity : AppCompatActivity() {

    val id : String // id of the view

    // Lazy inject ViewModel with id parameter
    val detailViewModel: DetailViewModel by viewModel{ parametersOf(id)}
}

5.7 SavedStateHandle 注入

添加键入到构造函数的新属性以处理 ViewModel 状态:SavedStateHandle

class MyStateVM(val handle: SavedStateHandle, val myService : MyService) : ViewModel() 在 Koin 模块中,只需使用或参数解析它:get()

viewModel { MyStateVM(get(), get()) } 或使用构造函数 DSL:

viewModelOf(::MyStateVM) 在 Activity Fragment

by viewModel()- 惰性委托属性,用于将状态视图模型实例注入属性

getViewModel()- 直接获取状态视图模型实例

class DetailActivity : AppCompatActivity() {

    // MyStateVM viewModel injected with SavedStateHandle
    val myStateVM: MyStateVM by viewModel()
}

5.8 Navigation 导航图中的 viewModel

您可以将 ViewModel 实例的范围限定为导航图。只需要传入 ID 给by koinNavGraphViewModel()

class NavFragment : Fragment() {

    val mainViewModel: NavViewModel by koinNavGraphViewModel(R.id.my_graph)

}

5.9 viewModel 通用 API

Koin 提供了一些“底层”API 来直接调整您的 ViewModel 实例。viewModelForClass ComponentActivity Fragment

ComponentActivity.viewModelForClass(
    clazz: KClass<T>,
    qualifier: Qualifier? = null,
    owner: ViewModelStoreOwner = this,
    state: BundleDefinition? = null,
    key: String? = null,
    parameters: ParametersDefinition? = null,
): Lazy<T>

还提供了顶级函数:

fun <T : ViewModel> getLazyViewModelForClass(
    clazz: KClass<T>,
    owner: ViewModelStoreOwner,
    scope: Scope = GlobalContext.get().scopeRegistry.rootScope,
    qualifier: Qualifier? = null,
    state: BundleDefinition? = null,
    key: String? = null,
    parameters: ParametersDefinition? = null,
): Lazy<T>

5.10 ViewModel API - Java Compat

必须将 Java 兼容性添加到依赖项中:

// Java Compatibility
implementation "io.insert-koin:koin-android-compat:$koin_version"
您可以使用以下函数或静态函数将 ViewModel 实例注入到 Java 代码库中:viewModel() getViewModel() ViewModelCompat

@JvmOverloads
@JvmStatic
@MainThread
fun <T : ViewModel> getViewModel(
    owner: ViewModelStoreOwner,
    clazz: Class<T>,
    qualifier: Qualifier? = null,
    parameters: ParametersDefinition? = null
)

6. 在 Jetpack Compose 中注入

请先了解 Jetpack Compose 相关内容:

developer.android.com/jetpack/com…

6.1 注入@Composable

在编写可组合函数时,您可以访问以下 Koin API:

get()- 从 Koin 容器中获取实例

getKoin()- 获取当前 Koin 实例

对于声明“MyService”组件的模块:

val androidModule = module {

    single { MyService() }
}

我们可以像这样获取您的实例:

@Composable
fun App() {
    val myService = get<MyService>()
}

注意:为了在 Jetpack Compose 的功能方面保持一致,最好的编写方法是将实例直接注入到函数属性中。这种方式允许使用 Koin 进行默认实现,但保持开放状态以根据需要注入实例。

@Composable
fun App(myService: MyService = get()) {
}

6.2 viewModel @Composable

与访问经典单/工厂实例的方式相同,您可以访问以下 Koin ViewModel API:

getViewModel()或 - 获取实例koinViewModel()

对于声明“MyViewModel”组件的模块:

module {
    viewModel { MyViewModel() }
    // or constructor DSL
    viewModelOf(::MyViewModel)
}

我们可以像这样获取您的实例:

@Composable
fun App() {
    val vm = koinViewModel<MyViewModel>()
}

我们可以在函数参数中获取您的实例:

@Composable
fun App(vm : MyViewModel = koinViewModel()) {

}

7. 管理 Android 作用域

Android 组件,如Activity、Fragment、Service都有生命周期,这些组件都是由 System 实例化,组件中有相应的生命周期回调。

正因为 Android 组件具有生命周期属性,所以不能在 koin 中传入组件实例。按照生命周期长短,组件可分为三类:

  • • 长周期组件(Service、database)——由多个屏幕使用,永不丢弃
  • • 中等周期组件(User session)——由多个屏幕使用,必须在一段时间后删除
  • • 短周期组件(ViewModel) ——仅由一个 Screen 使用,必须在 Screen 末尾删除

对于长周期组件,我们通常在应用全局使用 single 创建单实例

在 MVP 架构模式下,Presenter 是短周期组件

在 Activity 中创建方式如下

class DetailActivity : AppCompatActivity() {

    // injected Presenter
    override val presenter : Presenter by inject()

我们也可以在 module 中创建

我们使用 factory 作用域创建 Presenter 实例

val androidModule = module {

    // Factory instance of Presenter
    factory { Presenter() }
}

生成绑定到作用域的实例 scope

val androidModule = module {

    scope<DetailActivity> {
        scoped { Presenter() }
    }
}

大多数 Android 内存泄漏来自从非 Android 组件引用 UI/Android 组件。系统保留引用在它上面,不能通过垃圾收集完全回收它。

7.1 申明 Android 作用域

要限定 Android 组件上的依赖关系,您必须使用如下所示的块声明一个作用域:scope

class MyPresenter()
class MyAdapter(val presenter : MyPresenter)

module {
  // Declare scope for MyActivity
  scope<MyActivity> {
    // get MyPresenter instance from current scope
    scoped { MyAdapter(get()) }
    scoped { MyPresenter() }
  }
}

7.2 Android Scope 类

Koin 提供了 Android 生命周期组件相关的 Scope 类ScopeActivity Retained ScopeActivity ScopeFragment

class MyActivity : ScopeActivity() {

    // MyPresenter is resolved from MyActivity's scope
    val presenter : MyPresenter by inject()
}

Android Scope 需要与接口一起使用来实现这样的字段:AndroidScopeComponent scope

abstract class ScopeActivity(
    @LayoutRes contentLayoutId: Int = 0,
) : AppCompatActivity(contentLayoutId), AndroidScopeComponent {

    override val scope: Scope by activityScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        checkNotNull(scope)
    }
}

我们需要使用接口并实现属性。这将设置类使用的默认 Scope。AndroidScopeComponent scope

7.3 Android Scope 接口

要创建绑定到 Android 组件的 Koin 作用域,只需使用以下函数:

createActivityScope()- 为当前 Activity 创建 Scope(必须声明 Scope 部分)

createActivityRetainedScope()- 为当前 Activity 创建 RetainedScope(由 ViewModel Lifecycle 支持)(必须声明 Scope 部分)

createFragmentScope()- 为当前 Fragment 创建 Scope 并链接到父 Activity Scope 这些函数可作为委托使用,以实现不同类型的作用域:

activityScope()- 为当前 Activity 创建 Scope(必须声明 Scope 部分)

activityRetainedScope()- 为当前 Activity 创建 RetainedScope(由 ViewModel Lifecycle 支持)(必须声明 Scope 部分)

fragmentScope()- 为当前 Fragment 创建 Scope 并链接到父 Activity Scope

class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {

    override val scope: Scope by activityScope()

}

我们还可以使用以下内容设置保留范围(由 ViewModel 生命周期提供支持):

class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {

    override val scope: Scope by activityRetainedScope()
}

如果您不想使用 Android Scope 类,则可以使用自己的类并使用 Scope 创建 API AndroidScopeComponent

7.4 Scope 链接

Scope 链接允许在具有自定义作用域的组件之间共享实例。在更广泛的用法中,您可以跨组件使用实例。例如,如果我们需要共享一个实例。Scope UserSession

首先声明一个范围定义:

module {
    // Shared user session data
    scope(named("session")) {
        scoped { UserSession() }
    }
}

当需要开始使用实例时,请为其创建范围:UserSession

val ourSession = getKoin().createScope("ourSession",named("session"))

// link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
scope.linkTo(ourSession)

然后在您需要的任何地方使用它:

class MyActivity1 : ScopeActivity() {

    fun reuseSession(){
        val ourSession = getKoin().createScope("ourSession",named("session"))

        // link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
        scope.linkTo(ourSession)

        // will look at MyActivity1's Scope + ourSession scope to resolve
        val userSession = get<UserSession>()
    }
}
class MyActivity2 : ScopeActivity() {

    fun reuseSession(){
        val ourSession = getKoin().createScope("ourSession",named("session"))

        // link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
        scope.linkTo(ourSession)

        // will look at MyActivity2's Scope + ourSession scope to resolve
        val userSession = get<UserSession>()
    }
}

8.Fragment Factory

由于 AndroidX 已经发布了软件包系列以扩展 Android 的功能 androidx.fragment Fragment

developer.android.com/jetpack/and…

8.1 Fragment Factory

自版本以来,已经引入了 ,一个专门用于创建类实例的类:2.1.0-alpha-3 FragmentFactory Fragment

developer.android.com/reference/k…

Koin 也提供了创建 Fragment 的工厂类 KoinFragmentFactory Fragment

8.2 设置 Fragment Factory

首先,在 KoinApplication 声明中,使用关键字设置默认实例:fragmentFactory() KoinFragmentFactory

 startKoin {
    // setup a KoinFragmentFactory instance
    fragmentFactory()

    modules(...)
}

8.3 声明并注入 Fragment

声明一个 Fragment 并在 module 中注入

class MyFragment(val myService: MyService) : Fragment() {


}
val appModule = module {
    single { MyService() }
    fragment { MyFragment(get()) }
}

8.4 获取 Fragment

使用setupKoinFragmentFactory() 设置 FragmentFactory

查询您的 Fragment ,使用supportFragmentManager

supportFragmentManager.beginTransaction()
            .replace<MyFragment>(R.id.mvvm_frame)
            .commit()

加入可选参数

supportFragmentManager.beginTransaction()
            .replace<MyFragment>(
                containerViewId = R.id.mvvm_frame,
                args = MyBundle(),
                tag = MyString()
            )

8.5 Fragment Factory & Koin Scopes

如果你想使用 Koin Activity Scope,你必须在你的 Scope 声明你的 Fragment 作为一个定义:scoped

val appModule = module {
    scope<MyActivity> {
        fragment { MyFragment(get()) }
    }
}

并使用您的 Scope 设置您的 Koin Fragment Factory:setupKoinFragmentFactory(lifecycleScope)

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Koin Fragment Factory
        setupKoinFragmentFactory(lifecycleScope)

        super.onCreate(savedInstanceState)
        //...
    }
}

9. WorkManager 的 Koin 注入

koin 为 WorkManager 提供单独的组件包 koin-androidx-workmanager

首先,在 KoinApplication 声明中,使用关键字来设置自定义 WorkManager 实例:workManagerFactory()

class MainApplication : Application(), KoinComponent {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            // setup a WorkManager instance
            workManagerFactory()
            modules(...)
        }
        setupWorkManagerFactory()
}

AndroidManifest.xml 修改,避免使用默认的

    <application . . .>
        . . .
        <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:authorities="${applicationId}.workmanager-init"
            tools:node="remove" />
  </application>

9.1 声明 ListenableWorker

val appModule = module {
single { MyService() }
worker { MyListenableWorker(get()) }
}

9.2 创建额外的 WorkManagerFactory

class MainApplication : Application(), KoinComponent {

    override fun onCreate() {
        super.onCreate()

        startKoin {
           workManagerFactory(workFactory1, workFactory2)
           . . .
        }

        setupWorkManagerFactory()
    }

}

如果 Koin 和 workFactory1 提供的 WorkManagerFactory 都可以实例化 ListenableWorker,则 Koin 提供的工厂将是使用的工厂。

9.3 更改 koin lib 本身的清单

如果 koin-androidx-workmanager 中的默认 Factory 被禁用,而应用程序开发人员不初始化 koin 的工作管理器基础架构,他最终将没有可用的工作管理器工厂。

针对上面的情况,我们做如下 DSL 改进:

val workerFactoryModule = module {
factory<WorkFactory> { WorkFactory1() }
factory<WorkFactory> { WorkFactory2() }
}

然后让 koin 内部做类似的事情

fun Application.setupWorkManagerFactory(
// no vararg for WorkerFactory
) {
. . .
getKoin().getAll<WorkerFactory>()
.forEach {
delegatingWorkerFactory.addFactory(it)
}
}

参考链接

insert-koin.io/

推荐阅读


欢迎关注我的公众号“虎哥LoveDroid”,原创技术文章第一时间推送。