背景
在写安卓应用时,总是要写 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 2 3 class MainViewModel @Inject constructor (): ViewModel() { val name = "name in viewmodel" }
依赖的类中,被依赖的类的声明处
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 2 3 4 @Component interface MainComponent { fun inject (activity: MainActivity ) }
build一次,产生 Dagger
前缀的类,
然后才可以使用 DaggerMainComponent
需要在被注入的类中使用该注射器
但可能这样做的 ViewModel
不能监听声明周期?
当构造类时需要参数
ViewModel相关实践
全手动
基于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)
基于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 }
而最初的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 { @Binds @IntoMap @ViewModelKey(MainViewModel::class) abstract fun bindMainViewModel (mainViewModel: MainViewModel ) : ViewModel @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 { }
参考
知乎的科普贴
还算说人话但可惜是java的
说人话的
官方的指导 TODO
缺了这个无法理解