💖组件化+Jetpack+Kotlin+MVVM项目实战,涉及Jetpack相关组件,Kotlin相关技术,协程+Retrofit,Paging3+Room等。

Overview

组件化+Jetpack+Kotlin+MVVM

ivo.png

一、项目简介

微信截图_20210521163936.png 该项目主要以组件化+Jetpack+MVVM为架构,使用Kotlin语言,集合了最新的Jetpack组件,如NavigationPaging3Room等,另外还加上了依赖注入框架Koin和图片加载框架Coil

网络请求部分使用OkHttp+Retrofit,配合Kotlin的协程,完成了对Retrofit和协程的请求封装,结合LoadSir进行状态切换管理,让开发者只用关注自己的业务逻辑,而不要操心界面的切换和通知。

对于具体的网络封装思路,可参考

【Jetpack篇】协程+Retrofit网络请求状态封装实战

【Jetpack篇】协程+Retrofit网络请求状态封装实战(2)

项目地址:https://github.com/fuusy/wanandroid_jetpack_kt

如果此项目对你有帮助和价值,烦请给个star,或者有什么好的建议或意见,也可以发个issues,感谢!

二、项目详情

2.1、组件化搭建项目时暴露出的问题

2.1.1、如何独立运行一个Module?

运行总App时,子Module是属于library,而独立运行时,子Module是属于application。那么我们只需要在根目录下gradle.properties中添加一个标志位来区分一下子Module的状态,例如singleModule = false ,该标志位可以用来表示当前Module是否是独立模块,true表示处于独立模块,可单独运行,false则表示是一个library。

image-20210425094424273.png

如何使用呢?

在每个Modulebuild.gradle中加入singleModule的判断,以区分是application还是library。如下:

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

......
dependencies {
}

如果需要独立运行只需要修改gradle.properties标志位singleModule的值。

2.1.2、编译运行后,桌面会出现多个相同图标;

当新建多个Moudle的时候,运行后你会发现桌面上会出现多个相同的图标,

微信截图_20210522184326.png

其实每个图标都能够独立运行,但是到最后App发布的时候,肯定是只需要一个总入口就可以了。

发生这种情况的原因很简单,因为新建一个Module,结构相当于一个project,AndroidManifest.xml包括Activity都存在,在AndroidManifest.xml为Activity设置了actioncategory,当app运行时,也就在桌面上为webview这个模块生成了一个入口。

image-20210425102207853.png

解决方案很简单,删除上图红色框框中的代码即可。

但是...... 问题又双叒叕来了,删除了中代码,确实可以解决多个图标的问题,但是当该子Moudle需要独立运行时,由于缺少<intent-filter>中的声明,该Module就无法正常运行

以下图项目为例:

image-20210425103221979.png 我们可以在”webview“Module中,新建一个和java同层级的包,取名:manifest,将AndroidManifest.xml复制到该包下,并且将/manifest/AndroidManifest.xml中内容进行删除修改。

image-20210425104829329.png

只留有一个空壳子,原来的AndroidManifest.xml则保持不变。同时在webview的build.gradle中利用sourceSets进行区分。

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'
            }
        }
    }
}

通过修改SourceSets中的属性,可以指定需要被编译的源文件,根据singleModule.toBoolean()来判断当前Module是属于application还是library,如果是library,则编译manifest下AndroidManifest.xml,反之则直接编译主目录下AndroidManifest.xml。

上述处理后,子Moudule当作library时不会出现多个图标的情况,同时也可以独立运行。

2.1.3、组件间通信

主要借助阿里的路由框架ARouter,具体使用请参考https://github.com/alibaba/ARouter

2.2、Jetpack组件

2.2.1、Navigation

Navigation是一个管理Fragment切换的组件,支持可视化处理。开发者也完全不用操心Fragment的切换逻辑。基本使用请参考官方说明

在使用Navigation的过程中,会出现点击back按键,界面会重新走了onCreate生命周期,并且将页面重构。例如Navigation与BottomNavigationView结合时,点击tab,Fragment会重新创建。目前比较好的解决方法是自定义FragmentNavigator,将内部replace替换为show/hide

另外,官方对于与BottomNavigationView结合时的情况也提供了一种解决方案。 官方提供了一个BottomNavigationView的扩展函数NavigationExtensions

