2025年我用 Compose 写了一个 Todo App

news/2025/3/11 13:43:35/文章来源:https://www.cnblogs.com/wavky/p/18764850

标题党嫌疑犯实锤

序言

从2月12日到3月4日这整整三周时间里,我从零开始又学习了一次 Compose。

为什么说又,是因为这已经是我第二次学习这套课程了。

故事从 4 年前说起,2021 年在意外获悉扔物线朱凯老师准备发布一套名为 Compose 的新课程,意识到这是 Android 未来的方向,花重金从扔物线朱凯老师手里购买了这一套新鲜出炉的课程并为此沾沾自喜。

当时正值 Compose 正式版公布前后的时间点,朱老师作为推广 Compose 的亲善大使,在技术仍未稳定之时录制了第一期 Compose 课程,当时跟着朱老师系统地学习了一次。曾经也许计划过等待朱老师更新正式版课程后,再与朱老师重走长征路,但不出所料被我如期爽约。

之后 4 年里,完全没有再接触过 Compose 技术,所学知识一滴不漏地全部漏光。

在迎来 35 岁又数个月的今天,中年危机意识汹涌袭来,因而再次挑灯夜读,孤独地在每个月上柳梢头的饭后良宵,以及春意盎然睡意绵绵的周末,驻守在电脑面前,默默打开了朱老师的这个课程网站:https://edu.rengwuxian.com/p/t_pc/goods_pc_detail/goods_detail/course_2Dpw6101YdL7bHFs5LFpYyzSUS6

3422 分钟,70 节课程,似懂非懂一知半解不求甚解地再次游历一遍 Compose 的海洋,终于又再次站在了公司里 Compose 技术的顶点,登高望极,一览山下低头耕耘那片屎山的同事们。(同事里只有另一个人专职做安卓开发,而他估计还没学过我们朱老师的这套课程,所以此时山下只有一人)

2025年的第一个 Compose App

趁热打铁,赶在知识仍未漏光之前,着手编写了这个极简 Todo 应用。以功能极简,设计极简之名,将其命名为 Simple Todo。

原型设计

※ 出于嫌麻烦等不可控原因,实装中省略了时间部分的实现。

效果图:

项目源码:https://github.com/wavky/SimpleTodo-Compose-

技术框架:

  • Jetpack Compose
  • MVVM
  • Room
  • Koin

项目结构

项目整体采用 MVVM 结构:

  • UI 层使用 Compose
  • ViewModel 层提供数据与 UI 的耦合
  • UseCase 层提供数据增删改查等单点功能的业务逻辑封装
  • Domain 层定义底层数据仓库接口与数据源(文件、DB、API 等)
  • 最后通过 Koin 进行依赖注入,串连 Domain -> UseCase -> ViewModel -> UI 的数据单向流转

从 Room 到 ViewModel

Room 作为 DB 访问库,主要工作在 Domain 层。
在 domain \ infra \ db 目录中,放置所有 Room 定义的数据库文件。

Domain 层结构

  • infra 目录下的文件不对上层公开(UI 等上层类无法直接访问),只提供底层数据服务到 Repository。
  • model 目录定义对上层公开的数据层标准数据模型(data class)。
  • repository 目录向上层公开并提供数据服务,在 Repository 中将 Entity 类型转换成对等 Model 类型后传递到上层(UseCase 层)。
domain
├── infra
│   └── db
│       ├── AppDatabase.kt
│       ├── dao
│       │   └── TodoDao.kt
│       └── entity
│           └── TodoEntity.kt
├── model
│   └── Todo.kt
└── repository├── TodoRepository.kt└── TodoRepositoryImpl.kt

与 Compose 集成

Compose 中的第一方数据类型是 State 和 StateFlow,因此在 UI 需要订阅响应数据变化的时候,直接让 Room 返回 Flow 类型的查询结果,通过 ViewModel 传递到 UI 进行订阅绑定,这样在之后数据表发生数据变化时,Room 会自动重新执行一次查询并将结果通知到 UI 进行刷新。

  1. 在 Dao 中声明查询函数返回 Flow 类型,Room 在执行此类查询时会自动分发到线程池中执行。如果是只需单次执行的增删改查,则声明为 suspend 函数并直接返回普通结果类型。
@Dao
internal interface TodoDao {@Query("SELECT * FROM todo")fun getAll(): Flow<List<TodoEntity>>@Insertsuspend fun add(item: TodoEntity): Long
}
  1. 在 Repository 中将 Flow 中的 Entity 转换为标准 Model 类型。对于增删改函数,将参数中的 Model 类型转换成 Entity 类型。
