5557 字
28 分钟
Kotlin + Jetpack Compose 从零开发 Android 日记 App 完整入门教程

Kotlin + Jetpack Compose 从零开发 Android 日记 App 完整入门教程#

前言#

本文面向掌握 Kotlin 基础、零基础 Android 开发的编程学习者,将手把手带你从零搭建一款完整的 Android 本地日记 App。

全程采用目前 Google 官方推荐的最新技术栈:Kotlin + Jetpack Compose 全声明式UI + Room本地数据库 + MVVM架构 + Hilt依赖注入 + Coroutines协程。

文章内容极致详细,涵盖项目搭建、技术原理、分层架构、核心代码逐行实现、知识点讲解、项目运行与拓展优化,零基础可跟着复刻,完成后你将掌握现代 Android 主流开发范式,具备独立开发小型本地存储类 App 的能力。

一、项目背景与功能介绍#

1.1 项目背景#

在 Android 开发学习路径中,本地数据存储 + 页面增删改查是最核心、最基础的必备技能。日记 App 是 Android 入门最优实战项目之一,业务逻辑简单、功能闭环,完美贴合新手学习需求。

传统 Android 开发依赖 XML 布局、繁琐的生命周期管理,学习成本极高。而本项目采用 Jetpack Compose 声明式UI,抛弃XML,纯Kotlin代码编写界面,代码更简洁、逻辑更清晰,适配现代安卓开发规范。

同时结合官方组件完成数据持久化、异步处理、架构解耦,让项目规范、可拓展、企业级标准化,而非野路子demo。

1.2 核心功能#

本日记App实现完整的闭环业务功能,所有数据本地离线存储,无需网络:

  1. 日记新增:用户输入标题、正文、自动记录创建时间,保存本地数据库

  2. 日记查询:首页展示所有日记列表,按时间倒序排列(最新日记置顶)

  3. 日记修改:点击已有日记,进入编辑页面修改内容并更新数据

  4. 日记删除:支持单条日记删除操作

  5. 实时刷新:数据增删改后,页面自动刷新,无需手动刷新

  6. 状态管理:页面加载、数据为空等状态友好展示

1.3 项目学习价值#

  1. 掌握 MVVM 分层架构核心思想,彻底告别代码耦合

  2. 精通 Room 数据库本地持久化存储(安卓首选本地数据库框架)

  3. 熟练使用 Jetpack Compose 开发完整页面

  4. 理解 Kotlin 协程 & Flow 数据流异步数据处理机制

  5. 掌握 Hilt 依赖注入,实现组件解耦、统一管理依赖

二、全套技术栈选型详解#

本项目全部采用 Google 官方推荐 Jetpack 组件,无第三方冗余框架,是目前 Android 原生开发标准企业级技术栈。

2.1 核心技术清单#

  1. Kotlin:Android 官方首选开发语言,空安全、高阶函数、协程原生支持

  2. Jetpack Compose:新一代声明式UI框架,纯代码构建界面,替代传统XML

  3. Room Database:安卓官方ORM本地数据库,基于SQLite封装,简化数据库操作

  4. MVVM 架构:Model-View-ViewModel,实现视图与数据逻辑解耦

  5. Hilt:Google官方依赖注入框架,自动管理所有组件依赖

  6. Kotlin Coroutines 协程:轻量级异步任务处理,替代Handler、AsyncTask

  7. Kotlin Flow 数据流:响应式数据流,监听数据库数据实时变化

  8. Jetpack Lifecycle:生命周期感知组件,杜绝内存泄漏

2.2 技术选型详细说明#

2.2.1 放弃传统XML,选用Jetpack Compose#

传统Android XML布局存在布局嵌套冗余、代码分离、适配繁琐等问题。Jetpack Compose 采用声明式编程,UI随数据自动刷新,代码量减少60%以上,逻辑统一,新手更容易理解。

2.2.2 选用Room替代原生SQLite#

原生SQLite需要手写SQL语句、操作游标、处理数据映射,极易出错。Room对SQLite进行高度封装,通过注解自动生成数据库代码,支持对象化操作、异步协程、数据流监听,是本地存储最优方案。

2.2.3 MVVM架构(核心解耦)#

摒弃传统MVC架构的代码混乱问题:

  • View(视图层):只负责UI展示,无任何业务逻辑

  • ViewModel(视图模型层):承载业务逻辑、状态管理,生命周期独立于View

  • Model(数据层):包含实体类、数据库、仓库,只负责数据存取

