當Dagger2撞上ViewModel

ditclear發表於2018-12-18

本文已授權 微信公眾號 玉剛說 (@任玉剛)獨家釋出。

寫在前面

過去一年多的時間裡,我一直在致力於打造一個最簡單,並能讓普通Android開發者都能快速上手的框架,並陸續發表了多篇開發心得,最終彙總為了《使用Kotlin構建MVVM應用程式》系列文章。其中就涉及到Dagger2和ViewModel的使用,這兩者之間的碰撞令我想到了另一種十分簡單的去進行依賴注入的可能,並引發了一系列的化學反應,可以說是天作之合。

可以在Github上檢視相關程式碼:github.com/ditclear/Pa…

本文的寫法不區分MVP還是MVVM結構,只是提供了一種不那麼按部就班的注入方式。

開始之前,我們先來了解一下Dagger2和ViewModel。

Dagger2是由Google提供的一個適用於Android和Java的快速的依賴注入工具,是現今眾多Android開發者進行依賴注入的首選。

但由於其曲折的學習路線和較高的使用門檻,於是出現了一批又一批從入門到放棄的開發者,當然也包括我。

ViewModel是Google的Jetpack元件中的一個。它是用來儲存和管理UI相關的資料,將一個Activity或Fragment元件相關的資料邏輯抽象出來,並能適配元件的生命週期,如當螢幕旋轉Activity重建後,ViewModel中的資料依然有效。它還可以幫助開發者輕易實現 FragmentFragment 之間, ActivityFragment 之間的通訊以及共享資料

我們可以通過以下的程式碼來獲取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給出了兩點理由:

  1. 只是複製貼上上面的程式碼會讓以後的重構比較困難,還會讓一些開發者不知道Dagger到底是如何進行注入的(ps:然後就更不易理解了)
  2. 更重要的原因是:它要求注射型別(FrombulationActivity)知道其注射器。 即使這是通過介面而不是具體型別完成的,它打破了依賴注入的核心原則:一個類不應該知道如何實現依賴注入。

也就是說你就算是在基類(BaseActivity/BaseFragment)中將其封裝一下,也無可避免的需要寫getComponent.inject(this)這樣的程式碼,而且還必須在對應的Component中新增相應的inject方法,於是便有了以下的程式碼:

@ActivityScope@Subcomponentinterface ActivityComponent { 
fun inject(activity: ArticleDetailActivity) fun inject(activity: CodeDetailActivity) fun inject(activity: MainActivity) fun inject(activity: LoginActivity) fun supplyFragmentComponentBuilder():FragmentComponent.Builder
}@FragmentScope@Subcomponentinterface 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物件,然後你的目錄結構可能就會下圖一般

當Dagger2撞上ViewModel

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

當Dagger2撞上ViewModel

然後就要用Dagger-Android來解決這些問題

是也不是,可能Dagger-Android解決了這些問題,但是它本身就比Dagger2更復雜,解決了這些問題,卻引入了其它的問題,Android開發者並非都是Google開發者,不可能都具備這樣強的邏輯和素質,實踐之後我覺得還不如轉向其它依賴注入的框架。

我只是想注入一下我的ViewModel或Presenter,簡簡單單的開發,有必要這麼麻煩嗎?

當然不是,也許我們並不需要Dagger-Android,Dagger2本身就能做到。

當Dagger2遇上ViewModel

配合ViewModel元件,我們根本不需要這麼麻煩,而且也根本不需要再考慮注入到哪裡去,在Component/Activity/Fragment中新增亂七八糟的inject()方法和@Inject

我們只需要幾個檔案就好

當Dagger2撞上ViewModel

怎麼做?

通過@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. */@Moduleabstract 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的工廠類,進而new 出所需的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)
}
}
}複製程式碼

當creator.get()時則會去構造新的ViewModel例項

public ArticleDetailViewModel get() {return provideInstance(repoProvider, userRepoProvider);

}public static ArticleDetailViewModel provideInstance(Provider<
PaoRepository>
repoProvider, Provider<
UserRepository>
userRepoProvider)
{return new ArticleDetailViewModel(repoProvider.get(), userRepoProvider.get());

}複製程式碼

到這裡,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之中使用

//基類BaseActivityabstract 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分支】相應的程式碼。

寫在最後

我們可以和通常的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結構來說,這便已經足夠了。

其它

程式碼地址:github.com/ditclear/Pa…

《使用Kotlin構建MVVM應用程式系列》 :www.jianshu.com/c/50336d57e…

簡書:www.jianshu.com/p/d3c43b9dd…

來源:https://juejin.im/post/5c186d506fb9a049ee8061dc

相關文章