从0搭建Jetpack版的WanAndroid客户端
本文作者
作者:Alex@W
链接:
https://blog.csdn.net/Alexwll/article/details/84147127
本文由作者授权发布。
在接触Android Jetpack组件时, 就深深被其巧妙的设计和强大的功能所吸引,暗自告诉自己一定要学会这些组件,而网上并不能找到系统的学习资料,于是利用每天的时间访问Google Develper网站,把Jetpack的每个组件从使用到源码进行了系统的学习和总结。
于是就有了带你领略Android Jetpack组件的魅力系列文章,希望在总结自己学习的同时,也能帮助需要这一些资料的同学,在写完这些文章后,想在项目中使用这些强大组件的想法就更加想强烈了。
但又担心直接在公司项目中使用会又踩坑的危险,而且公司的项目又一时难以全部替换,好在WanAndroid提供了完整的应用接口,才有了这一个Jetpack版的WanAndroid客户端,项目功能比较简单,作为Jetpack组件的实战项目,旨在抛砖引玉和大家一起真正的使用Jetpack组件。
项目架构
既然本篇是对Android Jetpack组件的实战,那么就按照官方推荐的项目架构进行开发,架构内容见下图:
上面架构大家应是很熟悉的,基本原则和平时使用的MVC、MVP等一样,都是使界面、数据、和处理的逻辑进行解耦,打造稳定的、易测试、易扩展的项目架构,只是在这个过程中使用了全新的组件,如:ViewModel、LiveData等,使整个项目架构更加简单和灵活,关于使用的新组件不了解的可以点击文章开头的链接,学习相关组件的使用,本文默认读者已经了解组件的简单使用。
项目内容
项目结构
本项目按照前面项目架构的知道,根据各个模块的功能进行分包管理,如下图:
3.1、登陆模块
登陆模块遵循着一个Activit多Fragment的实现,提供注册(RegisterFragment)和登陆(LoginFragment)功能,相信这样的实现和写法对所有开发者来说都是So easy,甚至心里已将想好了如何像Activity添加Fragment,如何实现两个Fragment间的交互,我想说兄弟先停下脑子中的代码,来看看下面Loginactivity中的实现:
class LoginActivity : BaseCompatActivity() {
override fun onErrorViewClick(v: View?) {}
override fun initView(savedInstanceState: Bundle?) {}
override fun getLayoutId() = R.layout.activity_login
override fun onSupportNavigateUp() = Navigation.findNavController(this, R.id.fragment_navigation_login).navigateUp()}
onErrorViewClick()、initView()、getLayoutId()是在BaseCompatActivity中的抽象方法,用于加载布局和初始化控件,忽略这些方法后,真正实现像Activity中添加Fragment和Fragment的导航的代码就只有一行。
之所以这么简单完全得力于Navigation的使用,我们只需按规定的设置Navigation的xml文件,并将其加载到布局中,其他的操作都在Navigation中自动完成,下面看一下navigation.xml文件:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/login_navigation"
app:startDestination="@id/loginFragment">
<fragment
android:id="@+id/loginFragment"
android:name="com.example.administrator.wanandroid.ui.fragment.LoginFragment"
android:label="LoginFragment" >
<action
android:id="@+id/action_loginFragment_to_registerFragment"
app:destination="@id/registerFragment" />
</fragment>
<fragment
android:id="@+id/registerFragment"
android:name="com.example.administrator.wanandroid.ui.fragment.RegisterFragment"
android:label="RegisterFragment" />
</navigation>
效果展示
3.2、文章模块
3.2.1、文章列表展示
对于常规的内容展示,使用RecyclerView并实现上拉加载和下拉刷新即可,此处使用Paging组件实现这些功能,对于Paging的下拉加载之前文章已经介绍了,通过自定以DataSource控制数据的加载和分页,本文不再进行介绍,这里只介绍对Paging组件进行了简单的封装,代码结构如下:
除了DataBase、Factory和Adaoter之外,上述封装中主要的类是三个类:
Listing:用于封装需要监听的对象和执行的操作,用于系统交互
BaseRepository:配置并实例化LivePagedListBuilder()对象,根据设定的监听状态和数据,封装List<M>对象
BasePagingViewModel:保存所有的可观察的数据和所有的操作方法
Listing代码如下,属性和作用见代码注释:
/**
* 用于封装需要监听的对象和执行的操作,用于系统交互
* pagedList : 观察获取数据列表
* networkStatus:观察网络状态
* refreshState : 观察刷新状态
* refresh : 执行刷新操作
* retry : 重试操作
* @author : Alex
* @date : 2018/08/21
* @version : V 2.0.0
*/
data class Listing<T>(
val pagedList: LiveData<PagedList<T>>,
val networkStatus: LiveData<Resource<String>>,
val refreshState: LiveData<Resource<String>>,
val refresh: () -> Unit,
val retry: () -> Unit)
BaseRepositroy
abstract class BaseRepository<T, M> : Repository<M> {
/**
* 配置PagedList.Config实例化List<M>对象,初始化加载的数量默认为{@link #pageSize} 的两倍
* @param pageSize : 每次加载的数量
*/
override fun getDataList(pageSize: Int): Listing<M> {
val pageConfig = PagedList.Config.Builder()
.setPageSize(pageSize)
.setPrefetchDistance(pageSize)
.setInitialLoadSizeHint(pageSize * 2)
.setEnablePlaceholders(false)
.build()
val stuDataSourceFactory = createDataBaseFactory()
val pagedList = LivePagedListBuilder(stuDataSourceFactory, pageConfig)
val refreshState = Transformations.switchMap(stuDataSourceFactory.sourceLivaData) { it.refreshStatus }
val networkStatus = Transformations.switchMap(stuDataSourceFactory.sourceLivaData) { it.networkStatus }
return Listing<M>(
pagedList.build(),
networkStatus,
refreshState,
refresh = {
stuDataSourceFactory.sourceLivaData.value?.invalidate()
},
retry = {
stuDataSourceFactory.sourceLivaData.value?.retryFailed()
}
)
}
/**
* 创建DataSourceFactory
*/
abstract fun createDataBaseFactory(): BaseDataSourceFactory<T, M>
}
上述代码中做了以下事情:
创建BaseDataSourceFactory实例
初始化并配置Paging组件
转换并监听BaseDataSourceFactory中保存的可观察的DataSource状态的变化
将所有的监听状态封装到Listing的实例中
对于上拉加载之前的文章有介绍,可对于下拉刷新的实现并没有直接介绍,不过从上上面的代码可以看出,此处的refresh()调用DataSource的invalidate()方法,通知数据实失效,此时数据会从新加载。
BasePagingViewModel
BasePagingViewModel的作用就是ViewModel的基本作用,不过这里进行了相关状态的转换和监听,没错就是前面生成和封装的Listing实例中的操作,
open class BasePagingViewModel<T>(resposity: Repository<T>) : ViewModel() {
//开始时建立DataSource和LiveData<Ling<StudentBean>>的连接
val data = MutableLiveData<Int>()
// map的数据修改时,会执行studentResposity 重新创建 LiveData<Ling<StudentBean>>
private val repoResult = Transformations.map(data) {
resposity.getDataList(it)
}
// 从Ling对象中获取要观察的数据,调用switchMap当repoResult 修改时会自动更新 生成的LiveData
// 监听加载的数据
val pagedList = Transformations.switchMap(repoResult) {
it.pagedList
}!!
// 网络状况
val networkStatus = Transformations.switchMap(repoResult) { it.networkStatus }!!
// 刷新和加载更多的状态
val refreshState = Transformations.switchMap(repoResult) { it.refreshState }!!
/**
* 执行刷新操作
*/
fun refresh() {
repoResult.value?.refresh?.invoke()
}
/**
* 设置每次加载次数,初始化 data 和 repoResult
* @param int 加载个数
*/
fun setPageSize(int: Int = 10): Boolean {
if (data.value == int)
return false
data.value = int
return true
}
/**
* 执行点击重试操作
*/
fun retry() {
repoResult.value?.retry?.invoke()
}
}
ViewModel中储存和执行的方法见上面的注释,所有的监听状态都是转换Listing实例,而Listing实例的创建有时装欢DataSource,所以用户执行的操作和DataSource就联系起来了,当你使用了Paging组件的时候,你真的会有牵一发而动全身的感觉,简单来说只要DataSource的数据、请求状态、请求结果任意一个发生改变,相应的ViewModel中的数据就会改变,那在Fragment中监听的Observer就会执行相应的方法,响应用户的操作。
使用效果
3.3.2、文章阅读
这个部分的实现比较简单,也是组件的经典结构,详情页主要是根据文章的Url和Title决定,换句话说只要Url和Title改变文章的内容就会改变,所以只要在ViewModel中保存Title和Url的可观察类,在Activity中监听二者并在其改变时执行相应的操作。
val contentTitle = MutableLiveData<String>()
val contentUrl = MutableLiveData<String>()
Activity中观察数据:
model.contentTitle.observe(this, Observer {
supportActionBar?.title = it
})
model.contentTitle.value = mTitle
private fun initWebView() {
model.contentUrl.observe(this, Observer {
createWebView(it)
})
model.contentUrl.value = mUrl
}
效果展示
3.3.3、文章收藏和加入阅读计划
这部分和上面文章展示大致相似,只不过比它多了初始化收藏状态、收藏后上传服务器和保存数据库的操作,也就是多了ArticleDetailResposity中的调度操作,执行逻辑大致如下:
在显示详情时,初始化本篇文章的收藏状态和加入计划状态
点击收藏或计划后响应操作
执行逻辑后响应界面修改
实现过程如下:
在ArticleDetailRepository中创建LivaData标记收藏和阅读的状态
class ArticleDetailRepository(val api: Api, val context: Context) {
val articleIsCollected = MutableLiveData<Boolean>()
val articleIsReadLater = MutableLiveData<Boolean>()
}
在ViewModel中转换ArticleDetailRepository中的LiveData
/**
* 是否收藏
*/
val collected = Transformations.map(aricleDetailResposity.articleIsCollected) { it }!!
/**
* 是否加入阅读计划
*/
val readPlan = Transformations.map(aricleDetailResposity.articleIsReadLater) { it }!!
在UI界面中观察数据
//如果文章已收藏则显示“取消收藏”,否则显示“文章收藏”
model.collected.observe(this, Observer {
if (it!!) collectButton.setText(R.string.cancel_collect_article) else collectButton.setText(R.string.collect_article)
})
//如果文章已加入计划则显示“取消阅读计划”,否则显示“加入阅读计划”
model.readPlan.observe(this, Observer {
if (it!!) readPlanButton.setText(R.string.delete_read_plan) else readPlanButton.setText(R.string.add_read_plan)
})
到这里实现了监听文章的操作状态,根据文章收藏和计入计划的状态,改变相应的UI控件,那么剩下的是执行相应的操作,然后去改变ArticleDetailRepository中可观察数据的状态。
此处文章的收藏和阅读计划相同,都是根据本地数据的存储进行或服务端数据初始化,操作成功后再修改数据库的操作,关于网络的请求本文不做介绍了,只是在请求收藏链接成功后修改ArticleDetailRepository中状态即可,本文主要介绍“加入”和“取消”阅读计划,此部分是保留在本地的数据库中,所以结下来就看看阅读计划的数据库创建。
DataBase:本项目后面的几个关于数据库的操作,如:项目学习等,不一一介绍都以此阅读计划为例
abstract class AndroidDataBase : RoomDatabase() {
abstract fun getCollectDao() : CollectedDao // 用于收藏文章操作
abstract fun getReadPlanDao() : ReadPlanDao // 用于阅读计划操作
abstract fun getStudyProjectDao() : StudyProjectDao // 用于项目学习操作
abstract fun getRecentSearchDao() : RecentSearchDao // 用于最近搜索操作
companion object {
private var instence : AndroidDataBase? = null
fun getInstence(context: Context) : AndroidDataBase{
if (instence == null){
synchronized(AndroidDataBase::class){
if (instence == null){
instence = Room.databaseBuilder(context.applicationContext,AndroidDataBase::class.java,"WanAndroid")
.build()
}
}
}
return instence!!
}
}
}
Entity
data class ReadPlanArticle(var author: String? = null,
var chapterName: String? = null,
var link: String? = null,
var articleId: Int = 0,
var title: String? = null
){
var id: Int = 0
}
Dao
interface ReadPlanDao {
fun insert(readPlanArticle: ReadPlanArticle)
fun remove(readPlanArticle: ReadPlanArticle)
fun getArticleList():DataSource.Factory<Int,ReadPlanArticle>
fun getArticle(id :Int):ReadPlanArticle
}
数据库的创建和要执行的操作已在上述配置完成,关于Room的使用这里不再介绍,结下来看看ArticleDetailRepository中是如何使用数据库,响应和修改LivaData的数据,我们依次看看初始化、加入计划和取消计划的操作
初始化:主要查询数据库中是否保存此文章,并更新界面UI
fun isRaedPlan(context: Context, id: Int) {
runOnIoThread {
val liva = AndroidDataBase.getInstence(context).getReadPlanDao().getArticle(id)
if (liva != null) {
articleIsReadLater.postValue(true)
} else {
articleIsReadLater.postValue(false)
}
}
}
上述代码执行操作:根据文章Id从数据库查询此文章,如果存在将articleIsReadLater设置为true,否则设置为false,那么ViewModel和Activity中的观察者都会执行响应改变。
注意:数据库的所有操作都不嫩放在主线程中
加入阅读计划:向数据库添加一条记录,并在添加成功后修改articleIsReadLater值
fun addStudyProject(readPlanArticle: StudyProject) {
runOnIoThread {
AndroidDataBase.getInstence(context).getStudyProjectDao().insert(readPlanArticle)
articleIsReadLater.postValue(true)
}
}
取消阅读计划:删除数据库记录,并修改articleIsReadLater值
fun removeReadLater(id: Int) {
runOnIoThread {
val readPlanArticle = AndroidDataBase.getInstence(context).getReadPlanDao().getArticle(id)
AndroidDataBase.getInstence(context).getReadPlanDao().remove(readPlanArticle)
articleIsReadLater.postValue(false)
}
}
效果展示
3.3.4、阅读计划的展示
阅读计划的内容是储存在本地数据库中,所以对文章的展示自然是Room的数据库的查询,而查询后数据的展示又是RecyclerView的使用,提到RecyclerVie就会想到Paging组件,没错我们想到的Google已经想到了,他们对Room和Paging进行了额外的支持,即可以实现对数据库的监听,当数据库改变时直接显示在Recycler中,首先在Room中设置数据库和查询数据库,此步骤前面已经完成,看一下这个方法:
fun getArticleList():DataSource.Factory<Int,ReadPlanArticle>
这里Room查询直接返回了DataSource.Factory的实例,也就是说Room已经在查询的时候就直接初始化了DataSource,简化了我们的操作,接下来看看ViewModel中如何处理数据:
class PlanArticleModel(application: Application) : AndroidViewModel(application) {
val dao = AndroidDataBase.getInstence(application).getReadPlanDao()
val livePagingList : LiveData<PagedList<ReadPlanArticle = LivePagedListBuilder(dao.getArticleList(),PagedList.Config.Builder()
.setPageSize(5)
.build()).build()
}
上面代码执行如下操作:
继承AndroidViewModel
初始化数据库查询的ReadPlanDao实例
初始化并配置LivePagingList
在Ui中监听ViewModel中的LiveData:
model.livePagingList.observe(this, Observer {
adapter.submitList(it)
})
此时你在添加和移除数据库操作时,Room返回的DataSource中的数据会发生改变,进而RecyclerView自动实现数据刷新,效果如下:
3.3、其余模块
项目模块:实现代码和文章模块相似,Paging展示项目列表,Room保存数据,只是所有的操作都针对于玩安卓中的学习项目;
导航模块:根据Tag导航响应的文章
公众号:在Fragment中使用Paging展示各个公众号中的文章
搜索模块:SearchView搜索文章,Room保存最近搜索
4、总结
以上是本项目各个模块的实现和分析,实现的项目比较简单,主要是展示以下组件的使用以及组件间的配合使用,本计划加入WorkManger做定时提醒的功能,并加上一些完整的功能,但由于各种原因(具体大家都懂。。。),后面有机会会继续完善,那本文到此结束,希望对大家学习和了解Jetpack组件,以及灵活应用组件有所帮助,让大家一起更好的学安卓、玩安卓!
点击查看源码,欢迎Star!
https://github.com/AlexTiti/WanAndroid
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!