手把手搭建Okhttp+Retrofit+RxJava+ARouter+MVVM+组件化Android项目框架

      在现代 Android 开发中,选择合适的架构和技术栈对于项目的可维护性和扩展性至关重要。本文将介绍如何使用 OkHttp、Retrofit、RxJava、ARouter、MVVM 架构以及 组件化 来搭建一个高效、可扩展的 Android 项目框架。

1. 项目架构概述

1.1 MVVM 架构

MVVM(Model-View-ViewModel)是一种常见的 Android 架构模式,它将 UI 逻辑与业务逻辑分离,使得代码更加清晰和易于维护。MVVM 的核心思想是将视图(View)与视图模型(ViewModel)解耦,ViewModel 负责处理数据和业务逻辑,而 View 只负责展示数据。

1.2 组件化

组件化是一种将应用拆分为多个独立模块的开发方式,每个模块可以独立开发、测试和发布。组件化可以提高代码的复用性,减少模块之间的耦合,提升开发效率。

2. 技术栈选择

2.1 OkHttp

OkHttp 是一个高效的 HTTP 客户端,支持 HTTP/2 和连接池,能够减少网络请求的延迟。它是 Retrofit 的底层网络库,提供了强大的网络请求功能。

2.2 Retrofit

Retrofit 是一个类型安全的 HTTP 客户端,它将 HTTP API 转换为 Java 接口,简化了网络请求的编写。Retrofit 可以与 OkHttp 无缝集成,提供强大的网络请求功能。

2.3 RxJava

RxJava 是一个基于观察者模式的异步编程库,它提供了丰富的操作符和线程调度功能,能够简化复杂的异步操作。RxJava 可以与 Retrofit 结合使用,处理网络请求的异步回调。

2.4 LiveData

LiveData 是 Android Jetpack 中的一个组件,它是一个可观察的数据持有者类。LiveData 可以与 ViewModel 结合使用,实现数据的双向绑定,确保 UI 与数据保持同步。

2.5Arouter

Arouter是主要用于解决组件化开发中的模块间通信问题。它通过路由机制实现模块间的页面跳转、服务调用、参数传递等功能,帮助开发者更好地实现模块化、解耦和跨模块调用。

3.项目搭建

在Android Studio中创建新项目,如下图所示:

🤖 在项目根节点下创建config.gradle文件,这个文件主要是将项目的版本进行统一管理,代码如下所示:

ext {
    kotlin_version = "1.8.20"
    //android开发版本配置
    android = [
            compileSdk       : 33,
            buildToolsVersion: "28.0.0",
            applicationId    : "com.hong.mvvm",
            minSdk           : 24,
            targetSdk        : 33,
            versionCode      : 1,
            versionName      : "1.0.0",
    ]
    //version配置
    versions = [
            "junit-version"  : "4.13.2",
    ]
    //support配置
    support = [
            "constraint"              : "androidx.constraintlayout:constraintlayout:2.1.3",
            'appcompat'               : 'androidx.appcompat:appcompat:1.4.1',
            'design'                  : 'com.google.android.material:material:1.5.0',
            'junit'                   : "junit:junit:${versions["junit-version"]}",
            'ktx'                     : "androidx.core:core-ktx:1.8.0",
            'test'                    : "androidx.test.ext:junit:1.2.1",
            'espresso'                : "androidx.test.espresso:espresso-core:3.6.1",
    ]
    //依赖第三方配置
    dependencies = [
            //rxjava
            "rxjava"                               : "io.reactivex.rxjava2:rxjava:2.2.3",
            "rxandroid"                            : "io.reactivex.rxjava2:rxandroid:2.1.0",
            //rx系列与View生命周期同步
            "rxlifecycle"                          : "com.trello.rxlifecycle2:rxlifecycle:2.2.2",
            "rxlifecycle-components"               : "com.trello.rxlifecycle2:rxlifecycle-components:2.2.2",
            //rxbinding
            "rxbinding"                            : "com.jakewharton.rxbinding2:rxbinding:2.1.1",
            //rx 6.0权限请求
            "rxpermissions"                        : "com.github.tbruyelle:rxpermissions:0.10.2",
            //network
            "okhttp"                               : "com.squareup.okhttp3:okhttp:3.10.0",
            "retrofit"                             : "com.squareup.retrofit2:retrofit:2.4.0",
            "converter-gson"                       : "com.squareup.retrofit2:converter-gson:2.4.0",
            "adapter-rxjava"                       : "com.squareup.retrofit2:adapter-rxjava2:2.4.0",
            "logging-interceptor"                  : "com.squareup.okhttp3:logging-interceptor:3.9.1",

            //glide图片加载
            "glide"                                : "com.github.bumptech.glide:glide:4.8.0",
            "glide-compiler"                       : "com.github.bumptech.glide:compiler:4.8.0",
            //json解析
            "gson"                                 : "com.google.code.gson:gson:2.8.5",
            //Google AAC
            "lifecycle-extensions"                 : 'androidx.lifecycle:lifecycle-extensions:2.2.0',
            "lifecycle-compiler"                   : 'androidx.lifecycle:lifecycle-compiler:2.2.0',
            //阿里路由框架
            "arouter"                              : "com.alibaba:arouter-api:1.5.2",
            "arouter_compiler"                     : "com.alibaba:arouter-compiler:1.5.2",
            //异步消息处理
            "eventbus"                             : "org.greenrobot:eventbus:3.1.1",
            //图片选择器
            "MultiImageSelector"                   : "com.github.lovetuzitong:MultiImageSelector:1.2",
            //顶部菜单
            "flyco"                                : "com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar",
            //上拉加载,下拉刷新
            "SmartRefreshLayout"                   : "com.scwang.smartrefresh:SmartRefreshLayout:1.1.0-alpha-14",
            "mmkv"                                 : "com.tencent:mmkv:1.2.14",
            "StatusBarUtils"                       : "com.github.Ye-Miao:StatusBarUtil:1.7.5",
            "greenDao"                             : "org.greenrobot:greendao:3.3.0",
            "greenDao-generator"                   : "org.greenrobot:greendao-generator:3.3.0",
            "refresh-layout"                       : "io.github.scwang90:refresh-layout-kernel:2.1.0",
            "refresh-header"                       : "io.github.scwang90:refresh-header-classics:2.1.0",
            "loading-dialog"                       : "com.github.limxing:Android-PromptDialog:1.1.3",
    ]
}

