写点什么

神奇宝贝 眼前一亮的 Jetpack + MVVM 极简实战

用户头像
Android架构
关注
发布于: 5 小时前

满足不了 PokemonGo 项目的需求,在 PokemonGo 项目中采用 buildSrc 方式去管理所有依赖库,因为 PokemonGo 项目采用单模块结构,而且支持?自动补全?和?单击跳转?很方便,所这里用到了 Gradle Versions Plugin 插件去检查依赖库的最新版本,检查结果如下所示:


The following dependencies have later release versions:


  • androidx.swiperefreshlayout:swiperefreshlayout [1.0.0 -> 1.1.0]


https://developer.android.com/jetpack/androidx


  • com.squareup.okhttp3:logging-interceptor [3.9.0 -> 4.7.2]


https://square.github.io/okhttp/


  • junit:junit [4.12 -> 4.13]


http://junit.org


  • org.koin:koin-android [2.1.5 -> 2.1.6]

  • org.koin:koin-androidx-viewmodel [2.1.5 -> 2.1.6]

  • org.koin:koin-core [2.1.5 -> 2.1.6]


Gradle release-candidate updates:


  • Gradle: [6.1.1 -> 6.5.1]


复制代码


会列出所有需要更新的依赖库的最新版本,并且 Gradle Versions Plugin 比 AndroidStudio 所支持的更加全面:


  • 支持手动方式管理依赖库最新版本检查

  • 支持 ext 的方式管理依赖库最新版本检查

  • 支持 buildSrc 方式管理依赖库最新版本检查

  • 支持 gradle-wrapper 最新版本检查

  • 支持多模块的依赖库最新版本检查


那么如何使用呢?只需要三步


  • 1.将 PokemonGo 项目根目录 checkVersions.gradle 文件拷贝到你的项目根目录下面

  • 2.在项目的根目录 build.gradle 文件夹内添加以下代码 apply from: ‘./checkVersions.gradle’ buildscript { repositories { google() jcenter() } dependencies { classpath “com.github.ben-manes:gradle-versions-plugin:0.28.0” } } 复制代码

  • 3.添加完成之后,在根目录下执行以下命令。 ./gradlew dependencyUpdates 复制代码会在当前目录下生成 build/dependencyUpdates/report.txt 文件。


MVVM 架构


=========================================================================


Jetpack 实战项目 PokemonGo 基于 MVVM 架构和 Repository 设计模式,如今几乎所有的 Android 开发者至少都听过 MVVM 架构,在谷歌 Android 团队宣布了 Jetpack 的视图模型之后,它已经成为了现代 Android 开发模式最流行的架构之一,如下图所示:



MVVM 有助于将应用程序的业务逻辑与 UI 完全分开。 如果业务逻辑与 UI 逻辑之间的联系非常紧密,那么维护将很困难,由于很难重用业务逻辑,因此编写单元测试代码非常困难,一堆重复的代码和复杂的逻辑。


Jetpack 的视图模型的 MVVM 架构由 View + DataBinding + ViewModel + Model 组成。


DataBinding


=============================================================================


DataBinding(数据绑定)实际上是 XML 布局中的另一个视图结构层次,视图 (XML) 通过数据绑定层不断地与 ViewModel 交互。


我们来看一个例子,首页上有个 RecyclerView 用来展示神奇宝贝数据(名字、图片、点击事件等等),每一个 item 对应一个 ViewHolder,来看一下 ViewHolder 的实现。


class PokemonViewModel(view: View) : DataBindingViewHolder<PokemonListModel>(view) {


private val mBinding: RecycleItemPokemonBinding by viewHolderBinding(view)


override fun bindData(data: PokemonListModel, position: Int) {


mBinding.apply {


pokemon = data


executePendingBindings()


}


}


}


复制代码


正如你所看到的,由于使用了数据绑定,ViewHolder 里面的代码变的非常简单,可能这个例子不够明显,我们来看一个劲爆的,点击首页每一个 item 会跳转到详情页面,详情页面如下图所示:



详情页面(DetailActivity)展示了神奇宝贝的详细数据,先查询数据库,如果没有找到,读取网路数据然后保存到数据库,由于使用了数据绑定,代码变得非常简单,如下所示:


class DetailActivity : DataBindingAppCompatActivity() {


private val mBindingActivity: ActivityDetailsBinding by binding(R.layout.activity_details)


private val mViewModel: DetailViewModel by viewModels()


lateinit var mPokemonModel: PokemonListModel


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


mBindingActivity.apply {


mPokemonModel = requireNotNull(intent.getParcelableExtra(KEY_LIST_MODEL))


pokemonListModel = mPokemonModel


lifecycleOwner = this@DetailActivity


viewModel = mViewModel.apply {


fectchPokemonInfo(mPokemonModel.name)


.observe(this@DetailActivity, Observer {})


}


}


}


}


