
文章目录
-
- [1. 项目准备](#1. 项目准备)
-
- [1.1 创建新项目](#1.1 创建新项目)
- [1.2 添加必要依赖](#1.2 添加必要依赖)
- [2. 数据库设计](#2. 数据库设计)
- [3. 实现数据库](#3. 实现数据库)
-
- [3.1 创建实体类 (Entity)](#3.1 创建实体类 (Entity))
- [3.2 创建数据访问对象 (DAO)](#3.2 创建数据访问对象 (DAO))
- [3.3 创建数据库类](#3.3 创建数据库类)
- [4. 创建 Repository](#4. 创建 Repository)
- [5. 创建 ViewModel](#5. 创建 ViewModel)
- [6. 实现 UI 层](#6. 实现 UI 层)
-
- [6.1 创建笔记列表 Activity](#6.1 创建笔记列表 Activity)
- [6.2 创建笔记详情 Activity](#6.2 创建笔记详情 Activity)
- [6.3 创建 RecyclerView Adapter](#6.3 创建 RecyclerView Adapter)
- [6.4 创建 Application 类](#6.4 创建 Application 类)
- [7. 添加菜单资源](#7. 添加菜单资源)
- [8. 添加字符串资源](#8. 添加字符串资源)
- [9. 添加图标资源](#9. 添加图标资源)
- [10. 运行和测试应用](#10. 运行和测试应用)
- [11. 数据库调试技巧](#11. 数据库调试技巧)
-
- [11.1 查看数据库内容](#11.1 查看数据库内容)
- [11.2 使用 Stetho 进行调试](#11.2 使用 Stetho 进行调试)
- [12. 数据库迁移](#12. 数据库迁移)
-
- [12.1 修改实体类](#12.1 修改实体类)
- [12.2 更新数据库版本](#12.2 更新数据库版本)
- [12.3 添加迁移策略](#12.3 添加迁移策略)
- [13. 性能优化建议](#13. 性能优化建议)
- [14. 完整项目结构](#14. 完整项目结构)
- [15. 总结](#15. 总结)

1. 项目准备
1.1 创建新项目
- 打开 Android Studio
- 选择 "Start a new Android Studio project"
- 选择 "Empty Activity" 模板
- 设置项目名称(例如 "SQLiteDemo")
- 选择语言(Kotlin 或 Java,本教程以 Kotlin 为例)
- 设置最低 API 级别(建议 API 21 或更高)
- 点击 "Finish" 完成项目创建
1.2 添加必要依赖
确保 build.gradle (Module: app)
中包含以下依赖:
gradle
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
// Room 数据库(SQLite 的抽象层)
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
// 协程支持
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
// ViewModel 和 LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
点击 "Sync Now" 同步项目。
2. 数据库设计
假设我们要创建一个简单的笔记应用,包含以下数据表:
- notes 表:
- id: 主键,自增
- title: 笔记标题
- content: 笔记内容
- created_at: 创建时间
- updated_at: 更新时间
3. 实现数据库
3.1 创建实体类 (Entity)
在 com.yourpackage.model
包下创建 Note.kt
文件:
kotlin
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
var title: String,
var content: String,
val created_at: Date = Date(),
var updated_at: Date = Date()
)
3.2 创建数据访问对象 (DAO)
在 com.yourpackage.dao
包下创建 NoteDao.kt
文件:
kotlin
import androidx.lifecycle.LiveData
import androidx.room.*
import com.yourpackage.model.Note
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
@Query("SELECT * FROM notes ORDER BY updated_at DESC")
fun getAllNotes(): LiveData<List<Note>>
@Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getNoteById(noteId: Long): Note?
@Query("SELECT * FROM notes WHERE title LIKE :query OR content LIKE :query ORDER BY updated_at DESC")
fun searchNotes(query: String): LiveData<List<Note>>
}
3.3 创建数据库类
在 com.yourpackage.database
包下创建 AppDatabase.kt
文件:
kotlin
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.yourpackage.dao.NoteDao
import com.yourpackage.model.Note
@Database(entities = [Note::class], version = 1, exportSchema = false)
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) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"notes_database"
)
.fallbackToDestructiveMigration() // 数据库升级策略,简单应用可以这样设置
.build()
INSTANCE = instance
instance
}
}
}
}
4. 创建 Repository
在 com.yourpackage.repository
包下创建 NoteRepository.kt
文件:
kotlin
import androidx.lifecycle.LiveData
import com.yourpackage.dao.NoteDao
import com.yourpackage.model.Note
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class NoteRepository(private val noteDao: NoteDao) {
val allNotes: LiveData<List<Note>> = noteDao.getAllNotes()
suspend fun insert(note: Note): Long {
return withContext(Dispatchers.IO) {
noteDao.insertNote(note)
}
}
suspend fun update(note: Note) {
withContext(Dispatchers.IO) {
note.updated_at = Date()
noteDao.updateNote(note)
}
}
suspend fun delete(note: Note) {
withContext(Dispatchers.IO) {
noteDao.deleteNote(note)
}
}
suspend fun getNoteById(id: Long): Note? {
return withContext(Dispatchers.IO) {
noteDao.getNoteById(id)
}
}
fun searchNotes(query: String): LiveData<List<Note>> {
return noteDao.searchNotes("%$query%")
}
}
5. 创建 ViewModel
在 com.yourpackage.viewmodel
包下创建 NoteViewModel.kt
文件:
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.yourpackage.model.Note
import com.yourpackage.repository.NoteRepository
import kotlinx.coroutines.launch
class NoteViewModel(private val repository: NoteRepository) : ViewModel() {
val allNotes = repository.allNotes
fun insert(note: Note) = viewModelScope.launch {
repository.insert(note)
}
fun update(note: Note) = viewModelScope.launch {
repository.update(note)
}
fun delete(note: Note) = viewModelScope.launch {
repository.delete(note)
}
fun getNoteById(id: Long) = viewModelScope.launch {
repository.getNoteById(id)
}
fun searchNotes(query: String) = repository.searchNotes(query).asLiveData()
}
class NoteViewModelFactory(private val repository: NoteRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(NoteViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return NoteViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
6. 实现 UI 层
6.1 创建笔记列表 Activity
创建 NotesListActivity.kt
和对应的布局文件 activity_notes_list.xml
activity_notes_list.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.NotesListActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.SQLiteDemo.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.SQLiteDemo.PopupOverlay"
app:title="@string/app_name" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/search_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_hint"
android:imeOptions="actionSearch"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notes_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="72dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/add_note"
android:src="@drawable/ic_add"
app:backgroundTint="@color/purple_500"
app:tint="@android:color/white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
NotesListActivity.kt
kotlin
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.inputmethod.EditorInfo
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.yourpackage.R
import com.yourpackage.adapter.NotesAdapter
import com.yourpackage.databinding.ActivityNotesListBinding
import com.yourpackage.model.Note
import com.yourpackage.viewmodel.NoteViewModel
import com.yourpackage.viewmodel.NoteViewModelFactory
class NotesListActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotesListBinding
private lateinit var notesAdapter: NotesAdapter
private val viewModel: NoteViewModel by viewModels {
NoteViewModelFactory((application as NotesApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNotesListBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
setupRecyclerView()
setupSearch()
setupFAB()
observeNotes()
}
private fun setupRecyclerView() {
notesAdapter = NotesAdapter { note ->
// 点击笔记项时的操作
val intent = Intent(this, NoteDetailActivity::class.java).apply {
putExtra(NoteDetailActivity.EXTRA_NOTE_ID, note.id)
}
startActivity(intent)
}
binding.notesRecyclerView.apply {
layoutManager = LinearLayoutManager(this@NotesListActivity)
adapter = notesAdapter
setHasFixedSize(true)
}
}
private fun setupSearch() {
binding.searchInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val query = binding.searchInput.text.toString().trim()
if (query.isNotEmpty()) {
viewModel.searchNotes(query).observe(this) { notes ->
notesAdapter.submitList(notes)
}
} else {
observeNotes() // 如果查询为空,返回所有笔记
}
true
} else {
false
}
}
}
private fun setupFAB() {
binding.fabAddNote.setOnClickListener {
val intent = Intent(this, NoteDetailActivity::class.java)
startActivity(intent)
}
}
private fun observeNotes() {
viewModel.allNotes.observe(this) { notes ->
notesAdapter.submitList(notes)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_delete_all -> {
deleteAllNotes()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun deleteAllNotes() {
viewModel.allNotes.value?.let { notes ->
if (notes.isNotEmpty()) {
for (note in notes) {
viewModel.delete(note)
}
Snackbar.make(binding.root, "All notes deleted", Snackbar.LENGTH_SHORT).show()
}
}
}
}
6.2 创建笔记详情 Activity
创建 NoteDetailActivity.kt
和对应的布局文件 activity_note_detail.xml
activity_note_detail.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.NoteDetailActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.SQLiteDemo.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.SQLiteDemo.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/title_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_hint"
android:inputType="textCapSentences|textAutoCorrect"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/content_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content_hint"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:minLines="5"
android:gravity="top" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/save_note"
android:src="@drawable/ic_save"
app:backgroundTint="@color/purple_500"
app:tint="@android:color/white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
NoteDetailActivity.kt
kotlin
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import com.yourpackage.R
import com.yourpackage.databinding.ActivityNoteDetailBinding
import com.yourpackage.model.Note
import com.yourpackage.viewmodel.NoteViewModel
import com.yourpackage.viewmodel.NoteViewModelFactory
import java.util.*
class NoteDetailActivity : AppCompatActivity() {
companion object {
const val EXTRA_NOTE_ID = "extra_note_id"
}
private lateinit var binding: ActivityNoteDetailBinding
private val viewModel: NoteViewModel by viewModels {
NoteViewModelFactory((application as NotesApplication).repository)
}
private var noteId: Long = -1L
private var isNewNote = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNoteDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
noteId = intent.getLongExtra(EXTRA_NOTE_ID, -1L)
isNewNote = noteId == -1L
if (!isNewNote) {
loadNote()
}
setupSaveButton()
setupTextWatchers()
}
private fun loadNote() {
viewModel.getNoteById(noteId)
viewModel.allNotes.observe(this) { notes ->
notes.find { it.id == noteId }?.let { note ->
binding.titleInput.setText(note.title)
binding.contentInput.setText(note.content)
}
}
}
private fun setupSaveButton() {
binding.fabSave.setOnClickListener {
saveNote()
}
}
private fun setupTextWatchers() {
binding.titleInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
validateInputs()
}
})
binding.contentInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
validateInputs()
}
})
}
private fun validateInputs(): Boolean {
val titleValid = binding.titleInput.text?.isNotBlank() ?: false
val contentValid = binding.contentInput.text?.isNotBlank() ?: false
binding.titleLayout.error = if (!titleValid) getString(R.string.title_required) else null
binding.contentLayout.error = if (!contentValid) getString(R.string.content_required) else null
return titleValid && contentValid
}
private fun saveNote() {
if (!validateInputs()) return
val title = binding.titleInput.text.toString()
val content = binding.contentInput.text.toString()
if (isNewNote) {
val note = Note(title = title, content = content)
viewModel.insert(note)
Snackbar.make(binding.root, "Note saved", Snackbar.LENGTH_SHORT).show()
finish()
} else {
viewModel.allNotes.value?.find { it.id == noteId }?.let { existingNote ->
val updatedNote = existingNote.copy(
title = title,
content = content,
updated_at = Date()
)
viewModel.update(updatedNote)
Snackbar.make(binding.root, "Note updated", Snackbar.LENGTH_SHORT).show()
finish()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
6.3 创建 RecyclerView Adapter
在 com.yourpackage.adapter
包下创建 NotesAdapter.kt
文件:
kotlin
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.yourpackage.R
import com.yourpackage.databinding.ItemNoteBinding
import com.yourpackage.model.Note
import java.text.SimpleDateFormat
import java.util.*
class NotesAdapter(private val onItemClick: (Note) -> Unit) :
ListAdapter<Note, NotesAdapter.NoteViewHolder>(NoteDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val binding = ItemNoteBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return NoteViewHolder(binding, onItemClick)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.bind(getItem(position))
}
class NoteViewHolder(
private val binding: ItemNoteBinding,
private val onItemClick: (Note) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(note: Note) {
binding.apply {
noteTitle.text = note.title
noteContent.text = note.content
val dateFormat = SimpleDateFormat("MMM dd, yyyy - hh:mm a", Locale.getDefault())
noteDate.text = dateFormat.format(note.updated_at)
root.setOnClickListener {
onItemClick(note)
}
}
}
}
private class NoteDiffCallback : DiffUtil.ItemCallback<Note>() {
override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem == newItem
}
}
}
创建对应的列表项布局文件 item_note.xml
:
xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/note_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/note_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/darker_gray" />
<TextView
android:id="@+id/note_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
6.4 创建 Application 类
在 com.yourpackage
包下创建 NotesApplication.kt
文件:
kotlin
import android.app.Application
import com.yourpackage.database.AppDatabase
import com.yourpackage.repository.NoteRepository
class NotesApplication : Application() {
val database by lazy { AppDatabase.getDatabase(this) }
val repository by lazy { NoteRepository(database.noteDao()) }
}
更新 AndroidManifest.xml
文件,添加 android:name
属性:
xml
<application
android:name=".NotesApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SQLiteDemo">
<!-- 其他配置 -->
</application>
7. 添加菜单资源
在 res/menu
目录下创建 menu_main.xml
文件:
xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete_all"
android:icon="@drawable/ic_delete"
android:title="@string/delete_all"
app:showAsAction="never" />
</menu>
8. 添加字符串资源
在 res/values/strings.xml
文件中添加以下字符串:
xml
<resources>
<string name="app_name">SQLite Notes</string>
<string name="title_hint">Title</string>
<string name="content_hint">Content</string>
<string name="search_hint">Search notes...</string>
<string name="add_note">Add new note</string>
<string name="save_note">Save note</string>
<string name="delete_all">Delete all notes</string>
<string name="title_required">Title is required</string>
<string name="content_required">Content is required</string>
</resources>
9. 添加图标资源
确保在 res/drawable
目录下有以下矢量图标:
ic_add.xml
(添加按钮图标)ic_save.xml
(保存按钮图标)ic_delete.xml
(删除按钮图标)
10. 运行和测试应用
现在,您可以运行应用程序并测试以下功能:
- 添加新笔记
- 查看笔记列表
- 编辑现有笔记
- 删除笔记
- 搜索笔记
- 删除所有笔记
11. 数据库调试技巧
11.1 查看数据库内容
- 在 Android Studio 中打开 "Device File Explorer" (View -> Tool Windows -> Device File Explorer)
- 导航到
/data/data/com.yourpackage/databases/
- 找到
notes_database
文件 - 右键点击并选择 "Save As" 将其保存到本地
- 使用 SQLite 浏览器工具(如 DB Browser for SQLite)打开该文件查看内容
11.2 使用 Stetho 进行调试
添加 Stetho 依赖到 build.gradle
:
gradle
implementation 'com.facebook.stetho:stetho:1.6.0'
在 NotesApplication.kt
中初始化 Stetho:
kotlin
import com.facebook.stetho.Stetho
class NotesApplication : Application() {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
// 其他代码...
}
运行应用后,在 Chrome 浏览器中访问 chrome://inspect
可以查看和调试数据库。
12. 数据库迁移
当您需要更改数据库结构时(例如添加新表或修改现有表),需要进行数据库迁移。
12.1 修改实体类
例如,我们要为 Note 添加一个 is_pinned
字段:
kotlin
@Entity(tableName = "notes")
data class Note(
// 现有字段...
var is_pinned: Boolean = false
)
12.2 更新数据库版本
修改 AppDatabase.kt
:
kotlin
@Database(entities = [Note::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
// ...
}
12.3 添加迁移策略
kotlin
val migration1to2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE notes ADD COLUMN is_pinned INTEGER NOT NULL DEFAULT 0")
}
}
// 在 databaseBuilder 中添加迁移
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"notes_database"
)
.addMigrations(migration1to2)
.build()
13. 性能优化建议
- 使用事务:对于批量操作,使用事务可以显著提高性能:
kotlin
@Dao
interface NoteDao {
@Transaction
suspend fun insertAll(notes: List<Note>) {
notes.forEach { insertNote(it) }
}
}
- 索引优化:为常用查询字段添加索引:
kotlin
@Entity(tableName = "notes", indices = [Index(value = ["title"], unique = false)])
data class Note(
// ...
)
- 分页加载:对于大量数据,使用 Paging 库:
kotlin
@Query("SELECT * FROM notes ORDER BY updated_at DESC")
fun getPagedNotes(): PagingSource<Int, Note>
- 避免在主线程操作数据库:始终确保数据库操作在后台线程执行。
14. 完整项目结构
最终项目结构应类似于:
com.yourpackage
├── adapter
│ └── NotesAdapter.kt
├── dao
│ └── NoteDao.kt
├── database
│ └── AppDatabase.kt
├── model
│ └── Note.kt
├── repository
│ └── NoteRepository.kt
├── ui
│ ├── NotesListActivity.kt
│ └── NoteDetailActivity.kt
├── viewmodel
│ ├── NoteViewModel.kt
│ └── NoteViewModelFactory.kt
└── NotesApplication.kt
15. 总结
本指南详细介绍了在 Android Studio 中使用 SQLite 数据库的完整开发流程,包括:
- 设置项目和依赖
- 设计数据库结构
- 实现 Room 数据库组件(Entity, DAO, Database)
- 创建 Repository 层
- 实现 ViewModel
- 构建用户界面
- 添加数据库迁移支持
- 性能优化建议
通过遵循这些步骤,您可以构建一个功能完善、结构清晰的 Android 应用,充分利用 SQLite 数据库的强大功能。