iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 12
0
Software Development

Android Architecture系列 第 12

Dependency Injection with Dagger2: Part 4

  • 分享至 

  • xImage
  •  

今天是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

概念是使用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


上一篇
Dependency Injection with Dagger2: Part 3
下一篇
Architecture Components - Room
系列文
Android Architecture30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言