正如你所见 DetailActivity 代码变得非常简单,如果以后我们想要改变网络的 URL、Model、获取或保存数据的方式等等,我们不需要改变 DetailActivity 中的任何代码。


更多关于 DataBinding 的使用请参考我另外一个仓库 JDataBinding:目前已经封装了一系列的组件包含 DataBindingActivity、DataBindingAppCompatActivity、DataBindingFragmentActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter、DataBindingViewHolder 等等。


ViewModel


===========================================================================


ViewModel 是 MVVM 架构中非常重要的设计,它在 activities 或 fragments 和业务逻辑中起到了非常重要的作用,它不依赖于 UI 组件,使得单元测试更加容易,ViewModel 以生命周期的方式管理界面相关的数据,直到 Activity 被销毁。


LiveData 与 ViewModel 具有很好的协同作用,LiveData 持有从数据源获取到的数据,并且它可以被 DataBinding 组件观察,当 Activity 被销毁时,它将被取消订阅。


而详情页面(DetailActivity) 代码之所以能这么简单得益于 ViewModel、LiveData、DataBinding 协同工作, 我们来看一下 ViewModel 代码。


class DetailViewModel @ViewModelInject constructor(


val polemonRepository: Repository


) : ViewModel() {


private val _pokemon = MutableLiveData<PokemonInfoModel>()


val pokemon: LiveData<PokemonInfoModel> = _pokemon


@OptIn(ExperimentalCoroutinesApi::class)


fun fectchPokemonInfo(name: String) = liveData<PokemonInfoModel> {


polemonRepository.featchPokemonInfo(name)


.collectLatest {


_pokemon.postValue(it)


emit(it)


}


.......


// 省略部分代码,


}


}


activity_details.xml 代码


<layout xmlns:android="http://schemas.android.com/apk/res/android"


xmlns:app="http://schemas.android.com/apk/res-auto"


xmlns:tools="http://schemas.android.com/tools">


<data>


<variable


name="viewModel"


type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" />


</data>


......


<androidx.appcompat.widget.AppCompatTextView


android:id="@+id/weight"


android:text="@{viewModel.pokemon.getWeightString}"/>


......


</layout>


这是获取神奇宝贝的详细信息,通过 DataBinding 以声明方式将数据(神奇宝贝的体重)绑定到界面上,更多使用参考项目中的代码。


Repository


============================================================================


Repository 设计模式是最流行、应用最广泛的设计模式之一,在 Repository 层获取网络数据,并将数据存储到数据库中,在这一层中有两个非常重要的成员 Paging3 库中的 RemoteMediator 和 Data Mappers。


RemoteMediator


================================================================================


在之前的文章 Jetpack 成员 Paging3 实践以及源码分析(一) 和 Jetpack 新成员 Paging3 网络实践及原理分析(二) 分别分析了使用 Paging3 访问?数据库?和?网络,但是遗漏了 RemoteMediator 类的使用,RemoteMediator 是 Paging3 当中一个非常重要的成员,用于实现?数据库?和?网络?访问,所以这里是对之前的文章一个补充。


RemoteMediator 很重要,需要单独花一篇文章去分析,为了节省篇幅,在这里不会详细的去分析它,如果对 RemoteMediator 不太理解没有关系,我会在后续的文章里面详细的分析它。


项目中网络访问用的是 Retrofit2 & OkHttp3 用来请求网络数据,使用 Room 作为数据库存储,将获得的数据保存到数据库中,Room 在 SQLite 上提供了一个抽象层,流畅地访问 SQLite 数据库,同时拥有了 SQLite 全部功能,在编译的时候进行错误检查。


@OptIn(ExperimentalPagingApi::class)


class PokemonRemoteMediator(


val api: PokemonService,


val db: AppDataBase


) : RemoteMediator<Int, PokemonEntity>() {


val mPageKey = 0


override suspend fun load(


loadType: LoadType,


state: PagingState<Int, PokemonEntity>


): MediatorResult {


try {


......


val pageKey = when (loadType) {


// 首次访问 或者调用 PagingDataAdapter.refresh()


LoadType.REFRESH -> null


// 在当前加载的数据集的开头加载数据时


LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)


// 在当前数据集末尾添加数据


LoadType.APPEND -> {


......


if (remoteKey == null || remoteKey.nextKey == null) {


return MediatorResult.Success(endOfPaginationReached = true)


}


remoteKey.nextKey


}


}


......


// 使用 Retrofit2 获取网络数据


val page = pageKey ?: 0


val res


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ult = api.fetchPokemonList(


state.config.pageSize,


page * state.config.pageSize


).results


.......


db.withTransaction {


if (loadType == LoadType.REFRESH) { // 当首次加载,或者下拉刷新的时候,清空当前数据 }


......


// 存储获取到的数据


remoteKeysDao.insertAll(entity)


pokemonDao.insertPokemon(item)


}


return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)


} catch (e: IOException) {


return MediatorResult.Error(e)


} catch (e: HttpException) {


return MediatorResult.Error(e)


}


}


}


