I. Project Introduction

The project is mainly based onComponentization + Jetpack + MVVMFor architecture, useKotlinLanguage, a collection of the latestJetpackComponents, such asNavigation,Paging3,RoomAnd so on, in addition to the dependency injection frameworkKoinAnd the image loading frameCoil.

The network request part uses OkHttp+Retrofit, with Kotlin coroutine, completed the request packaging of Retrofit and coroutine, combined with LoadSir for state switching management, so that developers only pay attention to their own business logic, and do not worry about interface switching and notification.

For specific network encapsulation ideas, can refer to [Jetpack] coroutine +Retrofit network request state encapsulation combat and [Jetpack] coroutine +Retrofit network request state encapsulation combat (2)

Project address: github.com/fuusy/wanan…

If this project is helpful and valuable to you, please send to star⭐⭐, or if you have any good suggestions or comments, you can send issues, thanks!

Ii. Project Details

2.1 Problems exposed in the componentized construction of the project

2.1.1 How to run a Module independently?

When the overall App is running, the submodule belongs to the library; when the App is running independently, the submodule belongs to the Application. For example, singleModule = false. This flag can be used to indicate whether the current Module is an independent Module. True means that it is an independent Module. Can be run separately, false means a library.

How to use it?

Add a singleModule judgment to each Module’s build.gradle to distinguish between Application and Library. As follows:

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

If you want to run it independently, just change the singleModule value in gradle.properties flag bit.

2.1.2 After compiling and running, multiple same ICONS will appear on the desktop;

When you create more than one Moudle, you will see the same ICONS on your desktop.

Each icon works independently, but by the time the App is released, you’ll only need one main entry.

The reason for this is simple: create a new Module with a structure equivalent to a project, and androidManifest.xml includes the Activity. Androidmanifest.xml sets the actions and categories for the Activity. When the app is running, it generates an entry for the WebView module on the desktop.

The solution is simply to delete the code in the red box above.

But… To double 叒 Yi, remove the code to solve the problem of multiple ICONS, but when the child Moudle needs to run independently, due to the lack of < Intent-filter > declaration, the Module cannot run properly.

As an example:

We can “webview” Module, create a new hierarchy of packages with Java, named: the manifest, the AndroidManifest. XML copied to the package, and will manifest/AndroidManifest. Delete the XML content changes.

With only a shell left, the original AndroidManifest.xml remains intact. At the same time in webView build. Gradle use sourceSets to distinguish.

android{
    sourceSets{
        main {
            if(! singleModule.toBoolean()) {// In the case of library, compile androidManifest.xml in the manifest
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                // If it is application, compile androidmanifest.xml in the home directory
                manifest.srcFile 'src/main/AndroidManifest.xml'}}}}Copy the code

Singlemodule.toboolean () is used to determine whether the current Module belongs to the Application or the library. If the Module belongs to the library, Then compile androidmanifest.xml in the manifest, otherwise directly compile androidmanifest.xml in the home directory.

After this process, the child Moudule as a library does not have multiple ICONS and can run independently.

2.1.3 Communication between components

The main use of Ali routing framework ARouter, please refer to github.com/alibaba/ARo…

2.2. Jetpack component

2.2.1, Navigation,

Navigation is a component that manages Fragment toggles and supports visual processing. Developers also don’t have to worry about switching logic for fragments at all. Please refer to the official instructions for basic use

When using Navigation, the back button will be clicked, the interface will go through the onCreate life cycle, and the page will be refactored. For example, when Navigation is combined with BottomNavigationView, click TAB and the Fragment will be recreated. A good solution for now is to customize FragmentNavigator and replace internal replace with show/hide.

In addition, the official for the BottomNavigationView combined with the case also provides a solution. Navigationview is a BottomNavigationView extension function called NavigationExtensions.

The previous shared navigation is divided into a separate navigation for each module. For example, the project is divided into home page, project and my three tabs, and three new navigation are created accordingly: R.n avigation navi_home, R.n avigation navi_project, R.n avigation. Navi_personal, The BottomNavigationView in the Activity is bound to Navigation.

    /** * Navigation bind 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 }Copy the code

The official purpose of this is to allow each module to manage its own Fragment stack independently, so that the TAB switch will not affect each other.

2.2, 2, Paging3

Paging is a Paging component that loads data in conjunction with Recyclerview. For specific use, please refer to the “Daily questions” section of this project, as follows:

The UI layer:

class DailyQuestionFragment : BaseFragment<FragmentDailyQuestionBinding> (){... private funloadData() {
        lifecycleScope.launchWhenCreated {
            mViewModel.dailyQuestionPagingFlow().collectLatest {
                dailyPagingAdapter.submitData(it)
            }
        }
    }
...
}
Copy the code

The ViewModel layer:

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

}
Copy the code

The Repository layer

class HomeRepo(private val service: HomeService.private val db: AppDatabase) : BaseRepository(a){
    /** * Ask daily */
    fun getDailyQuestion(): Flow<PagingData<DailyQuestionData>> {

        return Pager(config) {
            DailyQuestionPagingSource(service)
        }.flow
    }
}
Copy the code

