从零开始:用 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 变了,以官方文档为准。

相关推荐
2501_916007472 小时前
在 CICD 中实践 Fastlane + Appuploader 命令行,构建可复制的 iOS 自动化发布流程
android·运维·ios·小程序·uni-app·自动化·iphone
sherlock_ye42 小时前
‘jupyter‘ 不是内部或外部命令,也不是可运行的程序或批处理文件,最终解决方案!
ide·python·jupyter·conda
煤球王子2 小时前
简单了解:Android14中的Input event
android
2501_915921433 小时前
从 HBuilder 到 App Store,uni-app 与 HBuilder 项目的 iOS 上架流程实战解析
android·ios·小程序·https·uni-app·iphone·webview
天向上3 小时前
ubuntu系统adb shell报错 ADB server didn‘t ACK
android·linux·ubuntu·adb
Xiaomostream3 小时前
Vscode + SSH + Clangd 配置, 查看内核源码
ide·vscode·ssh
xiaoyan20153 小时前
自研2025版flutter3.38实战抖音app短视频+聊天+直播商城系统
android·flutter·dart
愤怒的代码4 小时前
深入解析 SystemUI 依赖注入:Dagger2 实践剖析
android·dagger
游戏开发爱好者84 小时前
以 uni-app 为核心的 iOS 上架流程实践, 从构建到最终提交的完整路径
android·ios·小程序·https·uni-app·iphone·webview