今天是Dagger的最後一篇,會處理ViewModel的部分。
由於ViewModle需透過ViewModelProviders來實例化而不是直接呼叫constructor,所以在DI的操作上會不太相同,我們會使用Multibindings的方式來封裝ViewModelFactory。
這樣有什麼好處?目前的程式,要新增ViewModel的話須在ViewModelFactory增加判斷,例如要新增UserViewModel:
@Singleton
public class GithubViewModelFactory implements ViewModelProvider.Factory {
...
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(RepoViewModel.class)) {
return (T) new RepoViewModel(dataModel);
} else if (modelClass.isAssignableFrom(UserViewModel.class)) {
return (T) new UserViewModel(dataModel);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}
除了每次都要寫else-if之外,如果新的ViewModel其constructor parameter不同,就會連ViewModelFactory的其他地方也要修改。
那麼,使用Multibindings建立ViewModelModule來管理ViewModel的話,可以讓我們將來新增ViewModel時只要在Module中加上一行,不用再修改ViewModelFactory。
概念是使用Multibindings產生一個Map<Key, Provider<Value>>
來告訴ViewModelFactory要建立哪個ViewModel,令Key為ViewModel的class,而Value就是ViewModel本身,例如Map<RepoViewModel.class, Provider<RepoViewModel>>
,那就開始吧。
首先建立ViewModelKey:
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
Class<? extends ViewModel> value();
}
使用@MapKey
標註並讓value()
的型態為繼承ViewModel的Class。
建立ViewModelModule:
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(RepoViewModel.class)
abstract ViewModel bindRepoViewModel(RepoViewModel repoViewModel);
@Binds
abstract ViewModelProvider.Factory bindViewModelFactory(GithubViewModelFactory factory);
}
@IntoMap
會產生一個Map<Key, Provider<Value>>
,以此處而言Key是@ViewModelKey
的RepoViewModel.class,而Value為@Binds
的parameter即RepoViewModel。至於Provider我們待會再說。
其中@Binds
是一種簡化的寫法,用@Provides
的話必須這樣寫:
@Module
class AppModule {
@Provides
ViewModel provideRepoViewModel(DataModel dataModel) {
return new RepoViewModel(dataModel);
}
}
因為RepoViewModel的建立方式很簡單,只是用new把原本就在Dagger管理下的DataModel當作constructor parameter,節錄官方FAQ的說明:
whenever there is a @Provides whose implementation is simple and common enough to be inferred by Dagger, it makes sense to just declare that as a method without a body (an abstract method) and have Dagger apply the behavior.
當物件的建立方式只是用new呼叫constructor的時候,可以用@Binds
來取代@Provides
,完整說明再點連結去看囉。
RepoViewModel是bindRepoViewModel的參數所以也要加入Dagger之中,使用constructor injection:
public class RepoViewModel extends ViewModel {
...
@Inject
public RepoViewModel(DataModel dataModel) {
super();
...
}
...
}
將ViewModelModule加入Component中,可以用昨天的直接寫在Component方式,也可以用includes將它跟AppModule組在一起:
@Module(includes = ViewModelModule.class)
class AppModule {
...
}
修改GithubViewModelFactory:
@Singleton
public class GithubViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
@Inject
public GithubViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
這段貌似有點複雜,我們分解一下慢慢看,首先是:
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
這就是由@IntoMap
產生的Map<Key, Provider<Value>>
,其包含了ViewModelModule中宣告的ViewModel資訊,因為我們使用@ViewModelKey
所以Key的部分是Class<? extends ViewModel>,而Value的型態是ViewModel。
可以注意的是Value用Provider包起來,Provider表示物件並不於inject的時候就實例化,而是當使用get()
呼叫時才實例化一個物件,下一次呼叫get()
就再實例化另一個,為什麼這樣用呢?因為我們的ViewModel是隨著View產生且隨著View銷毀,並不像Retrofit是App從頭到尾都用同一個,所以用Provider讓我們每次進入View時可以用get()
取得新的ViewModel。官方文件有個範例比較一般inject和Provider的不同。
而這個Map會做為constructor parameter:
@Inject
public GithubViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
因為Map就是由Dagger產生的,所以同為Dagger管理的GithubViewModelFactory當然可以直接將其作為parameter取得。
接著就是在create(...)利用Map中的Key/Value資訊實例化對應的ViewModel了,首先用creators.get(modelClass)找出以modelClass為Key的Value,creators是前面的那個Map別看錯了。
Provider<? extends ViewModel> creator = creators.get(modelClass);
如果前面找不到Value的話,再用迴圈檢查一次。
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
迴圈檢查完還是沒有就丟exception報錯了。
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
如果前面已經有找到Value的話,這邊就用get()
取得新的ViewModel並回傳,就完成了。
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
以上這些修改完之後,將來要新增ViewModel時只要在ViewModelModule增加程式就好,不用改其它地方,例如新增UserViewModel:
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(RepoViewModel.class)
abstract ViewModel bindRepoViewModel(RepoViewModel repoViewModel);
@Binds
@IntoMap
@ViewModelKey(UserViewModel.class)
abstract ViewModel bindUserViewModel(UserViewModel userViewModel);
...
}
Dagger的部分就到這邊了,個人覺得Dagger真的不好上手,不論是概念或實作都要經過很多思考,不過成功建置之後對將來開發和維護測試都很有幫助,建議可以從相對基本的建立Module及@Provides
方式著手,之後再慢慢擴展。文中若有錯誤的地方還請不吝指教。
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day12-dagger-viewmodel
Reference:
Inject interfaces without provide methods on Dagger 2