PagingSource layer:

/ * * *@date: 2021/5/20 *@author fuusy
 * @instruction: daily ask the data source, mainly with Paging3 data request and display */
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 nullLoadResult.Page(data.data? .datas!! , prevKey = preKey, nextKey = pageNum +1)}catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
Copy the code
Then, the Room

Room is a database management component. This project mainly combines Paging3 with Room. Section 2.3 mainly describes Paging3 loading data pages from the network, but this is different, combined with Room needs to RemoteMediator collaborative processing.

The main purpose of RemoteMediator is that you can use this signal to load more data from the network and store it in a local database, and PagingSource can load the data from the local database and provide it to the interface for display. The Paging library calls the load() method from the RemoteMediator implementation when more data is needed. For specific usage, please refer to the list of articles on the first page of this project.

When Room and Paging3 are combined, the UI layer and ViewModel layer are implemented in the same way as in section 2.3. The Repository layer is mainly modified.

The Repository layer:

class HomeRepo(private val service: HomeService.private val db: AppDatabase) : BaseRepository(a){
   /** * request the first page article, * Room+network to cache */
    fun getHomeArticle(articleType: Int): Flow<PagingData<ArticleData>> {
        mArticleType = articleType
        return Pager(
            config = config,
            remoteMediator = ArticleRemoteMediator(service, db, 1),
            pagingSourceFactory = pagingSourceFactory
        ).flow
    }
}

Copy the code

The 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)
    
}
Copy the code

RoomDatabase:

@Database(
    entities = [ArticleData::class.RemoteKey: :class].version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase(a){

    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 {
            returninstance ? : Room.databaseBuilder(context,AppDatabase: :class.java,
                DB_NAME
            )
                .build().also {
                    instance = it
                }
        }
    }
}
Copy the code

Customizing RemoteMediator:

/ * * *@date: 2021/5/20 *@author fuusy
 * @instructionThe main purpose of the RemoteMediator is to load more data from the network when the Pager runs out of data or the existing data fails. You can use this signal to load more data from the network and store it in a local database. PagingSource can load the data from the local database and provide it to the interface for display. * The Paging library calls the load() method from the RemoteMediator implementation when more data is needed. This is a hang feature, so it's safe to run a long time. * This feature typically extracts new data from network sources and saves it to local storage space. * This process processes new data, but data that has been stored in the database for a long time needs to be invalidated (for example, when a user manually triggers a refresh). * This is represented by the LoadType attribute passed to the load() method. LoadType informs RemoteMediator whether it needs to refresh existing data or extract more data that needs to be attached or preloaded to an existing list. * /
@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: first visit or call PagingDataAdapter. REFRESH triggered () 2. LoadType. The PREPEND: Mediatorresult. Success(endOfPaginationReached = true) 3. Loadtype.append: Mediatorresult. Success(endOfPaginationReached = true) does not access the network and database */
        try {
            Log.d(TAG, "load: $loadType")
            val pageKey: Int? = when (loadType) {
                LoadType.REFRESH -> null
                LoadType.PREPEND -> return MediatorResult.Success(true)
                LoadType.APPEND -> {
                    // Use remoteKey to get the next or previous page.val remoteKey = state.lastItemOrNull()? .id? .let { db.remoteKeyDao().remoteKeysArticleId(it, articleType) }//remoteKey' null ', which means there are no items loaded after the initial refresh and no more items to load.
                    if(remoteKey? .nextKey ==null) {
                        return MediatorResult.Success(true) } remoteKey.nextKey } } val page = pageKey ? :0
            // Request data from the networkval result = api.getHomeArticle(page).data? .datas result? .forEach { it.articleType = articleType } val endOfPaginationReached = result? .isEmpty() db.withTransaction {if (loadType == LoadType.REFRESH) {
                    // Clear the data
                    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)
        }

    }
}
Copy the code

In addition, RemoteKey and RemoteKeyDao have been created to manage the number of pages in the list. For details, see the home module of this project.

2.2.4, LiveData

For the use and principles of LiveData, please refer to Jetpack. LiveData communication principle and viscous event analysis

There are also many useful Jetpack components that will be updated in the future.

Third, thank

API: WanAndroid API provided by Hongyang Dada

Third-party open source libraries:

✔ ️ Retrofit

✔ ️ OkHttp

✔ ️ Gson

✔ ️ Coil

✔ ️ Koin

✔ ️ Arouter

✔ ️ LoadSir

In addition, there are some excellent third-party open source libraries not listed above, thanks to open source.

Fourth, the 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.

Project address: github.com/fuusy/wanan…