从0搭建Jetpack版的WanAndroid客户端

一、项目目的:

在接触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以外,上述封装中主要的类是三个类:架构

  1. Listing:用于封装须要监听的对象和执行的操做,用于系统交互
  2. BaseRepository:配置并实例化LivePagedListBuilder()对象,根据设定的监听状态和数据,封装List<M>对象
  3. 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>
}复制代码

上述代码中作了如下事情:

  1. 建立BaseDataSourceFactory实例
  2. 初始化并配置Paging组件
  3. 转换并监听BaseDataSourceFactory中保存的可观察的DataSource状态的变化
  4. 将全部的监听状态封装到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.二、文章阅读

这个部分的实现比较简单,也是组件的经典结构,详情页主要是根据文章的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.三、文章收藏和加入阅读计划

这部分和上面文章展现大体类似,只不过比它多了初始化收藏状态、收藏后上传服务器和保存数据库的操做,也就是多了ArticleDetailResposity中的调度操做,执行逻辑大体以下:

  1. 在显示详情时,初始化本篇文章的收藏状态和加入计划状态
  2. 点击收藏或计划后响应操做
  3. 执行逻辑后响应界面修改

实现过程以下:

  • 在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:本项目后面的几个关于数据库的操做,如:项目学习等,不一一介绍都以此阅读计划为例
@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
@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
@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的数据,咱们依次看看初始化、加入计划和取消计划的操做

  • 初始化:主要查询数据库中是否保存此文章,并更新界面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.四、阅读计划的展现

阅读计划的内容是储存在本地数据库中,因此对文章的展现天然是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()
}复制代码

上面代码执行以下操做:

  • 继承AndroidViewModel
  • 初始化数据库查询的ReadPlanDao实例
  • 初始化并配置LivePagingList

在Ui中监听ViewModel中的LiveData:

model.livePagingList.observe(this, Observer {
      adapter.submitList(it)
})复制代码

此时你在添加和移除数据库操做时,Room返回的DataSource中的数据会发生改变,进而RecyclerView自动实现数据刷新,效果以下:


3.三、其他模块

  • 项目模块:实现代码和文章模块类似,Paging展现项目列表,Room保存数据,只是全部的操做都针对于玩安卓中的学习项目;
  • 导航模块:根据Tag导航响应的文章
  • 公众号:在Fragment中使用Paging展现各个公众号中的文章
  • 搜索模块:SearchView搜索文章,Room保存最近搜索

四、总结

以上是本项目各个模块的实现和分析,实现的项目比较简单,主要是展现如下组件的使用以及组件间的配合使用,本计划加入WorkManger作定时提醒的功能,并加上一些完整的功能,但因为各类缘由(具体你们都懂。。。),后面有机会会继续完善,那本文到此结束,但愿对你们学习和了解Jetpack组件,以及灵活应用组件有所帮助,让你们一块儿更好的学安卓、玩安卓!

点击查看源码,欢迎Star!

相关文章
相关标签/搜索