三层完全解耦,便于维护、测试、迭代拓展。

2.2.4 Hilt依赖注入#

手动创建对象会导致代码冗余、耦合严重。Hilt自动依赖注入,统一管理所有数据库、仓库、ViewModel对象实例,极简代码,符合工程化规范。

2.2.5 协程+Flow异步数据流#

协程解决Android主线程阻塞问题,所有数据库读写均在子线程完成; Flow实现数据订阅,数据库数据发生变化时,自动推送最新数据到UI,实现页面无感刷新。

三、项目整体结构详解#

3.1 标准化分层结构#

本项目严格按照 MVVM + 分层架构 划分目录,每一层职责单一,新手必须熟记分层思想:

  1. entity(实体层):定义数据库表结构,对应日记实体类

  2. dao(数据访问层):定义数据库增删改查抽象方法

  3. database(数据库核心层):Room数据库初始化、单例配置

  4. repository(仓库层):统一封装数据操作,对接数据库,提供数据接口给ViewModel

  5. viewmodel(视图模型层):管理UI状态、调用仓库层业务方法

  6. ui(视图层):Compose所有页面UI布局、组件展示

  7. di(依赖注入层):Hilt依赖配置,管理全局对象

  8. util(工具层):通用工具类、时间格式化、常量类

3.2 完整项目目录预览#

com.example.diary/
├── di/ // 依赖注入配置
│ └── AppModule.kt
├── entity/ // 数据库实体类
│ └── Diary.kt
├── dao/ // 数据库操作接口
│ └── DiaryDao.kt
├── database/ // 数据库初始化
│ └── DiaryDatabase.kt
├── repository/ // 数据仓库
│ └── DiaryRepository.kt
├── viewmodel/ // 视图模型&状态
│ ├── DiaryUiState.kt
│ └── DiaryViewModel.kt
├── ui/ // 所有UI页面
│ ├── DiaryListPage.kt
│ └── DiaryEditPage.kt
├── util/ // 工具类
│ └── DateUtil.kt
└── DiaryApp.kt // 项目入口Application

3.3 各层级核心职责#

  1. 实体层:映射数据库数据表,定义字段、主键、约束

  2. DAO层:定义所有数据库操作的抽象方法,Room自动实现

  3. 数据库层:创建数据库实例、绑定DAO、数据库版本管理

  4. 仓库层:中转层,隔离ViewModel和数据库,统一数据入口

  5. ViewModel层:接收UI操作,调用仓库方法,保存UI状态

  6. UI层:监听ViewModel状态变化,渲染页面,触发用户操作

四、项目环境初始化配置#

4.1 新建Android项目#

  1. 打开Android Studio,选择「New Project」

  2. 模板选择「Empty Compose Activity」

  3. 语言选择 Kotlin,最小SDK版本选择 Android 24(Android 7.0)

  4. 完成项目创建

4.2 全局依赖配置(build.gradle)#

项目需要手动引入Room、Hilt、协程核心依赖,替换项目根目录及模块gradle配置。

4.2.1 项目根目录 build.gradle#

plugins {
id 'com.android.application' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
// Hilt 插件
id 'com.google.dagger.hilt.android' version '2.48' apply false
}

4.2.2 模块目录 build.gradle(核心依赖)#

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
namespace "com.example.diary"
compileSdk 34
defaultConfig {
applicationId "com.example.diary"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "1.5.3"
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
// 基础Compose依赖
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
// Room 数据库核心依赖
implementation 'androidx.room:room-runtime:2.6.1'
kapt 'androidx.room:room-compiler:2.6.1'
// Room 协程、Flow 支持
implementation 'androidx.room:room-ktx:2.6.1'
// Hilt 依赖注入
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-android-compiler:2.48'
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
// 协程核心依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// 测试依赖
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}

配置完成后点击 Sync Now 同步所有依赖,等待依赖下载完成。

五、核心代码逐层实现(带超详细注释)#

5.1 项目入口配置(Hilt初始化)#

新建 DiaryApp.kt 项目入口文件,Hilt必须通过Application类初始化。

package com.example.diary
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
/**
* 项目全局入口类
* Hilt 依赖注入初始化必须注解
* 所有组件依赖自动托管
*/
@HiltAndroidApp
class DiaryApp : Application()