将之前共用一个navigation分为每个模块单独一个navigation,例如该项目分为首页项目我的三个tab,相应的新建了三个navigation:R.navigation.navi_home, R.navigation.navi_project, R.navigation.navi_personal, Activity中BottomNavigationViewNavigation进行绑定时也做出了相应的改变。

    /**
     * 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
    }

官方这么做的目的在于让每个模块单独管理自己的Fragment栈,在tab切换时,不会相互影响。

2.2,2、Paging3

Paging是一个分页组件,主要与Recyclerview结合分页加载数据。具体使用可参考此项目“每日一问”部分,如下:

UI层:

class DailyQuestionFragment : BaseFragment<FragmentDailyQuestionBinding>() {
...

private fun loadData() {
        lifecycleScope.launchWhenCreated {
            mViewModel.dailyQuestionPagingFlow().collectLatest {
                dailyPagingAdapter.submitData(it)
            }
        }
    }
...
}

ViewModel层:

class ArticleViewModel(private val repo: HomeRepo) : BaseViewModel(){
    /**
     * 请求每日一问数据
     */
    fun dailyQuestionPagingFlow(): Flow<PagingData<DailyQuestionData>> =
        repo.getDailyQuestion().cachedIn(viewModelScope)

}

Repository层

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

        return Pager(config) {
            DailyQuestionPagingSource(service)
        }.flow
    }
}

PagingSource层:

/**
 * @date:2021/5/20
 * @author fuusy
 * @instruction: 每日一问数据源,主要配合Paging3进行数据请求与显示
 */
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)
        }
    }
}
2.2.3、Room

Room是一个管理数据库的组件,此项目主要将Paging3与Room相结合。2.3小节主要介绍了Paging3从网络上加载数据分页,而这不同的是,结合Room需要RemoteMediator的协同处理。

RemoteMediator主要作用是:可以使用此信号从网络加载更多数据并将其存储在本地数据库中,PagingSource 可以从本地数据库加载这些数据并将其提供给界面进行显示。 当需要更多数据时,Paging 库从 RemoteMediator 实现调用load()方法。具体使用方法可参考此项目首页文章列表部分

RoomPaging3结合时,UI层ViewModel层的操作与2.3小节一致,主要修改在于Repository层。

Repository层:

class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository() {
   /**
     * 请求首页文章,
     * Room+network进行缓存
     */
    fun getHomeArticle(articleType: Int): Flow<PagingData<ArticleData>> {
        mArticleType = articleType
        return Pager(
            config = config,
            remoteMediator = ArticleRemoteMediator(service, db, 1),
            pagingSourceFactory = pagingSourceFactory
        ).flow
    }
}

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)
    
}

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
                }
        }
    }
}

自定义RemoteMediator:

/**
 * @date:2021/5/20
 * @author fuusy
 * @instruction:RemoteMediator 的主要作用是:在 Pager 耗尽数据或现有数据失效时,从网络加载更多数据。
 * 可以使用此信号从网络加载更多数据并将其存储在本地数据库中,PagingSource 可以从本地数据库加载这些数据并将其提供给界面进行显示。
 * 当需要更多数据时,Paging 库从 RemoteMediator 实现调用 load() 方法。这是一项挂起功能,因此可以放心地执行长时间运行的工作。
 * 此功能通常从网络源提取新数据并将其保存到本地存储空间。
 * 此过程会处理新数据,但长期存储在数据库中的数据需要进行失效处理(例如,当用户手动触发刷新时)。
 * 这由传递到 load() 方法的 LoadType 属性表示。LoadType 会通知 RemoteMediator 是需要刷新现有数据,还是提取需要附加或前置到现有列表的更多数据。
 */
@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:首次访问 或者调用 PagingDataAdapter.refresh() 触发
        2.LoadType.PREPEND:在当前列表头部添加数据的时候时触发,实际在项目中基本很少会用到直接返回 MediatorResult.Success(endOfPaginationReached = true) ,参数 endOfPaginationReached 表示没有数据了不在加载
        3.LoadType.APPEND:加载更多时触发,这里获取下一页的 key, 如果 key 不存在,表示已经没有更多数据,直接返回 MediatorResult.Success(endOfPaginationReached = true) 不会在进行网络和数据库的访问
         */
        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
            //从网络上请求数据
            val result = api.getHomeArticle(page).data?.datas
            result?.forEach {
                it.articleType = articleType
            }
            val endOfPaginationReached = result?.isEmpty()

            db.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    //清空数据
                    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)
        }

    }
}

另外新创建了RemoteKeyRemoteKeyDao来管理列表的页数,具体请参考此项目home模块。

2.2.4、LiveData

