一个基于现代 Android 技术栈开发的抖音风格图文投稿简化实现,采用 Jetpack Compose 和 MVVM 架构模式。
本项目是抖音"投稿"页面的简化版本实现,专注于图文模式的投稿功能。用户可以选择和编辑图片,撰写文案,添加话题标签和@好友,最后发布内容。
- 图文内容编辑:支持多图选择、预览和拖拽编辑
- 文案撰写:标题和描述编辑,可添加话题标签(@)和@好友
- 草稿管理:本地存储功能,支持草稿保存和恢复
- 现代化 UI:基于 Material 3 设计规范的 Compose 界面
- 主题切换与国际化语言支持: 使用用户设置的主题模式与语言,在亮色与暗色、中英文之间自动切换

| 领域 | 技术 |
|---|---|
| 语言 | Kotlin |
| UI 框架 | Jetpack Compose |
| 架构模式 | MVVM |
| 本地存储 | Room + SQLite |
| 网络请求 | Retrofit + OkHttp |
| 异步处理 | Kotlin Coroutines + Flow |
| 图片加载 | Coil |
UI Layer (ViewModel + Compose)
↓
Data Layer (Repository + Room + Retrofit)
↓
Data Source (Local DB + Remote API)
@Entity(tableName = "drafts")
data class PostDraftEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val title: String = "",
val description: String = "",
val imagePaths: List<String> = emptyList(),
val tags: List<String> = emptyList(),
val mentions: List<String> = emptyList(),
val locationName: String? = null,
val latitude: Double? = null,
val longitude: Double? = null,
val updatedAt: Long = System.currentTimeMillis()
)用户交互 → Event → ViewModel → State → UI更新
- 图片选择:集成 Android Photo Picker API
- 图片预览:大图预览和封面设置
- 拖拽排序:支持图片顺序调整
- 删除操作:长按或拖拽到垃圾桶删除
以缩略图点击事件为例:
-
通过section的传递, 最终触发
event调用viewmodel中的具体方法:onSelectImage = { index -> viewModel.setEvent(PostContract.Event.SelectImage(index)) },
-
在viewModel中的实现:
is PostContract.Event.SelectImage -> { if (event.index in _uiState.value.currentDraft.imagePaths.indices) { _uiState.update { it.copy(selectedImageIndex = event.index) } } }
可以看到, 我们先判断传入的
index是否合法,然后利用postDraft作为数据类的特性,使用kotlin自动生成的copy方法创建一个新的对象, 只修改其中选中的图像index并赋值, 最终推动UI侧更新.
此处维护的
uiState是外部可读内部可写的对象, 这种设计可以避免多源数据同时改写一个UI造成的问题.
- 实时字数统计:显示当前字数和限制
- 话题标签:自动识别
#开头的标签,支持推荐话题 - @好友功能:自动识别
@后缀,支持搜索好友
- 自动定位:应用启动时自动获取经纬度
- 反地理编码:经纬度转换为可读地址
- 权限处理:动态请求位置权限
对应文件结构:
- Room Database:本地 SQLite 数据库存储
- 草稿自动保存:应用切换到后台时保存
- 草稿恢复:应用重新进入时加载最近草稿
整体架构中, Room核心组件之间的关系是:
-
实体: 应用数据库中的表;
-
DAO(数据访问对象): 提供了供应用在数据库中检索、更新、插入和删除数据的方法
-
Room Database: 数据库类, 提供与该数据库关联的 DAO 实例
之后, 通过repository搭建底层数据库与viewmodel之间的桥梁——定义面向VM提供的数据库访问接口:
interface PostRepository {
fun getAllDrafts(): Flow<List<PostDraftEntity>>
suspend fun getDraftById(id: String): PostDraftEntity?
suspend fun saveDraft(draft: PostDraftEntity)
suspend fun deleteDraftById(id: String)
}在实现这个接口的类(RepositoryImp)中, 调用Dao中定义的方法来分别实现函数.
e.g
class PostRepositoryImpl(
private val postDraftDao: PostDraftDao
) : PostRepository {
override fun getAllDrafts(): Flow<List<PostDraftEntity>> {
return postDraftDao.getAllDrafts()
}
override suspend fun getDraftById(id: String): PostDraftEntity? {
return postDraftDao.getDraftById(id)
}
override suspend fun saveDraft(draft: PostDraftEntity) {
postDraftDao.upsertDraft(draft)
}
override suspend fun deleteDraftById(id: String) {
postDraftDao.deleteDraftById(id)
}
}使用App Inspection功能查看数据库, 可以看到演示过程中的数据成功通过Room持久化存储
app/src/main/java/com/example/tiktok_postscreen/
├── data/
│ ├── local/ # Room数据库相关
│ │ ├── AppDatabase.kt # 数据库配置
│ │ ├── dao/ # 数据访问对象
│ │ └── entity/ # 实体类
│ └── repository/ # 数据仓库层
├── di/ # 依赖注入
│ └── AppContainer.kt # 依赖容器配置
├── ui/
│ ├── components/ # 可复用UI组件
│ ├── post/ # 主要功能页面
│ │ ├── PostScreen.kt # 主UI界面
│ │ ├── PostViewModel.kt # 业务逻辑
│ │ └── PostContract.kt # UI状态协议
│ └── theme/ # 主题和样式
└── utils/ # 工具类
├── ILocationHelper.kt # 位置服务接口
└── LocationHelper.kt # 位置服务实现
// 使用Flow进行状态管理
private val _uiState = MutableStateFlow(PostContract.State())
val uiState = _uiState.asStateFlow()
// Compose监听状态变化自动更新
val uiState by viewModel.uiState.collectAsStateWithLifecycle()// 在Compose中监听生命周期变化
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> viewModel.loadLatestDraft()
Lifecycle.Event.ON_STOP -> viewModel.saveDraftSilently()
}
}
}// Room自动生成类型安全的查询
@Dao
interface PostDraftDao {
@Query("SELECT * FROM drafts ORDER BY updatedAt DESC")
fun getAllDrafts(): Flow<List<PostDraftEntity>>
}


