在接触Android Jetpack组件时, 就深深被其巧妙的设计和强大的功能所吸引,暗自告诉本身必定要学会这些组件,而网上并不能找到系统的学习资料,因而利用天天的时间访问Google Developers,把Jetpack的每一个组件从使用到源码进行了系统的学习和总结,因而就有了带你领略Android Jetpack组件的魅力系列文章,但愿在总结本身学习的同时,也能帮助须要这些资料的同窗,在写完这些文章后,想在项目中使用这些强大组件的想法就更加想强烈了, 但又担忧直接在公司项目中使用会又踩坑的危险,并且公司的项目又一时难以所有替换,好在WanAndroid提供了完整的应用接口,才有了这个Jetpack版的WanAndroid客户端,项目功能比较简单,做为Jetpack组件的实战项目,旨在抛砖引玉和你们一块儿真正的使用Jetpack组件。java
既然本篇是对Android Jetpack组件的实战,那么就按照官方推荐的项目架构进行开发,架构内容见下图:android
上面架构你们应是很熟悉的,基本原则和平时使用的MVC、MVP等同样,都是使界面、数据、和处理的逻辑进行解耦,打造稳定的、易测试、易扩展的项目架构,只是在这个过程当中使用了全新的组件,如:ViewModel、LiveData等,使整个项目架构更加简单和灵活,关于使用的新组件不了解的能够点击文章开头的连接,学习相关组件的使用,本文默认读者已经了解组件的简单使用。git
本项目按照前面项目架构的指导,根据各个模块的功能进行分包管理,以下图:github
3.一、登录模块
登录模块遵循着一个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文件:api
<?xml version="1.0" encoding="utf-8"?>
<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.二、文章模块bash
3.2.一、文章列表展现服务器
对于常规的内容展现,使用RecyclerView并实现上拉加载和下拉刷新便可,此处使用Paging组件实现这些功能,对于Paging的下拉加载以前文章已经介绍了,经过自定以DataSource控制数据的加载和分页,本文再也不进行介绍,这里只介绍对Paging组件进行了简单的封装,代码结构以下:网络
除了DataBase、Factory和Adaoter以外,上述封装中主要的类是三个类:架构
/**
* 用于封装须要监听的对象和执行的操做,用于系统交互
* 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)复制代码
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>
}复制代码
上述代码中作了如下事情:
对于上拉加载以前的文章有介绍,可对于下拉刷新的实现并无直接介绍,不过从上面的代码能够看出,此处的refresh()调用DataSource的invalidate()方法,通知数据实失效,此时数据会重新加载。
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.二、文章阅读
这个部分的实现比较简单,也是组件的经典结构,详情页主要是根据文章的Url和Title决定,换句话说只要Url和Title改变文章的内容就会改变,因此只要在ViewModel中保存Title和Url的可观察类,在Activity中监听两者并在其改变时执行相应的操做。
val contentTitle = MutableLiveData<String>()
val contentUrl = MutableLiveData<String>()
复制代码
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.三、文章收藏和加入阅读计划
这部分和上面文章展现大体类似,只不过比它多了初始化收藏状态、收藏后上传服务器和保存数据库的操做,也就是多了ArticleDetailResposity中的调度操做,执行逻辑大体以下:
实现过程以下:
class ArticleDetailRepository(val api: Api, val context: Context) {
val articleIsCollected = MutableLiveData<Boolean>()
val articleIsReadLater = MutableLiveData<Boolean>()
}复制代码
/**
* 是否收藏
*/
val collected = Transformations.map(aricleDetailResposity.articleIsCollected) { it }!!
/**
* 是否加入阅读计划
*/
val readPlan = Transformations.map(aricleDetailResposity.articleIsReadLater) { it }!!
复制代码
//若是文章已收藏则显示“取消收藏”,不然显示“文章收藏”
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(entities = [CollectArticle::class,ReadPlanArticle::class,StudyProject::class,RecentSearch::class],version = 1 ,exportSchema = false)
abstract class AndroidDataBase : RoomDatabase() {
abstract fun getCollectDao() : CollectedDao // 用于收藏文章操做
abstract fun getReadPlanDao() : ReadPlanDao // 用于阅读计划操做
abstract fun getStudyProjectDao() : StudyProjectDao // 用于项目学习操做
abstract fun getRecentSearchDao() : RecentSearchDao // 用于最近搜索操做
companion object {
@Volatile
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(tableName = "read_plan")
data class ReadPlanArticle(var author: String? = null,
var chapterName: String? = null,
var link: String? = null,
var articleId: Int = 0,
var title: String? = null
){
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}复制代码
@Dao
interface ReadPlanDao {
@Insert
fun insert(readPlanArticle: ReadPlanArticle)
@Delete
fun remove(readPlanArticle: ReadPlanArticle)
@Query("SELECT * from read_plan")
fun getArticleList():DataSource.Factory<Int,ReadPlanArticle>
@Query("SELECT * from read_plan WHERE articleId = :id")
fun getArticle(id :Int):ReadPlanArticle
}复制代码
数据库的建立和要执行的操做已在上述配置完成,关于Room的使用这里再也不介绍,结下来看看ArticleDetailRepository中是如何使用数据库,响应和修改LivaData的数据,咱们依次看看初始化、加入计划和取消计划的操做
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中的观察者都会执行响应改变。
注意:数据库的全部操做都不能放在主线程中
fun addStudyProject(readPlanArticle: StudyProject) {
runOnIoThread {
AndroidDataBase.getInstence(context).getStudyProjectDao().insert(readPlanArticle)
articleIsReadLater.postValue(true)
}
}复制代码
fun removeReadLater(id: Int) {
runOnIoThread {
val readPlanArticle = AndroidDataBase.getInstence(context).getReadPlanDao().getArticle(id)
AndroidDataBase.getInstence(context).getReadPlanDao().remove(readPlanArticle)
articleIsReadLater.postValue(false)
}
}复制代码
3.3.四、阅读计划的展现
阅读计划的内容是储存在本地数据库中,因此对文章的展现天然是Room的数据库的查询,而查询后数据的展现又是RecyclerView的使用,提到RecyclerVie就会想到Paging组件,没错咱们想到的Google已经想到了,他们对Room和Paging进行了额外的支持,便可以实现对数据库的监听,当数据库改变时直接显示在RecyclerView中,首先在Room中设置数据库和查询数据库,此步骤前面已经完成,看一下这个方法:
@Query("SELECT * from read_plan")
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()
}复制代码
上面代码执行以下操做:
在Ui中监听ViewModel中的LiveData:
model.livePagingList.observe(this, Observer {
adapter.submitList(it)
})复制代码
此时你在添加和移除数据库操做时,Room返回的DataSource中的数据会发生改变,进而RecyclerView自动实现数据刷新,效果以下:
3.三、其他模块
以上是本项目各个模块的实现和分析,实现的项目比较简单,主要是展现如下组件的使用以及组件间的配合使用,本计划加入WorkManger作定时提醒的功能,并加上一些完整的功能,但因为各类缘由(具体你们都懂。。。),后面有机会会继续完善,那本文到此结束,但愿对你们学习和了解Jetpack组件,以及灵活应用组件有所帮助,让你们一块儿更好的学安卓、玩安卓!