在接觸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組件,以及靈活應用組件有所幫助,讓你們一塊兒更好的學安卓、玩安卓!