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 数据库的强大功能。

相关推荐
前行的小黑炭42 分钟前
Android Lifecycle代码分析:为什么使用;注解的方式为什么过期?源码分析;状态与事件
android
和煦的春风1 小时前
案例分析 | SurfaceFlinger 大片Runnable引起的卡顿
android·linux
zcyf08091 小时前
kafka理论学习汇总
java·分布式·学习·kafka
爱编程的小庄1 小时前
Maven 4.0.0 模式-pom.xml配置详解
xml·java·maven
*TQK*1 小时前
线性代数——行列式⭐
笔记·学习·线性代数
四夕白告木贞2 小时前
stm32week13
stm32·单片机·嵌入式硬件·学习
九亿AI算法优化工作室&2 小时前
结合大语言模型的机械臂抓取操作学习
人工智能·学习·语言模型·自然语言处理
浩宇软件开发2 小时前
Android开发,实现一个简约又好看的登录页
android·java·android studio·android开发
未扬帆的小船2 小时前
在gpt的帮助下安装chales的证书,用于https在root情况下抓包
android·charles
万户猴2 小时前
【 Android蓝牙-十】Android各版本蓝牙行为变化与兼容性指南
android·蓝牙