dagger的使用

背景

在写安卓应用时,总是要写 AndroidViewModel 而不是 ViewModel 比较郁闷,
恰好官方在说到应用架构模式时,将 Repository 以参数形式给了 ViewModel,并使用了 @Inject 关键字,
使得本来需要在 ViewModel 中使用 context 初始化 Repository,
并在 Repository 中使用 context 初始化 Dao 的操作变得不需要了.
因此探究一番.

介绍

Dagger,Square公司旗下产品,半静态半运行完成依赖注入.
谷歌对其进行改造做成Dagger2,
基于Java注解,
完全在编译阶段完成依赖注入(不是在运行时依靠反射,不会造成运行时性能损耗)

依赖

1
2
3
4
5
6
apply plugin: 'kotlin-kapt'

dependencies {
implementation 'com.google.dagger:dagger:2.17'
kapt 'com.google.dagger:dagger-compiler:2.17'
}

基础使用

最简单的情形

都是自己构造的类,且不需要参数.
这样只需要使用 @Inject@Component 两种注解
需要写三处注解

  1. 被依赖的类的构造函数处
1
2
3
class MainViewModel @Inject constructor(): ViewModel() {
val name = "name in viewmodel"
}
  1. 依赖的类中,被依赖的类的声明处
1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MainViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

DaggerMainComponent.create().inject(this)
Log.d("print2", viewModel.name)
}
}
  1. 一个为写而写,表明谁被注入的一个注射器
1
2
3
4
@Component
interface MainComponent {
fun inject(activity: MainActivity)
}
  1. build一次,产生 Dagger 前缀的类,
    然后才可以使用 DaggerMainComponent
  2. 需要在被注入的类中使用该注射器

但可能这样做的 ViewModel 不能监听声明周期?

当构造类时需要参数

ViewModel相关实践

全手动

  1. 基于setter的注入

    viewmodel中定义setter函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MainViewModel() : ViewModel() {
    lateinit var bookDao : BookDao

    fun setBookDao(dao: BookDao) {
    this.bookDao = dao
    }

    // ...
    }

    在Activity中

    1
    2
    3
    val bookDao = AppDatabase.getInstance(application).bookDao()
    mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    mainViewModel.setBookDao(bookDao)
  2. 基于constructor的注入

    viewmodel中定义构造参数

    1
    2
    3
    4
    5
    class MainViewModel (
    var bookDao: BookDao
    ) : ViewModel() {
    // ...
    }

    Activity中

    1
    2
    val bookDao = AppDatabase.getInstance(application).bookDao()
    mainViewModel = ViewModelProvider(this).get((MainViewModel(bookDao)::class.java))

    然而这种方法是错误的,
    不知道什么原因导致在传递复杂的参数给 MainViewModel 类时会失败,
    (尽管给 AndroidViewModel 传递 application 会成功)
    正确的方法是使用 ViewModelProvider.Factory.

    在ViewModel中定义一个Factory,
    同时需要定义和ViewModel相同的参数.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MainViewModel (
    var bookDao: BookDao
    ) : ViewModel() {

    class Factory constructor(
    private val bookDao : BookDao
    ) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    return MainViewModel(bookDao) as T
    }
    }
    }

    在Activity中构造依赖和factory类,
    使用之前没有使用过的 ViewModelProvider(owner, factory) 构造方法来构造ViewModelProvider.

    1
    2
    3
    val bookDao = AppDatabase.getInstance(application).bookDao()
    val factory = MainViewModel.Factory(bookDao)
    mainViewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)

    可以推测不能直接以ViewModel的构造参数来初始化ViewModel,
    而是使用一个相同构造方法的factory来构造.

半自动方法

dagger似乎对基于setter的依赖注入方法不感兴趣,
因此半自动方法中会对基于constructor注入的方法.
在activity中,其他的东西比如DAO,ApiService等等,使用自动插入的方法.
而factory使用手动的方法生成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : AppCompatActivity() {
@Inject
lateinit var bookDao: BookDao

lateinit var mainViewModel: MainViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_main)

DaggerMainComponent.builder().application(application).build().inject(this)

val factory = MainViewModel.Factory(bookDao)
mainViewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)

// ...
}
}

这就需要在Module中写好提供Dao的方法.
而为了提供Dao,需要提供DB,ApplicationContext等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Module
class MainModule {
@Singleton
@Provides
fun provideContext(application: Application): Context {
return application.applicationContext
}

@Singleton
@Provides
fun provideDb(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "news-db").build()
}

@Singleton
@Provides
fun provideBookDao(db: AppDatabase): BookDao {
return db.bookDao()
}
}

在制作DB的实例时,代码几乎相同,但 Singleton 的使用免去了手动的判断 instance == null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Database(entities = [Book::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {

abstract fun bookDao(): BookDao

// companion object {
// private var instance: AppDatabase? = null
// fun getInstance(context: Context): AppDatabase {
//
// // 单例模式
// if (instance == null) {
// instance = Room.databaseBuilder(
// context.applicationContext,
// AppDatabase::class.java,
// "sample.db" //数据库名称
// ).allowMainThreadQueries().build()
// }
//
// // 单例模式
// return instance as AppDatabase
// }
// }
}

而最初的application则可能是由Component提供的.
此处的Component不单单是定义一个 fun inject(activity: MainActivity) 就完事.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Singleton
@Component(modules = [MainModule::class])
interface MainComponent {
@Component.Builder
interface Builder {

@BindsInstance
fun application(application: Application): Builder

fun build(): MainComponent
}

fun inject(activity: MainActivity)
}

但这样需要为每一个ViewModel都写一次factory子类,
在每个activity中都写一次构造factory的语句.
不够完全自动化.
如果viewmodel本身也可以使用一个 @Inject 自动装配就好了.

几乎全自动的方法

例子中没能将ViewModel也做成自动装配,
而是自动装配了factory.
可能由于使用factory时不能根据输出的ViewModel类型自动判断使用哪些组件来装配吧.

定义了一个全局通用的Factory,传入了一个ViewModel与Provider的对应表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Singleton
class ViewModelFactory @Inject constructor(
private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("unknown model class $modelClass")
}
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

使用高级的写法,目的可能是提供这个对应表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Module
abstract class ViewModelModule {

/**
* Binding NewsArticleViewModel using this key "NewsArticleViewModel::class"
* So you can get NewsArticleViewModel using "NewsArticleViewModel::class" key from factory
*/
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel

/**
* Binds ViewModels factory to provide ViewModels.
*/
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}

@MapKey
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ViewModelKey(val value: KClass<out ViewModel>)

然后在总的Module中包含一下

1
2
3
4
@Module(includes = [ViewModelModule::class])
class MainModule {
// ...
}

参考

  1. 知乎的科普贴
  2. 还算说人话但可惜是java的
  3. 说人话的
  4. 官方的指导 TODO
  5. 缺了这个无法理解