修改settings.gradle中的代码,添加maven url,代码如下:

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
        maven { url 'https://jitpack.io' }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

创建后项目结构如下所示:

🧜 编辑项目中的gradle.properties文件,添加 isBuildModel 字段 值为 boolean类型,同时开启AndroidX,这个字段是用来控制业务模块单独运行成,还是编译成模块,如下所示:

isBuildModel = false
android.useAndroidX=true
android.enableJetifier = true

🐶 在上面新建的config.gradle文件创建后无法直接使用,需要在根目录下的build.gradle文件引入,代码如下所示:

plugins {
    id 'com.android.application' version '8.0.2' apply false
    id 'com.android.library' version '8.0.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
}
apply from: "${rootProject.projectDir}/config.gradle"

在右上角同步一下即可。

🐧 修改app目录下的build.gradle文件,将之前的版本信息、以及依赖的版本等使用config.gradle里的引用,修改前的代码:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.hong.mvvm'
    compileSdk 33

    defaultConfig {
        applicationId "com.hong.mvvm.demo"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

修改后的代码:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.hong.mvvm.demo'
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdk rootProject.ext.android.minSdk
        targetSdk rootProject.ext.android.targetSdk
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
    buildFeatures {
        viewBinding true
    }
}
dependencies {
    implementation rootProject.ext.support.ktx
    implementation rootProject.ext.support.appcompat
    implementation rootProject.ext.support.design
    implementation rootProject.ext.support.constraint
    testImplementation rootProject.ext.support.junit
    androidTestImplementation rootProject.ext.support.test
    androidTestImplementation rootProject.ext.support.espresso
}

🧙 在根目录下创建module.build.gradle文件,内容如下:

if (isBuildModel.toBoolean()) {
    //作为独立App应用运行
    apply plugin: 'com.android.application'
} else {
    //作为组件运行
    apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-kapt'
android {

    compileSdkVersion rootProject.ext.android.compileSdk
    defaultConfig {
        minSdk rootProject.ext.android.minSdk
        targetSdk rootProject.ext.android.targetSdk
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true


    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isBuildModel.toBoolean()) {
                //独立运行
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                //合并到宿主
                manifest.srcFile 'src/main/AndroidManifest.xml'
                resources {
                    //正式版本时,排除alone文件夹下所有调试文件
                    exclude 'src/main/debug/*'
                }
            }
        }
    }
    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    buildFeatures {
        viewBinding true
    }
}
kapt {
    arguments {
        arg("HOST", project.getName())
        arg("Priority", "0")
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}
dependencies {
    implementation rootProject.ext.support.ktx
    implementation rootProject.ext.support.appcompat
    implementation rootProject.ext.support.design
    implementation rootProject.ext.support.constraint
}

这个模块主要是各个子模块依赖的模板。代码先是判断是作为组件还是app运行,其他的引用config.gradle里的依赖。

app模块主要是用来打包使用,或初始化一些第三方框架,不写具体的业务逻辑,删除Android Studio生成的MainActivity,和activity_main.xml布局代码,创建App类,代码如下:

class App : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

修改AndroidManifest.xml文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
   
    <application
        android:name=".App"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Mvvm_frame"
        tools:targetApi="31">
    </application>
</manifest>

🌻 创建基础的module模块

右键项目选择Module,选择Android Library,将Module名称修改为library-api如下图所示:

这个模块主要是负责网络接口。修改library-api模块下的build.gradle,如下所示:

修改前:

plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.hong.mvvm.library.api'
    compileSdk 33

    defaultConfig {
        minSdk 24
        targetSdk 33

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'com.google.android.material:material:1.12.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

修改后:

apply from:"../module.build.gradle"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
    namespace 'com.hong.library.api'
    defaultConfig {
        if(isBuildModel.toBoolean()){
            applicationId "com.hong.library.api"
        }
    }
}

dependencies {
    api rootProject.ext.dependencies.rxjava
    api rootProject.ext.dependencies.rxandroid
    api rootProject.ext.dependencies.okhttp
    api rootProject.ext.dependencies.retrofit
    api rootProject.ext.dependencies.gson
    api rootProject.ext.dependencies["adapter-rxjava"]
    api rootProject.ext.dependencies["logging-interceptor"]
}

代码很简单,apply from:"../module.build.gradle" 导入依赖模板,然后使用里面的依赖。

在创建的library-api包下创建基础网络类,BaseResponse,内容如下:

data class BaseResponse<T>(
    val errorCode: Int,
    val errorMsg: String,
    val data: T
) {
    fun isSuccessful() = errorCode == 0
}

创建接口数据类Articles,如下所示:

data class Articles(
    val datas: ArrayList<ArticlesItem>,
    val curPage: Int,
    val offset: Int,
    val pageCount: Int,
    val size: Int,
    val total: Int
)

data class ArticlesItem(
    val adminAdd: Boolean,
    val apkLink: String,
    val audit: Int,
    val author: String,
    val canEdit: Boolean,
    val chapterId: Int,
    val chapterName: String,
    val collect: Boolean,
    val courseId: Int,
    val desc: String,
    val descMd: String,
    val envelopePic: String,
    val fresh: Boolean,
    val host: String,
    val id: Int,
    val isAdminAdd: Boolean,
    val link: String,
    val niceDate: String,
    val niceShareDate: String,
    val origin: String,
    val prefix: String,
    val projectLink: String,
    val publishTime: Long,
    val realSuperChapterId: Int,
    val selfVisible: Int,
    val shareDate: Long,
    val shareUser: String,
    val superChapterId: Int,
    val superChapterName: String,
    val tags: List<Any>,
    val title: String,
    val type: Int,
    val userId: Int,
    val visible: Int,
    val zan: Int
)

再创建TestApi类,内容如下:

interface TestApi {
    @GET("article/list/0/json")
    fun test():Observable<BaseResponse<Articles>>
}

这样library-api模块就结束了,后续有新接口,可以在这个模块中添加。

🥃 创建基础类模块library-mvvm,这个模块主要是放一些基类,如BaseMvvmActivity、BaseMvvmFragment等等。同样修改library-mvvm模块下的build.gradle, 如下所示:

修改前:

plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.hong.mvvm.library.mvvm'
    compileSdk 33

    defaultConfig {
        minSdk 24
        targetSdk 33

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'com.google.android.material:material:1.12.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

修改后:

apply from:"../module.build.gradle"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
    namespace 'com.hong..mvvm.library.mvvm'
    defaultConfig {
        if(isBuildModel.toBoolean()){
            applicationId "com.hong..mvvm.library.mvvm"
        }
    }
}

dependencies {
    implementation rootProject.ext.support.appcompat
    implementation rootProject.ext.support.design
    testImplementation rootProject.ext.support.junit
    androidTestImplementation rootProject.ext.support.test
    androidTestImplementation rootProject.ext.support.espresso

    implementation rootProject.ext.dependencies.rxlifecycle
    implementation rootProject.ext.dependencies["rxlifecycle-components"]
    implementation rootProject.ext.dependencies.rxbinding
    implementation rootProject.ext.dependencies.rxpermissions
    implementation rootProject.ext.dependencies.rxjava
    implementation rootProject.ext.dependencies.rxandroid
    implementation rootProject.ext.dependencies.okhttp
    implementation rootProject.ext.dependencies.retrofit
    implementation rootProject.ext.dependencies['converter-gson']
    implementation rootProject.ext.dependencies['adapter-rxjava']
    implementation rootProject.ext.dependencies['logging-interceptor']
    implementation rootProject.ext.dependencies.glide
    implementation rootProject.ext.dependencies.gson
    implementation rootProject.ext.dependencies['loading-dialog']

    api rootProject.ext.dependencies.arouter
    api project(':library-api')
}

同样是引入module.build.gradle和使用config.gradle里的依赖。

👀 在library-mvvm包下创建BaseRepository,这个基类的Repository,负责线程和数据的切换(移除BaseResponse的壳,只要里面的data数据),文件代码如下:


open class BaseRepository {

    private fun <T> observeAt(observable: Observable<T>): Observable<T> {
        return observable
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
    }

    private fun <T> observeFg(observable: Observable<T>): Observable<T> {
        return observable.subscribeOn(Schedulers.io())
            .unsubscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
    }

    private fun <T, R> request(
        observable: Observable<BaseResponse<T>>,
        successMapper: (T) -> R
    ): Observable<R> {
        return observeAt(observable).processApiResponse(successMapper)
    }

    protected fun <T> request(block:() -> Observable<BaseResponse<T>>): Observable<T> {
        return request(block()) { it }
    }

}

👨 创建扩展函数Observable.kt文件(负责脱壳),代码如下:

fun <T, R> Observable<BaseResponse<T>>.processApiResponse(successMapper: (T) -> R): Observable<R> {
    return flatMap { response ->
        if (response.isSuccessful()) {
            Observable.just(successMapper(response.data))
        } else {
            Observable.error(RuntimeException(response.errorMsg))
        }
    }
}

🏫 创建基类BaseViewModel,这个是ViewModel的基类,负责Repository和View的传输层,这里抽离出共有的错误信息LiveData,和repository,代码如下所示:

open class BaseViewModel<T, M : BaseRepository>(model: M) : ViewModel(),LifecycleObserver {

    protected val repository = model

    protected val _errorLiveData = MutableLiveData<String>()
    val errorLiveData: LiveData<String> = _errorLiveData

    protected val compositeDisposable = CompositeDisposable()

    override fun onCleared() {
        super.onCleared()
        compositeDisposable.clear()
    }
}

👜 创建基类BaseMvvmActivity,activity的基类只写了toast和请求的等待对话框,有两个抽象的方法,一个是logic()另一个是observable(),logic主要是写业务代码的,observable主要是监听ViewModel里面的数据变化,代码如下所示:


abstract class BaseMvvmActivity<VM:BaseViewModel<*,*>,VB: ViewBinding>:AppCompatActivity() {
    lateinit var viewModel: VM
    lateinit var binding: VB
    lateinit var fylToast: FylToast
    private val promptDialog by lazy { PromptDialog(this) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = initBinding()
        setContentView(binding.root)
        viewModel = createViewModel()
        lifecycle.addObserver(viewModel)
        logic()
        observable()
    }

    private fun initBinding(): VB {
        val bindingClass = getBindingClass()
        val method: Method = bindingClass.getMethod("inflate", LayoutInflater::class.java)
        return method.invoke(null, layoutInflater) as VB
    }

    private fun getBindingClass(): Class<VB> {
        val type = javaClass.genericSuperclass as ParameterizedType
        return type.actualTypeArguments[1] as Class<VB>
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (event?.action == MotionEvent.ACTION_DOWN && null != this.currentFocus) {
            val mInputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
            return mInputMethodManager.hideSoftInputFromWindow(this.currentFocus!!.windowToken, 0)
        }
        return super.onTouchEvent(event)
    }

    fun hideShowKeyboard() {
        val manager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
        manager.hideSoftInputFromWindow(this.currentFocus!!.windowToken, 0)
    }

    fun t(message: String?) {
        if (::fylToast.isInitialized) {
            fylToast.setMessage(message)
            fylToast.show()
            return
        }
        fylToast = FylToast.makeText(this, message, Toast.LENGTH_SHORT)
        fylToast.setGravity(Gravity.CENTER, 0, 0)
        fylToast.setMessage(message)
        fylToast.show()
    }

    fun showDialog() {
        promptDialog.showLoading("")
    }

    fun showDialog(msg:String) {
        promptDialog.showLoading(msg)
    }

    fun dismissDialog() {
        promptDialog.dismiss()
    }

    abstract fun logic()

    abstract fun observable()

    abstract fun createViewModel(): VM
}

其中FylToast类如下:

package com.hong.library.mvvm;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class FylToast {
    private static Toast mToast;
    private static FylToast fylToast;
    private static View v;
    private static TextView textView;

    private FylToast(Context context, CharSequence text, int duration) {
        v = LayoutInflater.from(context).inflate(R.layout.fyl_toast, null);
        textView = v.findViewById(R.id.textToast);
        textView.setText(text);
        mToast = new Toast(context);
        mToast.setDuration(duration);
        mToast.setView(v);
    }

    public static FylToast makeText(Context context, CharSequence text, int duration) {
        if (fylToast == null)
            fylToast = new FylToast(context, text, duration);
        else
            return fylToast;
        return fylToast;
    }

    public void show() {
        if (mToast != null) {
            mToast.show();
        }
    }

    public void setGravity(int gravity, int xOffset, int yOffset) {
        if (mToast != null) {
            mToast.setGravity(gravity, xOffset, yOffset);
        }
    }

    public void setMessage(String message) {
        textView.setText(message);
        mToast.setView(v);
    }
}

其中的资源如下,在drawable文件下创建shape_toast_bg.xml文件,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="12dp"/>
    <solid android:color="#B0000000"/>
</shape>

在layout目录下创建fyl_toast.xml文件,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/shape_toast_bg"
    android:gravity="center"
    android:paddingTop="8dp"
    android:paddingBottom="8dp"
    android:paddingLeft="25dp"
    android:paddingRight="25dp">
        <TextView
            android:id="@+id/textToast"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#ffffff"/>
</FrameLayout>

🐳 创建BaseMvvmFragment,这个基本上和activity差不多,代码如下所示:


abstract class BaseMvvmFragment<VM : BaseViewModel<*, *>, VB : ViewBinding> : Fragment() {
    protected lateinit var viewModel: VM
    protected lateinit var binding: VB
    private lateinit var fylToast: FylToast
    private val promptDialog by lazy { PromptDialog(requireActivity()) }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = initBinding(inflater, container)
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = createViewModel()
        lifecycle.addObserver(viewModel)
        logic()
    }

    open fun t(message: String?) {
        if (::fylToast.isInitialized) {
            fylToast.setMessage(message)
            fylToast.show()
            return
        }
        fylToast = FylToast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
        fylToast.setGravity(Gravity.CENTER, 0, 0)
        fylToast.setMessage(message)
        fylToast.show()
    }

    fun showDialog() {
        promptDialog.showLoading("")
    }

    fun dismissDialog() {
        promptDialog.dismiss()
    }


    abstract fun logic()

    abstract fun initBinding(inflater: LayoutInflater, container: ViewGroup?): VB

    abstract fun createViewModel(): VM
}

