从零开始:用 Android Studio 开发一个 AI 智能日记 App

我最近完成了一个个人项目------一个能帮我整理日记的 Android App。整个开发过程踩了不少坑,也学到了很多东西。写这篇文章的目的,是想把我从完全不懂 Android 开发,到做出一个能用的 App 的过程记录下来,希望对同样想入门移动端开发的朋友有所帮助。

这不是一篇面面俱到的教科书,而是一个真实项目的开发笔记。我会尽量少讲理论,多讲实操,遇到什么问题就说什么问题。

一、先聊聊这个项目是干什么的

在开始写代码之前,我想先说说这个 App 要解决的问题。

我有记日记的习惯,但总是坚持不下去。原因很简单:每天晚上想起来今天发生了什么,脑子里一片空白。等想起来的时候,又懒得动笔(打字)。

后来我在 YouTube 上看到一个视频,博主用 iPhone 的快捷指令做了一个工作流:白天随时用语音记录零碎的想法(比如"今天中午吃的拉面不错"、"下午开会被老板骂了"),晚上让 AI 把这些碎片整理成一篇完整的日记。

这思路太妙了!但那个方案是 iOS 专属的,我用的是 Android。于是我决定自己做一个。

核心功能就三个:

随时记录碎片想法(语音或打字)

一键让 AI 整理成日记

顺便把日记里的待办事项提取出来

听起来不复杂对吧?但真正动手做的时候,你会发现需要学的东西还挺多。好,废话不多说,开始搞。

二、开发环境:Android Studio 的安装和配置

2.1 下载 Android Studio

去 Google 官网下载:https://developer.android.com/studio

下载下来是一个几百 MB 的安装包。Windows 用户直接双击 exe,Mac 用户拖到 Applications,Linux 用户解压后运行 studio.sh

第一次启动会比较慢,因为它要下载 SDK、Gradle 之类的东西。建议挂个梯子,不然可能卡很久。

2.2 创建第一个项目

安装好之后,点 New Project,选择 Empty Activity(注意是 Compose 版本的,不是传统 View 版本)。

填几个信息:

Name: Journal(或者你喜欢的名字)

Package name: com.example.journal

Language: Kotlin(别选 Java,2024 年了)

Minimum SDK: API 26(Android 8.0)

点 Finish,等它 Sync 完成。第一次 Sync 会下载一堆依赖,可能要等几分钟。

2.3 跑起来看看

点击工具栏上的绿色三角形(Run),选择一个模拟器或者连接的真机。如果没有模拟器,去 Device Manager 创建一个。

看到屏幕上显示 "Hello Android!" 就说明环境没问题了。

💡 小提示:真机调试比模拟器快很多。手机上打开「开发者选项」→「USB 调试」,用数据线连电脑就行。

三、项目结构:先搞清楚文件放哪里

打开项目,左边有一堆文件夹。别慌,常用的就这几个:

app/

├── src/main/

│ ├── java/com/example/journal/ # 写 Kotlin 代码的地方

│ ├── res/ # 资源文件(图片、字符串等)

│ └── AndroidManifest.xml # App 配置文件

├── build.gradle.kts # 这个模块的依赖配置

根目录还有个

build.gradle.kts

,是整个项目的配置。现在不用管它,后面加依赖的时候会用到。

四、Kotlin 速成:够用就行

Android 开发用的是 Kotlin 语言。如果你写过 Java、Python 或者 JavaScript,上手会很快。我挑几个最常用的语法讲一下。

4.1 变量

val name = "张三" // val 是常量,不能改

var age = 25 // var 是变量,可以改

age = 26 // OK

// name = "李四" // 报错!

4.2 函数

fun greet(name: String): String {

return "你好,KaTeX parse error: Expected 'EOF', got '}' at position 7: name" }̲ // 简写 fun gree...name"

4.3 数据类

这个特别好用,一行代码就能定义一个类,自动生成 equals、hashCode、toString:

data class Note(

val id: Long,

val content: String,

val timestamp: Long

)