同时打开 AndroidManifest.xml,绑定Application:

<application
android:name=".DiaryApp"
</application>

5.2 工具类:时间格式化工具#

新建 util/DateUtil.kt,统一处理日记时间展示格式

package com.example.diary.util
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 时间工具类
* 用于日记创建时间格式化展示
*/
object DateUtil {
// 定义统一时间格式:年-月-日 时:分
private val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA)
/**
* 获取当前系统时间
*/
fun getCurrentTime(): String {
return sdf.format(Date())
}
}

5.3 第一层:数据库实体类 Entity(Diary)#

新建 entity/Diary.kt,定义日记数据表结构,一个实体类对应一张数据库表

package com.example.diary.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* 日记实体类
* Room 数据表映射类
* tableName = "diary" 定义数据库表名
*/
@Entity(tableName = "diary")
data class Diary(
// 主键,自增id,唯一标识单条日记
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
// 日记标题
val title: String,
// 日记正文内容
val content: String,
// 日记创建时间
val createTime: String
)

核心知识点:

  • @Entity:Room注解,标识该类为数据库表

  • @PrimaryKey(autoGenerate = true):主键自增,无需手动赋值

  • 所有字段自动映射为数据库列

5.4 第二层:DAO数据访问接口#

新建 dao/DiaryDao.kt,定义数据库增删改查抽象方法,Room自动生成实现类

package com.example.diary.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.example.diary.entity.Diary
import kotlinx.coroutines.flow.Flow
/**
* 数据库数据访问接口
* 所有数据库操作方法定义在这里
*/
@Dao
interface DiaryDao {
/**
* 新增日记
* suspend:协程挂起函数,自动在子线程执行
*/
@Insert
suspend fun insertDiary(diary: Diary)
/**
* 更新日记
* 根据主键id匹配更新整条数据
*/
@Update
suspend fun updateDiary(diary: Diary)
/**
* 删除单条日记
*/
@Delete
suspend fun deleteDiary(diary: Diary)
/**
* 查询所有日记
* Flow<Diary>> 数据流形式
* 数据库数据变化时自动回调最新数据,实现页面自动刷新
* ORDER BY id DESC 按id倒序,最新日记在最顶部
*/
@Query("SELECT * FROM diary ORDER BY id DESC")
fun getAllDiary<Diary>>
}

核心知识点:

  1. @Dao:标识为数据访问接口

  2. suspend:协程挂起函数,所有数据库读写必须在子线程,避免主线程阻塞崩溃

  3. Flow:响应式数据流,永久监听数据表变化,无需手动刷新

5.5 第三层:Room数据库初始化#

新建 database/DiaryDatabase.kt,全局唯一数据库实例

package com.example.diary.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.example.diary.dao.DiaryDao
import com.example.diary.entity.Diary
/**
* Room数据库核心类
* entities:绑定所有数据表实体类
* version:数据库版本号
* exportSchema:关闭数据库日志导出
*/
@Database(entities = [Diary::class], version = 1, exportSchema = false)
abstract class DiaryDatabase : RoomDatabase() {
// 暴露DAO操作接口
abstract fun diaryDao(): DiaryDao
}

5.6 第四层:Hilt依赖注入模块#

新建 di/AppModule.kt,提供数据库单例对象,全局依赖注入

package com.example.diary.di
import android.content.Context
import androidx.room.Room
import com.example.diary.database.DiaryDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Hilt 依赖注入模块
* 提供全局单例对象
*/
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
/**
* 提供全局唯一Room数据库实例
*/
@Provides
@Singleton
fun provideDiaryDatabase(@ApplicationContext context: Context): DiaryDatabase {
return Room.databaseBuilder(
context,
DiaryDatabase::class.java,
"diary_database" // 数据库文件名
).build()
}
/**
* 提供DAO实例
*/
@Provides
fun provideDiaryDao(database: DiaryDatabase) = database.diaryDao()
}

5.7 第五层:Repository数据仓库#

新建 repository/DiaryRepository.kt,ViewModel唯一数据入口,隔离视图与数据库

