数据层与业务逻辑层实现

在现代Android应用架构中,数据层与业务逻辑层的设计直接决定了应用的可维护性、可测试性和性能表现。本章将围绕任务管理应用场景,从数据库设计、远程接口定义、Repository实现到ViewModel状态管理,系统讲解数据层与业务逻辑层的完整实现方案,遵循"数据驱动UI、业务逻辑与UI解耦"的核心原则。

一、数据库设计:本地数据持久化与优化

本地数据库是实现离线功能和数据缓存的核心组件,采用Room框架可大幅简化数据库操作并确保类型安全。任务管理应用的数据库设计需重点关注实体结构、索引策略及迁移机制,以支持高效的数据存取和版本演进。

1.1 Task实体类设计

Task实体类需包含业务所需的核心字段,并通过Room注解定义表结构。根据业务需求,实体类设计如下:

kotlin
复制代码
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import java.util.UUID

@Entity(
    tableName = "tasks",
    indices = [Index(value = [[49](isCompleted)])] // 为查询频繁的字段添加索引
)
data class Task(
    @PrimaryKey val id: String = UUID.randomUUID().toString(), // 自动生成唯一ID
    val title: String, // 任务标题(非空)
    val description: String? = null, // 任务描述(可选)
    val isCompleted: Boolean = false, // 任务完成状态(默认未完成)
    val createdAt: Long = System.currentTimeMillis() // 创建时间戳(用于排序)
)

字段说明

  • id:采用UUID作为主键,避免自增ID在分布式场景下的冲突问题
  • title:任务标题设为非空字段,确保业务数据完整性
  • description:可选描述字段,使用可空类型减少存储冗余
  • isCompleted:布尔型状态字段,用于标记任务完成状态,需添加索引优化查询
  • createdAt:时间戳字段,支持按创建时间排序任务列表

设计要点:实体类应遵循"最小够用原则",仅包含业务必需字段。通过默认参数为ID和创建时间提供自动生成逻辑,简化上层调用;为isCompleted添加索引可将查询性能从O(n)提升至O(log n),尤其在任务数量较大时效果显著。

1.2 数据库版本迁移策略

应用迭代过程中数据库结构可能发生变更,Room提供了Migration机制确保数据安全迁移。假设当前数据库版本为1,需新增priority字段(任务优先级),迁移实现如下:

kotlin
复制代码
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

// 从版本1迁移到版本2:新增priority字段
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0 NOT NULL")
    }
}