创建全局打印扩展类log.kt,代码如下所示:


fun Any.log(message: String) {
    if (Constant.IS_DEBUG) {
        Log.d(this::class.java.simpleName, message)
    }
}

全局常量类Constant如下所示:

object Constant {
    const val IS_DEBUG = true
}

有时候有的activity是全屏的,我们可以添加一个基类,默认实现全屏效果,创建FullActivity类,只要继承这个类默认就是全屏的,代码如下:

abstract class FullActivity<VM:BaseViewModel<*,*>,VB: ViewBinding> : BaseMvvmActivity<VM, VB>() {
    override fun onCreate(savedInstanceState: Bundle?) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val window = window
            window.decorView.systemUiVisibility =
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            window.statusBarColor = Color.TRANSPARENT
        }
        super.onCreate(savedInstanceState)
    }
}

至此,基类的代码已完成。结构如下图所示:

🐸 添加网络请求模块,创建library-okhttp Module模块主要是负责发送网络请求

同样修改该模块下的build.gradle代码,修改后如下所示:

apply from:"../module.build.gradle"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
    namespace 'com.hong.mvvm.library.okhttp'
    defaultConfig {
        if(isBuildModel.toBoolean()){
            applicationId "com.hong.mvvm.library.okhttp"
        }
    }
}

dependencies {

    api rootProject.ext.dependencies.rxjava
    api rootProject.ext.dependencies.rxandroid
    api rootProject.ext.dependencies.okhttp
    api rootProject.ext.dependencies.retrofit
    api rootProject.ext.dependencies.gson
    api rootProject.ext.dependencies["adapter-rxjava"]
    api rootProject.ext.dependencies["logging-interceptor"]
}

创建Okhttp请求类,代码如下:

object Okhttp {
    private const val WRITE_TIME_OUT = 15L
    private const val READ_TIME_OUT = 15L
    private const val CONNECT_TIME_OUT = 15L
    val okHttpClient: OkHttpClient by lazy { okhttpClient() }

    private fun okhttpClient(): OkHttpClient {
        val logging = HttpLoggingInterceptor()
        logging.level = HttpLoggingInterceptor.Level.BODY
        return OkHttpClient.Builder()
            .writeTimeout(WRITE_TIME_OUT,TimeUnit.SECONDS)
            .readTimeout(READ_TIME_OUT,TimeUnit.SECONDS)
            .connectTimeout(CONNECT_TIME_OUT,TimeUnit.SECONDS)
            .addInterceptor(logging).build()
    }
}

创建RetrofitManager,代码如下:

package com.hong.library.okhttp

import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory

object RetrofitManager {

    val instance: Retrofit by lazy { buildRetrofit() }

    private fun buildRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://www.wanandroid.com/")
            .addConverterFactory(CustomGsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(Okhttp.okHttpClient)
            .build()
    }
}

这里使用wan android的接口作为测试。

🕊️ 定义全局Api错误信息,ApiException类,代码如下:

/**
 * 服务器异常信息类
 */
public class ApiException extends RuntimeException {
    //异常码

    private int code;
    //异常信息
    private String msg;

    public ApiException() {
    }

    public ApiException(int errorCode, String errorMessage) {
        super(errorMessage);
        this.code = errorCode;
        this.msg = errorMessage;
    }

    public int getErrorCode() {
        return code;
    }

    public void setErrorCode(int errorCode) {
        this.code = errorCode;
    }

    public String getErrorMsg() {
        return msg;
    }

    public void setErrorMsg(String errorMsg) {
        this.msg = errorMsg;
    }

    @Override
    public String toString() {
        return "ApiException{" +
                "errorCode=" + code +
                ", errorMsg='" + msg + '\'' +
                '}';
    }
}

全局错误异常处理类,ExceptionHandle类,代码如下:

/**
 * 网络请求状态代码类
 */
public class ExceptionHandle {
    /**
     * 4xx(请求错误)
     * 这些状态代码表示请求可能出错,妨碍了服务器的处理。
     */
    public static final int HTTP_400 = 400;//400 (错误请求) 服务器不理解请求的语法。
    public static final int HTTP_401 = 401;//401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
    public static final int HTTP_403 = 403;//403 (禁止) 服务器拒绝请求。
    public static final int HTTP_404 = 404;//404 (未找到) 服务器找不到请求的网页。
    public static final int HTTP_405 = 405;//405 (方法禁用) 禁用请求中指定的方法。
    public static final int HTTP_406 = 406;//406 (不接受) 无法使用请求的内容特性响应请求的网页。
    public static final int HTTP_407 = 407;//407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
    public static final int HTTP_408 = 408;//408 (请求超时) 服务器等候请求时发生超时。
    public static final int HTTP_409 = 409;//409 (冲突) 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
    public static final int HTTP_410 = 410;//410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
    public static final int HTTP_411 = 411;//411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
    public static final int HTTP_412 = 412;//412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
    public static final int HTTP_413 = 413;//413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
    public static final int HTTP_414 = 414;//414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
    public static final int HTTP_415 = 415;//415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
    public static final int HTTP_416 = 416;//416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
    public static final int HTTP_417 = 417;//417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。

