android 依赖注入(Hilt, Koin)

github blog
qq: 2383518170
wx: lzyprime

λ:

仓库地址: https://github.com/lzyprime/android_demos

开发分支dev加入了compose, 图片库由 glide 换为 coil, DataStore代替SharedPreference。 同时剔除掉LiveData, 用Flow代替。

本来想完全用compose完成UI实现。但是目前compose组件贫瘠,与其他库的配合库也都没有稳定。部分场景下实现反倒费力。所以开两个分支:

  • dev: compose 只做部分控件实现,主体仍保留传统库和其他方式。
  • dev_compose: view 层完全用 compose 实现,包括路由导航。删除layout, navgation, menu等文件夹,删除compose 以外的依赖。

hilt 或者 koin 做依赖注入是贯穿全局的。所以得先会这个

android依赖注入 官网文档

hilt 官网

koin 官网

关于文档,还是尽量看英文原版。原版文档本身有一定延迟,而中文文档翻译又会延迟一段。就导致文档里api可能已过时,内容废弃等问题。比如hilt在android官网的文档。中文版本还是alpha版本的,@ViewModelInject接口已经废弃等.

当然中文文档中介绍和原理部分还是可以参考,即使api改变了,这种东西一般也不会变。

共性:

无论是哪个库,原理都是一样的:

  1. 根据Application, Activity, Fragment等为范围,开不同的容器Container

  2. 所依赖的实例从Container中获得,并以单例存在于Container

Android 手动依赖注入文档

//example
    class MyApplication : Application() {
        val appContainer = AppContainer()
        ...
    }

    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel

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

            val appContainer = (application as MyApplication).appContainer
            loginViewModel = LoginViewModel(appContainer.userRepository)
        }
    }

手动依赖注入问题:

  • 必须自行管理 Container,手动为依赖项创建实例,根据生命周期移除实例
  • 样板代码
  • ViewModel依赖注入,要靠ViewModelProvider.Factory才可以复用Jetpack中的获取方式. 或者放进容器,自己维护ViewModel生命周期。

依赖注入有什么好处是老生常谈。当然可以不用,通篇object, 直接就单例,到处可以使用。

依赖注入优势,官网给的总结:

  • 重用类以及分离依赖项:更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。
  • 易于重构:依赖项成为 API Surface 的可验证部分,因此可以在创建对象时或编译时进行检查,而不是作为实现详情隐藏。
  • 易于测试:类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。

Hilt

Hilt做法是改变并拓展基类,比如继承自class XXApplication : Application()的,编译器插件生成Hilt_XXApplication类,在里边实现容器维护。也就是变为class XXApplication : Hilt_XXApplication()。可以看生成的代码。

开容器

通过@HiltAndroidApp, @AndroidEntryPoint标记,生成对应基类,在基类里实现容器的维护逻辑。

@HiltAndroidApp
class UnsplashApplication : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity()

@Inject 标记需要注入项

class UnsplashRepository @Inject constructor(private val service: UnsplashService)

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var service: XXService
    ...
}

注册实例获取方式

创建 @Module, 通过 @Provides@Binds 标记实例获取方式,并通过 @InstallIn 标记实例放在哪个容器里。

@Binds

interface XXService { ... }

class XXServiceImpl @Inject constructor(...) : XXService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class XXModule {
    
  @Binds
  abstract fun bindAnalyticsService(impl: XXServiceImpl): XXService
}

// or

@Module
@InstallIn(ActivityComponent::class)
interface XXModule {
    
  @Binds
  fun bindAnalyticsService(impl: XXServiceImpl): XXService
}

@Provides

@Module
@InstallIn(SingletonComponent::class)
object XXModule {
  @Provides
  fun provideSDKManager(aInject:A, bInject:B): SDKManager = SDK.getManager(aInject, bIject)

  @Singleton
  @Provides
  fun provideYYService():YYService = Retrofit....create(YYService::class.java)
}

