|
@@ -1,792 +0,0 @@
|
|
|
-# MVVM 四层架构设计文档
|
|
|
|
|
-
|
|
|
|
|
-> 本文档说明新大洲 Android 项目采用的 MVVM 四层架构设计
|
|
|
|
|
-
|
|
|
|
|
-## 目录
|
|
|
|
|
-
|
|
|
|
|
-- [一、架构概述](#一架构概述)
|
|
|
|
|
-- [二、四层架构详解](#二四层架构详解)
|
|
|
|
|
-- [三、目录结构规范](#三目录结构规范)
|
|
|
|
|
-- [四、数据流向](#四数据流向)
|
|
|
|
|
-- [五、代码示例](#五代码示例)
|
|
|
|
|
-- [六、依赖关系](#六依赖关系)
|
|
|
|
|
-- [七、最佳实践](#七最佳实践)
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 一、架构概述
|
|
|
|
|
-
|
|
|
|
|
-### 1.1 架构说明
|
|
|
|
|
-
|
|
|
|
|
-本项目采用 **MVVM 四层架构**,基于 Clean Architecture 和 Android 架构组件,将代码分为以下四层:
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-┌─────────────────────────────────────────┐
|
|
|
|
|
-│ UI/Presentation 层 │
|
|
|
|
|
-│ (Fragment + ViewModel) │
|
|
|
|
|
-└──────────────┬──────────────────────────┘
|
|
|
|
|
- │
|
|
|
|
|
- ▼
|
|
|
|
|
-┌─────────────────────────────────────────┐
|
|
|
|
|
-│ Domain 层 │
|
|
|
|
|
-│ (UseCase + Model) │
|
|
|
|
|
-└──────────────┬──────────────────────────┘
|
|
|
|
|
- │
|
|
|
|
|
- ▼
|
|
|
|
|
-┌─────────────────────────────────────────┐
|
|
|
|
|
-│ Repository 层 │
|
|
|
|
|
-│ (数据仓库,统一数据源) │
|
|
|
|
|
-└──────────────┬──────────────────────────┘
|
|
|
|
|
- │
|
|
|
|
|
- ▼
|
|
|
|
|
-┌─────────────────────────────────────────┐
|
|
|
|
|
-│ Data 层 │
|
|
|
|
|
-│ (Remote + Local DataSource) │
|
|
|
|
|
-└─────────────────────────────────────────┘
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 1.2 架构优势
|
|
|
|
|
-
|
|
|
|
|
-1. **职责分离**:每一层都有明确的职责,互不干扰
|
|
|
|
|
-2. **易于测试**:各层独立,便于单元测试
|
|
|
|
|
-3. **可维护性**:代码结构清晰,易于维护和扩展
|
|
|
|
|
-4. **可复用性**:Domain 层不依赖 Android 框架,可在其他平台复用
|
|
|
|
|
-5. **数据统一**:Repository 层统一管理数据源,处理缓存策略
|
|
|
|
|
-
|
|
|
|
|
-### 1.3 与传统 MVVM 的区别
|
|
|
|
|
-
|
|
|
|
|
-**传统三层 MVVM:**
|
|
|
|
|
-```
|
|
|
|
|
-View → ViewModel → Repository → API
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**四层 MVVM(Clean Architecture):**
|
|
|
|
|
-```
|
|
|
|
|
-View → ViewModel → UseCase → Repository → DataSource
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**主要改进:**
|
|
|
|
|
-- 新增 **Domain 层**:封装业务逻辑,与 UI 和 Data 解耦
|
|
|
|
|
-- 新增 **DataSource 层**:将数据源(Remote/Local)与 Repository 分离
|
|
|
|
|
-- 更好的可测试性和可维护性
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 二、四层架构详解
|
|
|
|
|
-
|
|
|
|
|
-### 2.1 UI/Presentation 层
|
|
|
|
|
-
|
|
|
|
|
-**职责:**
|
|
|
|
|
-- 展示 UI,处理用户交互
|
|
|
|
|
-- 观察 ViewModel 的状态变化,更新 UI
|
|
|
|
|
-- 不包含业务逻辑
|
|
|
|
|
-
|
|
|
|
|
-**包含内容:**
|
|
|
|
|
-- Fragment/Activity
|
|
|
|
|
-- ViewModel
|
|
|
|
|
-- UI State(Sealed Class)
|
|
|
|
|
-
|
|
|
|
|
-**依赖关系:**
|
|
|
|
|
-- 只依赖 Domain 层(通过 UseCase)
|
|
|
|
|
-- 不直接依赖 Data 层
|
|
|
|
|
-
|
|
|
|
|
-**示例结构:**
|
|
|
|
|
-```
|
|
|
|
|
-ui/
|
|
|
|
|
-├── login/
|
|
|
|
|
-│ └── LoginFragment.kt
|
|
|
|
|
-├── viewmodel/
|
|
|
|
|
-│ ├── LoginViewModel.kt
|
|
|
|
|
-│ └── LoginViewModelFactory.kt
|
|
|
|
|
-└── state/
|
|
|
|
|
- └── LoginState.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 2.2 Domain 层
|
|
|
|
|
-
|
|
|
|
|
-**职责:**
|
|
|
|
|
-- 封装业务逻辑
|
|
|
|
|
-- 定义业务实体模型
|
|
|
|
|
-- 定义 UseCase(用例)
|
|
|
|
|
-
|
|
|
|
|
-**包含内容:**
|
|
|
|
|
-- UseCase(用例)
|
|
|
|
|
-- Domain Model(业务模型)
|
|
|
|
|
-- Repository Interface(可选)
|
|
|
|
|
-
|
|
|
|
|
-**依赖关系:**
|
|
|
|
|
-- 不依赖任何 Android 框架
|
|
|
|
|
-- 只依赖 Repository Interface(接口,不依赖实现)
|
|
|
|
|
-
|
|
|
|
|
-**示例结构:**
|
|
|
|
|
-```
|
|
|
|
|
-domain/
|
|
|
|
|
-├── model/
|
|
|
|
|
-│ └── User.kt
|
|
|
|
|
-└── usecase/
|
|
|
|
|
- ├── LoginUseCase.kt
|
|
|
|
|
- └── RegisterUseCase.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 2.3 Repository 层
|
|
|
|
|
-
|
|
|
|
|
-**职责:**
|
|
|
|
|
-- 统一数据源管理
|
|
|
|
|
-- 协调 Remote 和 Local 数据源
|
|
|
|
|
-- 处理数据缓存策略
|
|
|
|
|
-- 数据转换(Data Model → Domain Model)
|
|
|
|
|
-
|
|
|
|
|
-**包含内容:**
|
|
|
|
|
-- Repository 实现类
|
|
|
|
|
-- 数据转换逻辑
|
|
|
|
|
-
|
|
|
|
|
-**依赖关系:**
|
|
|
|
|
-- 依赖 Domain 层(UseCase)
|
|
|
|
|
-- 依赖 Data 层(RemoteDataSource + LocalDataSource)
|
|
|
|
|
-
|
|
|
|
|
-**示例结构:**
|
|
|
|
|
-```
|
|
|
|
|
-data/
|
|
|
|
|
-└── repository/
|
|
|
|
|
- └── AuthRepository.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 2.4 Data 层
|
|
|
|
|
-
|
|
|
|
|
-**职责:**
|
|
|
|
|
-- 提供数据源接口和实现
|
|
|
|
|
-- 处理网络请求(Remote)
|
|
|
|
|
-- 处理本地存储(Local)
|
|
|
|
|
-- 定义数据模型(Data Model)
|
|
|
|
|
-
|
|
|
|
|
-**包含内容:**
|
|
|
|
|
-- RemoteDataSource(远程数据源)
|
|
|
|
|
-- LocalDataSource(本地数据源)
|
|
|
|
|
-- Data Model(数据模型)
|
|
|
|
|
-- API 接口
|
|
|
|
|
-
|
|
|
|
|
-**依赖关系:**
|
|
|
|
|
-- 只依赖 Android 框架和第三方库
|
|
|
|
|
-- 不依赖其他业务层
|
|
|
|
|
-
|
|
|
|
|
-**示例结构:**
|
|
|
|
|
-```
|
|
|
|
|
-data/
|
|
|
|
|
-├── remote/
|
|
|
|
|
-│ └── AuthRemoteDataSource.kt
|
|
|
|
|
-├── local/
|
|
|
|
|
-│ ├── AuthLocalDataSource.kt
|
|
|
|
|
-│ └── TokenManager.kt
|
|
|
|
|
-├── model/
|
|
|
|
|
-│ ├── LoginRequest.kt
|
|
|
|
|
-│ └── LoginResponse.kt
|
|
|
|
|
-└── api/
|
|
|
|
|
- └── AuthApi.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 三、目录结构规范
|
|
|
|
|
-
|
|
|
|
|
-### 3.1 模块目录结构
|
|
|
|
|
-
|
|
|
|
|
-每个功能模块应遵循以下目录结构:
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-module_name/
|
|
|
|
|
-├── ui/ # UI 层
|
|
|
|
|
-│ ├── feature_name/
|
|
|
|
|
-│ │ └── FeatureFragment.kt
|
|
|
|
|
-│ ├── viewmodel/
|
|
|
|
|
-│ │ ├── FeatureViewModel.kt
|
|
|
|
|
-│ │ └── FeatureViewModelFactory.kt
|
|
|
|
|
-│ └── state/
|
|
|
|
|
-│ └── FeatureState.kt
|
|
|
|
|
-│
|
|
|
|
|
-├── domain/ # Domain 层
|
|
|
|
|
-│ ├── model/
|
|
|
|
|
-│ │ └── DomainModel.kt
|
|
|
|
|
-│ └── usecase/
|
|
|
|
|
-│ └── FeatureUseCase.kt
|
|
|
|
|
-│
|
|
|
|
|
-├── data/ # Data 层
|
|
|
|
|
-│ ├── repository/
|
|
|
|
|
-│ │ └── FeatureRepository.kt
|
|
|
|
|
-│ ├── remote/
|
|
|
|
|
-│ │ └── FeatureRemoteDataSource.kt
|
|
|
|
|
-│ ├── local/
|
|
|
|
|
-│ │ └── FeatureLocalDataSource.kt
|
|
|
|
|
-│ ├── model/
|
|
|
|
|
-│ │ ├── FeatureRequest.kt
|
|
|
|
|
-│ │ └── FeatureResponse.kt
|
|
|
|
|
-│ └── api/
|
|
|
|
|
-│ └── FeatureApi.kt
|
|
|
|
|
-│
|
|
|
|
|
-├── di/ # 依赖注入(可选)
|
|
|
|
|
-│ └── FeatureModule.kt
|
|
|
|
|
-│
|
|
|
|
|
-└── constant/ # 常量(可选)
|
|
|
|
|
- └── FeatureConstants.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 3.2 实际示例(用户模块)
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-user/
|
|
|
|
|
-├── ui/
|
|
|
|
|
-│ ├── login/
|
|
|
|
|
-│ │ └── LoginFragment.kt
|
|
|
|
|
-│ ├── register/
|
|
|
|
|
-│ │ └── RegisterFragment.kt
|
|
|
|
|
-│ ├── viewmodel/
|
|
|
|
|
-│ │ ├── LoginViewModel.kt
|
|
|
|
|
-│ │ ├── LoginViewModelFactory.kt
|
|
|
|
|
-│ │ ├── RegisterViewModel.kt
|
|
|
|
|
-│ │ └── RegisterViewModelFactory.kt
|
|
|
|
|
-│ └── state/
|
|
|
|
|
-│ ├── LoginState.kt
|
|
|
|
|
-│ └── RegisterState.kt
|
|
|
|
|
-│
|
|
|
|
|
-├── domain/
|
|
|
|
|
-│ ├── model/
|
|
|
|
|
-│ │ └── User.kt
|
|
|
|
|
-│ └── usecase/
|
|
|
|
|
-│ ├── LoginUseCase.kt
|
|
|
|
|
-│ └── RegisterUseCase.kt
|
|
|
|
|
-│
|
|
|
|
|
-├── data/
|
|
|
|
|
-│ ├── repository/
|
|
|
|
|
-│ │ └── AuthRepository.kt
|
|
|
|
|
-│ ├── remote/
|
|
|
|
|
-│ │ └── AuthRemoteDataSource.kt
|
|
|
|
|
-│ ├── local/
|
|
|
|
|
-│ │ ├── AuthLocalDataSource.kt
|
|
|
|
|
-│ │ └── TokenManager.kt
|
|
|
|
|
-│ ├── model/
|
|
|
|
|
-│ │ ├── LoginRequest.kt
|
|
|
|
|
-│ │ ├── LoginResponse.kt
|
|
|
|
|
-│ │ └── RegisterRequest.kt
|
|
|
|
|
-│ └── api/
|
|
|
|
|
-│ └── AuthApi.kt
|
|
|
|
|
-│
|
|
|
|
|
-└── constant/
|
|
|
|
|
- ├── ApiConstants.kt
|
|
|
|
|
- └── ValidationConstants.kt
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 四、数据流向
|
|
|
|
|
-
|
|
|
|
|
-### 4.1 请求数据流向
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-用户操作
|
|
|
|
|
- ↓
|
|
|
|
|
-Fragment (UI)
|
|
|
|
|
- ↓ 调用
|
|
|
|
|
-ViewModel.login()
|
|
|
|
|
- ↓ 调用
|
|
|
|
|
-UseCase.invoke()
|
|
|
|
|
- ↓ 调用
|
|
|
|
|
-Repository.login()
|
|
|
|
|
- ↓ 调用
|
|
|
|
|
-RemoteDataSource.login() 或 LocalDataSource.getData()
|
|
|
|
|
- ↓
|
|
|
|
|
-API/数据库
|
|
|
|
|
- ↓ 返回数据
|
|
|
|
|
-Repository (转换 Data Model → Domain Model)
|
|
|
|
|
- ↓ 返回
|
|
|
|
|
-UseCase (业务逻辑处理)
|
|
|
|
|
- ↓ 返回
|
|
|
|
|
-ViewModel (转换为 UI State)
|
|
|
|
|
- ↓ 更新 StateFlow
|
|
|
|
|
-Fragment (观察 StateFlow,更新 UI)
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 4.2 具体示例(登录流程)
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-1. 用户在 LoginFragment 点击登录按钮
|
|
|
|
|
- ↓
|
|
|
|
|
-2. LoginFragment 调用 viewModel.login(mobile, password)
|
|
|
|
|
- ↓
|
|
|
|
|
-3. LoginViewModel 调用 loginUseCase(mobile, password)
|
|
|
|
|
- ↓
|
|
|
|
|
-4. LoginUseCase 验证输入,调用 authRepository.login()
|
|
|
|
|
- ↓
|
|
|
|
|
-5. AuthRepository 调用 remoteDataSource.login()
|
|
|
|
|
- ↓
|
|
|
|
|
-6. AuthRemoteDataSource 调用 authApi.login() (Retrofit)
|
|
|
|
|
- ↓
|
|
|
|
|
-7. 网络请求返回 LoginResponse (Data Model)
|
|
|
|
|
- ↓
|
|
|
|
|
-8. AuthRemoteDataSource 返回 Result<LoginResponse>
|
|
|
|
|
- ↓
|
|
|
|
|
-9. AuthRepository 保存 Token 到 LocalDataSource
|
|
|
|
|
- ↓
|
|
|
|
|
-10. AuthRepository 返回 Result<LoginResponse>
|
|
|
|
|
- ↓
|
|
|
|
|
-11. LoginUseCase 返回 Result<Unit>
|
|
|
|
|
- ↓
|
|
|
|
|
-12. LoginViewModel 更新 _loginState (Success/Error)
|
|
|
|
|
- ↓
|
|
|
|
|
-13. LoginFragment 观察 loginState,更新 UI
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 五、代码示例
|
|
|
|
|
-
|
|
|
|
|
-### 5.1 UI 层示例
|
|
|
|
|
-
|
|
|
|
|
-#### LoginFragment.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.ui.login
|
|
|
|
|
-
|
|
|
|
|
-import android.os.Bundle
|
|
|
|
|
-import android.view.LayoutInflater
|
|
|
|
|
-import android.view.View
|
|
|
|
|
-import android.view.ViewGroup
|
|
|
|
|
-import androidx.fragment.app.Fragment
|
|
|
|
|
-import androidx.fragment.app.viewModels
|
|
|
|
|
-import androidx.lifecycle.lifecycleScope
|
|
|
|
|
-import com.narutohuo.xindazhou.R
|
|
|
|
|
-import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModel
|
|
|
|
|
-import com.narutohuo.xindazhou.user.ui.viewmodel.LoginViewModelFactory
|
|
|
|
|
-import kotlinx.coroutines.launch
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 登录Fragment
|
|
|
|
|
- */
|
|
|
|
|
-class LoginFragment : Fragment() {
|
|
|
|
|
-
|
|
|
|
|
- private val viewModel: LoginViewModel by viewModels {
|
|
|
|
|
- LoginViewModelFactory(requireContext().applicationContext as android.app.Application)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override fun onCreateView(
|
|
|
|
|
- inflater: LayoutInflater,
|
|
|
|
|
- container: ViewGroup?,
|
|
|
|
|
- savedInstanceState: Bundle?
|
|
|
|
|
- ): View? {
|
|
|
|
|
- return inflater.inflate(R.layout.fragment_login, container, false)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
|
- super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
-
|
|
|
|
|
- // 初始化视图
|
|
|
|
|
- initViews(view)
|
|
|
|
|
-
|
|
|
|
|
- // 观察状态
|
|
|
|
|
- observeState()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private fun initViews(view: View) {
|
|
|
|
|
- // 绑定视图和设置点击事件
|
|
|
|
|
- view.findViewById<View>(R.id.btnLogin).setOnClickListener {
|
|
|
|
|
- val mobile = view.findViewById<EditText>(R.id.etMobile).text.toString()
|
|
|
|
|
- val password = view.findViewById<EditText>(R.id.etPassword).text.toString()
|
|
|
|
|
- viewModel.login(mobile, password)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private fun observeState() {
|
|
|
|
|
- lifecycleScope.launch {
|
|
|
|
|
- viewModel.loginState.collect { state ->
|
|
|
|
|
- when (state) {
|
|
|
|
|
- is LoginState.Idle -> { /* 初始状态 */ }
|
|
|
|
|
- is LoginState.Loading -> { /* 显示加载 */ }
|
|
|
|
|
- is LoginState.Success -> { /* 登录成功 */ }
|
|
|
|
|
- is LoginState.Error -> { /* 显示错误 */ }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### LoginViewModel.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.ui.viewmodel
|
|
|
|
|
-
|
|
|
|
|
-import android.app.Application
|
|
|
|
|
-import androidx.lifecycle.AndroidViewModel
|
|
|
|
|
-import androidx.lifecycle.viewModelScope
|
|
|
|
|
-import com.narutohuo.xindazhou.user.domain.usecase.LoginUseCase
|
|
|
|
|
-import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
|
-import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
-import kotlinx.coroutines.launch
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 登录状态
|
|
|
|
|
- */
|
|
|
|
|
-sealed class LoginState {
|
|
|
|
|
- object Idle : LoginState()
|
|
|
|
|
- object Loading : LoginState()
|
|
|
|
|
- data class Success(val message: String) : LoginState()
|
|
|
|
|
- data class Error(val message: String) : LoginState()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 登录ViewModel
|
|
|
|
|
- */
|
|
|
|
|
-class LoginViewModel(
|
|
|
|
|
- application: Application,
|
|
|
|
|
- private val loginUseCase: LoginUseCase
|
|
|
|
|
-) : 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
|
|
|
|
|
- loginUseCase(mobile, password)
|
|
|
|
|
- .onSuccess {
|
|
|
|
|
- _loginState.value = LoginState.Success("登录成功")
|
|
|
|
|
- }
|
|
|
|
|
- .onFailure { e ->
|
|
|
|
|
- _loginState.value = LoginState.Error(e.message ?: "登录失败")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 5.2 Domain 层示例
|
|
|
|
|
-
|
|
|
|
|
-#### LoginUseCase.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.domain.usecase
|
|
|
|
|
-
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.repository.AuthRepository
|
|
|
|
|
-import com.narutohuo.xindazhou.user.domain.constant.ValidationConstants
|
|
|
|
|
-import timber.log.Timber
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 登录用例
|
|
|
|
|
- *
|
|
|
|
|
- * 封装登录业务逻辑
|
|
|
|
|
- */
|
|
|
|
|
-class LoginUseCase(
|
|
|
|
|
- private val authRepository: AuthRepository
|
|
|
|
|
-) {
|
|
|
|
|
- suspend operator fun invoke(mobile: String, password: String): Result<Unit> {
|
|
|
|
|
- // 业务逻辑验证
|
|
|
|
|
- if (mobile.isBlank()) {
|
|
|
|
|
- return Result.failure(IllegalArgumentException("手机号不能为空"))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (password.length < ValidationConstants.MIN_PASSWORD_LENGTH
|
|
|
|
|
- || password.length > ValidationConstants.MAX_PASSWORD_LENGTH) {
|
|
|
|
|
- return Result.failure(
|
|
|
|
|
- IllegalArgumentException("密码长度应为${ValidationConstants.MIN_PASSWORD_LENGTH}-${ValidationConstants.MAX_PASSWORD_LENGTH}位")
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 调用 Repository
|
|
|
|
|
- return authRepository.login(mobile, password).map { }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 5.3 Repository 层示例
|
|
|
|
|
-
|
|
|
|
|
-#### AuthRepository.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.data.repository
|
|
|
|
|
-
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.local.AuthLocalDataSource
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.model.LoginRequest
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.model.LoginResponse
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.remote.AuthRemoteDataSource
|
|
|
|
|
-import timber.log.Timber
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 认证数据仓库
|
|
|
|
|
- *
|
|
|
|
|
- * 统一管理认证相关的数据获取和缓存
|
|
|
|
|
- */
|
|
|
|
|
-class AuthRepository(
|
|
|
|
|
- private val remoteDataSource: AuthRemoteDataSource,
|
|
|
|
|
- private val localDataSource: AuthLocalDataSource
|
|
|
|
|
-) {
|
|
|
|
|
-
|
|
|
|
|
- companion object {
|
|
|
|
|
- private const val TAG = "AuthRepository"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 用户登录
|
|
|
|
|
- */
|
|
|
|
|
- suspend fun login(mobile: String, password: String): Result<LoginResponse> {
|
|
|
|
|
- return try {
|
|
|
|
|
- val request = LoginRequest(mobile, password)
|
|
|
|
|
-
|
|
|
|
|
- // 先尝试从远程获取
|
|
|
|
|
- val result = remoteDataSource.login(request)
|
|
|
|
|
-
|
|
|
|
|
- // 如果成功,保存到本地
|
|
|
|
|
- result.onSuccess { response ->
|
|
|
|
|
- localDataSource.saveToken(response)
|
|
|
|
|
- Timber.d("$TAG - login: Token已保存")
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- result
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- Timber.e(e, "$TAG - login: 登录异常")
|
|
|
|
|
- Result.failure(e)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 5.4 Data 层示例
|
|
|
|
|
-
|
|
|
|
|
-#### AuthRemoteDataSource.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.data.remote
|
|
|
|
|
-
|
|
|
|
|
-import com.narutohuo.xindazhou.user.api.AuthApi
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.constant.ApiConstants
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.model.LoginRequest
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.model.LoginResponse
|
|
|
|
|
-import timber.log.Timber
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 认证远程数据源接口
|
|
|
|
|
- */
|
|
|
|
|
-interface AuthRemoteDataSource {
|
|
|
|
|
- suspend fun login(request: LoginRequest): Result<LoginResponse>
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 认证远程数据源实现
|
|
|
|
|
- */
|
|
|
|
|
-class AuthRemoteDataSourceImpl(
|
|
|
|
|
- private val authApi: AuthApi
|
|
|
|
|
-) : AuthRemoteDataSource {
|
|
|
|
|
-
|
|
|
|
|
- companion object {
|
|
|
|
|
- private const val TAG = "AuthRemoteDataSource"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override suspend fun login(request: LoginRequest): Result<LoginResponse> {
|
|
|
|
|
- return try {
|
|
|
|
|
- val response = authApi.login(request)
|
|
|
|
|
-
|
|
|
|
|
- if (!response.isSuccessful) {
|
|
|
|
|
- val errorMsg = response.message() ?: "登录失败"
|
|
|
|
|
- Timber.w("$TAG - login: HTTP请求失败,code=${response.code()}")
|
|
|
|
|
- return Result.failure(Exception(errorMsg))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- val commonResult = response.body()
|
|
|
|
|
- ?: return Result.failure(Exception("响应体为空"))
|
|
|
|
|
-
|
|
|
|
|
- if (commonResult.code != ApiConstants.SUCCESS_CODE) {
|
|
|
|
|
- val errorMsg = commonResult.msg ?: "登录失败 (code: ${commonResult.code})"
|
|
|
|
|
- Timber.w("$TAG - login: 业务失败,code=${commonResult.code}")
|
|
|
|
|
- return Result.failure(Exception(errorMsg))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- val loginResponse = commonResult.data
|
|
|
|
|
- ?: return Result.failure(Exception("服务器返回数据为空"))
|
|
|
|
|
-
|
|
|
|
|
- Timber.d("$TAG - login: 登录成功")
|
|
|
|
|
- Result.success(loginResponse)
|
|
|
|
|
-
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- Timber.e(e, "$TAG - login: 登录异常")
|
|
|
|
|
- Result.failure(e)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-#### AuthLocalDataSource.kt
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-package com.narutohuo.xindazhou.user.data.local
|
|
|
|
|
-
|
|
|
|
|
-import com.narutohuo.xindazhou.user.data.model.LoginResponse
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 认证本地数据源接口
|
|
|
|
|
- */
|
|
|
|
|
-interface AuthLocalDataSource {
|
|
|
|
|
- suspend fun saveToken(loginResponse: LoginResponse)
|
|
|
|
|
- suspend fun getToken(): String?
|
|
|
|
|
- suspend fun clearToken()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 认证本地数据源实现
|
|
|
|
|
- */
|
|
|
|
|
-class AuthLocalDataSourceImpl : AuthLocalDataSource {
|
|
|
|
|
-
|
|
|
|
|
- override suspend fun saveToken(loginResponse: LoginResponse) {
|
|
|
|
|
- TokenManager.saveToken(
|
|
|
|
|
- loginResponse.accessToken,
|
|
|
|
|
- loginResponse.refreshToken,
|
|
|
|
|
- loginResponse.userId
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override suspend fun getToken(): String? {
|
|
|
|
|
- return TokenManager.getAccessToken()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override suspend fun clearToken() {
|
|
|
|
|
- TokenManager.clearToken()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 六、依赖关系
|
|
|
|
|
-
|
|
|
|
|
-### 6.1 依赖方向
|
|
|
|
|
-
|
|
|
|
|
-```
|
|
|
|
|
-UI 层
|
|
|
|
|
- ↓ 依赖
|
|
|
|
|
-Domain 层
|
|
|
|
|
- ↑ 被依赖
|
|
|
|
|
-Repository 层
|
|
|
|
|
- ↓ 依赖
|
|
|
|
|
-Data 层
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-**原则:**
|
|
|
|
|
-- 内层不依赖外层
|
|
|
|
|
-- 外层可以依赖内层
|
|
|
|
|
-- Domain 层不依赖任何 Android 框架
|
|
|
|
|
-
|
|
|
|
|
-### 6.2 依赖注入
|
|
|
|
|
-
|
|
|
|
|
-建议使用依赖注入框架(如 Hilt/Koin)管理依赖关系:
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-// 示例:使用 Koin
|
|
|
|
|
-val authModule = module {
|
|
|
|
|
- // Data 层
|
|
|
|
|
- factory<AuthRemoteDataSource> { AuthRemoteDataSourceImpl(get()) }
|
|
|
|
|
- factory<AuthLocalDataSource> { AuthLocalDataSourceImpl() }
|
|
|
|
|
- single { AuthApi.create() }
|
|
|
|
|
-
|
|
|
|
|
- // Repository 层
|
|
|
|
|
- single<AuthRepository> {
|
|
|
|
|
- AuthRepository(get(), get())
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Domain 层
|
|
|
|
|
- factory { LoginUseCase(get()) }
|
|
|
|
|
- factory { RegisterUseCase(get()) }
|
|
|
|
|
-
|
|
|
|
|
- // UI 层
|
|
|
|
|
- viewModel { LoginViewModel(get(), get()) }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 七、最佳实践
|
|
|
|
|
-
|
|
|
|
|
-### 7.1 状态管理
|
|
|
|
|
-
|
|
|
|
|
-使用 Sealed Class 定义 UI 状态:
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-sealed class LoginState {
|
|
|
|
|
- object Idle : LoginState()
|
|
|
|
|
- object Loading : LoginState()
|
|
|
|
|
- data class Success(val message: String) : LoginState()
|
|
|
|
|
- data class Error(val message: String) : LoginState()
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 7.2 错误处理
|
|
|
|
|
-
|
|
|
|
|
-统一使用 Result 类型处理错误:
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-suspend fun login(mobile: String, password: String): Result<LoginResponse> {
|
|
|
|
|
- return try {
|
|
|
|
|
- // 成功
|
|
|
|
|
- Result.success(data)
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- // 失败
|
|
|
|
|
- Result.failure(e)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 7.3 数据转换
|
|
|
|
|
-
|
|
|
|
|
-Repository 层负责 Data Model 到 Domain Model 的转换:
|
|
|
|
|
-
|
|
|
|
|
-```kotlin
|
|
|
|
|
-// Data Model (API 响应)
|
|
|
|
|
-data class LoginResponseData(...)
|
|
|
|
|
-
|
|
|
|
|
-// Domain Model (业务模型)
|
|
|
|
|
-data class User(...)
|
|
|
|
|
-
|
|
|
|
|
-// Repository 中转换
|
|
|
|
|
-fun toDomainModel(data: LoginResponseData): User {
|
|
|
|
|
- return User(
|
|
|
|
|
- id = data.userId,
|
|
|
|
|
- name = data.username,
|
|
|
|
|
- // ...
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
-### 7.4 测试建议
|
|
|
|
|
-
|
|
|
|
|
-- **UI 层**:使用 AndroidX Test 测试 Fragment/Activity
|
|
|
|
|
-- **ViewModel**:使用 JUnit 测试,不依赖 Android 框架
|
|
|
|
|
-- **UseCase**:使用 JUnit 测试,Mock Repository
|
|
|
|
|
-- **Repository**:使用 JUnit 测试,Mock DataSource
|
|
|
|
|
-- **DataSource**:使用 JUnit 测试,Mock API
|
|
|
|
|
-
|
|
|
|
|
-### 7.5 命名规范
|
|
|
|
|
-
|
|
|
|
|
-- **ViewModel**:`功能ViewModel`,如 `LoginViewModel`
|
|
|
|
|
-- **UseCase**:`功能UseCase`,如 `LoginUseCase`
|
|
|
|
|
-- **Repository**:`功能Repository`,如 `AuthRepository`
|
|
|
|
|
-- **DataSource**:`功能RemoteDataSource` / `功能LocalDataSource`
|
|
|
|
|
-- **State**:`功能State`,如 `LoginState`
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-## 附录:架构检查清单
|
|
|
|
|
-
|
|
|
|
|
-### 代码审查检查项
|
|
|
|
|
-
|
|
|
|
|
-- [ ] UI 层是否只包含 UI 逻辑,业务逻辑是否在 UseCase 中
|
|
|
|
|
-- [ ] ViewModel 是否只调用 UseCase,不直接调用 Repository
|
|
|
|
|
-- [ ] UseCase 是否封装了业务逻辑验证
|
|
|
|
|
-- [ ] Repository 是否统一管理数据源(Remote + Local)
|
|
|
|
|
-- [ ] DataSource 是否分离为 Remote 和 Local
|
|
|
|
|
-- [ ] 是否遵循依赖方向(内层不依赖外层)
|
|
|
|
|
-- [ ] 是否使用了 Result 类型处理错误
|
|
|
|
|
-- [ ] 是否使用 Sealed Class 管理 UI 状态
|
|
|
|
|
-- [ ] 命名是否规范(ViewModel、UseCase、Repository、DataSource)
|
|
|
|
|
-- [ ] 是否添加了必要的注释(类、方法)
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-**文档版本**: v1.0
|
|
|
|
|
-**最后更新**: 2024-01-01
|
|
|
|
|
-**维护者**: 开发团队
|
|
|
|
|
-
|
|
|