4.4 空安全

Kotlin 最大的特点是变量默认不能为 null。如果可能为空,要加问号:

var title: String = "日记" // 不能为 null

var subtitle: String? = null // 可以为 null

// 访问可空变量

println(subtitle?.length) // 如果 subtitle 是 null,返回 null

println(subtitle ?: "无副标题") // 如果 subtitle 是 null,返回默认值

4.5 Lambda

经常用来做回调:

button.setOnClickListener {

println("按钮被点击了")

}

// 带参数

list.filter { item -> item.length > 5 }

// 只有一个参数时可以用 it

list.filter { it.length > 5 }

Kotlin 的语法糖很多,但这些够应付大部分场景了。剩下的边写边查。

五、UI 开发:Jetpack Compose 入门

传统的 Android UI 用 XML 写布局,然后在 Java/Kotlin 里操作 View。现在 Google 推荐用 Jetpack Compose,直接用 Kotlin 代码写 UI,更简洁。

5.1 第一个 Composable 函数

@Composable

fun Greeting(name: String) {

Text(text = "你好,$name")

}

@Composable 注解表示这是一个 UI 组件。Compose 用函数来描述界面,所以叫「声明式 UI」。

5.2 常用组件

// 文本

Text("这是一段文字")

// 按钮

Button(onClick = { /* 点击事件 */ }) {

Text("点我")

}

// 输入框

var text by remember { mutableStateOf("") }

TextField(

value = text,

onValueChange = { text = it },

placeholder = { Text("请输入...") }

)

// 图片

Image(

painter = painterResource(R.drawable.icon),

contentDescription = "图标"

)

5.3 布局

// 垂直排列

Column {

Text("第一行")

Text("第二行")

Text("第三行")

}

// 水平排列

Row {

Text("左边")

Text("右边")

}

// 层叠

Box {

Image(...)

Text("覆盖在图片上的文字")

}

5.4 列表

普通 Column 会一次性渲染所有内容,数据多了会卡。用 LazyColumn 可以实现懒加载:

LazyColumn {

items(noteList) { note ->

NoteCard(note)

}

}

5.5 状态管理

Compose 的核心概念:状态变了,UI 自动更新。

@Composable

fun Counter() {

var count by remember { mutableStateOf(0) }

复制代码
Column {
    Text("当前计数:$count")
    Button(onClick = { count++ }) {
        Text("加一")
    }
}

}

remember 让状态在重组时保持不变,mutableStateOf 创建可观察的状态。当 count 变化时,用到它的 UI 会自动刷新。

六、做个能用的主页

理论讲完了,开始写实际代码。先做主页:显示今天的碎片想法,底部有个输入框可以添加新内容。

6.1 定义数据模型

在 data/local/database/entity 目录下创建

Note.kt

package com.example.journal.data.local.database.entity

import androidx.room.Entity

import androidx.room.PrimaryKey

@Entity(tableName = "notes")

data class Note(

@PrimaryKey(autoGenerate = true)

val id: Long = 0,

val content: String,

val timestamp: Long = System.currentTimeMillis(),

val date: Long // 归属日期,用于按天分组

)

@Entity 告诉 Room 数据库这是一张表。@PrimaryKey(autoGenerate = true) 表示 id 自动生成。

6.2 创建 DAO(数据访问对象)

在 data/local/database/dao 目录下创建

NoteDao.kt

package com.example.journal.data.local.database.dao

import androidx.room.*

import com.example.journal.data.local.database.entity.Note

import kotlinx.coroutines.flow.Flow

@Dao

interface NoteDao {

@Insert

suspend fun insertNote(note: Note): Long

@Delete

suspend fun deleteNote(note: Note)

@Query("SELECT * FROM notes WHERE date = :date ORDER BY timestamp DESC")

fun getNotesByDate(date: Long): Flow<List>

@Query("SELECT * FROM notes ORDER BY timestamp DESC")

fun getAllNotes(): Flow<List>

}

几个要点:

@Insert、@Delete 是 Room 提供的注解,自动生成 SQL

@Query 手写 SQL

suspend 表示这是个协程函数,不会阻塞主线程

返回

Flow

是响应式的,数据变了会自动通知

6.3 创建数据库

在 data/local/database 目录下创建

AppDatabase.kt

package com.example.journal.data.local.database

import android.content.Context

import androidx.room.Database

import androidx.room.Room

import androidx.room.RoomDatabase

import com.example.journal.data.local.database.dao.NoteDao

import com.example.journal.data.local.database.entity.Note

@Database(entities = [Note::class], version = 1)

abstract class AppDatabase : RoomDatabase() {

abstract fun noteDao(): NoteDao

companion object {

@Volatile

private var INSTANCE: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {

return INSTANCE ?: synchronized(this) {

Room.databaseBuilder(

context.applicationContext,

AppDatabase::class.java,

"journal_database"

).build().also { INSTANCE = it }

}

}

}

}

这是个典型的单例模式,保证整个 App 只有一个数据库实例。

6.4 添加依赖

打开

app/build.gradle.kts

,添加 Room 依赖:

dependencies {

// Room

val roomVersion = "2.6.1"

implementation("androidx.room:room-runtime:roomVersion")implementation("androidx.room:room−ktx:roomVersion") implementation("androidx.room:room-ktx:roomVersion")implementation("androidx.room:room−ktx:roomVersion")

ksp("androidx.room:room-compiler:$roomVersion")

}

在 plugins 块添加 KSP:

plugins {

// ... 其他插件

id("com.google.devtools.ksp") version "1.9.21-1.0.15"

}

点 Sync Now。

6.5 写主页 UI

创建

presentation/screen/main/MainScreen.kt

package com.example.journal.presentation.screen.main

import androidx.compose.foundation.layout.*

import androidx.compose.foundation.lazy.LazyColumn

import androidx.compose.foundation.lazy.items

import androidx.compose.material3.*

import androidx.compose.runtime.*

import androidx.compose.ui.Modifier

import androidx.compose.ui.platform.LocalContext

import androidx.compose.ui.unit.dp

import com.example.journal.data.local.database.AppDatabase

import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)

@Composable

fun MainScreen() {

val context = LocalContext.current

val database = AppDatabase.getDatabase(context)

val noteDao = database.noteDao()

val scope = rememberCoroutineScope()

// 今天的日期(去掉时分秒)

val today = remember {

val cal = java.util.Calendar.getInstance()

cal.set(java.util.Calendar.HOUR_OF_DAY, 0)

cal.set(java.util.Calendar.MINUTE, 0)

cal.set(java.util.Calendar.SECOND, 0)

cal.set(java.util.Calendar.MILLISECOND, 0)

cal.timeInMillis

}

// 收集今天的笔记

val notes by noteDao.getNotesByDate(today).collectAsState(initial = emptyList())

// 输入框内容

var inputText by remember { mutableStateOf("") }

Scaffold(

topBar = {

TopAppBar(title = { Text("今日想法") })

},

bottomBar = {

// 底部输入栏

Row(

modifier = Modifier

.fillMaxWidth()

.padding(16.dp)

) {

OutlinedTextField(

value = inputText,

onValueChange = { inputText = it },

modifier = Modifier.weight(1f),

placeholder = { Text("记录一个想法...") }

)

Spacer(modifier = Modifier.width(8.dp))

Button(

onClick = {

if (inputText.isNotBlank()) {

scope.launch {

noteDao.insertNote(

Note(content = inputText.trim(), date = today)

)

inputText = ""

}

}

}

) {

Text("添加")

}

}

}

) { padding ->

LazyColumn(

modifier = Modifier

.fillMaxSize()

.padding(padding),

contentPadding = PaddingValues(16.dp)

) {

items(notes) { note ->

Card(

modifier = Modifier

.fillMaxWidth()

.padding(vertical = 4.dp)

) {

Text(

text = note.content,

modifier = Modifier.padding(16.dp)

)

}

}

}

}

}