关于LiveData的使用和原理,可参考【Jetpack篇】LiveData取代EventBus?LiveData的通信原理和粘性事件刨析

还有很多好用的Jetpack组件,将在后续更新。

三、感谢

API: 鸿洋大大提供的 WanAndroid API

第三方开源库:

Retrofit

OkHttp

Gson

Coil

Koin

Arouter

LoadSir

另外还有上面没列举的一些优秀的第三方开源库,感谢开源。

四、版本

持续更新

2021.5.20更新

1.Paging3和Room结合;

2.将Glide替换为Coil

2021.5.17更新

1.新增BasePagingAdapter,减少Paging3Adapter冗余代码;

2.删除App Module Fragment的依赖。

2021.5.12/13更新

1.新增启动页,icon;

2.网络请求新增局部状态管理,结合loadSir切换界面,更直观简便;

3.新增Koin

V1.0.0

1.提交WanAndroid第一版,包括首页、个人中心、项目模块

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

http://www.apache.org/licenses/LICENSE-2.0

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.

You might also like...
Android News App built in kotlin with implementation of MVVM architecture, android navigation components and retrofit. Displays news to users allowing them to share and save news.
Android News App built in kotlin with implementation of MVVM architecture, android navigation components and retrofit. Displays news to users allowing them to share and save news.

News-App Android news app built in kotlin that fetches news data from news api with Retrofit and displays news to users. This App follow MVVM architec

Instagram clone App in android using Kotlin, LiveData, MVVM, Dagger, RxJava and Retrofit.
Instagram clone App in android using Kotlin, LiveData, MVVM, Dagger, RxJava and Retrofit.

Instagram-Project-in-android-with-MVVM-architecture Project from MindOrks Professional Bootcamp with self practice work and added some additional feat

Wallpaper app made using Hilt, Retrofit, Room, Navigation Components, MVI, Coroutines, Flows, ViewModel, LiveData, Datastore Preference.
Wallpaper app made using Hilt, Retrofit, Room, Navigation Components, MVI, Coroutines, Flows, ViewModel, LiveData, Datastore Preference.

Android Picture Engine Wallpaper app made using Hilt, Retrofit, Room, Navigation Components, MVI, Coroutines, Flows, ViewModel, LiveData, Datastore Pr

NewsApp - Modern Minimalistic Design, MVVM, Pagination, Retrofit, Coroutines, Room, Glide, Navigation Component (Clean Architecture)
NewsApp - Modern Minimalistic Design, MVVM, Pagination, Retrofit, Coroutines, Room, Glide, Navigation Component (Clean Architecture)

NewsApp is a modern news android application which features virtually ALL recent and recommended android development tech stack and tools used

Android Application built using MVVM + Retrofit + Hilt + Clean + Room to shown several movies from
Android Application built using MVVM + Retrofit + Hilt + Clean + Room to shown several movies from "The Movie DB" platform.

MovieHunter Android Application built using MVVM + Retrofit + Hilt + Clean + Room to shown several movies from "The Movie DB" platform. Contribution I

Finder Job simple app using (Retrofit , Dagger hilt , coroutines , navigation components)
Finder Job simple app using (Retrofit , Dagger hilt , coroutines , navigation components)

Job Finder I'm finished building a simple project Job Finder App technology used [dagger hilt, coroutines, navigation components, LiveData, Skelton pa

TzRecipes App With Retrofit,Coroutines,Flow
TzRecipes App With Retrofit,Coroutines,Flow

TzRecipesApp Main Stack : Retrofit,Coroutines,Flow,LiveData,Mvvm SOLID,OOP,ClenArchitecture Разбиение приложения на data, domain, presentation слои Ма

🔥Simple quote app using MVVM, Retrofit, Coroutines and Dagger Hilt 💉

🔥 simple quote app using MVVM, Retrofit, Coroutines and Dagger Hilt 💉 quote.mp4 📚 knowledges and technologies ViewBinding Retrofit Coroutines MVVM

Shumba money weather app, using openweather api and retrofit
Shumba money weather app, using openweather api and retrofit

Shumba Money Weather App Language KOTLIN Libraries used 1.) Retrofit library - REST Client for Java and Android. It makes it relatively easy to retrie

