組件化+Jetpack+MVVM項目實戰(zhàn),涉及協(xié)程+Retrofit,Paging3+Room等

一、項目簡介

微信截圖_20210521163936.png

該項目主要以組件化+Jetpack+MVVM為架構(gòu),使用Kotlin語言,集合了最新的Jetpack組件,如NavigationPaging3Room等,另外還加上了依賴注入框架Koin和圖片加載框架Coil。

網(wǎng)絡(luò)請求部分使用OkHttp+Retrofit,配合Kotlin的協(xié)程,完成了對Retrofit和協(xié)程的請求封裝,結(jié)合LoadSir進(jìn)行狀態(tài)切換管理,讓開發(fā)者只用關(guān)注自己的業(yè)務(wù)邏輯,而不要操心界面的切換和通知。

對于具體的網(wǎng)絡(luò)封裝思路,可參考【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請求狀態(tài)封裝實戰(zhàn)【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請求狀態(tài)封裝實戰(zhàn)(2)

項目地址:github.com/fuusy/wanan…

如果此項目對你有幫助和價值,煩請給個star??,或者有什么好的建議或意見,可以發(fā)個issues,感謝!

二、項目詳情

2.1、組件化搭建項目時暴露出的問題

2.1.1、如何獨立運行一個Module?

運行總App時,子Module是屬于library,而獨立運行時,子Module是屬于application。那么我們只需要在根目錄下gradle.properties中添加一個標(biāo)志位來區(qū)分一下子Module的狀態(tài),例如singleModule = false ,該標(biāo)志位可以用來表示當(dāng)前Module是否是獨立模塊,true表示處于獨立模塊,可單獨運行,false則表示是一個library。

image-20210425094424273.png

如何使用呢?

在每個Modulebuild.gradle中加入singleModule的判斷,以區(qū)分是application還是library。如下:

if (!singleModule.toBoolean()) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

......
dependencies {
}
復(fù)制代碼

如果需要獨立運行只需要修改gradle.properties標(biāo)志位singleModule的值。

2.1.2、編譯運行后,桌面會出現(xiàn)多個相同圖標(biāo);

當(dāng)新建多個Moudle的時候,運行后你會發(fā)現(xiàn)桌面上會出現(xiàn)多個相同的圖標(biāo),

image-20210425100807316.png

其實每個圖標(biāo)都能夠獨立運行,但是到最后App發(fā)布的時候,肯定是只需要一個總?cè)肟诰涂梢粤恕?/p>

發(fā)生這種情況的原因很簡單,因為新建一個Module,結(jié)構(gòu)相當(dāng)于一個project,AndroidManifest.xml包括Activity都存在,在AndroidManifest.xml為Activity設(shè)置了actioncategory,當(dāng)app運行時,也就在桌面上為webview這個模塊生成了一個入口。

image-20210425102207853.png

解決方案很簡單,刪除上圖紅色框框中的代碼即可。

但是...... 問題又雙叒叕來了,刪除了中代碼,確實可以解決多個圖標(biāo)的問題,但是當(dāng)該子Moudle需要獨立運行時,由于缺少<intent-filter>中的聲明,該Module就無法正常運行

以下圖項目為例:

image-20210425103221979.png

我們可以在”webview“Module中,新建一個和java同層級的包,取名:manifest,將AndroidManifest.xml復(fù)制到該包下,并且將/manifest/AndroidManifest.xml中內(nèi)容進(jìn)行刪除修改。

image-20210425104829329.png

只留有一個空殼子,原來的AndroidManifest.xml則保持不變。同時在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ù)制代碼

通過修改SourceSets中的屬性,可以指定需要被編譯的源文件,根據(jù)singleModule.toBoolean()來判斷當(dāng)前Module是屬于application還是library,如果是library,則編譯manifest下AndroidManifest.xml,反之則直接編譯主目錄下AndroidManifest.xml。

上述處理后,子Moudule當(dāng)作library時不會出現(xiàn)多個圖標(biāo)的情況,同時也可以獨立運行。

2.1.3、組件間通信

主要借助阿里的路由框架ARouter,具體使用請參考github.com/alibaba/ARo…

2.2、Jetpack組件

2.2.1、Navigation

Navigation是一個管理Fragment切換的組件,支持可視化處理。開發(fā)者也完全不用操心Fragment的切換邏輯?;臼褂谜垍⒖?a target="_blank">官方說明

在使用Navigation的過程中,會出現(xiàn)點擊back按鍵,界面會重新走了onCreate生命周期,并且將頁面重構(gòu)。例如Navigation與BottomNavigationView結(jié)合時,點擊tab,F(xiàn)ragment會重新創(chuàng)建。目前比較好的解決方法是自定義FragmentNavigator,將內(nèi)部replace替換為show/hide。

另外,官方對于與BottomNavigationView結(jié)合時的情況也提供了一種解決方案。 官方提供了一個BottomNavigationView的擴(kuò)展函數(shù)NavigationExtensions,

將之前共用一個navigation分為每個模塊單獨一個navigation,例如該項目分為首頁、項目、我的三個tab,相應(yīng)的新建了三個navigation:R.navigation.navi_home, R.navigation.navi_project, R.navigation.navi_personal, Activity中BottomNavigationViewNavigation進(jìn)行綁定時也做出了相應(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ù)制代碼

官方這么做的目的在于讓每個模塊單獨管理自己的Fragment棧,在tab切換時,不會相互影響。