package com.example.diary.repository
import com.example.diary.dao.DiaryDao
import com.example.diary.entity.Diary
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* 数据仓库层
* 所有数据操作统一中转
* ViewModel 只调用仓库方法,不直接操作数据库
*/
class DiaryRepository @Inject constructor(
private val diaryDao: DiaryDao
) {
// 注入DAO对象,由Hilt自动实例化
/**
* 新增日记
*/
suspend fun insertDiary(diary: Diary) {
diaryDao.insertDiary(diary)
}
/**
* 更新日记
*/
suspend fun updateDiary(diary: Diary) {
diaryDao.updateDiary(diary)
}
/**
* 删除日记
*/
suspend fun deleteDiary(diary: Diary) {
diaryDao.deleteDiary(diary)
}
/**
* 获取所有日记数据流
*/
fun getAllDiaryList(): Flow<Diary>> = diaryDao.getAllDiary()
}

分层核心意义:后续如果更换数据源(本地/网络),只需修改仓库层,无需改动UI和ViewModel。

5.8 第六层:UI状态管理#

新建 viewmodel/DiaryUiState.kt,统一管理页面所有状态

package com.example.diary.viewmodel
import com.example.diary.entity.Diary
/**
* 页面UI状态密封类
* 统一管理页面数据状态
*/
data class DiaryUiState(
// 日记列表数据
val<Diary> = emptyList(),
// 当前编辑的日记标题
val currentTitle: String = "",
// 当前编辑的日记内容
val currentContent: String = "",
// 当前编辑的日记实体(区分新增/编辑)
val currentDiary: Diary? = null
)

5.9 第七层:ViewModel业务逻辑层#

新建 viewmodel/DiaryViewModel.kt,承载所有业务逻辑、状态更新、异步请求

package com.example.diary.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.diary.entity.Diary
import com.example.diary.repository.DiaryRepository
import com.example.diary.util.DateUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* 日记视图模型
* 所有业务逻辑、状态管理、异步操作在这里实现
*/
@HiltViewModel
class DiaryViewModel @Inject constructor(
private val repository: DiaryRepository
) : ViewModel() {
// 可变UI状态
private val _uiState = MutableStateFlow(DiaryUiState())
// 只读UI状态,暴露给UI层使用
val uiState<DiaryUiState> = _uiState.asStateFlow()
init {
// 初始化:监听数据库日记列表数据变化
observeDiaryList()
}
/**
* 监听日记列表数据流,实时更新UI状态
*/
private fun observeDiaryList() {
viewModelScope.launch {
repository.getAllDiaryList().collect { list ->
// 更新状态,刷新页面列表
_uiState.update {
it.copy(diaryList = list)
}
}
}
}
/**
* 更新输入的标题
*/
fun updateTitle(title: String) {
_uiState.update {
it.copy(currentTitle = title)
}
}
/**
* 更新输入的正文
*/
fun updateContent(content: String) {
_uiState.update {
it.copy(currentContent = content)
}
}
/**
* 保存日记(新增/更新)
*/
fun saveDiary() {
val title = uiState.value.currentTitle
val content = uiState.value.currentContent
// 简单非空校验
if (title.isBlank() || content.isBlank()) return
viewModelScope.launch {
val currentDiary = uiState.value.currentDiary
if (currentDiary == null) {
// 新增日记
val diary = Diary(
title = title,
content = content,
createTime = DateUtil.getCurrentTime()
)
repository.insertDiary(diary)
} else {
// 更新已有日记
val updateDiary = currentDiary.copy(
title = title,
content = content
)
repository.updateDiary(updateDiary)
}
}
// 保存后清空输入框
clearInput()
}
/**
* 删除单条日记
*/
fun deleteDiary(diary: Diary) {
viewModelScope.launch {
repository.deleteDiary(diary)
}
}
/**
* 进入编辑页面,回填日记数据
*/
fun editDiary(diary: Diary) {
_uiState.update {
it.copy(
currentDiary = diary,
currentTitle = diary.title,
currentContent = diary.content
)
}
}
/**
* 清空输入框,重置编辑状态
*/
fun clearInput() {
_uiState.update {
it.copy(
currentTitle = "",
currentContent = "",
currentDiary = null
)
}
}
}

5.10 第八层:Compose UI界面实现#

5.10.1 日记列表页面#

新建 ui/DiaryListPage.kt,首页日记展示、删除、跳转编辑