    /**
     * 5xx(服务器错误)
     * 这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。
     */
    public static final int HTTP_500 = 500;//500 (服务器内部错误) 服务器遇到错误,无法完成请求。
    public static final int HTTP_501 = 501;//501 (尚未实施) 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码。
    public static final int HTTP_502 = 502;//502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
    public static final int HTTP_503 = 503;//503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态。
    public static final int HTTP_504 = 504;//504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
    public static final int HTTP_505 = 505;//505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

    /**
     * 自定义 约定异常
     */
    public static final int HTTP_1000 = 1000;//未知错误
    public static final int HTTP_1001 = 1001;//解析错误
    public static final int HTTP_1002 = 1002;//网络错误
    public static final int HTTP_1003 = 1003;//请求超时
    public static final int HTTP_1005 = 1005;//证书出错

    public static ApiException HandleException(Throwable e) {
        ApiException ex = new ApiException();
        if (e instanceof HttpException) {
            HttpException httpException = (HttpException) e;
            ex.setErrorCode(httpException.code());
            switch (httpException.code()) {
                case HTTP_400:
                    ex.setErrorMsg("错误请求");
                    break;
                case HTTP_401:
                    ex.setErrorMsg("未授权");
                    break;
                case HTTP_403:
                    ex.setErrorMsg("禁止");
                    break;
                case HTTP_404:
                    ex.setErrorMsg("请求失败,未找到该请求方法");
                    break;
                case HTTP_405:
                    ex.setErrorMsg("方法禁用");
                    break;
                case HTTP_406:
                    ex.setErrorMsg("不接受");
                    break;
                case HTTP_407:
                    ex.setErrorMsg("需要代理授权");
                    break;
                case HTTP_408:
                    ex.setErrorMsg("请求超时");
                    break;
                case HTTP_409:
                    ex.setErrorMsg("冲突");
                    break;
                case HTTP_410:
                    ex.setErrorMsg("已删除");
                    break;
                case HTTP_411:
                    ex.setErrorMsg("需要有效长度");
                    break;
                case HTTP_412:
                    ex.setErrorMsg("未满足前提条件");
                    break;
                case HTTP_413:
                    ex.setErrorMsg("请求实体过大");
                    break;
                case HTTP_414:
                    ex.setErrorMsg("请求的URI过长");
                    break;
                case HTTP_415:
                    ex.setErrorMsg("不支持的媒体类型");
                    break;
                case HTTP_416:
                    ex.setErrorMsg("请求范围不符合要求");
                    break;
                case HTTP_417:
                    ex.setErrorMsg("未满足期望值");
                    break;
                case HTTP_500:
                    ex.setErrorMsg("服务器内部错误");
                    break;
                case HTTP_501:
                    ex.setErrorMsg("服务器不具备完成请求的功能");
                    break;
                case HTTP_502:
                    ex.setErrorMsg("错误网关");
                    break;
                case HTTP_503:
                    ex.setErrorMsg("服务不可用");
                    break;
                case HTTP_504:
                    ex.setErrorMsg("网关超时");
                    break;
                case HTTP_505:
                    ex.setErrorMsg("HTTP版本不受支持");
                    break;
                default:
                    ex.setErrorMsg("HttpException未知错误:" + httpException.code() + "\n" + httpException.getMessage());
                    break;
            }
            return ex;
        } else if (e instanceof ApiException) {
            Log.d("TAG", "错处被处理了");
            ex.setErrorCode(((ApiException) e).getErrorCode());
            Log.d("TAG", "出错的消息为:" + e.getMessage());
            ex.setErrorMsg(e.getMessage());
            return ex;
        } else if (e instanceof JsonParseException || e instanceof JSONException) {
            ex.setErrorCode(HTTP_1001);
            ex.setErrorMsg("解析错误");
            return ex;
        } else if (e instanceof ConnectException) {
            Log.d("TAG", "连接失败");
            ex.setErrorCode(HTTP_1002);
            ex.setErrorMsg("连接失败");
            return ex;
        } else if (e instanceof SocketTimeoutException) {
            Log.d("TAG", "请求超时");
            ex.setErrorCode(HTTP_1003);
            ex.setErrorMsg("请求超时");
            return ex;
        } else if (e instanceof javax.net.ssl.SSLHandshakeException) {
            ex.setErrorCode(HTTP_1005);
            ex.setErrorMsg("证书验证失败");
            return ex;
        } else if (e instanceof SQLException) {
            int errorCode = ((SQLException) e).getErrorCode();
            if (errorCode == 17002) {
                ex.setErrorCode(errorCode);
                ex.setErrorMsg("服务器内部错误");
            } else {
                ex.setErrorCode(errorCode);
                ex.setErrorMsg(e.getMessage());
            }
            return ex;
        } else {
            ex.setErrorCode(HTTP_1000);
            return ex;
        }
    }
}

当前http状态类,HttpStatus类,代码如下:

class HttpStatus(val success: Boolean, val errorCode: Int, val errorMsg: String) {

    fun isCodeInvalid() = errorCode == 0
}

自定义解析转化类,CustomGsonConverterFactory代码如下:

public class CustomGsonConverterFactory extends Converter.Factory {

    private final Gson gson;

    private CustomGsonConverterFactory(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        this.gson = gson;
    }

    public static CustomGsonConverterFactory create() {
        return create(new Gson());
    }

    public static CustomGsonConverterFactory create(Gson gson) {
        return new CustomGsonConverterFactory(gson);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new CustomGsonResponseBodyConverter<>(gson, adapter);
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new CustomGsonRequestBodyConverter<>(gson, adapter);
    }
}

请求转化类,CustomGsonRequestBodyConverter,代码如下:

final class CustomGsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
    private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private final Gson gson;
    private final TypeAdapter<T> adapter;

    CustomGsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public RequestBody convert(T value) throws IOException {
        Buffer buffer = new Buffer();
        Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
        JsonWriter jsonWriter = gson.newJsonWriter(writer);
        adapter.write(jsonWriter, value);
        jsonWriter.close();
        return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
    }
}