两种方式都可以标记某个实例的获取方式。@Bind的参数内容是可以通过依赖注入得到的,然后会把它作为返回内容。@Provides可以提供具体构造方式, 比如Retrofit创建实例的过程。当然全写成Provides也是可以跑的。

@InstallIn(XXX::class) 标记Module存放在哪个容器中,也就是它的范围。参照表:

component 注入到 create at destroyed at
SingletonComponent Application Application#onCreate() Application#onDestroy()
ActivityRetainedComponent N/A Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel ViewModel created ViewModel destroyed
FragmentComponent Fragment Fragment#onAttach() Fragment#onDestroy()
ViewComponent View View#super() View destroyed
ViewWithFragmentComponent View annotated with @WithFragmentBindings View#super() View destroyed
ServiceComponent Service Service#onCreate() Service#onDestroy()

所以在Provides例子中,范围是SingletonComponent,此时函数上打了 @Singleton标签。作用是限定函数只调用一次,产生的实例以单例形式存在。因为Activity, Fragment等都能拿到Module,如果不打,则他们会各自维护一份的单例。

没有 @InstallIn(ActivityRetainedComponent::class)

每一个其实就是对应一个容器,要么自己维护,要么挂到其他容器下。

容器关系

Hilt 就是个 Dagger 的API层,限定简化了注入方式。所以 @HiltAndroidApp, @AndroidEntryPoint 最后还是 Dagger 实现。看一下生成的代码,大概就通过 EntryPoint 一层层关联起容器。就算没搞过 Dagger 也大概能看懂过程:

Application 中会创建 ApplicationComponentManager。同时提供下层容器单例的获取方式等。把自己放进 SingletonComponent

// Activity 创建自己的ActivityComponentManager
class Hilt_XXXActivity {
    val componentManager: ActivityComponentManager
    protected fun inject() {
        (componentManager.generatedComponent as Injector).inject(this as XXXActivity)
    }
}

class ActivityComponentManager(val activity:Activity) {
    val activityRetainedComponentManager = ActivityRetainedComponentManager(activity as ComponentActivity)

    fun generatedComponent() = activityRetainedComponentManager.generatedComponent()
}

class ActivityRetainedComponentManager(val activity: ComponentActivity) {
    val viewModelProvider = ViewModelProvider(activity, Factory {
        return ActivityRetainedComponentViewModel(
            // 也就是 ActivityRetainedComponent 的单例
            (获取Application).activityRetainedComponentBuilder.build()
        )
    })

    fun generatedComponent() = viewModelProvider.get(ActivityRetainedComponentViewModel.class).getComponent()
}

class ActivityRetainedComponentViewModel(val component : ActivityRetainedComponent) : ViewModel() {
    fun getComponent() = component
    override fun onCleared() {
        getActivityRetainedLifecycle().dispatchOnCleared()
    }
}

@ActivityRetainedScoped
@DefineComponent(parent = SingletonComponent.class)
public interface ActivityRetainedComponent {}

简化一下, 也就是把自己放进了 ActivityRetainedComponent 单例, 并通过ViewModel生命周期,在销毁时,摘掉自己:

class Hilt_XXXActivity {
    val componentManager: ActivityComponentManager
    protected fun inject() {
        activityRetainedComponentImpl.inject(this as XXXActivity)
    }
}

ActivityRetainedComponent 单例在构造时,会包含 SingletonComponent 的单例。ActivityComponent 中会包含这两个的单例。 同理依次类推。

除此之外,也可以通过标记作用域的方式,标记存放在哪个容器中,如:

@Singleton // 放入SingletonComponent中
class UserRepository @Inject constructor(...)

对照表:

class component Scope
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

源码比这绕口的多,但大致流程如此。这也是为什么低层次的可以直接用高层次里的东西。

官方给的关系图:

关系图

为实例提供多个获取方式

// 限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DebugService

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ReleaseService

// module
@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @DebugService
  @Provides
  fun provideDebugService(): XXService = XXService(debug_url)

  @ReleaseService
  @Provides
  fun provideReleaseService(): XXService = XXService(release_url)
}

