在现代 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模块,结果如下图所示: