一降宅、項(xiàng)目簡(jiǎn)介
該項(xiàng)目主要以組件化+Jetpack+MVVM
為架構(gòu)骂远,使用Kotlin
語(yǔ)言,集合了最新的Jetpack
組件腰根,如Navigation
激才、Paging3
、Room
等额嘿,另外還加上了依賴(lài)注入框架Koin
和圖片加載框架Coil
瘸恼。
網(wǎng)絡(luò)請(qǐng)求部分使用OkHttp
+Retrofit
,配合Kotlin的協(xié)程
册养,完成了對(duì)Retrofit和協(xié)程的請(qǐng)求封裝
东帅,結(jié)合LoadSir
進(jìn)行狀態(tài)切換管理,讓開(kāi)發(fā)者只用關(guān)注自己的業(yè)務(wù)邏輯捕儒,而不要操心界面的切換和通知冰啃。
對(duì)于具體的網(wǎng)絡(luò)封裝思路,可參考【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請(qǐng)求狀態(tài)封裝實(shí)戰(zhàn)和【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請(qǐng)求狀態(tài)封裝實(shí)戰(zhàn)(2)
項(xiàng)目地址:github.com/fuusy/wanan…
如果此項(xiàng)目對(duì)你有幫助和價(jià)值刘莹,煩請(qǐng)給個(gè)star??,或者有什么好的建議或意見(jiàn)阎毅,可以發(fā)個(gè)issues,感謝点弯!
二扇调、項(xiàng)目詳情
2.1、組件化搭建項(xiàng)目時(shí)暴露出的問(wèn)題
2.1.1抢肛、如何獨(dú)立運(yùn)行一個(gè)Module狼钮?
運(yùn)行總App時(shí),子Module是屬于library
捡絮,而獨(dú)立運(yùn)行時(shí)熬芜,子Module是屬于application
。那么我們只需要在根目錄下gradle.properties
中添加一個(gè)標(biāo)志位來(lái)區(qū)分一下子Module的狀態(tài)福稳,例如singleModule = false
涎拉,該標(biāo)志位可以用來(lái)表示當(dāng)前Module是否是獨(dú)立模塊,true
表示處于獨(dú)立模塊,可單獨(dú)運(yùn)行鼓拧,false
則表示是一個(gè)library半火。
如何使用呢?
在每個(gè)Module
的build.gradle
中加入singleModule
的判斷季俩,以區(qū)分是application
還是library
钮糖。如下:
if (!singleModule.toBoolean()) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
......
dependencies {
}
復(fù)制代碼
如果需要獨(dú)立運(yùn)行只需要修改gradle.properties
標(biāo)志位singleModule
的值。
2.1.2酌住、編譯運(yùn)行后店归,桌面會(huì)出現(xiàn)多個(gè)相同圖標(biāo);
當(dāng)新建多個(gè)Moudle的時(shí)候赂韵,運(yùn)行后你會(huì)發(fā)現(xiàn)桌面上會(huì)出現(xiàn)多個(gè)相同的圖標(biāo)娱节,
其實(shí)每個(gè)圖標(biāo)都能夠獨(dú)立運(yùn)行,但是到最后App發(fā)布的時(shí)候祭示,肯定是只需要一個(gè)總?cè)肟诰涂梢粤恕?/p>
發(fā)生這種情況的原因很簡(jiǎn)單,因?yàn)樾陆ㄒ粋€(gè)Module
谴古,結(jié)構(gòu)相當(dāng)于一個(gè)project质涛,AndroidManifest.xml包括Activity都存在,在AndroidManifest.xml
為Activity設(shè)置了action
和category
掰担,當(dāng)app運(yùn)行時(shí)汇陆,也就在桌面上為webview這個(gè)模塊生成了一個(gè)入口。
解決方案很簡(jiǎn)單带饱,刪除上圖紅色框框中的代碼即可毡代。
但是......
問(wèn)題又雙叒叕來(lái)了,刪除了中代碼勺疼,確實(shí)可以解決多個(gè)圖標(biāo)的問(wèn)題教寂,但是當(dāng)該子Moudle需要獨(dú)立運(yùn)行時(shí),由于缺少<intent-filter>
中的聲明执庐,該Module就無(wú)法正常運(yùn)行
酪耕。
以下圖項(xiàng)目為例:
我們可以在”webview“Module中,新建一個(gè)和java同層級(jí)的包轨淌,取名:manifest迂烁,將AndroidManifest.xml復(fù)制到該包下,并且將/manifest/AndroidManifest.xml中內(nèi)容進(jìn)行刪除修改递鹉。
只留有一個(gè)空殼子盟步,原來(lái)的AndroidManifest.xml
則保持不變。同時(shí)在webview的build.gradle
中利用sourceSets
進(jìn)行區(qū)分躏结。
android{
sourceSets{
main {
if (!singleModule.toBoolean()) {
//如果是library却盘,則編譯manifest下AndroidManifest.xml
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
//如果是application,則編譯主目錄下AndroidManifest.xml
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
復(fù)制代碼
通過(guò)修改SourceSets
中的屬性,可以指定需要被編譯的源文件谷炸,根據(jù)singleModule.toBoolean()
來(lái)判斷當(dāng)前Module是屬于application
還是library
北专,如果是library,則編譯manifest下AndroidManifest.xml旬陡,反之則直接編譯主目錄下AndroidManifest.xml拓颓。
上述處理后,子Moudule當(dāng)作library時(shí)不會(huì)出現(xiàn)多個(gè)圖標(biāo)的情況描孟,同時(shí)也可以獨(dú)立運(yùn)行驶睦。
2.1.3、組件間通信
主要借助阿里的路由框架ARouter匿醒,具體使用請(qǐng)參考github.com/alibaba/ARo…
2.2场航、Jetpack組件
2.2.1、Navigation
Navigation是一個(gè)管理Fragment切換的組件廉羔,支持可視化處理溉痢。開(kāi)發(fā)者也完全不用操心Fragment的切換邏輯”锼基本使用請(qǐng)參考官方說(shuō)明
在使用Navigation
的過(guò)程中孩饼,會(huì)出現(xiàn)點(diǎn)擊back按鍵,界面會(huì)重新走了onCreate
生命周期竹挡,并且將頁(yè)面重構(gòu)镀娶。例如Navigation與BottomNavigationView結(jié)合時(shí)
,點(diǎn)擊tab揪罕,F(xiàn)ragment會(huì)重新創(chuàng)建梯码。目前比較好的解決方法是自定義FragmentNavigator
,將內(nèi)部replace替換為show/hide
好啰。
另外轩娶,官方對(duì)于與BottomNavigationView
結(jié)合時(shí)的情況也提供了一種解決方案。 官方提供了一個(gè)BottomNavigationView
的擴(kuò)展函數(shù)NavigationExtensions
坎怪,
將之前共用一個(gè)navigation
分為每個(gè)模塊單獨(dú)一個(gè)navigation
罢坝,例如該項(xiàng)目分為首頁(yè)
、項(xiàng)目
搅窿、我的
三個(gè)tab嘁酿,相應(yīng)的新建了三個(gè)navigation:R.navigation.navi_home
, R.navigation.navi_project
, R.navigation.navi_personal
, Activity中BottomNavigationView
與Navigation
進(jìn)行綁定時(shí)也做出了相應(yīng)的改變男应。
/**
* navigation綁定BottomNavigationView
*/
private fun setupBottomNavigationBar() {
val navGraphIds =
listOf(R.navigation.navi_home, R.navigation.navi_project, R.navigation.navi_personal)
val controller = mBinding?.navView?.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
currentNavController = controller
}
復(fù)制代碼
官方這么做的目的在于讓每個(gè)模塊單獨(dú)管理自己的Fragment棧
闹司,在tab切換時(shí),不會(huì)相互影響沐飘。
2.2,2游桩、Paging3
Paging是一個(gè)分頁(yè)組件牲迫,主要與Recyclerview結(jié)合分頁(yè)加載數(shù)據(jù)。具體使用可參考此項(xiàng)目“每日一問(wèn)”部分
借卧,如下:
UI層:
class DailyQuestionFragment : BaseFragment<FragmentDailyQuestionBinding>() {
...
private fun loadData() {
lifecycleScope.launchWhenCreated {
mViewModel.dailyQuestionPagingFlow().collectLatest {
dailyPagingAdapter.submitData(it)
}
}
}
...
}
復(fù)制代碼
ViewModel層:
class ArticleViewModel(private val repo: HomeRepo) : BaseViewModel(){
/**
* 請(qǐng)求每日一問(wèn)數(shù)據(jù)
*/
fun dailyQuestionPagingFlow(): Flow<PagingData<DailyQuestionData>> =
repo.getDailyQuestion().cachedIn(viewModelScope)
}
復(fù)制代碼
Repository層
class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository(){
/**
* 請(qǐng)求每日一問(wèn)
*/
fun getDailyQuestion(): Flow<PagingData<DailyQuestionData>> {
return Pager(config) {
DailyQuestionPagingSource(service)
}.flow
}
}
復(fù)制代碼
PagingSource層:
/**
* @date:2021/5/20
* @author fuusy
* @instruction: 每日一問(wèn)數(shù)據(jù)源盹憎,主要配合Paging3進(jìn)行數(shù)據(jù)請(qǐng)求與顯示
*/
class DailyQuestionPagingSource(private val service: HomeService) :
PagingSource<Int, DailyQuestionData>() {
override fun getRefreshKey(state: PagingState<Int, DailyQuestionData>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DailyQuestionData> {
return try {
val pageNum = params.key ?: 1
val data = service.getDailyQuestion(pageNum)
val preKey = if (pageNum > 1) pageNum - 1 else null
LoadResult.Page(data.data?.datas!!, prevKey = preKey, nextKey = pageNum + 1)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
復(fù)制代碼
2.2.3、Room
Room
是一個(gè)管理數(shù)據(jù)庫(kù)的組件铐刘,此項(xiàng)目主要將Paging3與Room相結(jié)合
陪每。2.3小節(jié)主要介紹了Paging3
從網(wǎng)絡(luò)上加載數(shù)據(jù)分頁(yè),而這不同的是镰吵,結(jié)合Room
需要RemoteMediator
的協(xié)同處理檩禾。
RemoteMediator
的主要作用是:可以使用此信號(hào)從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲(chǔ)在本地?cái)?shù)據(jù)庫(kù)中,PagingSource
可以從本地?cái)?shù)據(jù)庫(kù)加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示疤祭。 當(dāng)需要更多數(shù)據(jù)時(shí)盼产,Paging 庫(kù)從 RemoteMediator
實(shí)現(xiàn)調(diào)用load()
方法。具體使用方法可參考此項(xiàng)目首頁(yè)文章列表部分勺馆。
Room
和Paging3
結(jié)合時(shí)戏售,UI層
和ViewModel層
的操作與2.3小節(jié)一致,主要修改在于Repository
層谓传。
Repository層:
class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository() {
/**
* 請(qǐng)求首頁(yè)文章蜈项,
* Room+network進(jìn)行緩存
*/
fun getHomeArticle(articleType: Int): Flow<PagingData<ArticleData>> {
mArticleType = articleType
return Pager(
config = config,
remoteMediator = ArticleRemoteMediator(service, db, 1),
pagingSourceFactory = pagingSourceFactory
).flow
}
}
復(fù)制代碼
DAO:
@Dao
interface ArticleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticle(articleDataList: List<ArticleData>)
@Query("SELECT * FROM tab_article WHERE articleType =:articleType")
fun queryLocalArticle(articleType: Int): PagingSource<Int, ArticleData>
@Query("DELETE FROM tab_article WHERE articleType=:articleType")
suspend fun clearArticleByType(articleType: Int)
}
復(fù)制代碼
RoomDatabase:
@Database(
entities = [ArticleData::class, RemoteKey::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
abstract fun remoteKeyDao(): RemoteKeyDao
companion object {
private const val DB_NAME = "app.db"
@Volatile
private var instance: AppDatabase? = null
fun get(context: Context): AppDatabase {
return instance ?: Room.databaseBuilder(context, AppDatabase::class.java,
DB_NAME
)
.build().also {
instance = it
}
}
}
}
復(fù)制代碼
自定義RemoteMediator:
/**
* @date:2021/5/20
* @author fuusy
* @instruction:RemoteMediator 的主要作用是:在 Pager 耗盡數(shù)據(jù)或現(xiàn)有數(shù)據(jù)失效時(shí),從網(wǎng)絡(luò)加載更多數(shù)據(jù)续挟。
* 可以使用此信號(hào)從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲(chǔ)在本地?cái)?shù)據(jù)庫(kù)中,PagingSource 可以從本地?cái)?shù)據(jù)庫(kù)加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示侥衬。
* 當(dāng)需要更多數(shù)據(jù)時(shí)诗祸,Paging 庫(kù)從 RemoteMediator 實(shí)現(xiàn)調(diào)用 load() 方法。這是一項(xiàng)掛起功能轴总,因此可以放心地執(zhí)行長(zhǎng)時(shí)間運(yùn)行的工作鳄乏。
* 此功能通常從網(wǎng)絡(luò)源提取新數(shù)據(jù)并將其保存到本地存儲(chǔ)空間课舍。
* 此過(guò)程會(huì)處理新數(shù)據(jù),但長(zhǎng)期存儲(chǔ)在數(shù)據(jù)庫(kù)中的數(shù)據(jù)需要進(jìn)行失效處理(例如,當(dāng)用戶(hù)手動(dòng)觸發(fā)刷新時(shí))眷细。
* 這由傳遞到 load() 方法的 LoadType 屬性表示。LoadType 會(huì)通知 RemoteMediator 是需要刷新現(xiàn)有數(shù)據(jù)坝疼,還是提取需要附加或前置到現(xiàn)有列表的更多數(shù)據(jù)翠拣。
*/
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: HomeService,
private val db: AppDatabase,
private val articleType: Int
) : RemoteMediator<Int, ArticleData>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ArticleData>
): MediatorResult {
/*
1.LoadType.REFRESH:首次訪問(wèn) 或者調(diào)用 PagingDataAdapter.refresh() 觸發(fā)
2.LoadType.PREPEND:在當(dāng)前列表頭部添加數(shù)據(jù)的時(shí)候時(shí)觸發(fā),實(shí)際在項(xiàng)目中基本很少會(huì)用到直接返回 MediatorResult.Success(endOfPaginationReached = true) 虑灰,參數(shù) endOfPaginationReached 表示沒(méi)有數(shù)據(jù)了不在加載
3.LoadType.APPEND:加載更多時(shí)觸發(fā)吨瞎,這里獲取下一頁(yè)的 key, 如果 key 不存在,表示已經(jīng)沒(méi)有更多數(shù)據(jù)穆咐,直接返回 MediatorResult.Success(endOfPaginationReached = true) 不會(huì)在進(jìn)行網(wǎng)絡(luò)和數(shù)據(jù)庫(kù)的訪問(wèn)
*/
try {
Log.d(TAG, "load: $loadType")
val pageKey: Int? = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(true)
LoadType.APPEND -> {
//使用remoteKey來(lái)獲取下一個(gè)或上一個(gè)頁(yè)面颤诀。
val remoteKey =
state.lastItemOrNull()?.id?.let {
db.remoteKeyDao().remoteKeysArticleId(it, articleType)
}
//remoteKey' null '字旭,這意味著在初始刷新后沒(méi)有加載任何項(xiàng)目,也沒(méi)有更多的項(xiàng)目要加載崖叫。
if (remoteKey?.nextKey == null) {
return MediatorResult.Success(true)
}
remoteKey.nextKey
}
}
val page = pageKey ?: 0
//從網(wǎng)絡(luò)上請(qǐng)求數(shù)據(jù)
val result = api.getHomeArticle(page).data?.datas
result?.forEach {
it.articleType = articleType
}
val endOfPaginationReached = result?.isEmpty()
db.withTransaction {
if (loadType == LoadType.REFRESH) {
//清空數(shù)據(jù)
db.remoteKeyDao().clearRemoteKeys(articleType)
db.articleDao().clearArticleByType(articleType)
}
val prevKey = if (page == 0) null else page - 1
val nextKey = if (endOfPaginationReached!!) null else page + 1
val keys = result.map {
RemoteKey(
articleId = it.id,
prevKey = prevKey,
nextKey = nextKey,
articleType = articleType
)
}
db.remoteKeyDao().insertAll(keys)
db.articleDao().insertArticle(articleDataList = result)
}
return MediatorResult.Success(endOfPaginationReached!!)
} catch (e: IOException) {
return MediatorResult.Error(e)
} catch (e: HttpException) {
return MediatorResult.Error(e)
}
}
}
復(fù)制代碼
另外新創(chuàng)建了RemoteKey
和RemoteKeyDao
來(lái)管理列表的頁(yè)數(shù)遗淳,具體請(qǐng)參考此項(xiàng)目home模塊。
2.2.4心傀、LiveData
關(guān)于LiveData
的使用和原理屈暗,可參考【Jetpack篇】LiveData取代EventBus?LiveData的通信原理和粘性事件刨析
還有很多好用的Jetpack組件剧包,將在后續(xù)更新恐锦。
三、感謝
API: 鴻洋大大提供的 WanAndroid API
第三方開(kāi)源庫(kù):
??Retrofit
??OkHttp
??Gson
??Coil
??Koin
??Arouter
??LoadSir
另外還有上面沒(méi)列舉的一些優(yōu)秀的第三方開(kāi)源庫(kù)疆液,感謝開(kāi)源一铅。
四、License??
License Copyright 2021 fuusy
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
項(xiàng)目地址:github.com/fuusy/wanan…
本文在開(kāi)源項(xiàng)目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄堕油,里面包含不同方向的自學(xué)編程路線潘飘、面試題集合/面經(jīng)、及系列技術(shù)文章等掉缺,資源持續(xù)更新中...