转化响应类,CustomGsonResponseBodyConverter,代码如下 :

package com.hong.library.okhttp;

import static okhttp3.internal.Util.UTF_8;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;

import okhttp3.MediaType;
import okhttp3.ResponseBody;
import retrofit2.Converter;


final class CustomGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final TypeAdapter<T> adapter;

    CustomGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        String response = value.string();
        HttpStatus httpStatus = gson.fromJson(response, HttpStatus.class);
        if (!httpStatus.isCodeInvalid()) {
            value.close();
            throw new ApiException(httpStatus.getErrorCode(), httpStatus.getErrorMsg());
        }

        MediaType contentType = value.contentType();
        Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
        InputStream inputStream = new ByteArrayInputStream(response.getBytes());
        Reader reader = new InputStreamReader(inputStream, charset);
        JsonReader jsonReader = gson.newJsonReader(reader);
        try {
            return adapter.read(jsonReader);
        } finally {
            value.close();
        }
    }
}

封装Retrofit请求,RetrofitManager类,代码如下:

object RetrofitManager {

    val instance: Retrofit by lazy { buildRetrofit() }

    private fun buildRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://www.wanandroid.com/")
            .addConverterFactory(CustomGsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(Okhttp.okHttpClient)
            .build()
    }
}

Disposable添加addTo的扩展方法类,

import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

fun Disposable.addTo(compositeDisposable: CompositeDisposable) {
    compositeDisposable.add(this)
}

完成后,模块结构如下所示:

这样只要有需要请求的网络模块就可以依赖这个

🌸 下面添加业务模块

鼠标右键项目,选择Module,选择Phone&Tablet,如图所示:

同样修改module-main模块的build.gradle,代码如下:

apply from:"../module.build.gradle"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
    namespace 'com.hong.mvvm.module.main'
    defaultConfig {
        if(isBuildModel.toBoolean()){
            applicationId "com.hong.mvvm.module.main"
        }
    }
}
dependencies {
    api project(':library-mvvm')
    kapt rootProject.ext.dependencies.arouter_compiler
}

🏒 module-main模块主要是负责引导界面、登录注册等不涉及主要的业务,可以理解为app的入口模块

在module-main的build.gradle中 使用了api而不是implementation,api会将依赖向上传递,而implementation则不会。在build.gradle中引入我们之前写的mvvm模块。com.hong.mvvm.module.main包下创建如下文件

MainRepository代码

class MainRepository:BaseRepository() {
}

MainViewModel代码:

class MainViewModel(model: MainRepository) : BaseViewModel<Any, MainRepository>(model) {
}

MainViewModelFactory代码:

class MainViewModelFactory:ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>)=
        MainViewModel(MainRepository()) as T
}

修改MainActivity代码,如下:

class MainActivity : BaseMvvmActivity<MainViewModel, ActivityMainBinding>() {

    override fun logic() {
       
    }

    override fun observable() {

    }
    
    override fun createViewModel() = ViewModelProvider(this, MainViewModelFactory())[
            MainViewModel::class.java
    ]
}

修改根目录下的app模块的build.gradle,引入module-main模块,代码如下所示:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.hong.mvvm.demo'
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdk rootProject.ext.android.minSdk
        targetSdk rootProject.ext.android.targetSdk
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
    buildFeatures {
        viewBinding true
    }
}
dependencies {

    implementation rootProject.ext.support.ktx
    implementation rootProject.ext.support.appcompat
    implementation rootProject.ext.support.design
    implementation rootProject.ext.support.constraint
    testImplementation rootProject.ext.support.junit
    androidTestImplementation rootProject.ext.support.test
    androidTestImplementation rootProject.ext.support.espresso
    implementation project(':module-main')
}

修改main模块下的AndroidManifest.xml文件,代码如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application></application>

</manifest>

修改app模块下的AnrdoidManifest.xml文件,将启动的activity修改为main模块下的MainActivity

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:name=".App"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Mvvm_frame"
        tools:targetApi="31">
        <activity
            android:name="com.hong.mvvm.module.main.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

启动app,就能看到module-main模块下MainActivity的界面了。

💄 新建三个模块,module-index,module-discovery,module-mine,来演示分模块开发,以及Arouter的使用,

右键项目,选择Phone&Tablet ,如下:

同样修改build.gradle的文件,与main模块的几乎一样,如下:

apply from:"../module.build.gradle"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
    namespace 'com.hong.mvvm.module.index'
    defaultConfig {
        if(isBuildModel.toBoolean()){
            applicationId "com.hong.mvvm.module.index"
        }
    }
}
dependencies {

    api project(':library-mvvm')
    kapt rootProject.ext.dependencies.arouter_compiler
}

创建对应的Repository、Viewmodel、和Fragment,结构如下所示

删除Android Studio生成的Activity和对应的xml文件,创建fragment_index.xml文件,添加一个简单的TextView文件,如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="首页"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改AndroidManifest.xml,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
    </application>
</manifest>

IndexRepository代码如下:

class IndexRepository:BaseRepository() {
    
    private val http by lazy { RetrofitManager.instance.create(TestApi::class.java) }
    
    fun getArticles(): Observable<Articles> = request { http.test() }
}

IndexViewModel代码如下:

class IndexViewModel(model: IndexRepository) : BaseViewModel<Any, IndexRepository>(model) {
    private val _articleLiveData = MutableLiveData<Articles>()
    val articleLiveData: LiveData<Articles> = _articleLiveData

    fun getArticles() =
        repository
            .getArticles()
            .subscribe(
            { _articleLiveData.value = it },
            { _errorLiveData.value = it.message }
        ).addTo(compositeDisposable)
}

IndexViewModelFactory代码如下:

class IndexViewModelFactory:ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>) = IndexViewModel(
        IndexRepository()
    ) as T
}

IndexFragment代码如下:

@Route(path="/index/IndexFragment")
class IndexFragment: BaseMvvmFragment<IndexViewModel, FragmentIndexBinding>() {

    override fun logic() {
        observable()
    }
    private fun observable() {
        viewModel.articleLiveData.observe(this) {
            log("articles->${it}")
        }
    }
    override fun initBinding(
        inflater: LayoutInflater,
        container: ViewGroup?
    ) = FragmentIndexBinding.inflate(inflater)

    override fun createViewModel() = ViewModelProvider(this,IndexViewModelFactory())[
            IndexViewModel::class.java
    ]
}

