当Dagger2撞上ViewModel

本文已授权 微信公众号 玉刚说 (@任玉刚)独家发布。

写在前面

过去一年多的时间里,我一直在致力于打造一个最简单,并能让普通Android开发者都能快速上手的框架,并陆续发表了多篇开发心得,最终汇总为了《使用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中的数据依然有效。它还可以帮助开发者轻易实现 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
@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)
        }
    }
}

当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分支】相应的代码。

写在最后

我们可以和通常的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应用程序系列》 ://www.greatytc.com/c/50336d57e9b0

掘金:https://juejin.im/post/5c186d506fb9a049ee8061dc

招人:

公司招Android开发,地点在上海,规模500+。朝九晚五,不加班。
如果你喜欢使用Kotlin语言和MVVM架构,喜欢尝试并运用最新的技术,欢迎投递简历到ditclear@qq.com,我可以帮忙进行内推。

要求:
本科及以上,计算机专业。要求有写博客的习惯,简历中请附上博客地址和Github地址。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容