當Dagger2撞上ViewModel

本文已受權 微信公衆號 玉剛說 (@任玉剛)獨家發佈。java

寫在前面

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

能夠在Github上查看相關代碼:github.com/ditclear/Pa…git

本文的寫法不區分MVP仍是MVVM結構,只是提供了一種不那麼循序漸進的注入方式。github

開始以前,咱們先來了解一下Dagger2和ViewModel。緩存

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

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

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

咱們能夠經過如下的代碼來獲取ViewModel實例ide

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
@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的工廠類,進而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之中使用

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

寫在最後

咱們能夠和一般的Dagger二、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…

相關文章
相關標籤/搜索