需要注意,IndexFragment类上有@Route的注解,这个很重要,而且path的路径必须要两级,否则编译会报错,各个模块的一级路径名不能相同,这里一级使用的是/index,其他的模块就不能使用相同的,否则编译报错。

📆 其他两个模块的代码和index模块的代码几乎一样,module-discovery,module-mine模块同样删除Android studio创建的activity和对应的布局文件,删除AndroidManifest.xml中的代码,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application></application>
</manifest>

整体的模块结构如下:

创建对应的包和结构,除了类名称不一样,其他的代码和index模块里面的代码几乎一样,

DiscoveryRepository代码如下:

class DiscoveryRepository:BaseRepository() {
}

MineRepository代码如下:

class MineRepository:BaseRepository() {
}

DiscoveryViewModel代码如下:

class DiscoveryViewModel(model: DiscoveryRepository) : BaseViewModel<Any, DiscoveryRepository>(model) {
}

MineViewModel代码如下:

class MineViewModel(model: MineRepository) : BaseViewModel<Any, MineRepository>(model) {
}

DiscoveryViewModelFactory代码如下:

class DiscoveryViewModelFactory:ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>) = DiscoveryViewModel(
        DiscoveryRepository()
    ) as T
}

MineViewModelFactory代码如下:

class MineViewModelFactory:ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>) = MineViewModel(
        MineRepository()
    ) as T
}

DiscoveryFragment代码如下:

@Route(path="/discovery/DiscoveryFragment")
class DiscoveryFragment: BaseMvvmFragment<DiscoveryViewModel, FragmentDiscoveryBinding>() {
    override fun logic() {

    }

    override fun initBinding(
        inflater: LayoutInflater,
        container: ViewGroup?
    ) = FragmentDiscoveryBinding.inflate(inflater)

    override fun createViewModel() = ViewModelProvider(this,DiscoveryViewModelFactory())[
            DiscoveryViewModel::class.java
    ]
}

MineFragment代码如下:

@Route(path="/mine/MineFragment")
class MineFragment : BaseMvvmFragment<MineViewModel, FragmentMineBinding>() {
    override fun logic() {

    }

    override fun initBinding(inflater: LayoutInflater, container: ViewGroup?) =
        FragmentMineBinding.inflate(inflater)

    override fun createViewModel() = ViewModelProvider(this, MineViewModelFactory())[
            MineViewModel::class.java
    ]
}

fragment_discovery.xml和fragment_mine.xml里面就一个TextView,显示对应模块的名称。

修改main模块的activity_main.xml布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fl_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_tab"
        style="@style/NoShadowBottomNavigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:itemBackground="@color/white"
        app:itemIconTint="@drawable/tab_bottom"
        app:itemTextColor="@drawable/tab_bottom"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/main" />
</LinearLayout>

代码很简单,就一个底部菜单栏

在res目录下创建menu目录,在menu下创建main.xml文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/device"
        android:icon="@drawable/ic_index"
        android:title="首页" />
    <item
        android:id="@+id/discovery"
        android:icon="@drawable/ic_discovery"
        android:title="发现" />

    <item
        android:id="@+id/mine"
        android:icon="@drawable/ic_mine"
        android:title="我的"/>
</menu>

 在drawable目录下添加ic_index.xml,ic_discovery.xml,ic_mine.xml,tab_bottom.xmlic_index.xml这个底部图标,内容如下:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="32dp"
    android:height="32dp"
    android:viewportWidth="1024"
    android:viewportHeight="1024">
  <path
      android:pathData="M529.2,820.4c-37.4,0 -74.8,-9.1 -104,-27.3L240.5,677.7c-10.3,-6.4 -13.4,-20 -7,-30.3s20,-13.4 30.3,-7l184.7,115.4c43.7,27.3 117.6,27.3 161.3,0l184.7,-115.4c10.3,-6.4 23.9,-3.3 30.3,7 6.4,10.3 3.3,23.9 -7,30.3L633.2,793.1c-29.2,18.2 -66.6,27.3 -104,27.3z"
      android:fillColor="#dbdbdb"/>
  <path
      android:pathData="M529.2,662.8c-37.4,0 -74.8,-9.1 -104,-27.3L240.5,520.1c-31.3,-19.6 -48.6,-46.7 -48.6,-76.4s17.3,-56.8 48.6,-76.4l184.7,-115.4c58.3,-36.4 149.7,-36.5 208,0l184.7,115.4c31.3,19.6 48.6,46.7 48.6,76.4s-17.3,56.8 -48.6,76.4L633.2,635.5c-29.2,18.2 -66.6,27.3 -104,27.3zM529.2,268.7c-29.4,0 -58.8,6.8 -80.7,20.5L263.9,404.7c-18,11.2 -27.9,25.1 -27.9,39s9.9,27.8 27.9,39l184.7,115.4c43.7,27.3 117.6,27.3 161.3,0l184.7,-115.4c18,-11.2 27.9,-25.1 27.9,-39s-9.9,-27.8 -27.9,-39L609.9,289.3c-21.9,-13.7 -51.3,-20.6 -80.7,-20.6z"
      android:fillColor="#dbdbdb"/>
</vector>