package com.example.diary.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsStateWithLifecycle
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.example.diary.R
import com.example.diary.entity.Diary
import com.example.diary.viewmodel.DiaryViewModel
/**
* 日记列表首页
*/
@Composable
fun DiaryListPage(
viewModel: DiaryViewModel,
onAddClick: () -> Unit,
onItemClick: () -> Unit
) {
// 监听UI状态,生命周期安全订阅
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
Scaffold(
floatingActionButton = {
// 新增日记悬浮按钮
FloatingActionButton(onClick = onAddClick) {
Icon(
painter = painterResource(id = R.drawable.ic_add),
contentDescription = "新增日记"
)
}
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
Text(
text = "我的日记",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 20.dp)
)
// 空数据提示
if (uiState.diaryList.isEmpty()) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "暂无日记,点击右下角新增吧~")
}
} else {
// 日记列表懒加载
LazyColumn {
items(uiState.diaryList) { diary ->
DiaryItem(
diary = diary,
onItemClick = {
viewModel.editDiary(diary)
onItemClick()
},
onDeleteClick = {
viewModel.deleteDiary(diary)
}
)
}
}
}
}
}
}
/**
* 单条日记Item组件
*/
@Composable
fun DiaryItem(
diary: Diary,
onItemClick: () -> Unit,
onDeleteClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
elevation = CardDefaults.cardElevation(4.dp),
onClick = onItemClick
) {
Column(
modifier = Modifier.padding(16.dp)
) {
// 日记标题
Text(
text = diary.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
// 日记内容(截取前50字预览)
Text(
text = if (diary.content.length > 50) diary.content.take(50) + "..." else diary.content,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// 时间+删除按钮行
androidx.compose.foundation.layout.Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = diary.createTime,
style = MaterialTheme.typography.bodySmall
)
androidx.compose.material3.TextButton(onClick = onDeleteClick) {
Text(text = "删除", color = androidx.compose.ui.graphics.Color.Red)
}
}
}
}
}

5.10.2 日记编辑页面#

新建 ui/DiaryEditPage.kt,新增/编辑日记统一页面

package com.example.diary.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsStateWithLifecycle
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.diary.viewmodel.DiaryViewModel
/**
* 日记新增/编辑页面
* 复用同一页面,根据状态区分新增/编辑模式
*/
@Composable
fun DiaryEditPage(
viewModel: DiaryViewModel,
onBack: () -> Unit
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
val isEdit = uiState.currentDiary != null
Scaffold(
modifier = Modifier.fillMaxSize()
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(20.dp)
) {
// 页面标题
Text(
text = if (isEdit) "编辑日记" else "新增日记",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 24.dp)
)
// 标题输入框
BasicTextField(
value = uiState.currentTitle,
onValueChange = viewModel::updateTitle,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
textStyle = MaterialTheme.typography.titleLarge,
placeholder = {
Text(text = "请输入日记标题")
}
)
// 内容输入框
BasicTextField(
value = uiState.currentContent,
onValueChange = viewModel::updateContent,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
textStyle = MaterialTheme.typography.bodyLarge,
placeholder = {
Text(text = "请输入日记内容")
}
)
// 保存按钮
Button(
onClick = {
viewModel.saveDiary()
onBack()
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp),
enabled = uiState.currentTitle.isNotBlank() && uiState.currentContent.isNotBlank()
) {
Text(text = "保存日记")
}
}
}
}

5.11 主页面路由整合#

修改默认 MainActivity.kt,实现页面跳转逻辑

package com.example.diary
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.diary.ui.DiaryEditPage
import com.example.diary.ui.DiaryListPage
import com.example.diary.viewmodel.DiaryViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var viewModel: DiaryViewModel
// 页面状态标识
private var isEditPage = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
if (isEditPage) {
DiaryEditPage(
viewModel = viewModel,
onBack = {
isEditPage = false
}
)
} else {
DiaryListPage(
viewModel = viewModel,
onAddClick = {
// 新增:清空旧数据,跳转编辑页
viewModel.clearInput()
isEditPage = true
},
onItemClick = {
// 编辑已有日记
isEditPage = true
}
)
}
}
}
}
}

六、核心关键知识点深度讲解#

6.1 Kotlin 协程(Coroutines)#

6.1.1 核心作用#

Android 禁止主线程耗时操作(数据库读写、网络请求),否则会造成ANR闪退。

协程是Kotlin轻量级异步框架,优势:

  1. 轻量:数千个协程同时运行无压力

  2. 代码同步写法,异步执行,可读性极强

  3. 生命周期可控,杜绝内存泄漏

