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 创建新项目
- 打开 Android Studio
- 选择 “Start a new Android Studio project”
- 选择 “Empty Activity” 模板
- 设置项目名称(例如 “SQLiteDemo”)
- 选择语言(Kotlin 或 Java,本教程以 Kotlin 为例)
- 设置最低 API 级别(建议 API 21 或更高)
- 点击 “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. 运行和测试应用
现在,您可以运行应用程序并测试以下功能:
- 添加新笔记
- 查看笔记列表
- 编辑现有笔记
- 删除笔记
- 搜索笔记
- 删除所有笔记
11. 数据库调试技巧
11.1 查看数据库内容
- 在 Android Studio 中打开 “Device File Explorer” (View -> Tool Windows -> Device File Explorer)
- 导航到
/data/data/com.yourpackage/databases/
- 找到
notes_database
文件 - 右键点击并选择 “Save As” 将其保存到本地
- 使用 SQLite 浏览器工具(如 DB Browser for SQLite)打开该文件查看内容
11.2 使用 Stetho 进行调试
添加 Stetho 依赖到 build.gradle
:
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. 性能优化建议
- 使用事务:对于批量操作,使用事务可以显著提高性能:
@Dao
interface NoteDao {@Transactionsuspend fun insertAll(notes: List<Note>) {notes.forEach { insertNote(it) }}
}
- 索引优化:为常用查询字段添加索引:
@Entity(tableName = "notes", indices = [Index(value = ["title"], unique = false)])
data class Note(// ...
)
- 分页加载:对于大量数据,使用 Paging 库:
@Query("SELECT * FROM notes ORDER BY updated_at DESC")
fun getPagedNotes(): PagingSource<Int, Note>
- 避免在主线程操作数据库:始终确保数据库操作在后台线程执行。
14. 完整项目结构
最终项目结构应类似于:
com.yourpackage
├── adapter
│ └── NotesAdapter.kt
├── dao
│ └── NoteDao.kt
├── database
│ └── AppDatabase.kt
├── model
│ └── Note.kt
├── repository
│ └── NoteRepository.kt
├── ui
│ ├── NotesListActivity.kt
│ └── NoteDetailActivity.kt
├── viewmodel
│ ├── NoteViewModel.kt
│ └── NoteViewModelFactory.kt
└── NotesApplication.kt
15. 总结
本指南详细介绍了在 Android Studio 中使用 SQLite 数据库的完整开发流程,包括:
- 设置项目和依赖
- 设计数据库结构
- 实现 Room 数据库组件(Entity, DAO, Database)
- 创建 Repository 层
- 实现 ViewModel
- 构建用户界面
- 添加数据库迁移支持
- 性能优化建议
通过遵循这些步骤,您可以构建一个功能完善、结构清晰的 Android 应用,充分利用 SQLite 数据库的强大功能。
相关文章:
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
是什么 高可用,可扩展,自动化,容器化,管理多容器 组件 master API server controller manager scheduler etcd node 对应一台机器 负责运行和托管容器化 kubelet container runtime kube-proxy pod 同一个pod内的容器…...
奥威BI+AI数据分析解决方案
在数字化时代,数据已成为企业决策的核心驱动力。随着大数据、云计算、人工智能等技术的快速发展,企业对数据分析的需求日益迫切。奥威BI(Business Intelligence)与AI(Artificial Intelligence)的结合&#…...
第36课 常用快捷操作——用“鼠标右键”退出当前命令
概述 在AD 20软件中,很多的命令都是可以一直连续下去的,比方说放置一个元器件符号,如果你当中不取消的话,那就可以一直执行下去,放完一个接着放下一个,放完一个接着放下一个…… 想要退出这种连续进行的命…...
用Java模拟打字:深入解析 java.awt.Robot 的键盘控制艺术
作为开发者,我们有时会遇到需要自动化用户界面交互的场景,比如自动化测试、脚本编写、或者制作一些辅助工具。而模拟键盘输入,尤其是“打字”,是这类自动化任务中非常基础且常见的一环。 在 Java 中,实现这一目标的利…...
基于STM32、HAL库的ATSHA204A安全验证及加密芯片驱动程序设计
一、简介: ATSHA204A是Microchip公司生产的一款高性能加密认证芯片,主要特性包括: 基于SHA-256哈希算法的安全认证 4.5KB EEPROM存储空间(可配置为密钥存储、OTP区域等) 唯一的72位序列号 支持I2C和单线接口 工作…...
2.2.1goweb内置的 HTTP 处理程序
net/http 使用源码分析 在 Go 语言的 HTTP 服务器里,HTTP handler 是实现了http.Handler接口的对象。该接口定义如下: type Handler interface {ServeHTTP(ResponseWriter, *Request) }ServeHTTP方法接收两个参数: http.ResponseWriter&am…...
vscode以管理员身份运行报错
1. 问题现象 对vscode设置了管理员权限,但是打开文件时报错或闪退。 2. 解决方法 可以看一下官方网址:[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里面,这样你就可以在代码里面设置value-1了,从而实现默认为空...
gem5教程 第七章 如何在 gem 5 中运行我自己的程序
首先,您必须决定是否运行完整系统(FS)或系统调用仿真(SE)。 在 gem5 仿真器中,“完整系统仿真(Full System Simulation, FS)”与“系统调用仿真(System Call Emulation, SE)”是两种不同的仿真模式,各自有其特点和适用场景。下面是对这两种模式的比较: 1. 完整系统仿…...
Java实现基数排序算法
1. 基数排序原理图解 基数排序是一种非比较的排序算法,其核心思想是通过将整数按位数切割成不同的数字,然后按每个位数分别比较。具体步骤如下: 1. 确定最大值:找到数组中的最大值,以确定需要处理的位数。 2. 分配到桶…...
Python项目-支持自然语言处理
研究生组-自然语言处理-第一章节 Python学习,数据挖掘基础 项目地址:pythonbook: 人工智能实战——从 Python 入门到机器学习...
科技助力防灾减灾:卫星电话走进应急救援队伍
在自然灾害与突发事件频发的当下,通信保障已成为应急救援的“生命线”。传统地面通信网络易受极端天气、地形环境等因素制约,而卫星通信技术的突破,尤其是国产天通卫星系统与北斗系统的成熟应用,正推动救援通信体系迈入全天候、无…...
Android adb 安装应用失败(安装次数限制)
adb安装应用失败 前言平台安装失败现象解决 前言 Android Debug Bridge (ADB) 是一个功能强大的命令行工具,用于与 Android 设备通信。 安装 APK 文件: 使用命令 adb install <apk文件路径> 可以将 APK 文件安装到设备上。如果需要覆盖安装&…...
100天精通Python挑战总览 | 零基础到应用实战!
目录 ✨ 为什么发起100天挑战?✨整体学习路线规划第一阶段|基础篇(第1天 - 第50天)第二阶段|应用篇(第51天 - 第100天)Web开发篇爬虫篇数据分析篇AI入门篇 🏆为什么这么划分…...
预训练大模型与元训练大模型在医疗AI项目中的选型对比分析
简要版: 以下是对预训练大模型与元训练大模型(基于元学习/Meta-Learning)在医疗AI项目中选型对比的总结表格: 对比维度预训练大模型元训练大模型(基于元学习)技术基础基于海量通用数据预训练,通过微调适配具体任务(如GPT-4、LLaMA)通过元学习框架训练,支持快速适应新…...
【JavaScript】相等运算符、条件运算符
1、相等运算符 (1)(相等) 相等运算符用来比较两个值是否相等,如果相等会返回true,否则返回false <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…...
企业用电管理革新利器 —— Acrel-3000 电能管理系统应用解析
电能,以其方便传输、易于转换、便于控制等特性,早已成为广大企事业单位生产、办公的核心能量来源。在 “双碳” 战略目标的宏大背景下,能源结构转型加速推进,电能清洁、高效、零排放的特点愈发凸显。在能源消费侧,“以…...
学生管理系统审计
1.环境搭建 项目地址: https://gitee.com/huang-yk/student-manage 项目下载到本地后IDEA打开,等待项目加载 配置Maven,修改数据库配置文件 然后启动 访问 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这个讲座感觉讲的挺好的,然后写了一篇总结,大家感兴趣的话可以直接看视频,链接我放到文章最后。 🎯 讲座核心 这个讲座主要讲的是,当我们有了一个基础的大语言模型(就像刚出厂的毛…...
厚铜pcb生产厂家哪家好?
在为您的项目选择厚铜PCB供应商时,技术实力、生产经验与交付能力是决定产品可靠性的关键。随着新能源汽车、工业电源、5G通信等领域对高电流承载、高效散热的需求激增,厚铜PCB(铜厚3oz以上)的工艺门槛不断提升。本文结合行业头部企…...
【重走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的核心功能之一,是根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则可正常访问,没有访问的权限时则会被拒绝访问。认证是为了保证用户身份的合法性,而授权则是为了更细粒度地对隐私数据进行划分…...
2025A卷-正整数到Excel编号之间的转换
题目描述 用过 excel 的都知道excel的列编号是这样的: a b c … z aa ab ac … az ba bb bc … yz za zb zc … zz aaa aab aac … 分别代表以下编号: 1 2 3 … 26 27 28 29 … 52 53 54 55 … 676 677 678 679 … 702 703 704 705 … 请写个函数&…...
算法设计与分析(期末试卷)
目录 一、频度计算(15 分) 二、项目工期问题(20 分) 三、TSP 问题的贪心算法(15 分) 四、“秤心如意”(15 分) 五、工作指派问题(20 分) 六、计算复杂度…...
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 编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接 watch 功能,能够及时收到数据变化通知,相较于 Zookeepe…...
uni-app vue3 实现72小时倒计时功能
功能介绍 ,数组项有一个下单时间 ,比如今天下单在72小时内可以继续支付,超过则默认取消订单 页面按钮处 加上倒计时 <!-- 倒计时 --> <text v-if"item.timeLeft > 0">{{ formatTime(item.remaining) }}</text&g…...
【C语言】初阶算法相关习题(二)
个人主页:夜晚中的人海 文章目录 ⭐一、两数之和🏠二、珠玑妙算🎡三、寻找奇数🚀四、截取字符串🎉五、寻找峰值 ⭐一、两数之和 题目描述:两数之和 解题思路: 1.先创建一个动态分配的数组ret&a…...
Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理
Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理 目录 Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理 一、简单介绍 二、Flutter 和 Android 原生之间的数据…...
STM32的SysTick
SysTick介绍 定义:Systick,即滴答定时器,是内核中的一个特殊定时器,用于提供系统级的定时服务。该定时器是一个24位的递减计数器,具有自动重载值寄存器的功能。当计数器到达自动重载值时,它会自动重新加载…...
【JS事件循环机制event-loop】
目录 0、总结1、Event-Loop 概念2、宏任务-微任务3、事件循环执行机制4、调用栈5、示例 0、总结 Tasks execute in order, and the browser may render between them 【宏任务按序执行,浏览器可以在它们之间进行渲染】Microtasks execute in order, and are execut…...
对比N+1查询和关联聚合查询
通常我们管第一种模式叫 “N1 查询”,第二种叫 “关联聚合查询”。下面从几个角度来比较,帮助你做出选择。 1. 性能与资源消耗 方案SQL 语句数网络往返次数数据库负载Java 处理N1 查询(先查项目,再遍历项目查设备状态数ÿ…...
优化 Flutter 应用启动:从冷启动到就绪仅需 2 秒
冷启动序列剖析:冷启动时,Flutter 应用需经历引擎和 Dart VM 初始化、启动 Dart Isolate、渲染第一帧等步骤。Android 和 iOS 系统分别通过启动屏幕和 Storyboard 缓解启动延迟。应用大小、初始化工作、调试模式下的 JIT 编译等因素会影响冷启动时间。优…...
牟乃夏《ArcGIS Engine 地理信息系统开发教程》学习笔记 4-空间分析与高级功能开发
目录 一、核心组件与接口回顾 (一)空间分析基础架构 (二)网络分析模块 二、矢量数据空间分析实战 (一)缓冲区分析 (二)叠加分析(以裁剪为例) 三、栅格…...
UE 滚动提示条材质制作
需要两个贴图 先制作条纹屏闪 这里RGB输出连到alpha,0为白色,到1就为黑色了 因为这个图片是RGB输出代表三个图片,看贴图颜色就知道了,然后把这三个相加一下;链接自发光颜色, 这里设置速度变量 通过网盘分…...
金融业数字化转型——深入解读77页2024年中国金融体系指标大全【附全文阅读】
本文主要介绍了金融业通行宝典中国金融体系指标大全的内容,包括央行体系、商业银行体系、非银金融机构与地方金融组织的各项指标。文章详细分析了美联储资产负債表的结构,并概述了美日欧等主要经济体资产负债表状况。 重点内容: 1. 央行体系是金融分析的重点。 2. 美联储资产…...
研究:大模型输出一致性:确定性与随机性的场景化平衡
大模型在相同输入下的输出是否一致,本质上取决于其设计目标、任务性质以及技术实现方式。这一问题需要从技术原理、应用场景、用户需求三个维度进行深度分析: 一、技术实现:确定性与随机性的平衡 模型架构的确定性基础 大模型的核心参数(如权重矩阵)在训练完成后是固定的…...
数据分析1
一、常用数据处理模块Numpy Numpy常用于高性能计算,在机器学习常常作为传递数据的容器。提供了两种基本对象:ndarray、ufunc。 ndarray具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。 ufunc提供了对数组快速运算的标准数学函数。 ndar…...
vmare pro安装报错用户在命令行上发出了EULAS_AGREED=1,表示不接受许可协议的错误解决方法
问题现状和原因 用户在命令行上发出了EULAS_AGREED1,表示不接受许可协议的错误。 以上错误主要原因是因为机器安装过了vmare 卸载时没有卸载干净导致的。 解决方法: 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是那个如何安装软件? 源码安装:软件是存在相互依赖的关系的࿰…...
AI图片跳舞生成视频,animate X本地部署。
本期内容打包限时免费下载https://www.kdocs.cn/l/cnQ5lNU5DFZB 对比不同算法,使用同一组图片和舞蹈视频。animate X官网,下载项目解压。按照官方教程下载模型,项目包和命名好的模型包已上传网盘,放到解压目录下即可。 安装好cond…...
Web技术与Apache网站部署
一、Web 基础与 HTTP 协议 1.1 静态网页与动态网页 静态网页 定义:由纯 HTML、CSS、JavaScript 构成,文件扩展名为 .htm 或 .html。内容在服务器生成后固定不变,仅通过客户端脚本(如 JS)实现视觉动态效果(…...
第七章:Server/Client Communication
Chapter 7: Server/Client Communication 从工具集成到服务器通信:如何让AI“远程协作”? 在上一章的工具与LLM集成中,我们已经能让AI调用真实世界的工具。但你是否想过:如果多个用户同时请求天气查询,或者需要远程控…...
Linux调试器 - gdb使用指南
目录 一、背景知识 二、开始使用 gdb (一)查看源代码相关指令 (二)程序执行控制指令 (三)断点相关指令 (四)变量操作相关指令 (五)其他常用指令 在Li…...
C++面试常青客:LRUCache最近最少使用算法
C面试常青客:LRUCache最近最少使用算法 文章目录 C面试常青客:LRUCache最近最少使用算法1.背景🏆2.原理🚀2.1基本原理2.2核心特性 3.结构3.1为什么需要 list<pair<int,int>>(双向链表)…...