在现代Android应用架构中,数据层与业务逻辑层的设计直接决定了应用的可维护性、可测试性和性能表现。本章将围绕任务管理应用场景,从数据库设计、远程接口定义、Repository实现到ViewModel状态管理,系统讲解数据层与业务逻辑层的完整实现方案,遵循"数据驱动UI、业务逻辑与UI解耦"的核心原则。
本地数据库是实现离线功能和数据缓存的核心组件,采用Room框架可大幅简化数据库操作并确保类型安全。任务管理应用的数据库设计需重点关注实体结构、索引策略及迁移机制,以支持高效的数据存取和版本演进。
Task实体类需包含业务所需的核心字段,并通过Room注解定义表结构。根据业务需求,实体类设计如下:
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),尤其在任务数量较大时效果显著。
应用迭代过程中数据库结构可能发生变更,Room提供了Migration机制确保数据安全迁移。假设当前数据库版本为1,需新增priority字段(任务优先级),迁移实现如下:
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
}
}
}
}
迁移最佳实践:
exportSchema,将Schema文件提交至版本控制系统,便于追踪结构变更fallbackToDestructiveMigration()快速迭代,生产环境必须使用显式迁移远程接口是应用与后端服务交互的桥梁,采用Retrofit定义接口契约,并通过MockWebServer实现本地模拟测试,可大幅提升开发效率和接口稳定性。
基于RESTful规范,定义任务管理API接口,包含获取、创建、更新、删除任务等操作:
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实现非阻塞调用@SerializedName处理命名差异(如蛇形命名转驼峰命名)为避免依赖后端服务,使用MockWebServer模拟API响应,实现接口的本地化测试:
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() // 关闭服务器释放资源
}
}
模拟测试优势:
Repository作为数据层的核心组件,负责整合本地与远程数据源,实现"本地优先"的数据访问策略,并统一处理数据转换和异常,为上层提供清晰的数据访问接口。
首先定义数据源接口,抽象本地与远程数据操作,便于测试和替换实现:
// 数据源接口
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-实体映射器实现数据转换,隔离数据层与领域层:
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
)
}
Repository实现"本地优先"策略:优先从本地数据库加载数据,同时后台同步远程数据并更新本地,确保UI展示的是最新且可用的数据:
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)
}
}
// 其他方法实现...
}
Kotlin 2.2引入的Rich Errors特性允许函数返回结果或错误类型,替代传统的异常抛出,使错误处理更显式:
// 定义错误类型
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作为业务逻辑层的核心组件,负责管理UI状态、处理用户交互、调用Repository获取数据,并通过StateFlow将状态变化通知UI,实现业务逻辑与UI的解耦。
定义不可变的UI状态类,封装界面所需的所有数据和状态:
// 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}"
) }
}
}
}
}
ViewModel通过viewModelScope确保协程在生命周期内执行,避免内存泄漏;使用StateFlow实现数据流的冷热转换,确保UI始终接收最新状态:
状态管理要点:
viewModelScope限定协程生命周期,自动取消未完成任务update方法原子操作,避免竞态条件数据流转换示例:将Repository返回的Flow<Result<List<Task>>>转换为UI状态:
// 过滤已完成任务
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实现状态管理与用户交互。核心亮点包括:
这种架构设计不仅满足当前任务管理应用的需求,更提供了良好的扩展性,可轻松支持功能迭代和复杂度增长。下一章将聚焦UI层实现,讲解如何通过Jetpack Compose构建响应式界面,完成应用的最后一环。