Android Studio 中使用 SQLite 数据库开发完整指南(Kotlin版本)

文章目录

    • [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 层)
    • [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 创建新项目

  1. 打开 Android Studio
  2. 选择 "Start a new Android Studio project"
  3. 选择 "Empty Activity" 模板
  4. 设置项目名称(例如 "SQLiteDemo")
  5. 选择语言(Kotlin 或 Java,本教程以 Kotlin 为例)
  6. 设置最低 API 级别(建议 API 21 或更高)
  7. 点击 "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. 运行和测试应用

现在,您可以运行应用程序并测试以下功能:

  1. 添加新笔记
  2. 查看笔记列表
  3. 编辑现有笔记
  4. 删除笔记
  5. 搜索笔记
  6. 删除所有笔记

11. 数据库调试技巧

11.1 查看数据库内容

  1. 在 Android Studio 中打开 "Device File Explorer" (View -> Tool Windows -> Device File Explorer)
  2. 导航到 /data/data/com.yourpackage/databases/
  3. 找到 notes_database 文件
  4. 右键点击并选择 "Save As" 将其保存到本地
  5. 使用 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. 性能优化建议

  1. 使用事务:对于批量操作,使用事务可以显著提高性能:
kotlin 复制代码
@Dao
interface NoteDao {
    @Transaction
    suspend fun insertAll(notes: List<Note>) {
        notes.forEach { insertNote(it) }
    }
}
  1. 索引优化:为常用查询字段添加索引:
kotlin 复制代码
@Entity(tableName = "notes", indices = [Index(value = ["title"], unique = false)])
data class Note(
    // ...
)
  1. 分页加载:对于大量数据,使用 Paging 库:
kotlin 复制代码
@Query("SELECT * FROM notes ORDER BY updated_at DESC")
fun getPagedNotes(): PagingSource<Int, Note>
  1. 避免在主线程操作数据库:始终确保数据库操作在后台线程执行。

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 数据库的完整开发流程,包括:

  1. 设置项目和依赖
  2. 设计数据库结构
  3. 实现 Room 数据库组件(Entity, DAO, Database)
  4. 创建 Repository 层
  5. 实现 ViewModel
  6. 构建用户界面
  7. 添加数据库迁移支持
  8. 性能优化建议

通过遵循这些步骤,您可以构建一个功能完善、结构清晰的 Android 应用,充分利用 SQLite 数据库的强大功能。

相关推荐
amazinging30 分钟前
北京-4年功能测试2年空窗-报培训班学测开-第四十一天
python·学习·appium
Jyywww12133 分钟前
微信小程序学习笔记
笔记·学习·微信小程序
amazinging35 分钟前
北京-4年功能测试2年空窗-报培训班学测开-第三十九天
python·学习·appium
每次的天空1 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
重庆小透明1 小时前
力扣刷题记录【1】146.LRU缓存
java·后端·学习·算法·leetcode·缓存
恋猫de小郭1 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日2 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安2 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑3 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
许白掰3 小时前
【stm32】HAL库开发——CubeMX配置RTC,单片机工作模式和看门狗
stm32·单片机·嵌入式硬件·学习·实时音视频