6.1.2 本项目协程使用场景#

  1. viewModelScope.launch:ViewModel专属协程作用域,页面销毁自动取消任务

  2. 所有Room数据库增删改查方法,全部通过协程在子线程执行

  3. 数据流监听持续运行,生命周期安全

6.2 Kotlin Flow 响应式数据流#

6.2.1 核心作用#

Flow 是可冷启动、可订阅、可监听变化的数据流,区别于普通List:

  • List:一次性获取数据,不会自动更新

  • Flow:持续监听数据源变化,数据改变自动推送最新结果

6.2.2 项目核心价值#

本项目无需手动刷新页面,新增/删除/修改日记后,页面自动更新,核心原理就是 Flow 实时订阅数据库数据。

6.3 Hilt 依赖注入核心原理#

  1. 无需手动new对象:所有DAO、仓库、ViewModel对象由Hilt自动创建

  2. 单例统一管理:数据库全局唯一实例,避免重复创建浪费资源

  3. 彻底解耦:类与类之间无硬依赖,便于维护测试

6.4 MVVM架构核心优势#

  1. View只负责展示:UI页面无业务逻辑,纯粹渲染数据

  2. ViewModel承载逻辑:独立于页面生命周期,屏幕旋转不丢失数据

  3. Model纯数据层:只负责数据存取,与UI完全无关

  4. 低耦合高复用:任意层级修改不影响其他层级

七、项目运行效果说明#

7.1 初始化效果#

  1. 首次打开App,数据库为空,页面展示「暂无日记,点击右下角新增吧~」提示

  2. 右下角悬浮按钮为新增日记入口

7.2 新增日记流程#

  1. 点击悬浮按钮,跳转编辑页面

  2. 输入标题、正文,点击保存

  3. 自动记录当前时间,返回列表页,实时展示新增日记

7.3 编辑日记流程#

  1. 点击任意日记Item,自动回填原有标题和内容

  2. 修改内容后点击保存,数据实时更新

7.4 删除日记流程#

  1. 点击日记底部「删除」按钮,即刻删除对应数据

  2. 列表自动刷新,删除的日记消失

7.5 核心特性效果#

  1. 数据持久化:退出App、重启手机,日记数据不会丢失

  2. 实时刷新:所有操作无需手动刷新页面

  3. 生命周期安全:屏幕旋转、页面重建,数据不丢失、无内存泄漏

八、项目总结与拓展优化建议#

8.1 项目总结#

本项目完整复刻了企业级Android MVVM开发流程,从零实现了本地日记App,覆盖安卓开发核心必备知识点:

  1. Jetpack Compose声明式UI开发

  2. Room本地数据库增删改查+数据流监听

  3. Kotlin协程异步任务处理

  4. Hilt依赖注入工程化管理

  5. MVVM分层架构规范开发

对于Android入门学习者来说,本项目是打通基础到进阶的核心实战项目,掌握后可独立开发备忘录、记事本、日志记录等同类App。

8.2 进阶拓展优化建议(可自主迭代)#

  1. 增加搜索功能:根据日记标题、正文关键词模糊搜索

  2. 增加时间筛选:按年月日筛选对应日记

  3. 增加日记收藏功能:新增收藏字段,区分普通/收藏日记

  4. 增加密码锁功能:App启动密码验证,保护隐私

  5. 增加数据备份/恢复:导出数据库文件、本地备份恢复

  6. 增加主题切换:深色/浅色模式切换

  7. 增加日记标签分类:给日记添加标签,分类展示

  8. 优化UI样式:增加动画、渐变、卡片阴影,提升颜值

8.3 学习后续方向#

  1. 学习 Retrofit + OkHttp 网络请求,对接云端接口

  2. 学习 Compose导航组件,实现多页面正式路由

  3. 学习 权限管理、文件存储 高级用法

  4. 学习 单元测试,保证项目稳定性

结语#

通过本教程,你已经完成了从Kotlin语法基础到完整Android项目落地的跨越。本项目完全遵循Google官方开发规范,无野路子代码,是入门Android原生开发最标准、最优质的实战案例。

熟练掌握本项目的架构思想和组件用法,你将彻底摆脱新手阶段,具备独立开发Android小型应用的能力。

(注:部分内容可能由 AI 生成)

Kotlin + Jetpack Compose 从零开发 Android 日记 App 完整入门教程
https://iyu6.com/posts/kotlin_android/
作者
yuyuovo
发布于
2026-08-04
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...