# android-architecture **Repository Path**: imyouyan/android-architecture ## Basic Information - **Project Name**: android-architecture - **Description**: android组件化项目,使用玩Android API 实现三个小功能,登录,注册,首页列表,每个功能写法都稍稍有些不同,重在练习代码编写结构 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-09-29 - **Last Updated**: 2024-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 架构实践 # 1.前言 老生常谈的架构实践,借鉴了很多前辈的东西,应该是没什么新东西吧,算是个人实践瞎搞 😂。 有兴趣就看看,文章共分理论与实践两部分,理论部分根据下面的层次架构图,分析每一层每一个组件的作用。 实践部分使用WanAndroidApi 搞了个组件化项目,分析组件化项目的结构并叙述每个组件存在的理由。 结合理论写了三个小功能,登录,注册,首页列表,每个功能的写法稍微有点点不同,讨论页面复杂时如何拆分代码。 # 2.理论 ## 2.1应用架构层次图 参考Google最新的架构层次图,做了一点小小的改动。下文将会每层每个组件逐个分析 ![输入图片说明](https://images.gitee.com/uploads/images/2022/0629/120423_a5a468f1_8828665.png "Untitled.png") ## 2.2 展示层 ### 2.2.1 Activity 做应用架构 核心思想就是“拆”, 在没有任何架构之前,完成一个功能所有的代码都在Activity中,随着业务越来越复杂,代码也越来越多,催生出做应用架构的需求,对Activity中大量庞杂的代码进行差分。 拆代码有一些可以拆,有些不可以拆。Activity很难复用,做Android又离不开Activity,页面加载的入口,contentApi的使用等等由于平台特性Activity必须存在。 以往Activity的任务很重,数据加载,逻辑处理,数据绑定,UI加载等等工作他都要负责。 给Activity减负,首先对Activity的定位要改变一下,不把它当作页面看,当作一个场景,功能组装者,把上述功能交给不同的组件完成。 页面是View ,ViewGroup 它们才是真正展示数据的角色。 Activity也不持有数据,数据管理交给ViewModel。 ### 2.2.2 View(可选) 以往的习惯,当前页面内所有的View初始化,数据绑定,设置监听等工作都在Activity内完成,比如:十多个组件,在onCreate() 中 findViewById。 在网络回调中设置数据。 可以使用自定义view进行优化,自定义ViewGroup,只是用来业务处理,将Activity中view的初始化,数据绑定转移到View中。对外开发必要的方法用来绑定数据 和 通信。 Activity持有ViewModel 和 自定义view的引用,负责连接两个组件通信,Activity只剩一些通信接口。 ### 2.2.3 ViewModel 关于ViewModel需要强调的一点是,它并不是MVVM架构中的VM,虽然VM很像ViewModel的缩写。可能这仅仅是我的一家之言,但是根据Google的官方文档,如果我没有看错的话,没有任何一处指明ViewModel是用来完成MVVM结构。 根据文档,它的作用应该是: 1. 不跟随Activity因配置变化而重建,替代Activity持有View使用的数据,当Activity配置变化重建时数据依然存在于内存中 2. 多个fragment间,在Activity范围内共享数据 3. 配合liveData使用 4. 配合kotlin 协程使用 5. 配合SavedStateHandle 实现Activity重建状态保存 文档提出上述几点ViewModel的使用,没有明确指定它是用来写MVVM的。 ViewModel应该将它理解Activity的数据管理者,ViewModel替Activity持有数据相关的对象,提供LiveData 或 kotlin Flow 使Activity订阅数据。 同时可以进行少量,不复杂的数据逻辑操作。如果ViewModel中代码量过多 或 存在可复用逻辑 应该将逻辑取出放到单独的类中实现。 在层次中ViewModel属于展示层,可以把它当作Activity的影子 或 分身,Activity负责UI,ViewModel负责UI需要的数据。所以ViewModel应该也该和Activity一样的待遇,尽量保持干净简单,不适合做复杂的数据逻辑。 ## 2.3 数据层 ### 2.3.1 DataSource 移动端可能出现的数据源大概有:网络,本地数据库,kv缓存,文件,内存。其中网络是最常见的情况,但是其他数据源也有可能出现。 dataSource 不包含任何业务相关的东西,单纯提供能力,提供访问网络,数据库的能力,在三方组件的基础上进行一定程度的抽象和封装。 目的在于屏蔽实现细节 和 刚简单的使用, 比如:kv缓存,可选的技术有,sharedpreferences,腾讯的MMKV,jetpack中的dataStore。 如果直接调用上述组件的api,日常使用和替换组件的时候都会非常麻烦。封装之后,内部实现可以随意替换,外部组件不受影响 ### 2.3.2 Repository 1. 作用:公开数据操作方式 1. 所有的数据方式都在repository中进行,不仅仅是访问网络。常见的一种情况是,在登录成功在Activity的成功回调中缓存用户信息,token等 2. 如果在Activity中进行缓存操作,那么就相当于Activity与DataSource直接对话。可以这样做,但是不符合规范,我相信任何有点规模的框架,在Activity与DataSource之间肯定存在中间层 3. 所以除了访问网络外,缓存,数据库等操作也要在Repository中管理 2. 作用:屏蔽数据源 1. 对于Repository的上层组件,不需要知道具体的数据源,只要调用方法即可 2. 比如:UserPository 公开方法 getToken() ,对于上层组件,不需要知道getToken()方法是从哪里获取的数据,只需要接受结果即可 3. 作用:处理一定的业务逻辑 1. 比如:某列表请求成功后,要求缓存到数据库中。缓存逻辑就要放到Repository中处理,对于上层组件来说,它们不需要知道缓存相关的内容,只要拿到返回的List。 4. 注意:不要对数据进行变换 1. 某些情况下,从网络获取的数据不能直接使用,要对数据进行二次变换 UI才可以使用。这种情况就不适合放到Repository中 2. 比如:获取课程列表,针对场景A进行一定程度的数据变换,如果直接在Repository中处理。那么方法返回的将会是变换后的数据,只有场景A适用。如果场景B可能也需要展示课程列表,使用针对场景A处理的数据就会不适用 3. 所以类似这种情况,要在Repository之外处理,Repository返回的应该是无关业务的纯粹数据 ## 2.4 网域层 ### 2.4.1 UseCase(可选) 在Repository 和 ViewModel中都提到过,不适合处理的一些情况需要拆分,这部分谁都不适合处理的逻辑就交给UseCase实现。 为什么说它是可选的呢,移动端大部分场景功能比较简单,没有复杂的逻辑,在固定层次中已经拆分的很好,如果把UseCase也当作固定层次,每个类中都代码不多,没有必要。 UseCase可以用作: 1. 承担拆分出代码,避免出现大型类 2. 数据变换,合并 3. 提取公共逻辑 ### 2.4.2 DataMapper(可选) 后端比较常见,后端一般会定义三种数据模型 1. xxxEntity 用于数据库模型,与数据库字段一一对应 2. xxxDTO 用于接口接收参数 3. xxxVO 用于接口返回数据模型 前端也可以适当引入本地模型,本地模型与设计图一一对应,进行一次隔离后,UI与本地模型的关系是非常稳定的,而且还可以屏蔽逻辑处理。 模拟场景 新闻列表场景 前端需要展示标题,服务端返回的数据是主标题,副标题,根据逻辑主副标题合起来才是前端展示的标题。 不引入本地模型 1. 服务端返回的主副标题是两个字段,一般的做法是在Activity的数据回调中,完成主副标题的拼接,设置到UI 2. 当业务变动,标题的展示逻辑修改,改动UI的代码 3. 多处UI都使用了同样的标题逻辑,那么每一处UI代码都要变化 4. 服务返回的新闻字段 可能有10个,但是新闻列表中,只需要用到3个字段,其他字段对于当前场景是无用的 5. 另一种方式,在数据实体中新增一个 getMergeTitle(),在方法内部实现主副标题的拼接,这样可以决解问题,我个人也用过这方式,还是有点不足。 1. 代码结构混乱,在Android中对数据Bean的定位,大多数情况仅仅用于承载数据,因为某个重要类的业务复杂,而在数据Bean添加逻辑,只做这件事的程序员才会知道,之前在Bean中添加了逻辑,用的时候去找找,其他人肯定是不会知道的,其他同事遇到同样的逻辑,除非他去翻代码,不然肯定会重新实现一遍 2. 系统中核心功能,比如:课程,套餐。 类似的小逻辑点很多,现有架构没有一个很好的位置去处理类似逻辑,A习惯在Repository中处理,B习惯在ViewModel中处理,C习惯在Activity处理,写着写着代码就乱了 3. 人是会忘的,时间长了自己也不知道上次是在哪里实现的,于是系统中可能又多了一段冗余代码 引入本地模型后 1. UI 与 本地模型的关系稳定,除非设计图修改 本地模型不会修改 2. 设计图中,列表元素有标题,摘要,封面图。那本地模型中的属性就只有标题,摘要封面图,UI与数据绑定非常简单,直接设置数据就好 3. 具体的业务逻辑 在网络模型 与 本地模型转换时 就处理了,UI无需知道这个过程,UI只加载标题,却不知道标题是由主副标题合成的 4. 即便是逻辑更改了,UI也不受影响 5. 整个展示层(ViewModel,Activity,View) 代码将会非常的干净,因为大部逻辑 在 网络模型与本地模型转换之前 处理掉了,展示层只绑定数据就好 6. 不仅仅是数据字段,颜色,字体大小,图片资源等等都可以放在本地模型 # 3.实践 组件化编码和单体项目是一致的,单体项目使用什么结构组件化项目也是一样的。个人感觉组件化难在项目配置,模块间通信,模块划分等问题。 ## 3.1组件化分析 ### 3.1.1 组件化结构划分 项目结构如图,接下来逐个解析 ![输入图片说明](https://images.gitee.com/uploads/images/2022/0629/120445_919aeb27_8828665.png "Untitled 1.png") ### 3.1.2 App壳工程 可以没有任何一行代码,主要也是唯一的工作是用来打包。 Demo项目中App壳工程中有三个文件: 1. `Application()` 实现类 2. AndroidManifest.xml 3. build.gradle 其中Application实现类 可以不在App壳工程中实现, AndroidManifest.xml 文件可以不写任何内容。只有build.gradle 是必须配置的 为什么这么说呢? 在多模块开发模式中,打包时有一个AndroidManifest.xml文件合并的步骤。 也很好理解,虽然开发是分开多个模块,但最终仍然是一个应用,只会有一个AndroidManifest文件,其中包含了每个模块在各自的AndroidManifest文件中声明的所有四大组件。 比如: 模块A的AndroidManifest 声明了 Activity-A-1 , Activity-A-2 模块B的AndroidManifest 声明了 Activity-B-3 , Activity-B-4 打包完成后,模块A,B的AndroidManifest 都不见了 ,应用只会存在一个AndroidManifest 文件其中包含内容: Activity-A-1 , Activity-A-2,Activity-B-3 , Activity-B-4。 **因为存在合并机制:** 1. App壳工程中可以不配置任何组件 2. `Application()` 实现类也可以不在壳工程中声明,在子模块声明也可,但是要记住只能在一个子模块中声明,多了会冲突。 为了方便 和 开发习惯 也可以在App工程配置 **build.gradle不可缺少** App壳工程只用来打包 ,在build.gradle文件中 引用需要的子模块,设置打包配置就可。 ### 3.1.3 子模块独立运行 在各种组件化文章中,都会有这样一个步骤,切换配置子模块由`com.android.library` 变为可独立运行的`com.android.application` 工程。用于开发时加载了过多没有必要的业务模块,缩减开发编译时间。 个人实践下来这个配置有点点鸡肋,项目运行至少要有两个模块:登录模块+目标业务模块,不然业务逻辑跑不通呀。 解决办法也很简单:修改app壳工程的build.gradle,只引用的需要的模块即可,同时修改AndroidManifest文件 更换一下启动页。 开关也有用 可以替换AndroidManifest,变量定义在项目根目录的 `gradle.properties` 中 ```kotlin sourceSets { main { jniLibs.srcDirs = ['libs'] if (mainRunAlone.toBoolean()) { //独立运行 manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } else { //合并到宿主 manifest.srcFile 'src/main/AndroidManifest.xml' resources { //正式版本时,排除manifest文件夹下的文件 exclude 'src/main/manifest/*' } } } } --- gradle.properties: userRunAlone=false mainRunAlone=false ``` ### 3.1.4 子模块配置 项目根目录新建`module.build.gradle` 子模块通用配置文件, ```kotlin apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.android.compileSdkVersion defaultConfig { minSdk rootProject.android.minSdkVersion targetSdk rootProject.android.targetSdkVersion versionCode rootProject.android.versionCode versionName rootProject.android.versionName 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' } viewBinding { enabled = true } } dependencies { implementation rootProject.ext.moduleDepend.common_sdk ``` 在子模块的build.gradle文件中只需要定义特殊配置即可,毕竟重要的是resourcePrefix, 设置资源前缀。 之前提到过AndroidManifest 文件会合并,其实打包时所有的资源都会合并,设置资源前缀后,模块内所有图片,颜色,布局等资源在声明的时候都会要求添加指定前缀。 防止资源重名导致打包失败 ```kotlin android { //统一资源前缀,规范资源引用,会让编译器自动提示你不规范的命名 resourcePrefix "user_" sourceSets { main { jniLibs.srcDirs = ['libs'] if (userRunAlone.toBoolean()) { //独立运行 manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } else { //合并到宿主 manifest.srcFile 'src/main/AndroidManifest.xml' resources { //正式版本时,排除manifest文件夹下的文件 exclude 'src/main/manifest/*' } } } } } dependencies { implementation rootProject.ext.moduleDepend.common_database } ``` ### 3.1.5 模块初始化 模块初始化任务,需要评估项目复杂度 酌情而定。 大部分应用没有大厂应用那么复杂,不需要设计应用启动框架 可能就几个启动组件,没有依赖关系,放在主线程,子线程都可。 按照简单情况,利用路由组件 设计通信接口IAppStartup , 只有路由组件需要 提前初始化,其他初始化任务放在路由后进行。 路由支持service 设置优先级 ,也可以简单的设置任务执行顺序 如果组件非常多,那就要考虑引入,启动任务框架了。 如果为了规避应用合规审查的问题,可以把代码挪到 提示弹窗确认后运行 ```kotlin class App : Application() { override fun onCreate() { super.onCreate() Thread { //初始化路由 DRouter.init(this) val list = DRouter.build(IAppStartup::class.java).getAllService() for (service: IAppStartup in list) { service.doWork(this) } }.start() } } ``` ### 3.1.6 数据库-common-database 数据库模块 我考虑过两种方案 1. 数据库独立模块,组件下沉,所有业务模块都可以引用 2. 每个业务模块,各自维护数据库业务,没有公共的数据库模块,业务模块需要调用其他模块的数据库业务通过路由通信即可 我个人比较倾向方案2,更符合组件化的思想。但是有两个问题: 1. 如果分开维护数据业务,一个项目中就可能存在多个数据库实例,白白浪费内存。如果把数据库实例下放到某个基础组件,那就又变成了 方案1 2. 数据Bean也不好办,比如:UserBean的逻辑在user模块中,其他模块用到UserBean的相关逻辑,但又拿不到UserBean的类引用,也很麻烦 所以想想还是数据库弄成一个公共组件比较方便。 需要注意的是要在数据库组件中额外做一次封装: 1. 数据封装,定义数据库专用的 数据类, 1. 比如:DBUser, 其他模块在存取的时候都要一次转换 2. 虽然多了一次转换,有点麻烦,但是数据隔离还是很又必要的,数据库层面变动不会对业务代码进行影响 2. 接口封装 1. 不要直接暴露三方数据库生成的 Dao类,提供一层接口封装,如果修改三方数据库,由于存在接口隔离 业务不会受影响 ### 3.1.7 资源模块-common-resources 存放启动图标,主色,项目名等公共资源。 为什么存在资源模块呢? 理由很简单,应用内的主色需要统一,不可能每一个业务模块内都维护一份颜色,启动图等资源。 那么为什么不放在sdk 或 base模块呢,从功能上看大多数sdk 或base模块 会存放一些通用的封装或工具类,供业务模块使用。 项目中不仅仅存在业务模块,还存在功能模块,比如:轮播图,播放器等等。这些功能模块也需要图片,颜色等资源。 所以单独抽取出一个模块比较恰当。 ### 3.1.8 通信模块-common-export 模块间通信,无论使用哪一个路由框架 或是 自定义路由 都会涉及到接口下沉的情况,export就是存放通信接口的。 使用路由组件跳转的时候 一般都会定义一个地址 `@Route(path = "/module1/Module1Activity")` 。可以把项目内所有用到的 路由地址 声明为常量 放在export模块中 统一管理。 模块间通信用到的数据bean 也可以放到export模块中,通过分包管理代码。 小结:export模块中存放**通信接口,路由地址,数据bean** 如果项目复杂模块间通信内容非常多,也可以把export模块进一步拆分,为每一个业务模块都设置一个对应的export模块,存放通讯代码。 比如:module_user 和 export_user,module_shop 和 export_shop ### 3.1.9 统一封装-common-sdk 一层功能封装,它的存在使得业务模块无需过多的封装,开箱即用。比如:baseActivity,baseFragment之类的,引用必要的三方组件。 ### 3.1.10 Library 功能组件 通用功能被定义为library,比如:播放器,轮播图,网络请求等等。 无关业务,即插即用,任何模块任何项目都可以直接使用。 Demo提供了`datasource` 数据源组件,内部封装了网络请求 和 key-value缓存,它们符合无关业务,即插即用的定义。具体如下: **网络请求封装** `RemoteDataSource` 基于retrofit,抽象类 ```kotlin abstract class RemoteDataSource protected constructor(iNetworkSetting: IRemoteSetting) { private val iNetworkSetting: IRemoteSetting init { this.iNetworkSetting = iNetworkSetting } companion object { private val cacheMap: MutableMap = ArrayMap() } /** *创建 网络请求接口 */ fun createService(clazz: Class): T { val retrofit: Retrofit = cacheMap[iNetworkSetting.baseUrl] ?: getRetrofitBuilder().build() cacheMap[iNetworkSetting.baseUrl] = retrofit return retrofit.create(clazz) } /** *获取 retrofit */ protected open fun getRetrofitBuilder(): Retrofit.Builder { return Retrofit.Builder() .baseUrl(iNetworkSetting.baseUrl) .client(getOkhttpClient()) .addConverterFactory(GsonConverterFactory.create()) } /** *获取 okhttp */ protected open fun getOkhttpClient(): OkHttpClient { val builder: OkHttpClient.Builder = OkHttpClient.Builder() val interceptorList: List = getInterceptorList() if (interceptorList.isNotEmpty()) { for (item in interceptorList) { builder.addInterceptor(item) } } val isDebug = iNetworkSetting.isDebug if (isDebug) { val interceptor = HttpLoggingInterceptor() interceptor.level= HttpLoggingInterceptor.Level.BODY builder.addInterceptor(interceptor) } return builder.build() } /** *获取拦截器 */ protected open fun getInterceptorList(): List { returnlistOf() } } ``` `IRemoteSetting` 网络请求配置接口(可根据需要自行扩展) ```kotlin interface IRemoteSetting { val isDebug: Boolean val baseUrl:String } ``` **sdk模块**中的具体实现 wanAndroid客户端 ```kotlin object WanAndroidRemote : RemoteDataSource(WanAndroidSetting()) { private val service: ApiService bylazy{ createService(ApiService::class.java) } //省略部分瞎封装的代码 } class WanAndroidSetting : IRemoteSetting { override val isDebug: Boolean get() = BuildConfig.DEBUG override val baseUrl: String get() = "https://www.wanandroid.com/" } ``` 抽象类`RemoteDataSource` 提供网络请求能力,一个项目中可能请求好几个服务器,继承`RemoteDataSource` 可以随意实现网络请求客户端。 Demo中只需要请求玩Android 则创建`WanAndroidRemote` ,有一天新出来个玩iOS,网络请求的基本模型不会被破坏,新创建`WaniOSRemote` 即可。 而且在`WanAndroidRemote` 中,根据业务需要可以随意二次封装。 但是需要注意`WanAndroidRemote` 属于业务 并不通用,需要存在在某个业务模块中 或 sdk模块。而不能存放在`datasource` 组件中。 **key-value缓存封装** 使用腾讯的`MMKV` 组件,并没有对外部直接暴露MMKV的方法,也是进行一次接口隔离,可以随时替换内部实现,业务组件不受影响。 ## 3.2 功能分析 ### 3.2.1 登录 逻辑: 1. 账号密码登录 2. 登录成功跳转首页 3. 登录成功把用户信息保存到数据库,账号密码存储到kv缓存,下次进入应用自动填写账号密码 实现涉及组件: 1. repository 2. viewModel 3. Activity **登录是想要表达数据逻辑编写的克制** 登录的核心逻辑有两个,保存用户信息到数据库,账号密码存储到kv缓存。都是数据相关的操作 也是很常见的需求。 经常见的写法是,在Activity的成功回调中,使用sharedpreferences 或 其他kv组件 保存账号密码。这样做很方便,符合开发和思维习惯, 。 面对这个需求自然而然的会想到:哦~ 登录后要做xxx,在Activity登录监听中 完成xxx。 这样做可以 但是不合符规范,需要克制。 原因:Activity不做逻辑操作,更何况是明晃晃的数据存储,直接引用sharedpreferences 工具类进行存储 提起理论大家都知道,但是开发的时候就怎么方便怎么来了,所以说需要克制。 数据操作应该在Reposiretory中处理,判断登录接口成功之后,进行数据库存储,KV缓存。 同时Reposiretory 提供读取 账号密码的方法供外部调用,数据与UI完全隔离。Activity中的代码非常干净,简单明了。 ```kotlin class LoginRepository : ILoginApi { companion object { private const val TAG = "LoginRepository" private const val CACHE_USERNAME = "cache_username" private const val CACHE_PASSWORD = "cache_password" } private val remoteDataSource = WanAndroidRemote private val userDao = UserDaoImpl() private val cacheDataSource = CacheDataSource override suspend fun login(userName: String, password: String): Flow> { //省略代码 returnflowWrapObject{ //网络请求 val uiResult = remoteDataSource.requestObject(parameterCollector) if (uiResult.status == UIResult.Status.Success) { val userEntity = uiResult.data //数据库存储 userEntity?.let{ val dbUser = DBUser(it.id,it.name) userDao.add(dbUser) Log.d(TAG, "保存到数据库成功") val user = userDao.getOne(it.id) Log.d(TAG, "数据库里的用户信息:${user}") } // KV 缓存 cacheDataSource.putString(CACHE_USERNAME, userName) cacheDataSource.putString(CACHE_PASSWORD, password) } return@flowWrapObject uiResult } } override fun getCacheUserName(): String { return cacheDataSource.getString(CACHE_USERNAME) } override fun getCachePassword(): String { return cacheDataSource.getString(CACHE_PASSWORD) } ``` ```kotlin class LoginActivity { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //...省略部分代码 modelLogin.onLoginResult.observerWithResult(this, this, true, onSuccess ={ toast("登录成功") DRouter.build("/main/home").start() }) mBinding.etUserName.setText(modelLogin.getCacheUserName()) mBinding.etPassword.setText(modelLogin.getCachePassword()) } } ``` ### 3.2.2 注册 逻辑: 1. 输入账号,密码,重复密码进行注册 实现涉及组件: 1. repository 2. viewModel 3. Activity 4. RegisterView 5. `user_activity_register.xml` 6. `user_sub_register_view.xml` **注册想要讨论当页面元素复杂时一种拆分方式** 登录中 Activity 是常见方式的编写,view管理,数据绑定都在Activity中进行。 注册把 Activity内所有的view全部交给自定义view `RegisterView` 管理。 `user_activity_register.xml` 只声明 `RegisterView` ``` ``` 其他子元素 如:输入框 ,按钮 等拆分到`user_sub_register_view.xml` 。使用merge标签不会增加层及。 ``` ``` 在`RegisterView` 的java 或 kotlin代码中 实现逻辑。 分成两个xml文件是因为项目中使用viewBinding的不得已。 如果不用viewBinding 正常定义xml即可,在`RegisterView` 的`onFinishInflate()`中 `**findViewById()**` 绑定子view。 ```kotlin class RegisterView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : LinearLayoutCompat(context, attrs) { private lateinit var mBinding: UserSubRegisterViewBinding var eventListener: RegisterViewEventListener? = null override fun onFinishInflate() { super.onFinishInflate() mBinding = UserSubRegisterViewBinding.inflate(LayoutInflater.from(context), this) mBinding.btnRegister.setOnClickListener{ val userName = mBinding.etUserName.text.toString().trim() val password = mBinding.etPassword.text.toString().trim() val rePassword = mBinding.etRePassword.text.toString().trim() eventListener?.onSendRegisterEvent(userName, password, rePassword) } } interface RegisterViewEventListener { fun onSendRegisterEvent(userName: String, password: String, rePassword: String) } } ``` 因为view代码从activity中拆分出去,产生了通信问题,根据需要定义通信接口。在注册情况下 只有触发登录事件时需要通信。 拆分后的Activity内部只需要持有,viewModel 和 view 对象 以及各种监听,连接UI与数据。 ```kotlin class RegisterActivity : BaseActivity(), RegisterView.RegisterViewEventListener { private lateinit var mBinding: UserActivityRegisterBinding private lateinit var registerModel: RegisterViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mBinding = UserActivityRegisterBinding.inflate(layoutInflater) setContentView(mBinding.root) mBinding.root.eventListener = this registerModel = ViewModelProvider(this)[RegisterViewModel::class.java] registerModel.onRegisterResult.observerWithResult(this, this, true, onSuccess = { toast("注册成功") finish() }) } override fun onSendRegisterEvent(userName: String, password: String, rePassword: String) { registerModel.register(userName, password, rePassword) } } ``` ### 3.2.3 首页列表 逻辑: 1. 登录后进入首页,展示分页列表 实现涉及组件: 1. repository 2. useCase 3. dataMapper 4. viewModel 5. Activity 6. `HomeArticleList` **首页列表也是当页面元素复杂时的一种拆分方式 和注册不同的是,注册是整个页面全部拆出去,首页列表是把页面再次划分为一个个小功能** Demo中的首页很简单 只有一个分页列表的功能,自定义RecyclerView `HomeArticleList` ,其内部实现: 1. recyclerView的初始化 2. 加载更多 3. 分页逻辑处理 4. 使用接口与外部通信 代码如下: ```kotlin class HomeArticleList @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : RecyclerView(context, attrs) { private var pageIndex = 0 private val mAdapter by lazy { HomeArticleAdapter() } var onEventListener: OnArticleListEventListener? = null override fun onFinishInflate() { super.onFinishInflate() adapter = mAdapter //设置 layoutManager layoutManager = LinearLayoutManager(context) //设置分割线 val itemDecoration =DividerItemDecoration(context, DividerItemDecoration.VERTICAL) itemDecoration.setDrawable(ColorDrawable(ContextCompat.getColor(context, R.color.main_color_home_article_list_divider))) addItemDecoration(itemDecoration) // 设置加载更多监听事件 mAdapter.loadMoreModule.setOnLoadMoreListener { loadData() } } override fun onAttachedToWindow() { super.onAttachedToWindow() //加载第一页数据 loadData() } private fun loadData() { pageIndex = pageIndex.plus(1) onEventListener?.onLoadMoreEvent(pageIndex) } /** * 绑定网络数据,处理分页逻辑 */ fun bindData(page: PageEntity?) { page?.run { if (pageIndex == 0) { if (list.isEmpty()) { // TODO: 展示空布局 } else { mAdapter.setNewInstance(list) } } else { mAdapter.addData(list) if (pageIndex > pageCount) { //没有更多数据了 mAdapter.loadMoreModule.loadMoreEnd() } else { //有下一页数据 mAdapter.loadMoreModule.loadMoreComplete() } } } } /** * 加载更多失败 */ fun loadMoreFail() { mAdapter.loadMoreModule.loadMoreFail() } /** * 通信接口 */ interface OnArticleListEventListener { fun onLoadMoreEvent(currPage: Int) } } ``` 分页列表逻辑拆分到自定义view后,activity中只有数据与UI通信的接口,都是胶水代码,如下: ```kotlin class HomeActivity : BaseActivity(), HomeArticleList.OnArticleListEventListener { private lateinit var mBinding: MainActivityHomeBinding private lateinit var modelHome: HomeViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mBinding = MainActivityHomeBinding.inflate(layoutInflater) setContentView(mBinding.root) mBinding.rvList.onEventListener = this modelHome = ViewModelProvider(this)[HomeViewModel::class.java] //网络请求回调 modelHome.onQueryArticleListResult.observerWithResult(this, this, onSuccess = { mBinding.rvList.bindData(page = it) }, onError = { code: Int, msg: String -> toast(msg) mBinding.rvList.loadMoreFail() }) } /** * 列表加载数据 */ override fun onLoadDataEvent(currPage: Int) { modelHome.queryArticleList(currPage) } } ``` —手动分隔线— Demo中列表UI如下:很简单 标题 + 摘要, UI 定义两个TextView实现 ![输入图片说明](https://images.gitee.com/uploads/images/2022/0629/120534_8d06251d_8828665.png "Untitled 2.png") 标题可以使用网络数据返回的title 直接展示到UI上, 但是摘要并不能直接使用,它是由**作者 和 发布时间**两个属性构成。 同时作者 还存在 :**直接作者 和 分享者** 两种情况 因为UI使用两个TextView实现,所以摘要存在 判断 和 字符串拼接的数据逻辑。 有的同学会说 我不拼接,就用多个TextView实现,这样会导致UI层级变的复杂,同时要控制TextView的展示隐藏,依然存在逻辑处理。 按照Repository - viewmodel - Activity - view 的层级 谁去处理 这个逻辑判断呢? (🐶俺一直都是 )一般情况直接在RecyclerView.Adapter 中处理了。可以,但是不合适 RecyclerView 属于UI层,逻辑不应该放在UI处理。 在有点吹毛求疵的性能说法,RecyclerView每次绑定数据的时候都要进行逻辑判断 不是浪费性能么 当数据交给RecyclerView时,应该是处理好的,RecyclerView直接展示就好。 那么谁去处理列表数据呢? 1. Repository 1. 不太合适,我对Repository 的定义,不允许它做数据变换。因为数据变换和UI是关联的,可能只适应一处UI,同样的文章列表,换个地儿就需要另一种样式了, 比如: 2. Repository 定义 queryArticleList() 获取文章列表 3. 在首页使用 直接修改 queryArticleList() 处理首页UI逻辑 4. 之后 在B页面 也需要文章列表 与 首页UI逻辑不一样。 原本的情况是 直接调用 queryArticleList() 就能满足,由于它已经针对首页进行了一次数据变换,其中的逻辑对于B页面是多余的。 5. 所以应该保持数据的纯净,不耦合UI逻辑 2. viewmodel 1. 如果UI逻辑简单,可以在ViewModel 中处理,但是逻辑复杂和有重用需求的时候 就不合适了 3. Activity 和 view 就不用说了 不能处理逻辑 4. 网络模型与UI模型的冲突 1. UI有摘要的概念 2. 网络模型中 并没有摘要的概念 3. UI展示的摘要 是有 网络模型中 多个属性 拼接而成的。 4. 因为预先处理逻辑,那么拼接后形成的 摘要 肯定要 对应一个 summary变量。 原本的网络模型中是没有summary变量的。 5. 在网络模型中所以定义UI需要的属性 也是不好的,别人接手你的代码 不去看逻辑 是不知道这个属性的作用的,时间长了 自己也会忘 经过以上的讨论,发现原有项目结构无法很好的处理现有问题,那就需要引入新成员:**usecase 和 datamapper** (mmp 铺垫了一堆 终于写出主角了) ```kotlin // usecase的核心逻辑 val mapperList =mutableListOf() for (item: ArticleEntity in list) { val author = if (item.author == "") item.shareUser else item.author val summary = "作者:${author}\t 发布时间:${item.niceShareDate}" val mapper = ArticleMapper(item.id, item.title, summary, item.link) mapperList.add(mapper) } mapperPage.list = mapperList // adapter class HomeArticleAdapter : BaseQuickAdapter(R.layout.main_item_home_article),LoadMoreModule { override fun convert(holder: BaseViewHolder, item: ArticleMapper) { holder .setText(R.id.item_tv_title, item.title) .setText(R.id.item_tv_summary, item.summary) } } ``` 看代码和理论预期的效果一直,adapter直接设置数据即可。 DataMapper 真的很好用! 真的很好用!! 网络模型 从 服务端来, 本地模型 从 UI模型来。 前端开发应该经常见 网络模型 与UI模型 有出入的情况 或者 需要维护 UI状态, 因为没有位置存放 UI状态 不得不放在网络模型中,比如:列表的选中状态。 有了DataMapper 后 UI相关的数据都可以扔到DataMapper中 ### 3.2.4 **小结** 数据逻辑 和 UI逻辑从Activity拆分后,Activity就不能当作页面看了,它并不承担实现代码,只有由于平台特性,实现一个页面离不开Activity而已。 一个Activity代表一个场景,这个场景下有N个View元素,需要对应的N中数据。Activity仅仅作为中间人 或 管理者 连接View 与 数据。 用MVC模型解释,当view 和 数据拆分出去后,Activity才是一个干净的C。 # 4.参考 [应用架构指南  |  Android 开发者  |  Android Developers (google.cn)](https://developer.android.google.cn/jetpack/guide?hl=zh-cn#ui-layer) [被误解的 MVC 和被神化的 MVVM - 掘金 (juejin.cn)](https://juejin.cn/post/6844903423846907918) [关于Android架构,你是否还在生搬硬套? - 掘金 (juejin.cn)](https://juejin.cn/post/6942464122273398820) [神奇宝贝 眼前一亮的 Jetpack + MVVM 极简实战 - 掘金 (juejin.cn)](https://juejin.cn/post/6850037271253483534?utm_source=gold_browser_extension) [我从 Android 官方 App 中学到了什么? - 掘金 (juejin.cn)](https://juejin.cn/post/7099245635269820453) [【Jetpack篇】协程+Retrofit网络请求状态封装实战 - 掘金 (juejin.cn)](https://juejin.cn/post/6958821338672955423) [玩Android的各种版本,包括单体版(kotlin+协程+jetpack+MVVM)、组件化版、Compose版...-玩Android - wanandroid.com](https://www.wanandroid.com/blog/show/3369)