这段代码做了几件事:

获取数据库实例和 DAO

用 Flow.collectAsState() 订阅数据变化

底部有个输入框和按钮,点击后插入数据

用 LazyColumn 显示列表

6.6 跑起来看看

MainActivity.kt

改成:

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContent {

MaterialTheme {

MainScreen()

}

}

}

}

运行,应该能看到一个简单的笔记列表了。输入文字点添加,列表会自动更新。

七、架构重构:为什么代码要分层

上面的代码能跑,但有个问题:UI 里直接操作数据库,耦合太紧了。如果以后要换数据库、加缓存、写测试,改起来会很痛苦。

所以我们要做架构分层。

7.1 Clean Architecture 简介

这个架构把代码分成三层:

┌─────────────────────────────────────┐

│ Presentation (UI + ViewModel) │ ← 展示层:用户看到的界面

├─────────────────────────────────────┤

│ Domain (UseCase + Model) │ ← 领域层:业务逻辑

├─────────────────────────────────────┤

│ Data (Repository + DataSource) │ ← 数据层:数据来源

└─────────────────────────────────────┘

好处:

每层职责单一,改一个地方不会影响其他

方便单元测试

代码可读性高

7.2 添加 Repository

Repository 是数据层的门面,UI 不直接访问 DAO,而是通过 Repository。

创建

data/repository/NoteRepository.kt

package com.example.journal.data.repository

import com.example.journal.data.local.database.dao.NoteDao

import com.example.journal.data.local.database.entity.Note

import kotlinx.coroutines.flow.Flow

class NoteRepository(private val noteDao: NoteDao) {

fun getNotesByDate(date: Long): Flow<List> {

return noteDao.getNotesByDate(date)

}

fun getAllNotes(): Flow<List> {

return noteDao.getAllNotes()

}

suspend fun insertNote(note: Note): Long {

return noteDao.insertNote(note)

}

suspend fun deleteNote(note: Note) {

noteDao.deleteNote(note)

}

}

看起来只是包了一层?没错,简单场景下确实是。但当你需要加缓存、做数据转换、或者同时从本地和网络取数据时,Repository 的价值就体现出来了。

7.3 添加 ViewModel

ViewModel 负责持有 UI 状态,处理用户操作。它独立于 UI 生命周期,屏幕旋转时数据不会丢失。

创建

presentation/screen/main/MainViewModel.kt

package com.example.journal.presentation.screen.main

import androidx.lifecycle.ViewModel

import androidx.lifecycle.ViewModelProvider

import androidx.lifecycle.viewModelScope

import com.example.journal.data.local.database.entity.Note

import com.example.journal.data.repository.NoteRepository

import kotlinx.coroutines.flow.*

import kotlinx.coroutines.launch

import java.util.Calendar

class MainViewModel(

private val noteRepository: NoteRepository

) : ViewModel() {

private val _inputText = MutableStateFlow("")

val inputText: StateFlow = _inputText.asStateFlow()

private val today: Long = Calendar.getInstance().apply {

set(Calendar.HOUR_OF_DAY, 0)

set(Calendar.MINUTE, 0)

set(Calendar.SECOND, 0)

set(Calendar.MILLISECOND, 0)

}.timeInMillis

val todayNotes: StateFlow<List> = noteRepository

.getNotesByDate(today)

.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

fun updateInput(text: String) {

_inputText.value = text

}

fun addNote() {

val content = _inputText.value.trim()

if (content.isBlank()) return

viewModelScope.launch {

noteRepository.insertNote(

Note(content = content, date = today)

)

_inputText.value = ""

}

}

fun deleteNote(note: Note) {

viewModelScope.launch {

noteRepository.deleteNote(note)

}

}

class Factory(private val repository: NoteRepository) : ViewModelProvider.Factory {

@Suppress("UNCHECKED_CAST")

override fun create(modelClass: Class): T {

return MainViewModel(repository) as T

}

}

}

7.4 更新 UI

