從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!

相關文章
相關標籤/搜索