// use
class UserRepository @Inject constructor(
    @DebugService service: XXService
)

// or
class Example {
    @ReleaseService
    @Inject lateinit var okHttpClient: OkHttpClient
}

预设限定符:@ApplicationContext@ActivityContext

class AnalyticsAdapter @Inject constructor(
    @ApplicationContext private val context: Context,
    private val service: AnalyticsService
) { ... }

ViewModel

使用 @HiltViewModel 标记,在Activity中仍然可以通过 by viewModels() 的方式获取到。

@HiltViewModel
class ExampleViewModel @Inject constructor(
  private val savedStateHandle: SavedStateHandle,
  private val repository: ExampleRepository
) : ViewModel() {
  ...
}

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}

要了解如何做到的,首先要知道by viewModels()如何实现。完整的函数是:

@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(noinline factoryProducer: (() -> Factory)? = null)

要传 ViewModel Factory 获取方式。 当为默认值(null)时,会调用Activity#getDefaultViewModelProviderFactory 得到 Factory

Hilt 实现的注入方式,就是改变并拓展基类。所以在生成的Activity基类里,override这个方法,先去Hilt容器中找,没有匹配的则返回默认行为。

这是 KoinHilt 明显差别之一。 Koin 类似于把手动注入的过程封装一下,所以koin 库做了by viewModel()函数,用于在容器获取。

Navigation

可以以导航图为单位共享一个 ViewModel

val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)

gradle plugin 生成代码

得益于Hilt通过Gradle Plugin生成代码, 否则我们就要:

@HiltAndroidApp(Application.class)
class FooApplication : Hilt_FooApplication

@AndroidEntryPoint(FragmentActivity.class)
class FooActivity : Hilt_FooActivity

...

但是以注解和编译器插件做的坏处是拖慢了编译速度,尤其是项目大了之后。因为有 kotlin -> java -> kotlin 的反复横跳。当然好处是兼容java,还有就是kotlin编译器插件没稳定。编译时间问题希望之后能优化,或者有更好方案。

Koin

当了解了容器的概念,再来看Koin,或者其他依赖注入库就很容易理解。Koin 源码在 github 有,可以自己扒。或者猜测一下实现,看库是不是与自己一样。或者觉得库哪些内容实现不优雅不好,有无更好的方案或写法。

这个库相当于提供容器维护的api。所以要手动做的事会多一点。得益于kotlin语法,提供的api也是简洁,灵活。

开容器

Koin 通过 startKoin

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // Koin Android logger
            androidLogger()
            //inject Android context
            androidContext(this@MainApplication)
            // use modules
            modules(myAppModules)
        }
        
    }
}

标记需要注入项

by inject()

class MySimpleActivity : AppCompatActivity() {
    val firstPresenter: MySimplePresenter by inject()
}

get()

class MySimpleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
           val firstPresenter: MySimplePresenter = get()
    }
}

注册实例获取方式

val appModule = module {

    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // Simple Presenter Factory
    factory { MySimplePresenter(get()) }
}
startKoin {
    ...
    // use modules
    modules(myAppModules)
}

module 有详细文档。至于实例的生命周期和作用域,肯定靠modulestartKoin注册方式维护。

ViewModel

class MyViewModel(val repo : HelloRepository) : ViewModel() {...}
val appModule = module {
    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // MyViewModel ViewModel
    viewModel { MyViewModel(get()) }
}

由于注入方式相当于手动维护容器,所以ViewModel也需要注册获取方式。

class MyViewModelActivity : AppCompatActivity() {   
    val myViewModel: MyViewModel by viewModel()
}

~λ:

其他详细内容可看文档和源码。两个库各有优劣,相比Koin, Hilt可能少写点东西,而且底层是Dagger,觉得提供的工具不够用时,可以直接用Dagger胡搞。编译真的慢, 这几天刚把公司项目加了Hilt,感觉编译长了1.5倍左右。当然项目不算大,总体也耗不了多久,可以接受。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容