當Dagger2遇上ViewModel
寫在前面
過去一年多的時間裡,我一直在致力於打造一個最簡單,並能讓普通Android開發者都能快速上手的框架,並陸續發表了多篇開發心得,最終彙總為了 ofollow,noindex">《使用Kotlin構建MVVM應用程式》 系列文章。其中就涉及到Dagger2和ViewModel的使用,這兩者之間的碰撞令我想到了另一種十分簡單的去進行依賴注入的可能,並引發了一系列的 化學反應 ,可以說是天作之合。
可以在Github上檢視相關程式碼: https://github.com/ditclear/PaoNet
本文的寫法不區分MVP還是MVVM結構,只是提供了一種不那麼按部就班的注入方式。
開始之前,我們先來了解一下Dagger2和ViewModel。
Dagger2 是由Google提供的一個適用於Android和Java的快速的依賴注入工具,是現今眾多Android開發者進行依賴注入的首選。
但由於其曲折的學習路線和較高的使用門檻,於是出現了一批又一批從入門到放棄的開發者,當然也包括我。
而 ViewModel 是Google的Jetpack元件中的一個。它是用來儲存和管理UI相關的資料,將一個Activity或Fragment元件相關的資料邏輯抽象出來,並能適配元件的生命週期, 如當螢幕旋轉Activity重建後,ViewModel中的資料依然有效 。它還可以幫助開發者輕易實現 Fragment 與 Fragment 之間, Activity 與 Fragment 之間的 通訊以及共享資料 。
我們可以通過以下的程式碼來獲取ViewModel例項
mViewModel=ViewModelProviders.of(this,factory).get(PaoViewModel::class.java)
其中要提供一個ViewModelProvider.Factory的例項來幫助構建你的ViewModel
public interface Factory { /** * Creates a new instance of the given {@code Class}. * <p> * * @param modelClass a {@code Class} whose instance is requested * @param <T>The type parameter for the ViewModel. * @return a newly created ViewModel */ @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass); }
PS:如果你使用的是MVP結構,那麼只需要讓其繼承自ViewModel,也應該能達到相同的效果
Dagger2?麻煩?
首先,我們先來看看Dagger2通常的依賴注入的方式
public class FrombulationActivity extends Activity { @Inject Frombulator frombulator; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // DO THIS FIRST. Otherwise frombulator might be null! ((SomeApplicationBaseType) getContext().getApplicationContext()) .getApplicationComponent() .newActivityComponentBuilder() .activity(this) .build() .inject(this); // ... now you can write the exciting code } }
這是 Dagger-Android 用來吐槽Dagger2不行的示例,並給出了原因,這裡我們也拿來用一次。
Dagger-Android 給出了兩點理由:
- 只是複製貼上上面的程式碼會讓以後的重構比較困難,還會讓一些開發者不知道Dagger到底是如何進行注入的(ps:然後就更不易理解了)
- 更重要的原因是:它要求注射型別(FrombulationActivity)知道其注射器。 即使這是通過介面而不是具體型別完成的,它打破了依賴注入的核心原則:一個類不應該知道如何實現依賴注入。
也就是說你就算是在基類(BaseActivity/BaseFragment)中將其封裝一下,也無可避免的需要寫 getComponent.inject(this)
這樣的程式碼,而且還必須在對應的Component中新增相應的inject方法 ,於是便有了以下的程式碼:
@ActivityScope @Subcomponent interface ActivityComponent { fun inject(activity: ArticleDetailActivity) fun inject(activity: CodeDetailActivity) fun inject(activity: MainActivity) fun inject(activity: LoginActivity) fun supplyFragmentComponentBuilder():FragmentComponent.Builder } @FragmentScope @Subcomponent interface FragmentComponent { fun inject(fragment: ArticleListFragment) fun inject(fragment: CodeListFragment) fun inject(fragment: CollectionListFragment) fun inject(fragment: MyCollectFragment) fun inject(fragment: HomeFragment) fun inject(fragment: RecentFragment) fun inject(fragment: SearchResultFragment) fun inject(fragment: RecentSearchFragment) fun inject(fragment: MyArticleFragment) @Subcomponent.Builder interface Builder { fun build(): FragmentComponent } }
而目的也許就只是為了自動注入你的ViewModel或者Presenter物件,然後你的目錄結構可能就會下圖一般

而build之後生成的檔案將會是這樣的

然後就要用Dagger-Android來解決這些問題?
是也不是,可能Dagger-Android解決了這些問題,但是它本身就比Dagger2更復雜,解決了這些問題,卻引入了其它的問題,Android開發者並非都是Google開發者,不可能都具備這樣強的邏輯和素質,實踐之後我覺得還不如轉向其它依賴注入的框架。
我只是想注入一下我的ViewModel或Presenter,簡簡單單的開發,有必要這麼麻煩嗎?
當然不是,也許我們並不需要Dagger-Android, Dagger2 本身就能做到。
當Dagger2遇上ViewModel
配合ViewModel元件,我們根本不需要這麼麻煩,而且也根本不需要再考慮注入到哪裡去,在Component/Activity/Fragment中新增亂七八糟的 inject()
方法和 @Inject
。
我們只需要幾個檔案就好

怎麼做?
通過 @Binds
和 @IntoMap
@Binds 和 @Provider的作用相差不大,區別在於@Provider需要寫明具體的實現,而@Binds只是告訴Dagger2誰是誰實現的,比如
@Provides fun provideUserService(retrofit: Retrofit) :UserService=retrofit.create(UserService::class.java) @Binds abstract fun bindCodeDetailViewModel(viewModel: CodeDetailViewModel):ViewModel
而@IntoMap則可以讓Dagger2將多個元素依賴注入到Map之中。
/** * 頁面描述:ViewModelModule * * Created by ditclear on 2018/8/17. */ @Module abstract class ViewModelModule{ // ... @Binds @IntoMap @ViewModelKey(CodeDetailViewModel::class) abstract fun bindCodeDetailViewModel(viewModel: CodeDetailViewModel):ViewModel @Binds @IntoMap @ViewModelKey(MainViewModel::class) //key abstract fun bindMainViewModel(viewModel: MainViewModel):ViewModel //... //提供ViewModel的工廠類 @Binds abstract fun bindViewModelFactory(factory:APPViewModelFactory): ViewModelProvider.Factory }
通過這些,Dagger2會根據這些資訊自動生成一個關鍵的Map。key為ViewModel的Class,value則為提供ViewModel例項的Provider物件,通過 provider.get()
方法就可以獲取到相應的ViewModel物件。
private Map<Class<? extends ViewModel>, Provider<ViewModel>> getMapOfClassOfAndProviderOfViewModel() { return MapBuilder.<Class<? extends ViewModel>, Provider<ViewModel>>newMapBuilder(7) .put(ArticleDetailViewModel.class, (Provider) articleDetailViewModelProvider) .put(CodeDetailViewModel.class, (Provider) codeDetailViewModelProvider) .put(MainViewModel.class, (Provider) mainViewModelProvider) .put(RecentViewModel.class, (Provider) recentViewModelProvider) .put(LoginViewModel.class, (Provider) loginViewModelProvider) .put(ArticleListViewModel.class, (Provider) articleListViewModelProvider) .put(CodeListViewModel.class, (Provider) codeListViewModelProvider) .build(); }
而這些物件也是由Dagger2幫我們自動組裝的。

DaggerAppComponent
有了這些,我們就可以很方便的去構造ViewModel的工廠類 APPViewModelFactory
,並構造到所需的ViewModel。
/** * 頁面描述:APPViewModelFactory提供ViewModel 快取的例項 * 通過Dagger2將Map直接注入,通過key直接獲取到相應的ViewModel * Created by ditclear on 2018/8/17. */ class APPViewModelFactory @Inject constructor(private val creators:Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory{ override fun <T : ViewModel?> create(modelClass: Class<T>): T { //通過class找到相應ViewModel的Provider val creator = creators[modelClass]?:creators.entries.firstOrNull{ modelClass.isAssignableFrom(it.key) }?.value?:throw IllegalArgumentException("unknown model class $modelClass") try { @Suppress("UNCHECKED_CAST") return creator.get() as T //通過get()方法獲取到ViewModel } catch (e: Exception) { throw RuntimeException(e) } } }
到這裡,ViewModel與Dagger2已經緊密聯絡起來,那如何不去寫那麼多惱人的 inject()
呢?
答案就是讓你的 Application
持有你的 ViewModelProvider.Factory
例項,Talk is Cheap~
在Application中進行注入
class PaoApp : Application() { @Inject lateinit var factory: APPViewModelFactory val appModule by lazy { AppModule(this) } override fun onCreate() { super.onCreate() //... DaggerAppComponent.builder().appModule(appModule).build().inject(this) } }
在Activity/Fragment之中使用
//基類BaseActivity abstract class BaseActivity : AppCompatActivity(), Presenter { //... val factory:ViewModelProvider.Factory by lazy { if (application is PaoApp) { val mainApplication = application as PaoApp return@lazy mainApplication.factory }else{ throw IllegalStateException("application is not PaoApp") } } fun <T :ViewModel> getInjectViewModel (c:Class<T>)= ViewModelProviders.of(this,factory).get(c) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //.... initView() } abstract fun initView() //.... }
需要進行注入的Activity,比如ArticleDetailActivity就不需要再寫@inject之類的註解
class ArticleDetailActivity : BaseActivity() { //很方便就可獲取到ViewModel private val mViewModel: ArticleDetailViewModel by lazy {getInjectViewModel(ArticleDetailViewModel::class.java) } override fun initView() { //呼叫方法 mViewModel.dosth() } }
Fragment相同的道理,具體可以檢視 【PaoNet : Master分支】 相應的程式碼。
如果你還是覺得沒什麼大不了,那麼你甚至還能這麼寫
居然還能這樣?
怎樣?如果你是按照筆者的《MVVM With Kotlin》專題學過來的,或者你現在正在使用DataBinding和Kotlin語言,那麼恭喜你,你甚至可以不去宣告你的ViewModel物件
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() { //... override fun initView() { //呼叫方法 mBinding.vm?.dosth() } }
得益於Kotlin的型別推導,我們可以在基類BaseActivity對 getInjectViewModel
方法進行優化
inline fun <reified T :ViewModel> getInjectViewModel ()= ViewModelProviders.of(this,factory).get(T::class.java)
最後的 BaseActivity.kt 如下所示,注意 mBinding.setVariable(BR.vm,getInjectViewModel())
這一行。
abstract class BaseActivity<VB : ViewDataBinding> : AppCompatActivity(), Presenter { protected lateinit var mBinding: VB //獲取factory val factory:ViewModelProvider.Factory by lazy { if (application is PaoApp) { val mainApplication = application as PaoApp return@lazy mainApplication.factory }else{ throw IllegalStateException("app is not PaoApp") } } //獲取viewmodel inline fun <reified T :ViewModel> getInjectViewModel ()= ViewModelProviders.of(this,factory).get(T::class.java) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mBinding = DataBindingUtil.setContentView<VB>(this, getLayoutId()) mBinding.setVariable(BR.presenter,this) mBinding.setVariable(BR.vm,getInjectViewModel())//這一行懂了才是真的懂 mBinding.executePendingBindings() mBinding.setLifecycleOwner(this) //... initView() } }
這裡可以自行體悟,真的是奇妙的連鎖化學反應。
寫在最後
我們可以和通常的Dagger2、Dagger-Android的原理比較一下
- 普通的賦值:手動構造,十分繁瑣,浪費時間
viewmodel = ViewModel(Repo(remote,local,prefrence))
- 通常的Dagger2注入:需要在Activity中用@Inject標識哪些需要被注入,並在Component中新增
inject(activity)
方法,會生成很多java類,有些繁瑣
instance.viewmodel = component.viewmodel
- Dagger-Android的注入:需要編寫很多module,component,門檻高,不方便使用,還不如不用
app.map = Map<Class<? extends Activity>, Provider<AndroidInjector.Factory<? extends Activity>>> activity.viewmodel = app.map.get(activity.class).getComponent().viewmodel
- Dagger2-ViewModel的注入:不需要在Activity中標識和inject,不會生成各種
XX_MemberInjectors
的java類,修改時改動最少,純粹的一個 依賴檢索容器 。
app.factory = component.AppViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>>) viewmodel = ViewModelProviders.of(this,app.factory).get(viewmodel.class)
對比Dagger-Android和Dagger2-ViewModel,兩者都是間接通過Map來進行注入,不過一個的key是 Class<Activity>
,一個是 Class<ViewModel>
,而且都是在Application中inject一下。而Dagger2-ViewModel不需要向Dagger-Android那樣新增 AndroidInjection.inject(this)
程式碼,更像是一個用來構造ViewModel的依賴管理容器,但對於我或者我希望打造的MVVM結構來說,這便已經足夠了。
其它
程式碼地址: https://github.com/ditclear/PaoNet
《使用Kotlin構建MVVM應用程式系列》 : https://www.jianshu.com/c/50336d57e9b0