当前位置: 首页 > news >正文

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

在这里插入图片描述

文章目录

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

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 和 LiveDataimplementation "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 文件:

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 文件:

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@Updatesuspend fun updateNote(note: Note)@Deletesuspend 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 文件:

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(): NoteDaocompanion object {@Volatileprivate var INSTANCE: AppDatabase? = nullfun getDatabase(context: Context): AppDatabase {return INSTANCE ?: synchronized(this) {val instance = Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"notes_database").fallbackToDestructiveMigration() // 数据库升级策略,简单应用可以这样设置.build()INSTANCE = instanceinstance}}}
}

4. 创建 Repository

com.yourpackage.repository 包下创建 NoteRepository.kt 文件:

import androidx.lifecycle.LiveData
import com.yourpackage.dao.NoteDao
import com.yourpackage.model.Note
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContextclass 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 文件:

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.launchclass NoteViewModel(private val repository: NoteRepository) : ViewModel() {val allNotes = repository.allNotesfun 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 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.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:theme="@style/Theme.SQLiteDemo.AppBarOverlay"><androidx.appcompat.widget.Toolbarandroid: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.TextInputLayoutandroid: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.TextInputEditTextandroid: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.RecyclerViewandroid: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.FloatingActionButtonandroid: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
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.NoteViewModelFactoryclass NotesListActivity : AppCompatActivity() {private lateinit var binding: ActivityNotesListBindingprivate lateinit var notesAdapter: NotesAdapterprivate 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 = notesAdaptersetHasFixedSize(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 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.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:theme="@style/Theme.SQLiteDemo.AppBarOverlay"><androidx.appcompat.widget.Toolbarandroid: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.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:padding="16dp"><com.google.android.material.textfield.TextInputLayoutandroid: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.TextInputEditTextandroid: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.TextInputLayoutandroid: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.TextInputEditTextandroid: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.FloatingActionButtonandroid: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
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: ActivityNoteDetailBindingprivate val viewModel: NoteViewModel by viewModels {NoteViewModelFactory((application as NotesApplication).repository)}private var noteId: Long = -1Lprivate var isNewNote = trueoverride 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 == -1Lif (!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() ?: falseval contentValid = binding.contentInput.text?.isNotBlank() ?: falsebinding.titleLayout.error = if (!titleValid) getString(R.string.title_required) else nullbinding.contentLayout.error = if (!contentValid) getString(R.string.content_required) else nullreturn titleValid && contentValid}private fun saveNote() {if (!validateInputs()) returnval 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 文件:

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.titlenoteContent.text = note.contentval 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 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"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:padding="16dp"><TextViewandroid: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" /><TextViewandroid: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" /><TextViewandroid: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 文件:

import android.app.Application
import com.yourpackage.database.AppDatabase
import com.yourpackage.repository.NoteRepositoryclass NotesApplication : Application() {val database by lazy { AppDatabase.getDatabase(this) }val repository by lazy { NoteRepository(database.noteDao()) }
}

更新 AndroidManifest.xml 文件,添加 android:name 属性:

<applicationandroid: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 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"><itemandroid:id="@+id/action_delete_all"android:icon="@drawable/ic_delete"android:title="@string/delete_all"app:showAsAction="never" />
</menu>

8. 添加字符串资源

res/values/strings.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:

implementation 'com.facebook.stetho:stetho:1.6.0'

NotesApplication.kt 中初始化 Stetho:

import com.facebook.stetho.Stethoclass NotesApplication : Application() {override fun onCreate() {super.onCreate()Stetho.initializeWithDefaults(this)}// 其他代码...
}

运行应用后,在 Chrome 浏览器中访问 chrome://inspect 可以查看和调试数据库。

12. 数据库迁移

当您需要更改数据库结构时(例如添加新表或修改现有表),需要进行数据库迁移。

12.1 修改实体类

例如,我们要为 Note 添加一个 is_pinned 字段:

@Entity(tableName = "notes")
data class Note(// 现有字段...var is_pinned: Boolean = false
)

12.2 更新数据库版本

修改 AppDatabase.kt:

@Database(entities = [Note::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {// ...
}

12.3 添加迁移策略

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. 使用事务:对于批量操作,使用事务可以显著提高性能:
@Dao
interface NoteDao {@Transactionsuspend fun insertAll(notes: List<Note>) {notes.forEach { insertNote(it) }}
}
  1. 索引优化:为常用查询字段添加索引:
@Entity(tableName = "notes", indices = [Index(value = ["title"], unique = false)])
data class Note(// ...
)
  1. 分页加载:对于大量数据,使用 Paging 库:
@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 数据库的强大功能。

相关文章:

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

文章目录 1. 项目准备1.1 创建新项目1.2 添加必要依赖 2. 数据库设计3. 实现数据库3.1 创建实体类 (Entity)3.2 创建数据访问对象 (DAO)3.3 创建数据库类 4. 创建 Repository5. 创建 ViewModel6. 实现 UI 层6.1 创建笔记列表 Activityactivity_notes_list.xmlNotesListActivity…...

K8S学习笔记01

是什么 高可用&#xff0c;可扩展&#xff0c;自动化&#xff0c;容器化&#xff0c;管理多容器 组件 master API server controller manager scheduler etcd node 对应一台机器 负责运行和托管容器化 kubelet container runtime kube-proxy pod 同一个pod内的容器…...

奥威BI+AI数据分析解决方案

在数字化时代&#xff0c;数据已成为企业决策的核心驱动力。随着大数据、云计算、人工智能等技术的快速发展&#xff0c;企业对数据分析的需求日益迫切。奥威BI&#xff08;Business Intelligence&#xff09;与AI&#xff08;Artificial Intelligence&#xff09;的结合&#…...

第36课 常用快捷操作——用“鼠标右键”退出当前命令

概述 在AD 20软件中&#xff0c;很多的命令都是可以一直连续下去的&#xff0c;比方说放置一个元器件符号&#xff0c;如果你当中不取消的话&#xff0c;那就可以一直执行下去&#xff0c;放完一个接着放下一个&#xff0c;放完一个接着放下一个…… 想要退出这种连续进行的命…...

用Java模拟打字:深入解析 java.awt.Robot 的键盘控制艺术

作为开发者&#xff0c;我们有时会遇到需要自动化用户界面交互的场景&#xff0c;比如自动化测试、脚本编写、或者制作一些辅助工具。而模拟键盘输入&#xff0c;尤其是“打字”&#xff0c;是这类自动化任务中非常基础且常见的一环。 在 Java 中&#xff0c;实现这一目标的利…...

基于STM32、HAL库的ATSHA204A安全验证及加密芯片驱动程序设计

一、简介&#xff1a; ATSHA204A是Microchip公司生产的一款高性能加密认证芯片&#xff0c;主要特性包括&#xff1a; 基于SHA-256哈希算法的安全认证 4.5KB EEPROM存储空间&#xff08;可配置为密钥存储、OTP区域等&#xff09; 唯一的72位序列号 支持I2C和单线接口 工作…...

2.2.1goweb内置的 HTTP 处理程序

net/http 使用源码分析 在 Go 语言的 HTTP 服务器里&#xff0c;HTTP handler 是实现了http.Handler接口的对象。该接口定义如下&#xff1a; type Handler interface {ServeHTTP(ResponseWriter, *Request) }ServeHTTP方法接收两个参数&#xff1a; http.ResponseWriter&am…...

vscode以管理员身份运行报错

1. 问题现象 对vscode设置了管理员权限&#xff0c;但是打开文件时报错或闪退。 2. 解决方法 可以看一下官方网址&#xff1a;[Visual Studio Code on Windows](https://code.visualstudio.com/docs/setup/windows#_unable-to-run-as-admin-when-applocker-is-enabled) 2.1 …...

【Unity】 Dropdown默认选择不选择任何选项

你需要新建一个text文本并将其拖入Placeholder里面&#xff0c;这样你就可以在代码里面设置value-1了&#xff0c;从而实现默认为空...

gem5教程 第七章 如何在 gem 5 中运行我自己的程序

首先,您必须决定是否运行完整系统(FS)或系统调用仿真(SE)。 在 gem5 仿真器中,“完整系统仿真(Full System Simulation, FS)”与“系统调用仿真(System Call Emulation, SE)”是两种不同的仿真模式,各自有其特点和适用场景。下面是对这两种模式的比较: 1. 完整系统仿…...

Java实现基数排序算法

1. 基数排序原理图解 基数排序是一种非比较的排序算法&#xff0c;其核心思想是通过将整数按位数切割成不同的数字&#xff0c;然后按每个位数分别比较。具体步骤如下&#xff1a; 1. 确定最大值&#xff1a;找到数组中的最大值&#xff0c;以确定需要处理的位数。 2. 分配到桶…...

Python项目-支持自然语言处理

研究生组-自然语言处理-第一章节 Python学习&#xff0c;数据挖掘基础 项目地址&#xff1a;pythonbook: 人工智能实战——从 Python 入门到机器学习...

科技助力防灾减灾:卫星电话走进应急救援队伍

在自然灾害与突发事件频发的当下&#xff0c;通信保障已成为应急救援的“生命线”。传统地面通信网络易受极端天气、地形环境等因素制约&#xff0c;而卫星通信技术的突破&#xff0c;尤其是国产天通卫星系统与北斗系统的成熟应用&#xff0c;正推动救援通信体系迈入全天候、无…...

Android adb 安装应用失败(安装次数限制)

adb安装应用失败 前言平台安装失败现象解决 前言 Android Debug Bridge (ADB) 是一个功能强大的命令行工具&#xff0c;用于与 Android 设备通信。 安装 APK 文件&#xff1a; 使用命令 adb install <apk文件路径> 可以将 APK 文件安装到设备上。如果需要覆盖安装&…...

100天精通Python挑战总览 | 零基础到应用实战!

目录 ✨ 为什么发起100天挑战&#xff1f;✨整体学习路线规划第一阶段&#xff5c;基础篇&#xff08;第1天 - 第50天&#xff09;第二阶段&#xff5c;应用篇&#xff08;第51天 - 第100天&#xff09;Web开发篇爬虫篇数据分析篇AI入门篇 &#x1f3c6;为什么这么划分&#xf…...

预训练大模型与元训练大模型在医疗AI项目中的选型对比分析

简要版: 以下是对预训练大模型与元训练大模型(基于元学习/Meta-Learning)在医疗AI项目中选型对比的总结表格: 对比维度预训练大模型元训练大模型(基于元学习)技术基础基于海量通用数据预训练,通过微调适配具体任务(如GPT-4、LLaMA)通过元学习框架训练,支持快速适应新…...

【JavaScript】相等运算符、条件运算符

1、相等运算符 &#xff08;1&#xff09;&#xff08;相等&#xff09; 相等运算符用来比较两个值是否相等&#xff0c;如果相等会返回true&#xff0c;否则返回false <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…...

企业用电管理革新利器 —— Acrel-3000 电能管理系统应用解析

电能&#xff0c;以其方便传输、易于转换、便于控制等特性&#xff0c;早已成为广大企事业单位生产、办公的核心能量来源。在 “双碳” 战略目标的宏大背景下&#xff0c;能源结构转型加速推进&#xff0c;电能清洁、高效、零排放的特点愈发凸显。在能源消费侧&#xff0c;“以…...

学生管理系统审计

1.环境搭建 项目地址: https://gitee.com/huang-yk/student-manage 项目下载到本地后IDEA打开&#xff0c;等待项目加载 配置Maven&#xff0c;修改数据库配置文件 然后启动 访问 2.代码审计 1.垂直越权未授权敏感信息泄露 找到拦截器看看对登录做了什么校验 Override public b…...

OpenGL----OpenGL纹理与纹理缓存区

在现代计算机图形学中,纹理(Texture)是一个至关重要的概念。它不仅可以为几何体表面添加细节和真实感,还可以用于实现各种复杂的视觉效果和数据处理。在OpenGL中,纹理的应用范围非常广泛,从基本的颜色映射到高级的阴影映射、环境映射等。本文将深入探讨OpenGL纹理与纹理缓…...

QT开发技术【qcustomplot 曲线与鼠标十字功能】

一、效果 二、代码 #include "obsersingle.h" #include "ui_obsersingle.h" #pragma execution_character_set("utf-8")右键菜单acion回调 // 适应窗口大小 void ObserSingle::RescaleActionFun::fun(ObserSingle *form) {// 自适应y轴数据范围…...

[特殊字符] 大模型后训练指南:从毛坯引擎到智能助手的进化之路 [特殊字符]️

最近看了MIT 6.S191 2025这个讲座感觉讲的挺好的&#xff0c;然后写了一篇总结&#xff0c;大家感兴趣的话可以直接看视频&#xff0c;链接我放到文章最后。 &#x1f3af; 讲座核心 这个讲座主要讲的是&#xff0c;当我们有了一个基础的大语言模型&#xff08;就像刚出厂的毛…...

厚铜pcb生产厂家哪家好?

在为您的项目选择厚铜PCB供应商时&#xff0c;技术实力、生产经验与交付能力是决定产品可靠性的关键。随着新能源汽车、工业电源、5G通信等领域对高电流承载、高效散热的需求激增&#xff0c;厚铜PCB&#xff08;铜厚3oz以上&#xff09;的工艺门槛不断提升。本文结合行业头部企…...

【重走C++学习之路】22、C++11语法

目录 一、列表初始化 1.1 {}初始化 1.2 std::initializer_list 二、变量类型推导 2.1 auto 2.2 decltype 三、右值引用和移动语义 3.1 左值与左值引用 3.2 右值与右值引用 3.3 左值引用与右值引用比较 3.4 右值引用使用场景和意义 3.5 move 3.6 完美转发和万能引…...

Spring Security授权管理

授权是Spring Security的核心功能之一&#xff0c;是根据用户的权限来控制用户访问资源的过程&#xff0c;拥有资源的访问权限则可正常访问&#xff0c;没有访问的权限时则会被拒绝访问。认证是为了保证用户身份的合法性&#xff0c;而授权则是为了更细粒度地对隐私数据进行划分…...

2025A卷-正整数到Excel编号之间的转换

题目描述 用过 excel 的都知道excel的列编号是这样的&#xff1a; a b c … z aa ab ac … az ba bb bc … yz za zb zc … zz aaa aab aac … 分别代表以下编号&#xff1a; 1 2 3 … 26 27 28 29 … 52 53 54 55 … 676 677 678 679 … 702 703 704 705 … 请写个函数&…...

算法设计与分析(期末试卷)

目录 一、频度计算&#xff08;15 分&#xff09; 二、项目工期问题&#xff08;20 分&#xff09; 三、TSP 问题的贪心算法&#xff08;15 分&#xff09; 四、“秤心如意”&#xff08;15 分&#xff09; 五、工作指派问题&#xff08;20 分&#xff09; 六、计算复杂度…...

springboot(2.6.13)自定义用户授权管理

1.自定义用户访问控制 a.重写configure(HttpSecurity http)方法 在自定义配置类SecurityConfig中重写 Override protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/").permitAll().antMatchers("/deta…...

JavaWeb:vueaxios

一、简介 什么是vue? 快速入门 <!-- 3.准备视图元素 --><div id"app"><!-- 6.数据渲染 --><h1>{{ msg }}</h1></div><script type"module">// 1.引入vueimport { createApp, ref } from https://unpkg.com/vu…...

uniapp常用

1.下载文件带进度提示 <template> <view> <button click"startDownload">下载文件</button> <progress :percent"progress" stroke-width"3" /> </view> </template> <…...

etcd 的安装及使用

介绍 Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统&#xff0c;用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性&#xff0c;且客户端通过长连接 watch 功能&#xff0c;能够及时收到数据变化通知&#xff0c;相较于 Zookeepe…...

uni-app vue3 实现72小时倒计时功能

功能介绍 &#xff0c;数组项有一个下单时间 &#xff0c;比如今天下单在72小时内可以继续支付&#xff0c;超过则默认取消订单 页面按钮处 加上倒计时 <!-- 倒计时 --> <text v-if"item.timeLeft > 0">{{ formatTime(item.remaining) }}</text&g…...

【C语言】初阶算法相关习题(二)

个人主页&#xff1a;夜晚中的人海 文章目录 ⭐一、两数之和&#x1f3e0;二、珠玑妙算&#x1f3a1;三、寻找奇数&#x1f680;四、截取字符串&#x1f389;五、寻找峰值 ⭐一、两数之和 题目描述&#xff1a;两数之和 解题思路&#xff1a; 1.先创建一个动态分配的数组ret&a…...

Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理

Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理 目录 Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理 一、简单介绍 二、Flutter 和 Android 原生之间的数据…...

STM32的SysTick

SysTick介绍 定义&#xff1a;Systick&#xff0c;即滴答定时器&#xff0c;是内核中的一个特殊定时器&#xff0c;用于提供系统级的定时服务。该定时器是一个24位的递减计数器&#xff0c;具有自动重载值寄存器的功能。当计数器到达自动重载值时&#xff0c;它会自动重新加载…...

【JS事件循环机制event-loop】

目录 0、总结1、Event-Loop 概念2、宏任务-微任务3、事件循环执行机制4、调用栈5、示例 0、总结 Tasks execute in order, and the browser may render between them 【宏任务按序执行&#xff0c;浏览器可以在它们之间进行渲染】Microtasks execute in order, and are execut…...

对比N+1查询和关联聚合查询

通常我们管第一种模式叫 “N1 查询”&#xff0c;第二种叫 “关联聚合查询”。下面从几个角度来比较&#xff0c;帮助你做出选择。 1. 性能与资源消耗 方案SQL 语句数网络往返次数数据库负载Java 处理N1 查询&#xff08;先查项目&#xff0c;再遍历项目查设备状态数&#xff…...

优化 Flutter 应用启动:从冷启动到就绪仅需 2 秒

冷启动序列剖析&#xff1a;冷启动时&#xff0c;Flutter 应用需经历引擎和 Dart VM 初始化、启动 Dart Isolate、渲染第一帧等步骤。Android 和 iOS 系统分别通过启动屏幕和 Storyboard 缓解启动延迟。应用大小、初始化工作、调试模式下的 JIT 编译等因素会影响冷启动时间。优…...

牟乃夏《ArcGIS Engine 地理信息系统开发教程》学习笔记 4-空间分析与高级功能开发

目录 一、核心组件与接口回顾 &#xff08;一&#xff09;空间分析基础架构 &#xff08;二&#xff09;网络分析模块 二、矢量数据空间分析实战 &#xff08;一&#xff09;缓冲区分析 &#xff08;二&#xff09;叠加分析&#xff08;以裁剪为例&#xff09; 三、栅格…...

UE 滚动提示条材质制作

需要两个贴图 先制作条纹屏闪 这里RGB输出连到alpha&#xff0c;0为白色&#xff0c;到1就为黑色了 因为这个图片是RGB输出代表三个图片&#xff0c;看贴图颜色就知道了&#xff0c;然后把这三个相加一下&#xff1b;链接自发光颜色&#xff0c; 这里设置速度变量 通过网盘分…...

金融业数字化转型——深入解读77页2024年中国金融体系指标大全【附全文阅读】

本文主要介绍了金融业通行宝典中国金融体系指标大全的内容,包括央行体系、商业银行体系、非银金融机构与地方金融组织的各项指标。文章详细分析了美联储资产负債表的结构,并概述了美日欧等主要经济体资产负债表状况。 重点内容: 1. 央行体系是金融分析的重点。 2. 美联储资产…...

研究:大模型输出一致性:确定性与随机性的场景化平衡

大模型在相同输入下的输出是否一致,本质上取决于其设计目标、任务性质以及技术实现方式。这一问题需要从技术原理、应用场景、用户需求三个维度进行深度分析: 一、技术实现:确定性与随机性的平衡 模型架构的确定性基础 大模型的核心参数(如权重矩阵)在训练完成后是固定的…...

数据分析1

一、常用数据处理模块Numpy Numpy常用于高性能计算&#xff0c;在机器学习常常作为传递数据的容器。提供了两种基本对象&#xff1a;ndarray、ufunc。 ndarray具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。 ufunc提供了对数组快速运算的标准数学函数。 ndar…...

vmare pro安装报错用户在命令行上发出了EULAS_AGREED=1,表示不接受许可协议的错误解决方法

问题现状和原因 用户在命令行上发出了EULAS_AGREED1&#xff0c;表示不接受许可协议的错误。 以上错误主要原因是因为机器安装过了vmare 卸载时没有卸载干净导致的。 解决方法&#xff1a; 1、控制面板-程序和功能-卸载程序。找到vamre卸载掉。 2、打开开始菜单输入注册表 …...

《Linux篇》基础开发工具——vim详细介绍

文章目录 1.软件包管理1.1 什么是软件包1.2 Linux软件生态 2.编辑器vim2.1 vim的正常/命令模式2.2 vim的末行模式2.3 vim的插入模式 3.配置vim 1.软件包管理 我们先来看一下再Linux是那个如何安装软件&#xff1f; 源码安装&#xff1a;软件是存在相互依赖的关系的&#xff0…...

AI图片跳舞生成视频,animate X本地部署。

本期内容打包限时免费下载https://www.kdocs.cn/l/cnQ5lNU5DFZB 对比不同算法&#xff0c;使用同一组图片和舞蹈视频。animate X官网&#xff0c;下载项目解压。按照官方教程下载模型&#xff0c;项目包和命名好的模型包已上传网盘&#xff0c;放到解压目录下即可。 安装好cond…...

Web技术与Apache网站部署

一、Web 基础与 HTTP 协议 1.1 静态网页与动态网页 静态网页 定义&#xff1a;由纯 HTML、CSS、JavaScript 构成&#xff0c;文件扩展名为 .htm 或 .html。内容在服务器生成后固定不变&#xff0c;仅通过客户端脚本&#xff08;如 JS&#xff09;实现视觉动态效果&#xff08…...

第七章:Server/Client Communication

Chapter 7: Server/Client Communication 从工具集成到服务器通信&#xff1a;如何让AI“远程协作”&#xff1f; 在上一章的工具与LLM集成中&#xff0c;我们已经能让AI调用真实世界的工具。但你是否想过&#xff1a;如果多个用户同时请求天气查询&#xff0c;或者需要远程控…...

Linux调试器 - gdb使用指南

目录 一、背景知识 二、开始使用 gdb &#xff08;一&#xff09;查看源代码相关指令 &#xff08;二&#xff09;程序执行控制指令 &#xff08;三&#xff09;断点相关指令 &#xff08;四&#xff09;变量操作相关指令 &#xff08;五&#xff09;其他常用指令 在Li…...

C++面试常青客:LRUCache最近最少使用算法

C面试常青客&#xff1a;LRUCache最近最少使用算法 文章目录 C面试常青客&#xff1a;LRUCache最近最少使用算法1.背景&#x1f3c6;2.原理&#x1f680;2.1基本原理2.2核心特性 3.结构3.1为什么需要 list<pair<int,int>>&#xff08;双向链表&#xff09;&#xf…...