# MvvmKtproject **Repository Path**: konallzong/mvvm-ktproject ## Basic Information - **Project Name**: MvvmKtproject - **Description**: kotlin mvvm jetpack 通俗易懂的敏捷开发框架 - **Primary Language**: Kotlin - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-08-22 - **Last Updated**: 2024-08-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 目录 注:最新代码中后台请求地址为个人本地java代码,如需启动请自行切换为wanandroid或其他地址自行编写业务 * [架构总体介绍](#架构总体介绍) * [Gradle配置统一管理](#gradle配置统一管理) * [基类封装](#基类封装) * [视图绑定](#视图绑定) * [网络架构搭建](#网络架构搭建) * [持久化](#持久化) * [期望和总结](#期望和总结) ### 架构总体介绍 以下是使用该框架能够学习巩固的知识: * Kotlin各种语法等 * Jetpack:主要是ViewModel、LifeCycle(生命周期感知)、LiveData(数据观察)、Room(本地数据库)、ViewBinding(视图绑定) * Kotlin协程 * Retrofit+OkHttp(网络请求) * MMKV(轻量数据存储) * 等等 ``` 项目总体的包划分: library-common:存放全局常量、公共工具类,数据Bean、自定义view、(可以由全部模块调用) library-base:存放所有业务的基础类,包括BaseActivity、BaseFragment、BaseViewModel、等功能的封装 library-network:基于Retrofit+OkHttp app:业务层 DataRepository(使用协程取代Rxjava完成业务层与网络层数据转换及调度)+具体业务实现 ``` ### Gradle配置统一管理 搭建一个项目,先从Gradle入手,把所有需要的依赖都依赖进来,为后面的工作打下基础。 对于Gradle配置统一管理这一块,笔者写了一个config.gradle脚本: 包含所有依赖库的管理和app版本sdk版本管理 ``` ext { android = [ compileSdk : 32, minSdk : 26, targetSdk : 31, versionCode: 1, versionName: "1.0.0" ] dependencies = [ //----------- Android------------- coreKtx : "androidx.core:core-ktx:1.7.0", appcompat : "androidx.appcompat:appcompat:1.4.1", material : "com.google.android.material:material:1.6.0", constraintlayout : "androidx.constraintlayout:constraintlayout:2.0.4", junit : "junit:junit:4.13.2", junitExt : "androidx.test.ext:junit:1.1.3", espresso : "androidx.test.espresso:espresso-core:3.4.0", //----------- Jetpack---------------- viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1", liveDataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1", activityKtx : "androidx.activity:activity-ktx:1.2.3", //---------------UI相关------------- swiperefreshlayout : 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0', recyclerview : 'androidx.recyclerview:recyclerview:1.2.1', // google官方的flex布局 flexbox : 'com.google.android.flexbox:flexbox:3.0.0', // BaseRecyclerViewAdapterHelper BaseRecyclerViewAdapterHelper: 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.7', // 屏幕适配https://github.com/JessYanCoding/AndroidAutoSize androidautosize : 'com.github.JessYanCoding:AndroidAutoSize:v1.2.1', xpopup : 'com.github.li-xiaojun:XPopup:2.5.18', //页面显示状态控制 loadsir : 'com.kingja.loadsir:loadsir:1.3.8', xpopup : 'com.github.li-xiaojun:XPopup:2.5.18', //---------------存储相关------------------ // 替代SharePreference mmkv : 'com.tencent:mmkv:1.2.13', roomruntime : 'androidx.room:room-runtime:2.4.3', //---------------网络相关------------------ retrofit : "com.squareup.retrofit2:retrofit:2.9.0", converterGson : 'com.squareup.retrofit2:converter-gson:2.9.0', // okhttp3 日志拦截器 loggingInterceptor : 'com.squareup.okhttp3:logging-interceptor:3.8.1', // okhttp3 缓存 PersistentCookieJar : 'com.github.franmontiel:PersistentCookieJar:v1.0.1', //---------------图片相关------------------ glide : 'com.github.bumptech.glide:glide:4.11.0', // 应用优化升级 leakcanary : 'com.squareup.leakcanary:leakcanary-android:2.9.1', //log打印 logger : 'com.orhanobut:logger:2.2.0', //内部消息传输 eventbus : 'org.greenrobot:eventbus:3.2.0' ]} ``` 其中 * dependencies是所有第三方依赖库的版本 * android是所有构建相关的版本,比如最小SDK、APP版本号等 ### 基类封装 首先参考谷歌提供的官方架构图: ``` ![推荐架构](https://upload-images.jianshu.io/upload_images/2570030-54ed5407470d4c91.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ``` 注意我们的架构搭建方式完全按照数据Google架构图完成 下面正式开始写代码,先从最常用的基类的封装入手,在这之前要想明白我们的框架是友好的让新接触的人很方便的对接业务,那么我们的基类中就需要将业务逻辑的处理全部完成,业务层只需要调用基类和接受基类数据,所有的数据处理请求都是在基类完成。 BaseActivity/BaseFragment做出如下操作: ``` abstract class BaseActivity : AppCompatActivity() { /** * ViewModel */ protected lateinit var mViewModel: VIEW_MODEL /** * ViewBinding */ protected lateinit var mViewBinding: VIEW /** * loadSir页面处理 */ private var mLoadService: LoadService<*>? = null /** * 获取一个loadSir加载的view */ protected abstract fun getLoadSirView(): View? /** * @return 程序运行的fragment名 */ protected abstract var mActivityTag: String /** * 屏幕朝向,默认竖直 */ protected var orientationPortrait = true protected val loadingProgress by lazy { LoadingProgress(this) } @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { requestedOrientation = if (orientationPortrait) { ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } else { ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE } super.onCreate(savedInstanceState) Logger.d("$mActivityTag${" ——————————————————> onCreate"}") mViewBinding = getBinding() setContentView(mViewBinding.root) initLoadSir() initViewModel() onCreateEd(savedInstanceState) dataObserver() errViewObserver() } /** * 初始化loadSir */ private fun initLoadSir() { val loadSir = LoadSir.Builder() .addCallback(ErrorCallback()) .addCallback(EmptyCallback()) .addCallback(LoadingCallback()) .addCallback(TimeoutCallback()) .build() if (getLoadSirView() != null) { mLoadService = loadSir.register( getLoadSirView() ) { mViewModel.loadByDefaultData() } } } /** * 处理正常返回成功的数据 */ abstract fun dataObserver() /** * 处理页面异常状态 */ private fun errViewObserver() { mViewModel.apply { viewStatus.observe(this@BaseActivity) { when (it) { ViewStatus.SHOW_CONTENT -> mLoadService?.showCallback(SuccessCallback::class.java) ViewStatus.EMPTY -> mLoadService?.showCallback(EmptyCallback::class.java) ViewStatus.NO_MORE_DATA -> ToastUtil.showShort( BaseApplication.sApplication.applicationContext, getString(com.example.common.R.string.no_more_data) ) ViewStatus.CONNECTION_FAILED -> mLoadService?.showCallback(TimeoutCallback::class.java) ViewStatus.REFRESH_ERROR -> mLoadService?.showCallback(ErrorCallback::class.java) ViewStatus.LOADING -> mLoadService?.showCallback(LoadingCallback::class.java) ViewStatus.LOAD_MORE_FAILED -> ToastUtil.showShort( BaseApplication.sApplication.applicationContext, errorResponse.value.toString() ) else -> {} } viewObserved() } } } /** * 页面监听完毕 */ abstract fun viewObserved() /** * ViewModel初始化 */ @Suppress("UNCHECKED_CAST") private fun initViewModel() { // 这里利用反射获取泛型中第一个参数ViewModel val type: Class = (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class mViewModel = ViewModelProvider(this).get(type) mViewModel.loadByDefaultData() } abstract fun getBinding(): VIEW abstract fun onCreateEd(savedInstanceState: Bundle?) override fun onStart() { super.onStart() Logger.d("$mActivityTag${" ——————————————————> onStart"}") } override fun onResume() { super.onResume() Logger.d("$mActivityTag${" ——————————————————> onResume"}") } override fun onPause() { super.onPause() Logger.d("$mActivityTag${" ——————————————————> onPause"}") } override fun onStop() { super.onStop() Logger.d("$mActivityTag${" ——————————————————> onStop"}") } override fun onDestroy() { super.onDestroy() Logger.d("$mActivityTag${" ——————————————————> onDestroy"}") } protected fun showLoadingProgress() { ShowCustomDialogUtil.show(this, loadingProgress) } protected fun disMissLoadingProgress() { loadingProgress.dismiss() } } ``` * 给子界面添加必要的钩子函数和子类可以定制的函数和变量 * 给子界面能直接调用ViewModel的钩子函数 * 数据处理之后将界面显示情况展现给用户(包含分页数据)使用的loadSir+liveData处理, BaseViewModel ``` abstract class BaseViewModel : ViewModel() { /** * 分页数据页数 */ var mPageNum = 0 /** * 分页数据每页数量 */ var mPageSize = 20 /** * 页面状态 */ var viewStatus = MutableLiveData() /** * 请求服务器返回错误(服务器请求成功但status错误,譬如:登录过期等) */ val errorResponse = MutableLiveData() /** * 页面加载逻辑通过loadSir处理完毕 */ val viewObserved = MutableLiveData() /** * 界面启动时要进行的初始化逻辑,如网络请求,数据初始化等 * 是否显示loadSir 中内置 loading加载框 */ abstract fun loadByDefaultData(showLoadSirLoading: Boolean = true) } ``` BaseViewModelExt: ``` fun BaseViewModel.launch(tryBlock: suspend CoroutineScope.() -> Unit, isPadding: Boolean = false) { // 默认是执行在主线程,相当于launch(Dispatchers.Main) viewModelScope.launch { try { tryBlock() } catch (e: Exception) { var errorMes = "" when (e) { is HttpException -> { when (e.code()) { UNAUTHORIZED -> { // ex.message = // BaseApplication.sApplication.getString(R.string.login_timed_out) // if (!ExceptionHandle.HTTP_404ED) { // val evenBusGoLoginData = EvenBusGoLoginData("") // EventBusUtils.post(evenBusGoLoginData) // ExceptionHandle.HTTP_404ED = true // } } else -> { errorMes = e.message().toString() } } } is ExceptionHandle.ServerException -> { errorMes = e.message.toString() } is JsonParseException, is JSONException, is ParseException -> { errorMes = PARSE } is ConnectException -> { errorMes = CONNECTION_FAILED } is SSLHandshakeException -> { errorMes = CERTIFICATE_VERIFICATION_FAILED } is InterruptedIOException -> { errorMes = CONNECTION_TIMED_OUT } is SocketTimeoutException -> { errorMes = CONNECTION_TIMED_OUT } else -> { errorMes = UNKNOWN_MISTAKE } } LiveDataUtils.postSetValue(errorResponse, errorMes) if (isPadding) { if (mPageNum == 0) { //第一页数据需要处理为加载失败 LiveDataUtils.postSetValue(viewStatus, ViewStatus.REFRESH_ERROR) } else { //不是第一页数据 LiveDataUtils.postSetValue(viewStatus, ViewStatus.LOAD_MORE_FAILED) } } } } } /** * 请求结果处理 * * @param response ApiResponse * @param successBlock 服务器请求成功返回成功码的执行回调,默认空实现 * @param errorBlock 服务器请求成功返回错误码的执行回调,默认返回false的空实现,函数返回值true:拦截统一错误处理,false:不拦截 */ suspend fun BaseViewModel.handleRequest( response: BaseResponse, successBlock: suspend CoroutineScope.(response: BaseResponse) -> Unit = {}, errorBlock: suspend CoroutineScope.(response: BaseResponse) -> Unit = { } ) { coroutineScope { when (response.baseResponseCode) { 0 -> { if (response.data is PageResponse<*>) { if ((response.data as PageResponse<*>).size == 0) { //如果当前界面主业务为分页数据 并且页数为第一页 if (mPageNum == 0) { //如果第一页数据为空,说明当前无数据 LiveDataUtils.postSetValue(viewStatus, ViewStatus.EMPTY) } else { //如果不是第一页数据说明数据数据加载完毕 LiveDataUtils.postSetValue(viewStatus, ViewStatus.NO_MORE_DATA) } } else { if (mPageNum == 5) { LiveDataUtils.postSetValue(viewStatus, ViewStatus.NO_MORE_DATA) } mPageNum++ } } LiveDataUtils.postSetValue(viewStatus, ViewStatus.SHOW_CONTENT) successBlock(response) } else -> { // 服务器返回的其他错误码 if (response.data is PageResponse<*>) { if (mPageNum == 0) { //第一页数据需要处理为加载失败 LiveDataUtils.postSetValue(viewStatus, ViewStatus.REFRESH_ERROR) } else { //不是第一页数据 LiveDataUtils.postSetValue(viewStatus, ViewStatus.LOAD_MORE_FAILED) } } LiveDataUtils.postSetValue(errorResponse, response.baseResponseReason) errorBlock(response) } } } } ``` * 给所有子viewModel添加必要函数 * 定义BaseViewModelExt作为BaseViewModel扩展函数,调用网络层并接受返回的数据 * 定义BaseViewModel的扩展函数获取所有的异常操作状态和数据加载完毕之后展现给用户的界面(包含分页数据的处理),loadSir+liveData DataRepository ``` open class BaseRepository { suspend fun apiCall(api: suspend () -> BaseResponse): BaseResponse { return withContext(Dispatchers.IO) { api.invoke() } } } ``` * 处理所有界面数据请求(内部使用协程完成线程调度) * 使用者是需要调用暴露出来的函数即可完成数据处理 在具体使用方面 笔者建议是将所有的模块(界面)都划分为一个package,例如 MainActivity只包含一个MainActivty和他的ViewModel 本项目忽略了每一个业务(界面)所对应的Model层,直接由Repository包含 下边我们来看一下具体的实现点 ### 视图绑定 提到视图绑定,我们一般会想到如下的方式: * findviewById andorid中最传统的视图绑定方式 缺点:重复繁琐,无法规避空指针 * DataBinging 实现mvvm双向绑定的工具,严格来说不属于视图绑定工具,绑定视图只有DataBinding的部分功能 * ButterKnife/kotlin-Android-Extention,视图绑定工具 从AGP 5.0开始R值的生成不再是常量,这两个工具已经废弃 * Viewbinging 视图绑定不用手写findviewById,而且避免了findviewByid可能会带来的空指针带来的问题 基于以上考虑项目决定使用ViewBinding ### 事件总线框架封装 提到事件总线,我们会想到: * EventBus * Rxjava * LiveData 那么既然上了JetPack这条贼船,我们就用LiveData来实现一个简单可用的事件 具体实现方式和代码注释查看XEventBus类有关代码 ``` XEventBus.post(EventName.REFRESH_HOME_LIST, "领现金页面通知首页刷新数据") 订阅方接收: XEventBus.observe(viewLifecycleOwner, EventName.REFRESH_HOME_LIST) { message: String -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } ``` ### 网络架构层搭建 网络层这一块,采用主流的Retrofit+Okhttp+协程来进行封装 封装调用思想还是传统的懒汉式单列Retrofit + 自定义接口类来实现,但是我们将所有的接口类的实现全部放到了DataRepository中 使用协程吊起即可,具体的用户如何调用协程,包含请求数据、返回数据和异常状态的处理全部都放在了BaseViewModel的扩展函数中。 ``` object NetWorkUtil { /** * OkHttpClient相关配置 */ private fun getClient(): OkHttpClient { val sslContext: SSLContext = SSLContextUtil.getDefaultSLLContext() val socketFactory = sslContext.socketFactory return OkHttpClient.Builder() // 请求过滤器 .addInterceptor(RequestInterceptor()) .addInterceptor(ResponseInterceptor()) //设置缓存配置,缓存最大10M,设置了缓存之后可缓存请求的数据到data/data/包名/cache/net_cache目录中 .cache( Cache( File(BaseApplication.sApplication.cacheDir, NET_CACHE), 10 * 1024 * 1024 ) ) // 请求超时时间 .connectTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS) .sslSocketFactory(socketFactory, SSLContextUtil.trustManagers as X509TrustManager) .build() } /** * 创建一个retrofit实例,并存入map * * @param service retrofit接口 * @return 返回一个retrofit实例 */ private fun getRetrofit(service: Class<*>): Retrofit? { if (retrofitHashMap[BASE_URL + service.name] != null) { return retrofitHashMap[BASE_URL + service.name] } val retrofitBuilder = Retrofit.Builder() retrofitBuilder.baseUrl(BASE_URL) retrofitBuilder.client(getClient()) retrofitBuilder.addConverterFactory(GsonConverterFactory.create()) val retrofit = retrofitBuilder.build() retrofitHashMap[BASE_URL + service.name] = retrofit return retrofit } @Volatile private var sInstance: NetWorkUtil? = null private val retrofitHashMap = HashMap() private val instance: NetWorkUtil? get() { if (sInstance == null) { synchronized(NetWorkUtil::class.java) { if (sInstance == null) { sInstance = NetWorkUtil } } } return sInstance } fun getService(service: Class?): T { return instance!!.getRetrofit(service!!)!!.create(service) } } ``` 具体使用如下 在Repository层直接调用数据请求、或者room的Dao层即可 ``` class Repository() : BaseRepository() { /** * 登录 */ suspend fun login(username: String, pwd: String): BaseResponse { return apiCall { NetWorkUtil.getService(Api::class.java).login(username, pwd) } } /** * Room存储用户信息 */ suspend fun saveUserData(user: User) { AppDatabase.userDao().insert(user) } /** * 获取文章列表数据 */ suspend fun getArticlePageList( pageNo: Int, pageSize: Int ): BaseResponse> { return apiCall { NetWorkUtil.getService(Api::class.java).getArticlePageList(pageNo, pageSize) } } } //使用方式:查看网络层介绍有提到 ``` ### 持久化本地存储数据 持久化分为两个部分MMKV和Room的简单分装实例代码参考 AppDatabase.kt ```` ```kotlin @Database(entities = [User::class], version = 1) abstract class XDatabase : RoomDatabase() { abstract fun userDao(): UserDao companion object { private val db: XDatabase by lazy { Room.databaseBuilder( XArchApplication.instance, XDatabase::class.java, "database-name" ).build() } fun userDao(): UserDao { return db.userDao() } } } @Dao interface UserDao { @Query("SELECT * FROM user") suspend fun getAll(): List ... } // 多个主键 // @Entity(tableName = "user", primaryKeys = ["first_name", "last_name"]) // 定义表名称,SQLite 中的表名称不区分大小写 @Entity(tableName = "user") data class User( // 主键分配自动ID // @PrimaryKey(autoGenerate = true) val uid: Int, @PrimaryKey var uid: Int, // 如果您希望列具有不同的名称,请将 @ColumnInfo 注释添加到字段 @ColumnInfo(name = "first_name") var firstName: String?, @ColumnInfo(name = "last_name") var lastName: String?, // 如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段添加注释 @Ignore val picture: Bitmap? ) { constructor() : this(0, "", "", null) { } } ```` MMkvUtil.kt ``` package com.example.common.utils import android.os.Parcelable import com.tencent.mmkv.MMKV /** * Date: 2022/8/4 * des: */ class MMkvUtil private constructor() { companion object { private var mInstance: MMkvUtil? = null private lateinit var mv: MMKV /** * 初始化MmKV,只需要初始化一次,建议在Application中初始化 */ @JvmStatic val instance: MMkvUtil? get() { if (mInstance == null) { synchronized(MMkvUtil::class.java) { if (mInstance == null) { mInstance = MMkvUtil() } } } return mInstance } /** * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法 * * @param key * @param object */ @JvmStatic fun encode(key: String?, `object`: Any) { when (`object`) { is String -> { mv.encode(key, `object`) } is Int -> { mv.encode(key, `object`) } is Boolean -> { mv.encode(key, `object`) } is Float -> { mv.encode(key, `object`) } is Long -> { mv.encode(key, `object`) } is Double -> { mv.encode(key, `object`) } is ByteArray -> { mv.encode(key, `object`) } else -> { mv.encode(key, `object`.toString()) } } } fun encodeSet(key: String?, sets: Set?) { mv.encode(key, sets) } /** * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值 */ fun decodeInt(key: String?): Int { return mv.decodeInt(key, 0) } fun decodeDouble(key: String?): Double { return mv.decodeDouble(key, 0.00) } fun decodeLong(key: String?): Long { return mv.decodeLong(key, 0L) } fun decodeBoolean(key: String?): Boolean { return mv.decodeBool(key, false) } fun decodeFloat(key: String?): Float { return mv.decodeFloat(key, 0f) } fun decodeBytes(key: String?): ByteArray? { return mv.decodeBytes(key) } @JvmStatic fun decodeString(key: String?): String? { return mv.decodeString(key, "") } fun decodeStringSet(key: String?): Set? { return mv.decodeStringSet(key, emptySet()) } /** * 存 * @param key key * @param obj Parcelable * @return Boolean */ @JvmStatic fun encodeParcelable(key: String?, obj: Parcelable?): Boolean { return mv.encode(key, obj) } /** * 取 * * @param key key * @param tClass class * @param class * @return Boolean */ @JvmStatic fun decodeParcelables(key: String?, tClass: Class?): T? { return mv.decodeParcelable(key, tClass) } /** * 移除某个key对 * * @param key */ fun removeKey(key: String?) { mv.removeValueForKey(key) } /** * 清除所有key */ fun clearAll() { mv.clearAll() } } init { mv = MMKV.defaultMMKV() } } //使用: MMkvUtil.encodeParcelable("AA", it) val user = MMkvUtil.decodeParcelables("AA", User::class.java) ``` ### Java实现方式 由于开发者可能存在不擅长使用Kotlin的情况存在, 如果存在这种情况使用Java的开发方式, 笔者建议在View层与ViewModel层中间新建每一个业务的JavaImpl实现类来实现具体的业务和需求, 每个业务的底层还是由Kotlin来实现,只是将开发者的调用层和数据处理层单独拿出来实现而已。 具体实现参考HomeFragment JavaImpl.Class