当Dagger2撞上ViewModel

作者 : 开心源码 本文共8816个字,预计阅读时间需要23分钟 发布时间: 2022-05-12 共146人阅读

写在前面

过去一年多的时间里,我一直在致力于打造一个最简单,并能让普通Android开发者都能快速上手的框架,并陆续发表了多篇开发心得,最终汇总为了《使用Kotlin构建MVVM应用程序》系列文章。其中就涉及到Dagger2和ViewModel的使用,这两者之间的碰撞令我想到了另一种十分简单的去进行依赖注入的可能,并引发了一系列的化学反应,可以说是天作之合。

可以在Github上查看相关代码: 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@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对象,而后你的目录结构可能就会下图一般

而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. */@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 * 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)        }    }}

到这里,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分支】相应的代码。

假如你还是觉得没什么大不了,那么你甚至还能这么写

居然还能这样?

怎么?假如你是按照笔者的《MVVM With Kotlin》专题学过来的,或者者你现在正在使用DataBinding和Kotlin语言,那么恭喜你,你甚至可以不去公告你的ViewModel对象

class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {    //...    override fun initView() {        //调用方法        mBinding.vm?.dosth()    }}

得益于Kotlin的类型推导,我们可以在基类BaseActivity对getInjectViewModel方法进行优化

inline fun <reified T :ViewModel> getInjectViewModel ()= ViewModelProviders.of(this,factory).get(T::class.java)

最后的BaseActivity.kt如下所示,注意mBinding.setVariable(BR.vm,getInjectViewModel())这一行。

abstract class BaseActivity<VB : ViewDataBinding> : AppCompatActivity(), Presenter {    protected lateinit var mBinding: VB    //获取factory    val factory:ViewModelProvider.Factory by lazy {        if (application is PaoApp) {            val mainApplication = application as PaoApp           return@lazy mainApplication.factory        }else{            throw IllegalStateException("app is not PaoApp")        }    }    //获取viewmodel    inline fun <reified T :ViewModel> getInjectViewModel ()= ViewModelProviders.of(this,factory).get(T::class.java)    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        mBinding = DataBindingUtil.setContentView<VB>(this, getLayoutId())        mBinding.setVariable(BR.presenter,this)        mBinding.setVariable(BR.vm,getInjectViewModel())//这一行懂了才是真的懂        mBinding.executePendingBindings()        mBinding.setLifecycleOwner(this)        //...        initView()    }}

这里可以自行体悟,真的是奇妙的连锁化学反应。

写在最后

我们可以和通常的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结构来说,这便已经足够了。

其它

代码地址: ditclear/PaoNet

《使用Kotlin构建MVVM应用程序系列》 :https://www.songma.com/c/50336d57e9b0

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 当Dagger2撞上ViewModel

发表回复