Comments
  • 构建失败问题

    构建失败问题

    构建的时候失败 失败日志如下 请问是什么问题呢 FAILURE: Build completed with 2 failures.

    1: Task failed with an exception.

    • What went wrong: Execution failed for task ':common:kaptDebugKotlin'.

    A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution java.lang.reflect.InvocationTargetException (no error message)

    • Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. ==============================================================================

    2: Task failed with an exception.

    • What went wrong: Execution failed for task ':service:kaptDebugKotlin'.

    A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution java.lang.reflect.InvocationTargetException (no error message)

    • Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. ==============================================================================

    • Get more help at https://help.gradle.org

    BUILD FAILED in 1s

    opened by Godlwl 3
  • 协程多请求处理

    协程多请求处理

    关于BaseRepository#executeResp 封装的挺好的,在学习这个东西。有以下问题:

    1.现在这个封装和LiveData绑定在一起。比如我想只想使用协程去做网络请求,而不是和LiveData绑定在一起,就很麻烦。能否剥离出来? 2.组合请求,比如两个请求A、B,必须两个结果拿到后一起渲染。这个封装该怎么操作。同样的,先请求A,再根据A的请求结果去请求B,怎么处理。这种需求就是对应的Rx里面的zip,flat。

    opened by scsfwgy 2
  • Cannot access 'androidx.databinding.Observable' which is a supertype of 'androidx.databinding.ViewDataBinding'. Check your module classpath for missing or conflicting dependencies

    Cannot access 'androidx.databinding.Observable' which is a supertype of 'androidx.databinding.ViewDataBinding'. Check your module classpath for missing or conflicting dependencies

    Cannot access 'androidx.databinding.Observable' which is a supertype of 'androidx.databinding.ViewDataBinding'. Check your module classpath for missing or conflicting dependencies

    Android Studio 4.2.2

    opened by cikichen 1
Releases(1.2.0)
Owner
fuusy
fuusy
CoroutinesFlow and ViewModel Retrofit API - Practice with CoroutinesFlow, ViewModel and Retrofit API

Practice with Coroutines Flow, ViewModel and get data with Retrofit from two API

Komiljon Zokirov 3 Feb 6, 2022
MVVM + Kotlin + Jetpack Compose +Navigation Compose + Hilt + Retrofit + Unit Testing + Compose Testing + Coroutines + Kotlin Flow + Io mockK

MvvmKotlinJetpackCompose Why do we need an architecture even when you can make an app without it? let's say you created a project without any architec

Sayyed Rizwan 46 Nov 29, 2022
It is a NBAApp developed by Kotlin. It uses MVVM design pattern, Coroutines, Retrofit and JetPack libraries like Room, Lifecycle, ViewBinding, DataBinding, Hilt and Navigation.

NbaApp It is a NBAApp developed by Kotlin. It uses MVVM design pattern, Coroutines, Retrofit and JetPack libraries like Room, Lifecycle, ViewBinding,

Tuna Ateş Koç 2 Feb 26, 2022
The News App has been carried out within the framework of the MVVM architecture, information about news is obtained by consulting an API, it is built usisng Jetpack Copose, Coroutines, Dependency Injection with Hilt and Retrofit

Journalist The News App consists of an application that displays the latest news from EEUU from an API that provides official and updated information.

null 0 Nov 3, 2021
Shreyas Patil 2.1k Dec 30, 2022
Android News Reader app. Kotlin Coroutines, Retrofit and Realm

News Reader Android News Reader app Code that follows Packt Publishing Kotlin in Practice Video Course Example of Kotlin Coroutine usage, with Realm a

Marko Devcic 22 Oct 3, 2022
Sample news app using Kotlin, Hilt, Coroutines, Coil, Room, Retrofit

Tech stack & News App libraries Navigation component - navigation graph for navigating and replacing screens/fragments DataBinding - allows to more ea

Mina Mikhail 3 Dec 28, 2021
Wallum is a superfast ⚡ lightweight wallpaper app, built using Kotlin, Retrofit, MVVM, Paging 3, Hilt, and Navigation Components

Show some ❤️ and star the repo to show support for the project Wallum Android App Wallum is a super-fast ?? , lightweight wallpaper app built purely w

Aditya Verma 19 Dec 6, 2022
MarsRealEstate is a simple demo app using ViewModel & LiveData with Retrofit, Glide and Moshi in Kotlin.

Android-NASA-Real-Estate-Mars Is Android app that uses Kotlin and MVVM architecture is a simple app that uses Coroutines, LiveData with Retrofit, Glid

Dmytro K 0 Nov 17, 2021
Weather App using Retrofit Kotlin Android

NewsFeed Built an Android App which displays current weather and weather forecast of a selected city to the user. Features Coroutines Hilt JSON Parsin

Kumar Baibhav 1 Oct 4, 2022