注意:使用了 @OptIn(ExperimentalPagingApi::class) 需要在 App 模块 build.gradle 文件内添加以下代码。


android {


kotlinOptions {


freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]


}


}


复制代码


在 RemoteMediator 的实现类 PokemonRemoteMediator 中的核心部分是关于参数 LoadType 的判断。


  • LoadType.REFRESH:首次访问?或者调用?PagingDataAdapter.refresh() 触发,这里不需要做任何操作,返回 null 就可以

  • LoadType.PREPEND:在当前列表头部添加数据的时候时触发,实际在项目中基本很少会用到直接返回 MediatorResult.Success(endOfPaginationReached = true) ,参数 endOfPaginationReached 表示没有数据了不在加载

  • LoadType.APPEND:下拉加载更多时触发,这里获取下一页的 key, 如果 key 不存在,表示已经没有更多数据,直接返回 MediatorResult.Success(endOfPaginationReached = true) 不会在进行网络和数据库的访问


接下来的逻辑和之前请求网络数据的逻辑没有什么区别了,使用 Retrofit2 获取网络数据,然后使用 Room 将数据保存到数据库中。


接下来聊一下 Repository 中另外一个重要的成员 Data Mapper,在项目中起到了非常的重要,在一个快速开发的项目中,为了越快完成第一个版本交付,下意识的将数据源和 UI 绑定到一起,当业务逐渐增多,数据源变化了,上层也要一起变化,导致后期的重构工作量很大,核心的原因耦合性太强了。


Data Mapper(个人建议)


===================================================================================


Data Mapper 的意识非常重要,在项目中起到了非常的重要,关于 Data Mappers 在 Repository 中的重要性可以看一下国外大神写的这篇文章 The “Real” Repository Pattern in Android 在 Medium 上获得了 4.9K 的赞。


使用 Data Mapper 分离数据源的 Model 和 页面显示的 Model,不要因为数据源的增加、修改或者删除,导致上层页面也要跟着一起修改,换句话说使用 Data Mapper 做一个中间转换,如下图所示,来源于网络:



使用 Data Mapper(数据映射)优点如下:


  • 数据源的更改不会影响上层的业务

  • 糟糕的后端实现不会影响上层的业务 ( 想象一下,如果你被迫执行 2 个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗? )

  • Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务


如果在一个大型项目中直接使用 Data Mapper 会有适得其反的效果,所以需要结合设计模式来完善,这不在本文讨论范围之内,其实在这里我想表达是,不要因为快速实现某个功能,下意识的将数据源的 model 和 UI 绑定在一起。


Data Mappe 实现方式有很多种,可以手动实现,也可以通过引入第三方框架,其中有名框架 modelmapper,在 PokemonGo 项目中是手动实现的。


Koltin Flow


=============================================================================


停止使用 RxJava,尝试一下 Flow,不仅简单而且功能很强大,Retrofit2 和 Room 也都提供了对应的支持。


Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,在 PokemonGo 项目中也用到了 Flow。


override suspend fun featchPokemonInfo(name: String): Flow<PokemonInfoModel> {


return flow {


val pokemonDao = db.pokemonInfoDao()


var infoModel = pokemonDao.getPokemon(name)


// 查询数据库是否存在,如果不存在请求网络


if (infoModel == null) {


// 网络请求


val netWorkPokemonInfo = api.fetchPokemonInfo(name)


......


pokemonDao.insertPokemon(infoModel) // 插入更新数据库


}


val model = mapper2InfoModel.map(infoModel) // 数据转换


emit(model)


}.flowOn(Dispatchers.IO)


}


在这里做了三件事:


  • 查询数据库是否存在,如果不存在请求网络

  • 请求网络获取数据,更新数据库

  • 将数据源的 Model 转换为页面显示的 Model


依赖注入


======================================================================


Hilt、Dagger、Koin 等等都是依赖注入库,使用依赖注入库有以下优点:


  • 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。

  • 在配置 scopes 范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。

  • 代码变得更具可读性。

  • 易于构建对象。

  • 编写低耦合代码,更容易测试。


在 PokemonGo 项目中使用的是 Hilt,Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持,来看一下 Hilt 与 Room 在一起使用的例子。


@Module


@InstallIn(ApplicationComponent::class)


object RoomModule {


/**


  • @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

  • @Singleton 提供单例


*/


@Provides


@Singleton


fun provideAppDataBase(application: Application): AppDataBase {


return Room


.databaseBuilder(application, AppDataBase::class.java, "dhl.db")


.fallbackToDestructiveMigration()


.allowMainThreadQueries()


.build()


}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
神奇宝贝 眼前一亮的 Jetpack + MVVM 极简实战