现在

MainScreen

变得很干净:

@Composable

fun MainScreen() {

val context = LocalContext.current

val database = AppDatabase.getDatabase(context)

val repository = NoteRepository(database.noteDao())

val viewModel: MainViewModel = viewModel(factory = MainViewModel.Factory(repository))

val notes by viewModel.todayNotes.collectAsState()

val inputText by viewModel.inputText.collectAsState()

Scaffold(

topBar = { TopAppBar(title = { Text("今日想法") }) },

bottomBar = {

Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {

OutlinedTextField(

value = inputText,

onValueChange = { viewModel.updateInput(it) },

modifier = Modifier.weight(1f),

placeholder = { Text("记录一个想法...") }

)

Spacer(modifier = Modifier.width(8.dp))

Button(onClick = { viewModel.addNote() }) {

Text("添加")

}

}

}

) { padding ->

LazyColumn(

modifier = Modifier.fillMaxSize().padding(padding),

contentPadding = PaddingValues(16.dp)

) {

items(notes) { note ->

NoteCard(note = note, onDelete = { viewModel.deleteNote(note) })

}

}

}

}

UI 只负责展示和收集用户操作,业务逻辑都在 ViewModel 里。

八、接入 AI:让日记自己写

这是最有意思的部分。我们要调用 LLM API,把碎片想法整理成日记。

8.1 选择一个 LLM 服务

国内能用的 LLM API 很多:

DeepSeek:便宜好用,中文效果不错

通义千问:阿里的,有免费额度

智谱 GLM:清华背景,也不错

我用的是 DeepSeek,注册后充几块钱能用很久。

8.2 添加网络权限和依赖

AndroidManifest.xml

添加:
在 build.gradle.kts 添加 OkHttp(网络请求库)和 Gson(JSON 解析):

implementation("com.squareup.okhttp3:okhttp:4.12.0")

implementation("com.google.code.gson:gson:2.10.1")

8.3 写 LLM 调用代码

创建 data/remote/LlmService.kt:

package com.example.journal.data.remote

import com.google.gson.Gson

import kotlinx.coroutines.Dispatchers

import kotlinx.coroutines.withContext

import okhttp3.MediaType.Companion.toMediaType

import okhttp3.OkHttpClient

import okhttp3.Request

import okhttp3.RequestBody.Companion.toRequestBody

import java.util.concurrent.TimeUnit