internal class TodoRepositoryImpl(private val dao: TodoDao) : TodoRepository {override fun getAll(): Flow<List<Todo>> = dao.getAll().map { list ->list.map { it.convertToModel() }}override suspend fun add(todo: Todo): Long =dao.add(todo.convertToEntity())
}
  1. 在 UseCase 中根据需要整合业务逻辑(例如指派到 IO 线程执行),将查询结果 Flow 返回到上层(ViewModel)。
interface GetTodoListUseCase {fun execute(): Flow<List<Todo>>
}
  1. 在 ViewModel 中将冷流 Flow 转换为热流 StateFlow。StateFlow 相当于 LiveData,可缓存最后数据,并避免每次订阅时都重新执行数据库查询。
    对于 Flow 类型的查询,直接将结果保存在 val 变量;对于单次执行的增删改查,则通过 viewModelScope 启动协程来执行。
class MainViewModel(getTodoListUseCase: GetTodoListUseCase,private val addTodoUseCase: AddTodoUseCase,
) : ViewModel() {val todoList: StateFlow<List<Todo>> = getTodoListUseCase.execute().stateIn(viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList())fun addTodo(title: String, description: String) {viewModelScope.launch {addTodoUseCase.execute(title, description)}}
}
  1. 在 UI 层的 Composable 函数中,订阅 ViewModel 的 StateFlow 并转换成 State 类型,提供给 Composable 进行绑定。
val todoList by viewModel.todoList.collectAsState()LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {items(todoList, key = { item -> item.id }) { item ->TodoItem(item.title, item.isDone) { isChecked ->viewModel.updateTodo(item.copy(isDone = isChecked))}}
}

至此,完成了数据从 DB -> Repository -> UseCase -> ViewModel -> UI 的单向传输。在增删改动作发生时,数据表中数据发生变化都会触发 Flow 类型查询的执行,并将最新结果通知到 UI 进行渲染输出,实现数据订阅刷新。


集成 Koin

Koin 用于提供依赖注入,在项目中连接 Repository -> UseCase -> ViewModel -> UI(Composable)。

这一节内容用于引流,有兴趣的同学请移步至小站:https://wavky.top/SimpleTodo/


(我看看是谁在白嫖)


解决 ViewModel 与预览功能的冲突

在 Android Studio 中,如果 Composable 函数依赖 ViewModel,IDE 的预览功能会无法正常工作。
因为 ViewModel 是属于 Android 环境中的对象,并且我们是通过依赖注入方式动态获取的,IDE 的预览功能无法对此类对象进行有效的实例化工作。

破局方法是,将 ViewModel 的功能接口化,将 ViewModel 中所有公开的函数抽离到上层接口中,将 Composable 中对 ViewModel 的依赖转变成对一个普通的回调接口类型的依赖。

以 MainViewModel 为例

在 MainViewModel 的实现中,添加声明实现一个同名接口 MainViewModelFunc:

class MainViewModel(getTodoListUseCase: GetTodoListUseCase,private val addTodoUseCase: AddTodoUseCase,...// 添加实现接口声明 MainViewModelFunc
) : ViewModel(), MainViewModelFunc {val todoList: StateFlow<List<Todo>> = getTodoListUseCase.execute().stateIn(emptyList())override fun addTodo(title: String, description: String) {...}override fun updateTodo(todo: Todo) {...}
}

将 MainViewModel 中的公开函数抽离到接口 MainViewModelFunc 中:

interface MainViewModelFunc {fun addTodo(title: String, description: String)fun updateTodo(todo: Todo)
}

在 UI 的 Composable 中分离对 ViewModel 与 MainViewModelFunc 的依赖:

// MainScreen 依赖 ViewModel,但不能被直接预览,实际预览的是内容函数 MainScreenContent
@Composable
fun MainScreen(modifier: Modifier = Modifier, viewModel: MainViewModel = koinViewModel(),
) {// 提取出需要直接访问的 ViewModel 中的变量对象(StateFlow)val todoList by viewModel.todoList.collectAsState()// 将 Composable 的所有 UI 内容封装成子 ComposableMainScreenContent(modifier, todoList, viewModel)
}// MainScreenContent 中依赖 ViewModel 的功能接口
@Composable
fun MainScreenContent(modifier: Modifier = Modifier,todoList: List<Todo>,// 对 ViewModel 的依赖改为对接口的依赖viewModel: MainViewModelFunc
) {Scaffold(...) {...}
}// 实现 Composable 内容的预览
@Preview(showBackground = true)
@Composable
private fun PreviewMainScreen() {MainScreenContent(todoList = (1..10).map { Todo(...) },// 实现一个 ViewModel 功能接口的空实现对象viewModel = object : MainViewModelFunc {override fun addTodo(title: String, description: String) {}override fun updateTodo(todo: Todo) {}override fun deleteTodo(todo: Todo) {}})
}

至此完成对 MainScreen 的预览。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/897239.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu设置静态IP——NetworkManager方式

1、直接在系统界面上设置静态IP的方式,不再赘述 2、命令行方式查看已经有哪些工具#查看状态 sudo systemctl status Netplan sudo systemctl status NetworkManager sudo systemctl status systemd-networkd sudo systemctl status NetworkManager出现Active,说明电脑已经安装…

《Quick Start Kubernetes》读后感

一、 为什么选择这本书? 面试的时候经常被问到 kubernetes(下称 k8s),所以打算学习 k8s。看到《Quick Start Kubernetes》的作者对自己所写的书持续地更新,被这种认真打动了,外加这本书只有100多页,所以选择了这本书作为入门 k8s 的教材。 二、这本书写了什么? 这本书介绍…

正交实验法python实现

1.等水平正交表 每个条件下的种类一样多 例1: 这是一个7因子2状态 列表里内部每一个[]表示一个因子,然后每个因子都有2种类型 #7因子2状态 from allpairspy import AllPairs parameters = [["Chrome", "Firefox"],#因子1有"Chrome"或"Fir…

如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统

本文将提供一个详细的示例,展示如何调用 DeepSeek 的自然语言处理 API 接口。我们将以情感分析为例,演示如何发送请求、处理响应以及处理可能的错误。我在业余时间开发了一款自己的独立产品:升讯威在线客服与营销系统。陆陆续续开发了几年,从一开始的偶有用户尝试,到如今线…

使用 Pixi.js 插件实现探险者小游戏(一)

什么是 Pixi Pixi 是一个非常快的 2D sprite 渲染引擎。使用它你可以轻松的利用 JavaScript 和其他 HTML5 技术制作游戏和应用程序。 Pixi 的官网地址:https://pixijs.com/ 本游戏使用的是 Pixi 的 V4.5.5 版本,官网最新版本更新到了 V8.x,两个版本 API 相差很大,建议大家学…

【设计模式】利用组合模式带你走进树形结构的世界

概述对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员…

20250311

1. 沪镍还有两个上涨波段

【设计模式】从智能音箱到软件设计:探索外观模式的实际应用案例

概述 有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于…

PMC必须要懂的四个关键流程:生产、库存、交期全过程解析!

PMC(生产计划与物料控制)这个岗位,看起来就是三个字,但实际干起来,简直是让人上蹿下跳、手忙脚乱。一边要盯着生产线, 一边要和供应商、采购、销售对接,稍微一个环节没控好,就可能导致生产停滞、库存爆仓、交期延误,直接影响公司运营。 很多PMC天天在救火,但其实掌握…

JavaScript HTML DOM - 改变 HTML 功能 用法运用 详解

JavaScript中的HTML DOM提供了强大的功能来改变HTML文档的内容和结构。通过JavaScript,我们可以动态地更新网页上的文本、属性、样式以及整个HTML结构。以下是对这些功能的详细解释和用法示例: 一、改变HTML内容使用innerHTML:innerHTML属性用于获取或设置元素的HTML内容。这…

20241905 2024-2025-2 《网络攻防实践》 第2次作业

1. 实验内容 本次实验为网络信息收集技术,主要有以下五个任务选择一个DNS域名进行查询获取信息 通过IP地址查询地理位置的信息 使用nmap扫描靶机环境 使用nessus扫描靶机环境 通过搜索引擎查询自己的隐私和信息泄露问题结合实验内容阅读书本,总结知识如下:网络踩点:攻击者通…

Ubuntu 20.4安装.Net 9

20.4默认包时没有.Net 9 需要用脚本形式安装wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh给执行权限chmod +x ./dotnet-install.sh可以通过 --channel 参数更改特定主要版本来指示特定版本。 以下命令安装 .NET 9.0 SDK。./dotnet-install.sh --channel 9.…