2.2,2、Paging3

Paging是一個分頁組件,主要與Recyclerview結(jié)合分頁加載數(shù)據(jù)。具體使用可參考此項目“每日一問”部分,如下:

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(){
    /**
     * 請求每日一問數(shù)據(jù)
     */
    fun dailyQuestionPagingFlow(): Flow<PagingData<DailyQuestionData>> =
        repo.getDailyQuestion().cachedIn(viewModelScope)

}
復(fù)制代碼

Repository層

class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository(){
    /**
     * 請求每日一問
     */
    fun getDailyQuestion(): Flow<PagingData<DailyQuestionData>> {

        return Pager(config) {
            DailyQuestionPagingSource(service)
        }.flow
    }
}
復(fù)制代碼

PagingSource層:

/**
 * @date:2021/5/20
 * @author fuusy
 * @instruction: 每日一問數(shù)據(jù)源,主要配合Paging3進(jìn)行數(shù)據(jù)請求與顯示
 */
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是一個管理數(shù)據(jù)庫的組件,此項目主要將Paging3與Room相結(jié)合。2.3小節(jié)主要介紹了Paging3從網(wǎng)絡(luò)上加載數(shù)據(jù)分頁,而這不同的是,結(jié)合Room需要RemoteMediator的協(xié)同處理。

RemoteMediator主要作用是:可以使用此信號從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲在本地數(shù)據(jù)庫中,PagingSource 可以從本地數(shù)據(jù)庫加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示。 當(dāng)需要更多數(shù)據(jù)時,Paging 庫從 RemoteMediator 實現(xiàn)調(diào)用load()方法。具體使用方法可參考此項目首頁文章列表部分。

RoomPaging3結(jié)合時,UI層ViewModel層的操作與2.3小節(jié)一致,主要修改在于Repository層。

Repository層:

class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository() {
   /**
     * 請求首頁文章,
     * 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ù)失效時,從網(wǎng)絡(luò)加載更多數(shù)據(jù)。
 * 可以使用此信號從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲在本地數(shù)據(jù)庫中,PagingSource 可以從本地數(shù)據(jù)庫加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示。
 * 當(dāng)需要更多數(shù)據(jù)時,Paging 庫從 RemoteMediator 實現(xiàn)調(diào)用 load() 方法。這是一項掛起功能,因此可以放心地執(zhí)行長時間運行的工作。
 * 此功能通常從網(wǎng)絡(luò)源提取新數(shù)據(jù)并將其保存到本地存儲空間。
 * 此過程會處理新數(shù)據(jù),但長期存儲在數(shù)據(jù)庫中的數(shù)據(jù)需要進(jìn)行失效處理(例如,當(dāng)用戶手動觸發(fā)刷新時)。
 * 這由傳遞到 load() 方法的 LoadType 屬性表示。LoadType 會通知 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:首次訪問 或者調(diào)用 PagingDataAdapter.refresh() 觸發(fā)
        2.LoadType.PREPEND:在當(dāng)前列表頭部添加數(shù)據(jù)的時候時觸發(fā),實際在項目中基本很少會用到直接返回 MediatorResult.Success(endOfPaginationReached = true) ,參數(shù) endOfPaginationReached 表示沒有數(shù)據(jù)了不在加載
        3.LoadType.APPEND:加載更多時觸發(fā),這里獲取下一頁的 key, 如果 key 不存在,表示已經(jīng)沒有更多數(shù)據(jù),直接返回 MediatorResult.Success(endOfPaginationReached = true) 不會在進(jìn)行網(wǎng)絡(luò)和數(shù)據(jù)庫的訪問
         */
        try {
            Log.d(TAG, "load: $loadType")
            val pageKey: Int? = when (loadType) {
                LoadType.REFRESH -> null
                LoadType.PREPEND -> return MediatorResult.Success(true)
                LoadType.APPEND -> {
                    //使用remoteKey來獲取下一個或上一個頁面。
                    val remoteKey =
                        state.lastItemOrNull()?.id?.let {
                            db.remoteKeyDao().remoteKeysArticleId(it, articleType)
                        }

                    //remoteKey' null ',這意味著在初始刷新后沒有加載任何項目,也沒有更多的項目要加載。
                    if (remoteKey?.nextKey == null) {
                        return MediatorResult.Success(true)
                    }
                    remoteKey.nextKey
                }
            }

            val page = pageKey ?: 0
            //從網(wǎng)絡(luò)上請求數(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)建了RemoteKeyRemoteKeyDao來管理列表的頁數(shù),具體請參考此項目home模塊。

2.2.4、LiveData

關(guān)于LiveData的使用和原理,可參考【Jetpack篇】LiveData取代EventBus?LiveData的通信原理和粘性事件刨析

還有很多好用的Jetpack組件,將在后續(xù)更新。

三、感謝

API: 鴻洋大大提供的 WanAndroid API

第三方開源庫:

??Retrofit

??OkHttp

??Gson

??Coil

??Koin

??Arouter

??LoadSir

另外還有上面沒列舉的一些優(yōu)秀的第三方開源庫,感謝開源。

四、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

www.apache.org/licenses/LI…

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.

項目地址github.com/fuusy/wanan…

本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含不同方向的自學(xué)編程路線、面試題集合/面經(jīng)、及系列技術(shù)文章等,資源持續(xù)更新中...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內(nèi)容