object LlmService {

private val client = OkHttpClient.Builder()

.connectTimeout(60, TimeUnit.SECONDS)

.readTimeout(60, TimeUnit.SECONDS)

.build()

private val gson = Gson()

private const val API_URL = "https://api.deepseek.com/v1/chat/completions"

suspend fun generateDiary(notes: List, apiKey: String): Result {

return withContext(Dispatchers.IO) {

try {

val notesText = notes.mapIndexed { i, n -> "{i + 1}. n" }.joinToString("\n")

复制代码
            val prompt = """
                请将以下碎片想法整理成一篇日记。要求:
                1. 保持原意,可以适当润色
                2. 按时间或主题组织
                3. 语气自然,像在和朋友聊天
                4. 结尾可以加一两句感悟
                
                碎片想法:
                $notesText
            """.trimIndent()
            val requestBody = mapOf(
                "model" to "deepseek-chat",
                "messages" to listOf(
                    mapOf("role" to "user", "content" to prompt)
                )
            )
            val request = Request.Builder()
                .url(API_URL)
                .addHeader("Authorization", "Bearer $apiKey")
                .addHeader("Content-Type", "application/json")
                .post(gson.toJson(requestBody).toRequestBody("application/json".toMediaType()))
                .build()
            val response = client.newCall(request).execute()
            val body = response.body?.string() ?: throw Exception("Empty response")
            
            // 解析响应
            val json = gson.fromJson(body, Map::class.java)
            val choices = json["choices"] as? List<*>
            val message = (choices?.firstOrNull() as? Map<*, *>)?.get("message") as? Map<*, *>
            val content = message?.get("content") as? String ?: throw Exception("Invalid response")
            
            Result.success(content)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

}

8.4 添加生成按钮

MainScreen

添加一个「生成日记」按钮:

Button(

onClick = {

scope.launch {

val noteContents = notes.map { it.content }

val result = LlmService.generateDiary(noteContents, "your-api-key")

result.onSuccess { diary ->

// 保存到数据库或显示出来

}.onFailure { e ->

// 显示错误提示

}

}

}

) {

Text("生成日记")

}

⚠️ 注意:不要把 API Key 硬编码在代码里!应该让用户在设置页面输入,然后存到 SharedPreferences。

九、添加更多功能

基础框架搭好后,加功能就快了。我简单列一下我后来加的东西:

9.1 日记列表页

新建一个页面,用 LazyColumn 显示所有日记。每个日记卡片显示日期、内容预览、情绪标签。

9.2 搜索功能

在 DAO 里加一个搜索方法:

@Query("SELECT * FROM diaries WHERE content LIKE '%' || :query || '%'")

fun searchByContent(query: String): Flow<List>

UI 上加个搜索框,输入时调用这个查询。

9.3 导出功能

把日记导出成 Markdown 文件:

fun exportToMarkdown(diary: Diary): String {

return """

${formatDate(diary.date)} 的日记

复制代码
    ${diary.content}
    
    ---
    *情绪:${diary.moodLabel}*
""".trimIndent()

}

然后用 MediaStore API 保存到 Downloads 文件夹。

9.4 待办事项提取

让 AI 在生成日记的同时提取待办:

val prompt = """

请完成两件事:

  1. 将以下想法整理成日记

  2. 提取其中提到的待办事项

    输出格式:
    【日记】
    ...

    【待办】

    • 待办1
    • 待办2

"""

然后解析响应,分别保存日记和待办。

9.5 语音输入

Android 自带语音识别 API:

val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {

putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)

putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN")

}

speechRecognizer.startListening(intent)

识别结果通过回调返回,直接添加到笔记列表。

十、调试和发布

10.1 常见问题

Q: 应用闪退了怎么办? 看 Logcat。在 Android Studio 底部找到 Logcat 标签,过滤 Error 级别,找红色的 Exception。

Q: 数据库结构改了,应用崩溃? 数据库版本号要加 1,并且添加 Migration 或者用 fallbackToDestructiveMigration()(开发阶段)。

Q: 网络请求没反应?

检查有没有加 INTERNET 权限

检查是不是主线程调的(要用协程)

看 Logcat 有没有报错

10.2 打包 APK

菜单栏 Build → Generate Signed Bundle / APK,选 APK,创建或选择签名文件,选 release 版本,点 Finish。

生成的 APK 在 app/release 目录下。

十一、总结

写到这里,你应该对 Android 开发有个基本概念了。回顾一下我们做了什么:

环境搭建:安装 Android Studio,创建 Compose 项目

Kotlin 基础:变量、函数、数据类、空安全、Lambda

Compose UI:组件、布局、状态管理、列表

Room 数据库:Entity、DAO、Database

架构设计:Repository、ViewModel、分层

网络请求:调用 LLM API

这些是开发任何 Android App 都会用到的基础。剩下的就是根据需求堆功能了。

我的建议是:别光看,动手写。遇到问题就去查文档、搜 Stack Overflow。踩坑是学习的必经之路。

最后附上项目地址:https://github.com/kai200407/AI-Diary-App-Android

有问题欢迎在评论区讨论。

这篇文章写于 2025 年 12 月,如果你看到时 API 变了,以官方文档为准。

相关推荐
Kapaseker1 天前
你不看会后悔的2025年终总结
android·kotlin
alexhilton1 天前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke1 天前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday04261 天前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理1 天前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台1 天前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐1 天前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极1 天前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan1 天前
setHintTextColor不生效
android
ChangYan.1 天前
VSCode终端设置为管理员权限,解决operation not permitted问题
ide·vscode·编辑器