ic_discovery.xml图标代码如下:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="32dp"
    android:height="32dp"
    android:viewportWidth="1024"
    android:viewportHeight="1024">
  <path
      android:pathData="M906,292.6c47,-80.7 92.8,-187.3 43.7,-236.4 -25.2,-25.2 -94.2,-48.7 -275,67.4a430.8,430.8 0,0 0,-192.4 -45.2C243.2,78.4 48.8,272.9 48.8,512A430.8,430.8 0,0 0,96.3 708.9l-0.6,-0.4c-55.2,89.3 -110,206.7 -57.5,259.2 14.9,15 35.2,21.1 58.5,21.1 53.1,-0 121.8,-32.1 177.9,-64.8 9.4,-5.5 19,-11.4 28.7,-17.4a430.9,430.9 0,0 0,179.1 38.9c239.1,0 433.6,-194.5 433.6,-433.6 0,-62.4 -13.3,-121.6 -37.1,-175.3a1093.6,1093.6 0,0 0,27.3 -44.2zM920.8,85.2c17.9,17.9 14.2,76.4 -50.1,186.8 -4.3,7.4 -8.8,14.9 -13.5,22.4a436.8,436.8 0,0 0,-141.8 -147.8c113.9,-69.7 184.7,-82.2 205.5,-61.5zM482.3,119.4c152.2,0 284.4,87.1 349.5,214.1 -59.9,88.7 -141.6,186.1 -236,280.4 -99.6,99.6 -202.7,185.3 -295.4,245.8C175.3,794.1 89.7,662.9 89.7,512c0,-216.5 176.1,-392.6 392.6,-392.6zM253.9,888.7c-110.4,64.3 -168.9,68.1 -186.8,50.1 -17.8,-17.8 -13.7,-78.1 52.2,-190.2a436.7,436.7 0,0 0,141.5 135.9c-2.3,1.4 -4.6,2.8 -6.9,4.2zM874.9,512c0,216.5 -176.1,392.6 -392.6,392.6 -48.5,0 -94.9,-8.9 -137.8,-25 89.3,-61 186.4,-142.8 280.3,-236.7 88.7,-88.7 166.6,-180.3 226.4,-265.3A390.9,390.9 0,0 1,874.9 512z"
      android:fillColor="#dbdbdb"/>
  <path
      android:pathData="M485.1,419.6c53.1,0 96.3,-43.2 96.3,-96.3 0,-53.1 -43.2,-96.3 -96.3,-96.3s-96.3,43.2 -96.3,96.3c0,53.1 43.2,96.3 96.3,96.3zM485.1,268a55.4,55.4 0,0 1,55.4 55.4,55.4 55.4,0 0,1 -55.4,55.3 55.4,55.4 0,0 1,-55.3 -55.3,55.4 55.4,0 0,1 55.3,-55.4zM373,648.9c42.3,0 76.7,-34.4 76.7,-76.7s-34.4,-76.7 -76.7,-76.7 -76.7,34.4 -76.7,76.7 34.4,76.7 76.7,76.7zM373,536.6c19.7,0 35.7,16 35.7,35.7s-16,35.7 -35.7,35.7 -35.7,-16 -35.7,-35.7 16,-35.7 35.7,-35.7zM670.1,791.6c34.9,0 63.3,-28.4 63.3,-63.3s-28.4,-63.3 -63.3,-63.3 -63.3,28.4 -63.3,63.3 28.4,63.3 63.3,63.3zM670.1,706c12.3,0 22.3,10 22.3,22.3 0,12.3 -10,22.3 -22.3,22.3s-22.3,-10 -22.3,-22.3a22.4,22.4 0,0 1,22.3 -22.3z"
      android:fillColor="#dbdbdb"/>
</vector>

ic_mine图标代码如下:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="32dp"
    android:height="32dp"
    android:viewportWidth="1024"
    android:viewportHeight="1024">
  <path
      android:pathData="M182,909.5c-9.9,-0 -17.8,-8 -17.8,-17.9 0.2,-125.3 35.7,-224 105.4,-293.1 36.1,-35.8 80.6,-62.9 132.2,-80.5l19.9,-6.8 -17.6,-11.7c-12.2,-8.2 -23.8,-17.9 -34.2,-28.8 -37.7,-39.4 -58.4,-91.8 -58.4,-147.4 0,-55.6 20.7,-108 58.4,-147.4 37.9,-39.6 88.3,-61.5 142,-61.5 53.7,0 104.1,21.8 142,61.5 37.7,39.4 58.4,91.8 58.4,147.4 0,55.7 -20.7,108 -58.4,147.4 -35.5,37.2 -82.6,58.9 -132.7,61.3l-2.5,0.4a18.8,18.8 0,0 1,-5.1 0.7c-0.8,0 -81.2,0.5 -158.7,45C252.4,637.1 200.2,742.6 200,891.7c-0,9.8 -8,17.8 -17.9,17.8h-0.1zM512,150.1c-90.8,0 -164.7,77.7 -164.7,173.2 0,95.5 73.9,173.2 164.7,173.2 90.8,0 164.7,-77.7 164.7,-173.2 0,-95.5 -73.9,-173.2 -164.7,-173.2zM841.7,909.7c-9.9,0 -17.8,-9.3 -17.7,-19.1 1,-110.7 -34.2,-191.1 -62.1,-229.4 -29.9,-41 -67.2,-67 -67.4,-67.2 -4,-2.4 -6.4,-6.8 -7.6,-11.4 -1.2,-4.6 -0.5,-9.5 1.9,-13.6 3.2,-5.4 9.1,-8.8 15.4,-8.8 3.2,0 6.5,1.7 9.1,3.7 3.4,2.6 148,88 146.6,326.7 -0.1,9.9 -8.2,19.1 -18.1,19.1z"
      android:fillColor="#dbdbdb"/>
</vector>

tab_bottom.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_checked="false" android:color= "#595959"/>
    <item android:state_checked="true" android:color= "#2f77f8"/>
</selector>

修改MainActivity代码,如下:

class MainActivity : BaseMvvmActivity<MainViewModel, ActivityMainBinding>() {

    private val fragments by lazy { mutableListOf<Fragment>() }
    private var lastFragment = 0
    override fun logic() {
        initFragment()
        initListener()
    }

    private fun initFragment() {
        val indexFragment =
            ARouter.getInstance().build("/index/IndexFragment").navigation() as Fragment
        val discoveryFragment =
            ARouter.getInstance().build("/discovery/DiscoveryFragment").navigation() as Fragment
        val mineFragment =
            ARouter.getInstance().build("/mine/MineFragment").navigation() as Fragment
        fragments.add(indexFragment)
        fragments.add(discoveryFragment)
        fragments.add(mineFragment)
        supportFragmentManager.beginTransaction().replace(R.id.fl_content, indexFragment).commit()
    }

    private fun switchFragment(i: Int) {
        val beginTransaction = supportFragmentManager.beginTransaction()
        beginTransaction.hide(fragments[lastFragment])
        if (!fragments[i].isAdded) {
            beginTransaction.add(R.id.fl_content, fragments[i])
        }
        beginTransaction.show(fragments[i]).commitAllowingStateLoss()
        lastFragment = i
    }


    override fun observable() {

    }

    private fun initListener() {
        binding.bottomTab.setOnNavigationItemSelectedListener { item ->
            when (item.itemId) {
                R.id.index -> {
                    switchFragment(0)
                }

                R.id.discovery -> {
                    switchFragment(1)
                }

                R.id.mine -> {
                    switchFragment(2)
                }
            }
            true
        }
    }

    override fun createViewModel() = ViewModelProvider(this, MainViewModelFactory())[
            MainViewModel::class.java
    ]
}

运行app模块,结果如下图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值