|
|
@@ -0,0 +1,934 @@
|
|
|
+# 网络请求 MVVM 架构封装方案(优化版)
|
|
|
+
|
|
|
+## 一、设计目标
|
|
|
+
|
|
|
+将现有的 `ApiResponseParser` 和 `ApiServiceFactory` 封装成符合 MVVM 架构的完整 Data 层组件,统一处理公共逻辑,业务层只需简单调用。
|
|
|
+
|
|
|
+## 二、架构设计
|
|
|
+
|
|
|
+### 2.1 现有组件
|
|
|
+
|
|
|
+- ✅ `ApiResponseParser` - 响应解析器(已存在)
|
|
|
+- ✅ `ApiServiceFactory` - API 服务工厂(已存在)
|
|
|
+
|
|
|
+### 2.2 新增组件
|
|
|
+
|
|
|
+- **BaseRemoteDataSource** - 基础远程数据源抽象类(增强版)
|
|
|
+ - 封装通用网络请求逻辑
|
|
|
+ - 统一错误处理、异常捕获、日志记录
|
|
|
+ - 使用 reified 泛型简化调用,无需传入 dataClass
|
|
|
+ - 自动切换到 IO 线程
|
|
|
+
|
|
|
+- **BaseRepository** - 基础仓库类(可选)
|
|
|
+ - 封装通用仓库逻辑
|
|
|
+ - 统一数据转换和缓存处理
|
|
|
+
|
|
|
+## 三、实现方案
|
|
|
+
|
|
|
+### 3.1 BaseRemoteDataSource 抽象类(增强版)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.common.network
|
|
|
+
|
|
|
+import android.util.Log
|
|
|
+import retrofit2.Response
|
|
|
+import kotlinx.coroutines.Dispatchers
|
|
|
+import kotlinx.coroutines.withContext
|
|
|
+import java.net.UnknownHostException
|
|
|
+import java.net.SocketTimeoutException
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基础远程数据源
|
|
|
+ *
|
|
|
+ * 封装通用网络请求逻辑,统一处理:
|
|
|
+ * - 错误处理和异常捕获
|
|
|
+ * - 日志记录
|
|
|
+ * - 线程切换(自动切换到 IO 线程)
|
|
|
+ * - 网络异常友好提示
|
|
|
+ *
|
|
|
+ * 使用方式:
|
|
|
+ * ```kotlin
|
|
|
+ * class AuthRemoteDataSource : BaseRemoteDataSource() {
|
|
|
+ * private val authApi: AuthApi = ApiServiceFactory.create()
|
|
|
+ *
|
|
|
+ * suspend fun login(request: LoginRequest): Result<LoginResponse> {
|
|
|
+ * return executeRequest(
|
|
|
+ * request = { authApi.login(request) },
|
|
|
+ * errorMessage = "登录失败"
|
|
|
+ * )
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+abstract class BaseRemoteDataSource {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取日志标签(子类可重写)
|
|
|
+ */
|
|
|
+ protected open val tag: String
|
|
|
+ get() = this::class.simpleName ?: "BaseRemoteDataSource"
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理网络请求(通用方法)
|
|
|
+ *
|
|
|
+ * 自动处理:
|
|
|
+ * - HTTP 错误码
|
|
|
+ * - 业务错误码
|
|
|
+ * - 网络异常
|
|
|
+ * - 超时异常
|
|
|
+ * - 日志记录
|
|
|
+ *
|
|
|
+ * @param request 网络请求的 suspend 函数
|
|
|
+ * @param errorMessage 错误消息前缀
|
|
|
+ * @return Result<T>
|
|
|
+ */
|
|
|
+ protected suspend inline fun <reified T> executeRequest(
|
|
|
+ crossinline request: suspend () -> Response<CommonResult<T>>,
|
|
|
+ errorMessage: String = "请求失败"
|
|
|
+ ): Result<T> = withContext(Dispatchers.IO) {
|
|
|
+ try {
|
|
|
+ Log.d(tag, "开始请求: ${T::class.simpleName}")
|
|
|
+ val response = request()
|
|
|
+
|
|
|
+ // 使用 reified 泛型,自动获取 Class
|
|
|
+ ApiResponseParser.handleResponse(
|
|
|
+ response = response,
|
|
|
+ dataClass = T::class.java,
|
|
|
+ tag = tag,
|
|
|
+ errorMessage = errorMessage
|
|
|
+ )
|
|
|
+ } catch (e: SocketTimeoutException) {
|
|
|
+ val errorMsg = "请求超时,请检查网络连接"
|
|
|
+ Log.e(tag, errorMsg, e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ } catch (e: UnknownHostException) {
|
|
|
+ val errorMsg = "网络连接失败,请检查网络设置"
|
|
|
+ Log.e(tag, errorMsg, e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ } catch (e: Exception) {
|
|
|
+ val errorMsg = e.message ?: errorMessage
|
|
|
+ Log.e(tag, "请求异常: $errorMsg", e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理可空数据的网络请求
|
|
|
+ *
|
|
|
+ * 用于处理服务器可能返回 null 的情况
|
|
|
+ *
|
|
|
+ * @param request 网络请求的 suspend 函数
|
|
|
+ * @param errorMessage 错误消息前缀
|
|
|
+ * @return Result<T?>
|
|
|
+ */
|
|
|
+ protected suspend inline fun <reified T> executeNullableRequest(
|
|
|
+ crossinline request: suspend () -> Response<CommonResult<T>>,
|
|
|
+ errorMessage: String = "请求失败"
|
|
|
+ ): Result<T?> = withContext(Dispatchers.IO) {
|
|
|
+ try {
|
|
|
+ Log.d(tag, "开始请求(可空): ${T::class.simpleName}")
|
|
|
+ val response = request()
|
|
|
+
|
|
|
+ ApiResponseParser.handleNullableResponse(
|
|
|
+ response = response,
|
|
|
+ tag = tag,
|
|
|
+ errorMessage = errorMessage
|
|
|
+ )
|
|
|
+ } catch (e: SocketTimeoutException) {
|
|
|
+ val errorMsg = "请求超时,请检查网络连接"
|
|
|
+ Log.e(tag, errorMsg, e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ } catch (e: UnknownHostException) {
|
|
|
+ val errorMsg = "网络连接失败,请检查网络设置"
|
|
|
+ Log.e(tag, errorMsg, e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ } catch (e: Exception) {
|
|
|
+ val errorMsg = e.message ?: errorMessage
|
|
|
+ Log.e(tag, "请求异常: $errorMsg", e)
|
|
|
+ Result.failure(Exception(errorMsg))
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 BaseRepository 基础仓库类(可选)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.common.network
|
|
|
+
|
|
|
+import android.util.Log
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基础仓库类
|
|
|
+ *
|
|
|
+ * 封装通用仓库逻辑:
|
|
|
+ * - 统一错误处理
|
|
|
+ * - 数据转换(Data Model → Domain Model)
|
|
|
+ * - 缓存策略(可选)
|
|
|
+ *
|
|
|
+ * 使用方式:
|
|
|
+ * ```kotlin
|
|
|
+ * class AuthRepository(
|
|
|
+ * private val remoteDataSource: AuthRemoteDataSource
|
|
|
+ * ) : BaseRepository() {
|
|
|
+ *
|
|
|
+ * suspend fun login(mobile: String, password: String): Result<LoginResponse> {
|
|
|
+ * val request = LoginRequest(mobile, password)
|
|
|
+ * return remoteDataSource.login(request)
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+open class BaseRepository {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取日志标签(子类可重写)
|
|
|
+ */
|
|
|
+ protected open val tag: String
|
|
|
+ get() = this::class.simpleName ?: "BaseRepository"
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理数据转换(Data Model → Domain Model)
|
|
|
+ *
|
|
|
+ * 子类可重写此方法实现自定义转换逻辑
|
|
|
+ */
|
|
|
+ protected open fun <T> transform(data: T): T {
|
|
|
+ return data
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一错误处理
|
|
|
+ *
|
|
|
+ * 子类可重写此方法实现自定义错误处理
|
|
|
+ */
|
|
|
+ protected open fun handleError(throwable: Throwable): Result<Nothing> {
|
|
|
+ val errorMsg = throwable.message ?: "操作失败"
|
|
|
+ Log.e(tag, "操作失败: $errorMsg", throwable)
|
|
|
+ return Result.failure(throwable)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 使用示例
|
|
|
+
|
|
|
+#### 示例 1:AuthRemoteDataSource(优化后)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.data.remote
|
|
|
+
|
|
|
+import com.narutohuo.xindazhou.common.network.BaseRemoteDataSource
|
|
|
+import com.narutohuo.xindazhou.common.network.ApiServiceFactory
|
|
|
+import com.narutohuo.xindazhou.user.data.model.LoginRequest
|
|
|
+import com.narutohuo.xindazhou.user.data.model.LoginResponse
|
|
|
+import com.narutohuo.xindazhou.user.data.api.AuthApi
|
|
|
+
|
|
|
+/**
|
|
|
+ * 认证远程数据源
|
|
|
+ *
|
|
|
+ * 继承 BaseRemoteDataSource,自动获得:
|
|
|
+ * - 统一的错误处理
|
|
|
+ * - 自动日志记录
|
|
|
+ * - 网络异常友好提示
|
|
|
+ * - 线程自动切换
|
|
|
+ */
|
|
|
+class AuthRemoteDataSource : BaseRemoteDataSource() {
|
|
|
+
|
|
|
+ private val authApi: AuthApi = ApiServiceFactory.create()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 登录
|
|
|
+ *
|
|
|
+ * 无需传入 dataClass,使用 reified 泛型自动推断
|
|
|
+ */
|
|
|
+ suspend fun login(request: LoginRequest): Result<LoginResponse> {
|
|
|
+ return executeRequest(
|
|
|
+ request = { authApi.login(request) },
|
|
|
+ errorMessage = "登录失败"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册
|
|
|
+ */
|
|
|
+ suspend fun register(request: RegisterRequest): Result<RegisterResponse> {
|
|
|
+ return executeRequest(
|
|
|
+ request = { authApi.register(request) },
|
|
|
+ errorMessage = "注册失败"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新Token(可空数据)
|
|
|
+ */
|
|
|
+ suspend fun refreshToken(refreshToken: String): Result<TokenResponse?> {
|
|
|
+ return executeNullableRequest(
|
|
|
+ request = { authApi.refreshToken(refreshToken) },
|
|
|
+ errorMessage = "刷新Token失败"
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 示例 2:UserRemoteDataSource(优化后)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.data.remote
|
|
|
+
|
|
|
+import com.narutohuo.xindazhou.common.network.BaseRemoteDataSource
|
|
|
+import com.narutohuo.xindazhou.common.network.ApiServiceFactory
|
|
|
+import com.narutohuo.xindazhou.user.data.model.UserInfo
|
|
|
+import com.narutohuo.xindazhou.user.data.api.UserApi
|
|
|
+
|
|
|
+/**
|
|
|
+ * 用户远程数据源
|
|
|
+ */
|
|
|
+class UserRemoteDataSource : BaseRemoteDataSource() {
|
|
|
+
|
|
|
+ // 可以自定义日志标签
|
|
|
+ override val tag: String = "UserRemoteDataSource"
|
|
|
+
|
|
|
+ private val userApi: UserApi = ApiServiceFactory.create()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取用户信息
|
|
|
+ */
|
|
|
+ suspend fun getUserInfo(userId: String): Result<UserInfo> {
|
|
|
+ return executeRequest(
|
|
|
+ request = { userApi.getUserInfo(userId) },
|
|
|
+ errorMessage = "获取用户信息失败"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新用户信息(可空数据)
|
|
|
+ */
|
|
|
+ suspend fun updateUserInfo(request: UpdateUserRequest): Result<UserInfo?> {
|
|
|
+ return executeNullableRequest(
|
|
|
+ request = { userApi.updateUserInfo(request) },
|
|
|
+ errorMessage = "更新用户信息失败"
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 示例 3:AuthRepository(使用 BaseRepository)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.data.repository
|
|
|
+
|
|
|
+import com.narutohuo.xindazhou.common.network.BaseRepository
|
|
|
+import com.narutohuo.xindazhou.user.data.remote.AuthRemoteDataSource
|
|
|
+import com.narutohuo.xindazhou.user.data.model.LoginRequest
|
|
|
+import com.narutohuo.xindazhou.user.data.model.LoginResponse
|
|
|
+
|
|
|
+/**
|
|
|
+ * 认证仓库
|
|
|
+ *
|
|
|
+ * 继承 BaseRepository,自动获得:
|
|
|
+ * - 统一的错误处理
|
|
|
+ * - 日志记录
|
|
|
+ * - 数据转换扩展点
|
|
|
+ */
|
|
|
+class AuthRepository(
|
|
|
+ private val remoteDataSource: AuthRemoteDataSource
|
|
|
+) : BaseRepository() {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 登录
|
|
|
+ */
|
|
|
+ suspend fun login(mobile: String, password: String): Result<LoginResponse> {
|
|
|
+ val request = LoginRequest(mobile, password)
|
|
|
+ return remoteDataSource.login(request)
|
|
|
+ .onFailure { throwable ->
|
|
|
+ // 可以在这里添加额外的错误处理逻辑
|
|
|
+ // 例如:记录日志、发送错误上报等
|
|
|
+ handleError(throwable)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册
|
|
|
+ */
|
|
|
+ suspend fun register(mobile: String, password: String, verifyCode: String): Result<RegisterResponse> {
|
|
|
+ val request = RegisterRequest(mobile, password, verifyCode)
|
|
|
+ return remoteDataSource.register(request)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 ViewModel 层使用(保持不变)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.ui.viewmodel
|
|
|
+
|
|
|
+import androidx.lifecycle.AndroidViewModel
|
|
|
+import androidx.lifecycle.viewModelScope
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import com.narutohuo.xindazhou.user.data.repository.AuthRepository
|
|
|
+
|
|
|
+class LoginViewModel(
|
|
|
+ application: Application,
|
|
|
+ private val authRepository: AuthRepository
|
|
|
+) : AndroidViewModel(application) {
|
|
|
+
|
|
|
+ private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
|
|
|
+ val loginState: StateFlow<LoginState> = _loginState
|
|
|
+
|
|
|
+ fun login(mobile: String, password: String) {
|
|
|
+ viewModelScope.launch {
|
|
|
+ _loginState.value = LoginState.Loading
|
|
|
+
|
|
|
+ authRepository.login(mobile, password)
|
|
|
+ .onSuccess {
|
|
|
+ _loginState.value = LoginState.Success("登录成功")
|
|
|
+ }
|
|
|
+ .onFailure { e ->
|
|
|
+ _loginState.value = LoginState.Error(e.message ?: "登录失败")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 四、目录结构
|
|
|
+
|
|
|
+```
|
|
|
+base-common/src/main/java/com/narutohuo/xindazhou/common/
|
|
|
+├── network/
|
|
|
+│ ├── ApiResponseParser.kt # 响应解析器(已存在)
|
|
|
+│ ├── ApiServiceFactory.kt # API服务工厂(已存在)
|
|
|
+│ ├── CommonResult.kt # 通用响应模型(已存在)
|
|
|
+│ ├── BaseRemoteDataSource.kt # 基础远程数据源(新增,增强版)
|
|
|
+│ └── BaseRepository.kt # 基础仓库类(新增,可选)
|
|
|
+│
|
|
|
+└── ui/
|
|
|
+ ├── BaseActivity.kt # 基础Activity(新增)
|
|
|
+ └── BaseFragment.kt # 基础Fragment(新增)
|
|
|
+
|
|
|
+示例模块:
|
|
|
+user/
|
|
|
+├── data/
|
|
|
+│ ├── remote/
|
|
|
+│ │ └── AuthRemoteDataSource.kt # 认证远程数据源(继承BaseRemoteDataSource)
|
|
|
+│ └── repository/
|
|
|
+│ └── AuthRepository.kt # 认证仓库(继承BaseRepository,可选)
|
|
|
+│
|
|
|
+└── ui/
|
|
|
+ ├── login/
|
|
|
+ │ ├── LoginActivity.kt # 登录Activity(继承BaseActivity)
|
|
|
+ │ └── LoginFragment.kt # 登录Fragment(继承BaseFragment)
|
|
|
+ └── viewmodel/
|
|
|
+ └── LoginViewModel.kt # 登录ViewModel
|
|
|
+```
|
|
|
+
|
|
|
+## 五、优化亮点
|
|
|
+
|
|
|
+### 5.1 使用 reified 泛型,简化调用
|
|
|
+- ❌ **之前**:需要手动传入 `dataClass = LoginResponse::class.java`
|
|
|
+- ✅ **现在**:自动推断类型,无需传入 `dataClass`
|
|
|
+
|
|
|
+### 5.2 统一公共处理
|
|
|
+- ✅ **自动线程切换**:所有请求自动切换到 IO 线程
|
|
|
+- ✅ **统一错误处理**:网络异常、超时等自动处理并给出友好提示
|
|
|
+- ✅ **自动日志记录**:请求开始、成功、失败自动记录日志
|
|
|
+- ✅ **自定义日志标签**:子类可重写 `tag` 属性自定义日志标签
|
|
|
+
|
|
|
+### 5.3 网络异常友好提示
|
|
|
+- `SocketTimeoutException` → "请求超时,请检查网络连接"
|
|
|
+- `UnknownHostException` → "网络连接失败,请检查网络设置"
|
|
|
+- 其他异常 → 显示具体错误信息
|
|
|
+
|
|
|
+### 5.4 代码对比
|
|
|
+
|
|
|
+**优化前:**
|
|
|
+```kotlin
|
|
|
+suspend fun login(request: LoginRequest): Result<LoginResponse> {
|
|
|
+ return try {
|
|
|
+ val response = authApi.login(request)
|
|
|
+ ApiResponseParser.handleResponse(
|
|
|
+ response,
|
|
|
+ LoginResponse::class.java,
|
|
|
+ "登录失败"
|
|
|
+ )
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Result.failure(e)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**优化后:**
|
|
|
+```kotlin
|
|
|
+suspend fun login(request: LoginRequest): Result<LoginResponse> {
|
|
|
+ return executeRequest(
|
|
|
+ request = { authApi.login(request) },
|
|
|
+ errorMessage = "登录失败"
|
|
|
+ )
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 六、优势
|
|
|
+
|
|
|
+✅ **代码简化**:统一的网络请求处理,减少重复代码 70%+
|
|
|
+✅ **错误统一**:统一的错误处理逻辑,友好的错误提示
|
|
|
+✅ **易于测试**:DataSource 和 Repository 可以轻松 Mock
|
|
|
+✅ **符合架构**:完全符合 MVVM 四层架构设计
|
|
|
+✅ **自动处理**:线程切换、日志记录、异常处理全自动
|
|
|
+✅ **类型安全**:使用 reified 泛型,编译时类型检查
|
|
|
+
|
|
|
+## 七、业务层基类设计
|
|
|
+
|
|
|
+### 7.1 BaseActivity 基类
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.common.ui
|
|
|
+
|
|
|
+import android.os.Bundle
|
|
|
+import android.view.View
|
|
|
+import androidx.appcompat.app.AppCompatActivity
|
|
|
+import androidx.lifecycle.lifecycleScope
|
|
|
+import androidx.viewbinding.ViewBinding
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import android.widget.Toast
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基础 Activity
|
|
|
+ *
|
|
|
+ * 封装通用功能:
|
|
|
+ * - ViewBinding 支持
|
|
|
+ * - 统一的加载状态管理
|
|
|
+ * - 统一的错误提示
|
|
|
+ * - 生命周期管理
|
|
|
+ *
|
|
|
+ * 使用方式:
|
|
|
+ * ```kotlin
|
|
|
+ * class LoginActivity : BaseActivity<ActivityLoginBinding>() {
|
|
|
+ * override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
+ * super.onCreate(savedInstanceState)
|
|
|
+ * // 你的代码
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
|
|
+
|
|
|
+ protected lateinit var binding: VB
|
|
|
+ protected abstract fun getViewBinding(): VB
|
|
|
+
|
|
|
+ // 加载状态(子类可重写自定义实现)
|
|
|
+ protected open fun showLoading() {
|
|
|
+ // 默认实现:显示加载提示
|
|
|
+ // 子类可重写自定义加载UI
|
|
|
+ }
|
|
|
+
|
|
|
+ protected open fun hideLoading() {
|
|
|
+ // 默认实现:隐藏加载提示
|
|
|
+ // 子类可重写自定义加载UI
|
|
|
+ }
|
|
|
+
|
|
|
+ // 错误提示(子类可重写自定义实现)
|
|
|
+ protected open fun showError(message: String) {
|
|
|
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+
|
|
|
+ protected open fun showSuccess(message: String) {
|
|
|
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
+ super.onCreate(savedInstanceState)
|
|
|
+ binding = getViewBinding()
|
|
|
+ setContentView(binding.root)
|
|
|
+ initView()
|
|
|
+ initObserver()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化视图(子类实现)
|
|
|
+ */
|
|
|
+ protected open fun initView() {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化观察者(子类实现)
|
|
|
+ */
|
|
|
+ protected open fun initObserver() {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一执行网络请求
|
|
|
+ *
|
|
|
+ * 自动处理加载状态和错误提示
|
|
|
+ */
|
|
|
+ protected fun <T> executeRequest(
|
|
|
+ request: suspend () -> Result<T>,
|
|
|
+ onSuccess: (T) -> Unit,
|
|
|
+ onError: ((String) -> Unit)? = null,
|
|
|
+ showLoading: Boolean = true
|
|
|
+ ) {
|
|
|
+ lifecycleScope.launch {
|
|
|
+ try {
|
|
|
+ if (showLoading) showLoading()
|
|
|
+
|
|
|
+ request()
|
|
|
+ .onSuccess { data ->
|
|
|
+ hideLoading()
|
|
|
+ onSuccess(data)
|
|
|
+ }
|
|
|
+ .onFailure { throwable ->
|
|
|
+ hideLoading()
|
|
|
+ val errorMsg = throwable.message ?: "操作失败"
|
|
|
+ if (onError != null) {
|
|
|
+ onError(errorMsg)
|
|
|
+ } else {
|
|
|
+ showError(errorMsg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ hideLoading()
|
|
|
+ val errorMsg = e.message ?: "操作失败"
|
|
|
+ if (onError != null) {
|
|
|
+ onError(errorMsg)
|
|
|
+ } else {
|
|
|
+ showError(errorMsg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 BaseFragment 基类
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.common.ui
|
|
|
+
|
|
|
+import android.os.Bundle
|
|
|
+import android.view.LayoutInflater
|
|
|
+import android.view.View
|
|
|
+import android.view.ViewGroup
|
|
|
+import android.widget.Toast
|
|
|
+import androidx.fragment.app.Fragment
|
|
|
+import androidx.lifecycle.lifecycleScope
|
|
|
+import androidx.viewbinding.ViewBinding
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基础 Fragment
|
|
|
+ *
|
|
|
+ * 封装通用功能:
|
|
|
+ * - ViewBinding 支持
|
|
|
+ * - 统一的加载状态管理
|
|
|
+ * - 统一的错误提示
|
|
|
+ * - 生命周期管理
|
|
|
+ *
|
|
|
+ * 使用方式:
|
|
|
+ * ```kotlin
|
|
|
+ * class LoginFragment : BaseFragment<FragmentLoginBinding>() {
|
|
|
+ * override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
+ * super.onViewCreated(view, savedInstanceState)
|
|
|
+ * // 你的代码
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+abstract class BaseFragment<VB : ViewBinding> : Fragment() {
|
|
|
+
|
|
|
+ protected lateinit var binding: VB
|
|
|
+ protected abstract fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?): VB
|
|
|
+
|
|
|
+ // 加载状态(子类可重写自定义实现)
|
|
|
+ protected open fun showLoading() {
|
|
|
+ // 默认实现:显示加载提示
|
|
|
+ // 子类可重写自定义加载UI
|
|
|
+ }
|
|
|
+
|
|
|
+ protected open fun hideLoading() {
|
|
|
+ // 默认实现:隐藏加载提示
|
|
|
+ // 子类可重写自定义加载UI
|
|
|
+ }
|
|
|
+
|
|
|
+ // 错误提示(子类可重写自定义实现)
|
|
|
+ protected open fun showError(message: String) {
|
|
|
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+
|
|
|
+ protected open fun showSuccess(message: String) {
|
|
|
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCreateView(
|
|
|
+ inflater: LayoutInflater,
|
|
|
+ container: ViewGroup?,
|
|
|
+ savedInstanceState: Bundle?
|
|
|
+ ): View? {
|
|
|
+ binding = getViewBinding(inflater, container)
|
|
|
+ return binding.root
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
+ super.onViewCreated(view, savedInstanceState)
|
|
|
+ initView()
|
|
|
+ initObserver()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化视图(子类实现)
|
|
|
+ */
|
|
|
+ protected open fun initView() {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化观察者(子类实现)
|
|
|
+ */
|
|
|
+ protected open fun initObserver() {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一执行网络请求
|
|
|
+ *
|
|
|
+ * 自动处理加载状态和错误提示
|
|
|
+ */
|
|
|
+ protected fun <T> executeRequest(
|
|
|
+ request: suspend () -> Result<T>,
|
|
|
+ onSuccess: (T) -> Unit,
|
|
|
+ onError: ((String) -> Unit)? = null,
|
|
|
+ showLoading: Boolean = true
|
|
|
+ ) {
|
|
|
+ lifecycleScope.launch {
|
|
|
+ try {
|
|
|
+ if (showLoading) showLoading()
|
|
|
+
|
|
|
+ request()
|
|
|
+ .onSuccess { data ->
|
|
|
+ hideLoading()
|
|
|
+ onSuccess(data)
|
|
|
+ }
|
|
|
+ .onFailure { throwable ->
|
|
|
+ hideLoading()
|
|
|
+ val errorMsg = throwable.message ?: "操作失败"
|
|
|
+ if (onError != null) {
|
|
|
+ onError(errorMsg)
|
|
|
+ } else {
|
|
|
+ showError(errorMsg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ hideLoading()
|
|
|
+ val errorMsg = e.message ?: "操作失败"
|
|
|
+ if (onError != null) {
|
|
|
+ onError(errorMsg)
|
|
|
+ } else {
|
|
|
+ showError(errorMsg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 八、业务层使用示例
|
|
|
+
|
|
|
+### 8.1 Activity 中使用(推荐使用 ViewModel)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.ui.login
|
|
|
+
|
|
|
+import androidx.lifecycle.ViewModelProvider
|
|
|
+import com.narutohuo.xindazhou.common.ui.BaseActivity
|
|
|
+import com.narutohuo.xindazhou.user.databinding.ActivityLoginBinding
|
|
|
+import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModel
|
|
|
+import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModelFactory
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录 Activity
|
|
|
+ */
|
|
|
+class LoginActivity : BaseActivity<ActivityLoginBinding>() {
|
|
|
+
|
|
|
+ private lateinit var viewModel: LoginViewModel
|
|
|
+
|
|
|
+ override fun getViewBinding(): ActivityLoginBinding {
|
|
|
+ return ActivityLoginBinding.inflate(layoutInflater)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun initView() {
|
|
|
+ // 初始化 ViewModel
|
|
|
+ viewModel = ViewModelProvider(
|
|
|
+ this,
|
|
|
+ LoginViewModelFactory(application)
|
|
|
+ )[LoginViewModel::class.java]
|
|
|
+
|
|
|
+ // 设置点击事件
|
|
|
+ binding.btnLogin.setOnClickListener {
|
|
|
+ val mobile = binding.etMobile.text.toString()
|
|
|
+ val password = binding.etPassword.text.toString()
|
|
|
+ viewModel.login(mobile, password)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun initObserver() {
|
|
|
+ // 观察登录状态
|
|
|
+ lifecycleScope.launch {
|
|
|
+ viewModel.loginState.collect { state ->
|
|
|
+ when (state) {
|
|
|
+ is LoginState.Idle -> {}
|
|
|
+ is LoginState.Loading -> showLoading()
|
|
|
+ is LoginState.Success -> {
|
|
|
+ hideLoading()
|
|
|
+ showSuccess(state.message)
|
|
|
+ // 跳转到主页
|
|
|
+ }
|
|
|
+ is LoginState.Error -> {
|
|
|
+ hideLoading()
|
|
|
+ showError(state.message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 Fragment 中使用(推荐使用 ViewModel)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.ui.login
|
|
|
+
|
|
|
+import androidx.fragment.app.viewModels
|
|
|
+import com.narutohuo.xindazhou.common.ui.BaseFragment
|
|
|
+import com.narutohuo.xindazhou.user.databinding.FragmentLoginBinding
|
|
|
+import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModel
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录 Fragment
|
|
|
+ */
|
|
|
+class LoginFragment : BaseFragment<FragmentLoginBinding>() {
|
|
|
+
|
|
|
+ private val viewModel: LoginViewModel by viewModels {
|
|
|
+ LoginViewModelFactory(requireActivity().application)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getViewBinding(
|
|
|
+ inflater: LayoutInflater,
|
|
|
+ container: ViewGroup?
|
|
|
+ ): FragmentLoginBinding {
|
|
|
+ return FragmentLoginBinding.inflate(inflater, container, false)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun initView() {
|
|
|
+ // 设置点击事件
|
|
|
+ binding.btnLogin.setOnClickListener {
|
|
|
+ val mobile = binding.etMobile.text.toString()
|
|
|
+ val password = binding.etPassword.text.toString()
|
|
|
+ viewModel.login(mobile, password)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun initObserver() {
|
|
|
+ // 观察登录状态
|
|
|
+ lifecycleScope.launch {
|
|
|
+ viewModel.loginState.collect { state ->
|
|
|
+ when (state) {
|
|
|
+ is LoginState.Idle -> {}
|
|
|
+ is LoginState.Loading -> showLoading()
|
|
|
+ is LoginState.Success -> {
|
|
|
+ hideLoading()
|
|
|
+ showSuccess(state.message)
|
|
|
+ }
|
|
|
+ is LoginState.Error -> {
|
|
|
+ hideLoading()
|
|
|
+ showError(state.message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 Activity 中直接使用 Repository(简单场景)
|
|
|
+
|
|
|
+```kotlin
|
|
|
+package com.narutohuo.xindazhou.user.ui.login
|
|
|
+
|
|
|
+import com.narutohuo.xindazhou.common.ui.BaseActivity
|
|
|
+import com.narutohuo.xindazhou.user.databinding.ActivityLoginBinding
|
|
|
+import com.narutohuo.xindazhou.user.data.repository.AuthRepository
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录 Activity(直接使用 Repository)
|
|
|
+ *
|
|
|
+ * 适用于简单的业务场景,不推荐用于复杂业务
|
|
|
+ */
|
|
|
+class LoginActivity : BaseActivity<ActivityLoginBinding>() {
|
|
|
+
|
|
|
+ private val authRepository = AuthRepository(...) // 通过依赖注入获取
|
|
|
+
|
|
|
+ override fun getViewBinding(): ActivityLoginBinding {
|
|
|
+ return ActivityLoginBinding.inflate(layoutInflater)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun initView() {
|
|
|
+ binding.btnLogin.setOnClickListener {
|
|
|
+ val mobile = binding.etMobile.text.toString()
|
|
|
+ val password = binding.etPassword.text.toString()
|
|
|
+
|
|
|
+ // 使用基类的 executeRequest 方法
|
|
|
+ executeRequest(
|
|
|
+ request = { authRepository.login(mobile, password) },
|
|
|
+ onSuccess = { response ->
|
|
|
+ showSuccess("登录成功")
|
|
|
+ // 处理登录成功
|
|
|
+ },
|
|
|
+ onError = { errorMsg ->
|
|
|
+ // 默认会调用 showError,也可以自定义处理
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 九、完整架构层次
|
|
|
+
|
|
|
+```
|
|
|
+业务层(UI层)
|
|
|
+├── BaseActivity / BaseFragment # 基础UI组件(新增)
|
|
|
+│ ├── ViewBinding 支持
|
|
|
+│ ├── 加载状态管理
|
|
|
+│ ├── 错误提示
|
|
|
+│ └── executeRequest() 便捷方法
|
|
|
+│
|
|
|
+├── Activity / Fragment # 业务UI组件
|
|
|
+│ └── 继承 BaseActivity / BaseFragment
|
|
|
+│
|
|
|
+└── ViewModel # 视图模型
|
|
|
+ └── 使用 Repository
|
|
|
+
|
|
|
+数据层
|
|
|
+├── Repository # 数据仓库
|
|
|
+│ └── 继承 BaseRepository(可选)
|
|
|
+│
|
|
|
+├── RemoteDataSource # 远程数据源
|
|
|
+│ └── 继承 BaseRemoteDataSource
|
|
|
+│
|
|
|
+└── ApiServiceFactory # API服务工厂
|
|
|
+```
|
|
|
+
|
|
|
+## 十、使用注意事项
|
|
|
+
|
|
|
+1. **必须继承基类**:
|
|
|
+ - 所有 RemoteDataSource 必须继承 `BaseRemoteDataSource`
|
|
|
+ - 所有 Activity 建议继承 `BaseActivity`
|
|
|
+ - 所有 Fragment 建议继承 `BaseFragment`
|
|
|
+
|
|
|
+2. **使用 executeRequest**:
|
|
|
+ - DataSource 层:使用 `executeRequest()` 处理非空数据
|
|
|
+ - DataSource 层:使用 `executeNullableRequest()` 处理可空数据
|
|
|
+ - Activity/Fragment 层:使用 `executeRequest()` 统一处理请求(如果直接使用 Repository)
|
|
|
+
|
|
|
+3. **推荐使用 ViewModel**:
|
|
|
+ - 复杂业务推荐使用 ViewModel + StateFlow
|
|
|
+ - 简单业务可以直接使用基类的 `executeRequest()` 方法
|
|
|
+
|
|
|
+4. **自定义实现**:
|
|
|
+ - 可以重写 `showLoading()`, `hideLoading()`, `showError()` 等方法自定义UI
|
|
|
+ - 可以重写 `tag` 属性自定义日志标签
|
|
|
+
|
|
|
+5. **CommonResult**:已在 `ApiResponseParser.kt` 中定义,无需重复定义
|
|
|
+
|