// 数据库实例定义
@Database(
    entities = [Task::class],
    version = 2,
    exportSchema = true // 启用Schema导出,便于版本追踪
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
    
    companion object {
        // 单例模式避免数据库连接泄露
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "task_database"
                ).addMigrations(MIGRATION_1_2) // 注册迁移规则
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

迁移最佳实践

  1. 始终启用exportSchema,将Schema文件提交至版本控制系统,便于追踪结构变更
  2. 迁移操作需保证原子性,复杂变更建议先创建临时表,迁移数据后替换原表
  3. 测试环境中使用fallbackToDestructiveMigration()快速迭代,生产环境必须使用显式迁移

二、远程接口:REST API定义与模拟测试

远程接口是应用与后端服务交互的桥梁,采用Retrofit定义接口契约,并通过MockWebServer实现本地模拟测试,可大幅提升开发效率和接口稳定性。

2.1 TasksService接口定义

基于RESTful规范,定义任务管理API接口,包含获取、创建、更新、删除任务等操作:

kotlin
复制代码
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path

interface TasksService {
    // 获取所有任务
    @GET("/tasks")
    suspend fun getTasks(): List<TaskDto>
    
    // 创建新任务
    @POST("/tasks")
    suspend fun createTask(@Body task: TaskDto): TaskDto
    
    // 更新任务状态
    @PUT("/tasks/{id}")
    suspend fun updateTask(
        @Path("id") id: String,
        @Body task: TaskDto
    ): TaskDto
    
    // 删除任务
    @DELETE("/tasks/{id}")
    suspend fun deleteTask(@Path("id") id: String)
}

// 数据传输对象(DTO):与API响应格式严格匹配
data class TaskDto(
    val id: String,
    val title: String,
    val description: String?,
    @SerializedName("is_completed") val isCompleted: Boolean,
    @SerializedName("created_at") val createdAt: Long
)

接口设计说明

  • 使用suspend函数标记异步操作,配合Coroutine实现非阻塞调用
  • DTO类与API响应字段严格对应,通过@SerializedName处理命名差异(如蛇形命名转驼峰命名)
  • 接口方法与HTTP动词一一对应(GET查询、POST创建、PUT更新、DELETE删除)
2.2 MockWebServer模拟测试

为避免依赖后端服务,使用MockWebServer模拟API响应,实现接口的本地化测试:

kotlin
复制代码
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import kotlin.test.assertEquals

class TasksServiceTest {
    private lateinit var mockWebServer: MockWebServer
    private lateinit var tasksService: TasksService
    
    @Before
    fun setup() {
        // 启动本地模拟服务器
        mockWebServer = MockWebServer()
        mockWebServer.start()
        
        // 创建Retrofit实例,连接模拟服务器
        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        
        tasksService = retrofit.create(TasksService::class.java)
    }
    
    @Test
    suspend fun `getTasks returns mock data`() {
        // 配置模拟响应
        val mockResponse = MockResponse()
            .setResponseCode(200)
            .setBody("""
                [
                    {
                        "id": "1",
                        "title": "测试任务",
                        "description": "使用MockWebServer",
                        "is_completed": false,
                        "created_at": 1620000000000
                    }
                ]
            """.trimIndent())
        
        mockWebServer.enqueue(mockResponse)
        
        // 执行测试
        val tasks = tasksService.getTasks()
        
        // 验证结果
        assertEquals(1, tasks.size)
        assertEquals("测试任务", tasks[0].title)
    }
    
    @After
    fun teardown() {
        mockWebServer.shutdown() // 关闭服务器释放资源
    }
}

模拟测试优势

  • 不受网络环境影响,测试稳定性提升
  • 可模拟各种异常场景(如500错误、超时),验证错误处理逻辑
  • 加速开发迭代,前端与后端可并行开发

三、Repository实现:数据整合与异常处理

Repository作为数据层的核心组件,负责整合本地与远程数据源,实现"本地优先"的数据访问策略,并统一处理数据转换和异常,为上层提供清晰的数据访问接口。

3.1 数据源抽象与实现

首先定义数据源接口,抽象本地与远程数据操作,便于测试和替换实现:

kotlin
复制代码
// 数据源接口
interface TasksDataSource {
    fun observeTasks(): Flow<List<Task>> // 观察任务数据变化
    suspend fun getTasks(): List<Task> // 获取所有任务
    suspend fun getTaskById(id: String): Task? // 根据ID获取任务
    suspend fun saveTasks(tasks: List<Task>) // 保存任务列表
    suspend fun saveTask(task: Task) // 保存单个任务
    suspend fun deleteTask(id: String) // 删除任务
}

// 本地数据源(Room实现)
class LocalTasksDataSource(
    private val taskDao: TaskDao
) : TasksDataSource {
    override fun observeTasks(): Flow<List<Task>> = taskDao.observeAllTasks()
    
    override suspend fun getTasks(): List<Task> = taskDao.getAllTasks()
    
    override suspend fun getTaskById(id: String): Task? = taskDao.getTaskById(id)
    
    override suspend fun saveTasks(tasks: List<Task>) {
        taskDao.insertAll(tasks)
    }
    
    override suspend fun saveTask(task: Task) {
        taskDao.insert(task)
    }
    
    override suspend fun deleteTask(id: String) {
        taskDao.deleteById(id)
    }
}

// 远程数据源(Retrofit实现)
class RemoteTasksDataSource(
    private val tasksService: TasksService,
    private val dtoMapper: TaskDtoMapper // DTO-实体转换映射器
) : TasksDataSource {
    override fun observeTasks(): Flow<List<Task>> {
        // 远程数据源通常不提供观察能力,此处抛出异常或返回空流
        throw UnsupportedOperationException("Remote data source does not support observation")
    }
    
    override suspend fun getTasks(): List<Task> {
        return tasksService.getTasks().map(dtoMapper::toDomain)
    }
    
    override suspend fun getTaskById(id: String): Task? {
        // 假设API支持单个任务查询
        return try {
            dtoMapper.toDomain(tasksService.getTask(id))
        } catch (e: Exception) {
            null
        }
    }
    
    override suspend fun saveTasks(tasks: List<Task>) {
        tasks.forEach { saveTask(it) }
    }
    
    override suspend fun saveTask(task: Task) {
        tasksService.createTask(dtoMapper.toDto(task))
    }
    
    override suspend fun deleteTask(id: String) {
        tasksService.deleteTask(id)
    }
}

DTO-实体映射器实现数据转换,隔离数据层与领域层:

kotlin
复制代码
class TaskDtoMapper {
    fun toDomain(dto: TaskDto): Task = Task(
        id = dto.id,
        title = dto.title,
        description = dto.description,
        isCompleted = dto.isCompleted,
        createdAt = dto.createdAt
    )
    
    fun toDto(domain: Task): TaskDto = TaskDto(
        id = domain.id,
        title = domain.title,
        description = domain.description,
        isCompleted = domain.isCompleted,
        createdAt = domain.createdAt
    )
}
3.2 "本地优先"策略与数据流实现

Repository实现"本地优先"策略:优先从本地数据库加载数据,同时后台同步远程数据并更新本地,确保UI展示的是最新且可用的数据:

kotlin
复制代码
class DefaultTasksRepository(
    private val localDataSource: TasksDataSource,
    private val remoteDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
    // 观察任务数据(本地优先+远程同步)
    override fun observeTasks(): Flow<Result<List<Task>>> = flow {
        // 1. 先发射本地数据(离线可用)
        localDataSource.observeTasks().collect { localTasks ->
            emit(Result.success(localTasks))
        }
    }.catch { e ->
        emit(Result.failure(e)) // 捕获本地观察异常
    }.flowOn(ioDispatcher).combine(syncRemoteTasks()) { localResult, syncResult ->
        // 2. 合并本地数据与远程同步结果
        when {
            localResult.isSuccess && syncResult.isSuccess -> localResult
            syncResult.isFailure -> Result.failure(syncResult.exceptionOrNull()!!)
            else -> localResult
        }
    }
    
    // 同步远程数据到本地
    private fun syncRemoteTasks(): Flow<Result<Unit>> = flow {
        try {
            val remoteTasks = remoteDataSource.getTasks()
            localDataSource.saveTasks(remoteTasks)
            emit(Result.success(Unit))
        } catch (e: Exception) {
            emit(Result.failure(e))
        }
    }.flowOn(ioDispatcher)
    
    // 添加任务(本地+远程双写)
    override suspend fun addTask(task: Task): Result<Unit> = withContext(ioDispatcher) {
        return@withContext try {
            // 1. 先保存到本地,确保UI即时更新
            localDataSource.saveTask(task)
            // 2. 同步到远程
            remoteDataSource.saveTask(task)
            Result.success(Unit)
        } catch (e: Exception) {
            // 远程同步失败时,可选择回滚本地或标记为待同步
            Result.failure(e)
        }
    }
    
    // 删除任务(本地+远程双删)
    override suspend fun deleteTask(id: String): Result<Unit> = withContext(ioDispatcher) {
        try {
            localDataSource.deleteTask(id)
            remoteDataSource.deleteTask(id)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // 其他方法实现...
}
3.3 Kotlin 2.2 Rich Errors异常处理

Kotlin 2.2引入的Rich Errors特性允许函数返回结果或错误类型,替代传统的异常抛出,使错误处理更显式:

kotlin
复制代码
// 定义错误类型
sealed interface DataError {
    data class Network(val message: String) : DataError // 网络错误
    data class Server(val code: Int) : DataError // 服务器错误
    data class Local(val message: String) : DataError // 本地数据库错误
    object Unknown : DataError // 未知错误
}

// 使用Rich Errors重构数据获取方法
suspend fun getTaskSafe(id: String): Task? | DataError {
    return try {
        remoteDataSource.getTaskById(id) ?: localDataSource.getTaskById(id)
    } catch (e: IOException) {
        DataError.Network("网络连接失败:${e.message}")
    } catch (e: HttpException) {
        DataError.Server(e.code())
    } catch (e: Exception) {
        DataError.Local("本地数据库错误:${e.message}")
    }
}

异常处理最佳实践:Repository应将底层异常转换为上层可理解的错误类型,避免UI层处理具体异常。通过Rich Errors或Result封装错误,使调用方能够明确处理成功与失败场景,提升代码健壮性。

四、ViewModel实现:状态管理与用户交互

ViewModel作为业务逻辑层的核心组件,负责管理UI状态、处理用户交互、调用Repository获取数据,并通过StateFlow将状态变化通知UI,实现业务逻辑与UI的解耦。

4.1 UI状态定义与管理

定义不可变的UI状态类,封装界面所需的所有数据和状态:

kotlin
复制代码
// UI状态数据类(不可变)
data class TasksUiState(
    val loading: Boolean = false,
    val tasks: List<TaskUiModel> = emptyList(),
    val error: String? = null
)

// 任务UI模型(转换领域模型为UI友好格式)
data class TaskUiModel(
    val id: String,
    val title: String,
    val description: String,
    val isCompleted: Boolean,
    val createdAt: String // 格式化后的时间字符串
)

class TasksViewModel(
    private val tasksRepository: TasksRepository,
    private val dateFormatter: DateTimeFormatter // 日期格式化工具
) : ViewModel() {
    // 私有可变状态
    private val _uiState = MutableStateFlow<TasksUiState>(TasksUiState(loading = true))
    // 公开不可变状态(供UI观察)
    val uiState: StateFlow<TasksUiState> = _uiState.asStateFlow()
    
    init {
        loadTasks() // 初始化加载任务
    }
    
    // 加载任务数据
    fun loadTasks() {
        viewModelScope.launch {
            _uiState.update { it.copy(loading = true, error = null) }
            
            tasksRepository.observeTasks().collect { result ->
                _uiState.update { state ->
                    when {
                        result.isSuccess -> {
                            val taskUiModels = result.getOrNull()?.map { task ->
                                TaskUiModel(
                                    id = task.id,
                                    title = task.title,
                                    description = task.description ?: "无描述",
                                    isCompleted = task.isCompleted,
                                    createdAt = dateFormatter.format(task.createdAt)
                                )
                            } ?: emptyList()
                            state.copy(loading = false, tasks = taskUiModels)
                        }
                        result.isFailure -> {
                            state.copy(
                                loading = false,
                                error = "加载失败:${result.exceptionOrNull()?.message}"
                            )
                        }
                        else -> state.copy(loading = false)
                    }
                }
            }
        }
    }
    
    // 添加任务
    fun addTask(title: String, description: String?) {
        if (title.isBlank()) {
            _uiState.update { it.copy(error = "任务标题不能为空") }
            return
        }
        
        viewModelScope.launch {
            _uiState.update { it.copy(loading = true, error = null) }
            
            val newTask = Task(
                title = title,
                description = description
            )
            
            val result = tasksRepository.addTask(newTask)
            if (result.isFailure) {
                _uiState.update { it.copy(
                    loading = false,
                    error = "添加失败:${result.exceptionOrNull()?.message}"
                ) }
            }
        }
    }
    
    // 删除任务
    fun deleteTask(id: String) {
        viewModelScope.launch {
            _uiState.update { it.copy(loading = true, error = null) }
            
            val result = tasksRepository.deleteTask(id)
            if (result.isFailure) {
                _uiState.update { it.copy(
                    loading = false,
                    error = "删除失败:${result.exceptionOrNull()?.message}"
                ) }
            }
        }
    }
}
4.2 数据流转换与生命周期安全

ViewModel通过viewModelScope确保协程在生命周期内执行,避免内存泄漏;使用StateFlow实现数据流的冷热转换,确保UI始终接收最新状态:

状态管理要点

  • 暴露不可变StateFlow给UI层,防止外部修改状态
  • 使用viewModelScope限定协程生命周期,自动取消未完成任务
  • 状态更新通过update方法原子操作,避免竞态条件
  • 数据转换(如领域模型→UI模型)放在ViewModel层,确保UI层只处理展示逻辑

数据流转换示例:将Repository返回的Flow<Result<List<Task>>>转换为UI状态:

kotlin
复制代码
// 过滤已完成任务
val activeTasks: StateFlow<List<TaskUiModel>> = _uiState.map { state ->
    state.tasks.filterNot { it.isCompleted }
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000), // 5秒后停止共享
    initialValue = emptyList()
)

// 计算任务统计
val taskStats: StateFlow<TaskStats> = _uiState.map { state ->
    TaskStats(
        total = state.tasks.size,
        completed = state.tasks.count { it.isCompleted },
        pending = state.tasks.count { !it.isCompleted }
    )
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly, // 立即开始共享
    initialValue = TaskStats(0, 0, 0)
)

data class TaskStats(
    val total: Int,
    val completed: Int,
    val pending: Int
)

总结

本章系统讲解了数据层与业务逻辑层的实现方案,从数据库设计的实体定义与迁移策略,到远程接口的Retrofit定义与MockWebServer测试,再到Repository的数据整合与异常处理,最终通过ViewModel实现状态管理与用户交互。核心亮点包括:

  1. 分层架构:通过数据源抽象、Repository中介、ViewModel解耦,实现关注点分离
  2. 响应式数据流:使用Flow和StateFlow实现数据的实时观察与高效更新
  3. 健壮性设计:"本地优先"策略确保离线可用,Rich Errors统一异常处理
  4. 可测试性:依赖注入和接口抽象使单元测试覆盖各层组件

这种架构设计不仅满足当前任务管理应用的需求,更提供了良好的扩展性,可轻松支持功能迭代和复杂度增长。下一章将聚焦UI层实现,讲解如何通过Jetpack Compose构建响应式界面,完成应用的最后一环。