将数据库使用Jetpack Room重构

This commit is contained in:
Super12138 2024-01-07 14:32:25 +08:00
parent 49b50ef4ca
commit 60f99bb8f9
14 changed files with 237 additions and 197 deletions

View file

@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
} }
val baseVersionName = "1.0.0" val baseVersionName = "1.0.0"
@ -60,6 +61,11 @@ dependencies {
implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.preference:preference-ktx:1.2.1")
// Material Design // Material Design
implementation("com.google.android.material:material:1.12.0-alpha02") implementation("com.google.android.material:material:1.12.0-alpha02")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Test // Test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")

View file

@ -3,13 +3,19 @@ package cn.super12138.todo
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import cn.super12138.todo.logic.dao.ToDoRoomDB
import cn.super12138.todo.views.crash.CrashHandler import cn.super12138.todo.views.crash.CrashHandler
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ToDoApplication : Application() { class ToDoApplication : Application() {
private val database by lazy { ToDoRoomDB.getDatabase(this) }
companion object { companion object {
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
lateinit var context: Context lateinit var context: Context
lateinit var db: ToDoRoomDB
} }
override fun onCreate() { override fun onCreate() {
@ -19,5 +25,7 @@ class ToDoApplication : Application() {
val crashHandler = CrashHandler(this) val crashHandler = CrashHandler(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler) Thread.setDefaultUncaughtExceptionHandler(crashHandler)
db = database
} }
} }

View file

@ -1,21 +1,88 @@
package cn.super12138.todo.logic package cn.super12138.todo.logic
import android.content.ContentValues
import android.content.Context import android.content.Context
import cn.super12138.todo.logic.database.DBHelper import cn.super12138.todo.ToDoApplication
import cn.super12138.todo.logic.dao.ToDoRoom
import cn.super12138.todo.logic.database.SPHelper import cn.super12138.todo.logic.database.SPHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object Repository { object Repository {
fun getCompleteTotalCount() = DBHelper.getCompleteTotalCount() /**
* 获取应用设置里的数据
fun insertData(data: ContentValues) = DBHelper.insertData(data) */
fun deleteData(deleteAll: Boolean, uuid: String?) = DBHelper.deleteData(deleteAll, uuid)
fun updateData(uuid: String, newData: ContentValues) = DBHelper.updateData(uuid, newData)
fun getAllData() = DBHelper.getAllData()
fun getPreferenceString(context: Context, key: String, defaultValue: String) = fun getPreferenceString(context: Context, key: String, defaultValue: String) =
SPHelper.getPreferenceString(context, key, defaultValue) SPHelper.getPreferenceString(context, key, defaultValue)
// Room
private val db get() = ToDoApplication.db
val todoDao = db.toDoRoomDao()
/**
* @param toDoRoom 要插入的数据
*/
suspend fun insert(toDoRoom: ToDoRoom) {
withContext(Dispatchers.IO) {
todoDao.insert(toDoRoom)
}
}
/**
* 获取全部未完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllUncomplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllUnfinished()
}
}
/**
* 获取全部已完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllComplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllComplete()
}
}
/**
* 获取全部待办
* @return List<ToDoRoom>
*/
suspend fun getAll(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAll()
}
}
/**
* 根据待办的UUID删除指定待办
* @param uuid 待办的UUID
*/
suspend fun deleteByUUID(uuid: String) {
withContext(Dispatchers.IO) {
todoDao.deleteByUUID(uuid)
}
}
/**
* 删除全部代办
*/
suspend fun deleteAll() {
withContext(Dispatchers.IO) {
todoDao.deleteAll()
}
}
/**
* 根据代办的UUID来把待办状态更新为已完成
* @param uuid 待办的UUID
*/
suspend fun updateStateByUUID(uuid: String) {
withContext(Dispatchers.IO) {
todoDao.updateStateByUUID(uuid)
}
}
} }

View file

@ -0,0 +1,19 @@
package cn.super12138.todo.logic.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* @param uuid String 待办的uuid
* @param state Int 待办的完成状态0表示未完成1表示完成
* @param subject String 待办的学科
* @param context String 待办的内容
*/
@Entity(tableName = "todo")
data class ToDoRoom(
@PrimaryKey @ColumnInfo(name = "uuid") val uuid: String,
@ColumnInfo(name = "state") val state: Int,
@ColumnInfo(name = "subject") val subject: String,
@ColumnInfo(name = "context") val context: String
)

View file

@ -0,0 +1,27 @@
package cn.super12138.todo.logic.dao
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [ToDoRoom::class], version = 1)
abstract class ToDoRoomDB : RoomDatabase() {
abstract fun toDoRoomDao(): ToDoRoomDao
companion object {
@Volatile
private var INSTANCE: ToDoRoomDB? = null
fun getDatabase(context: Context): ToDoRoomDB {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ToDoRoomDB::class.java,
"todo"
).build()
INSTANCE = instance
instance
}
}
}
}

View file

@ -0,0 +1,29 @@
package cn.super12138.todo.logic.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface ToDoRoomDao {
@Insert
suspend fun insert(toDoRoom: ToDoRoom)
@Query("SELECT * FROM todo")
suspend fun getAll(): List<ToDoRoom>
@Query("SELECT * FROM todo WHERE state = 0")
suspend fun getAllUnfinished(): List<ToDoRoom>
@Query("SELECT * FROM todo WHERE state = 1")
suspend fun getAllComplete(): List<ToDoRoom>
@Query("DELETE FROM todo")
suspend fun deleteAll()
@Query("DELETE FROM todo WHERE uuid = :uuid")
suspend fun deleteByUUID(uuid: String)
@Query("UPDATE todo SET state = 1 WHERE uuid = :uuid")
suspend fun updateStateByUUID(uuid: String)
}

View file

@ -1,109 +0,0 @@
package cn.super12138.todo.logic.database
import android.annotation.SuppressLint
import android.content.ContentValues
import cn.super12138.todo.ToDoApplication
import cn.super12138.todo.constant.Constants
import cn.super12138.todo.logic.model.Progress
import cn.super12138.todo.logic.model.ToDo
import cn.super12138.todo.logic.model.ToDoDatabase
object DBHelper {
fun insertData(data: ContentValues) {
ToDoDatabase(
ToDoApplication.context,
Constants.DB_NAME,
Constants.DB_VERSION
).writableDatabase.use { db ->
db.insert(Constants.TABLE_NAME, null, data)
}
}
fun deleteData(delAll: Boolean, uuid: String?) {
ToDoDatabase(
ToDoApplication.context,
Constants.DB_NAME,
Constants.DB_VERSION
).writableDatabase.use { db ->
if (delAll) {
db.delete(Constants.TABLE_NAME, null, null)
} else {
if (uuid == null) {
throw RuntimeException("uuid cannot be null")
}
db.delete(Constants.TABLE_NAME, "uuid = ?", arrayOf(uuid))
}
}
}
fun updateData(uuid: String, newData: ContentValues) {
val dbHelper = ToDoDatabase(
ToDoApplication.context,
Constants.DB_NAME,
Constants.DB_VERSION
).writableDatabase
dbHelper.update(Constants.TABLE_NAME, newData, "uuid = ?", arrayOf(uuid))
}
@SuppressLint("Range")
fun getCompleteTotalCount(): Progress {
var total = 0
var complete = 0
ToDoDatabase(
ToDoApplication.context,
Constants.DB_NAME,
Constants.DB_VERSION
).writableDatabase.use { db ->
db.query(Constants.TABLE_NAME, null, null, null, null, null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
do {
val state = cursor.getString(cursor.getColumnIndex("state"))
if (state.toInt() == 1) {
complete += 1
}
total += 1
} while (cursor.moveToNext())
cursor.close()
}
}
}
return Progress(complete, total)
}
@SuppressLint("Range")
fun getAllData(): MutableList<ToDo> {
val todoList = mutableListOf<ToDo>()
ToDoDatabase(
ToDoApplication.context,
Constants.DB_NAME,
Constants.DB_VERSION
).writableDatabase.query(
Constants.TABLE_NAME,
null,
null,
null,
null,
null,
null,
null
).use { cursor ->
if (cursor.moveToFirst()) {
do {
val uuid = cursor.getString(cursor.getColumnIndex("uuid"))
val subject = cursor.getString(cursor.getColumnIndex("subject"))
val state = cursor.getString(cursor.getColumnIndex("state"))
val todoContext = cursor.getString(cursor.getColumnIndex("context"))
if (state.toInt() != 1) {
todoList.add(ToDo(uuid, todoContext, subject))
}
} while (cursor.moveToNext())
cursor.close()
}
}
return todoList
}
}

View file

@ -1,3 +0,0 @@
package cn.super12138.todo.logic.model
data class Progress(val complete: Int, val total: Int)

View file

@ -1,28 +0,0 @@
package cn.super12138.todo.logic.model
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class ToDoDatabase(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
/*
* @param uuid: String 待办的uuid
* @param state: Int 待办的完成状态0表示未完成1表示完成
* @param subject: String 待办的学科
* @param context: String 待办的内容
*/
private val createToDo = "create table ToDo (" +
"uuid text primary key," +
"state integer," +
"subject text," +
"context text)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createToDo)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}

View file

@ -3,6 +3,7 @@ package cn.super12138.todo.views.crash
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Process import android.os.Process
import kotlin.system.exitProcess
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler { class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
@ -18,11 +19,11 @@ class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandl
} }
context.startActivity(intent) context.startActivity(intent)
// 杀掉崩溃的应用程序进程
Process.killProcess(Process.myPid())
System.exit(10)
// 传递异常给默认的异常处理器 // 传递异常给默认的异常处理器
defaultUEH?.uncaughtException(thread, ex) defaultUEH?.uncaughtException(thread, ex)
// 杀掉崩溃的应用程序进程
Process.killProcess(Process.myPid())
exitProcess(10)
} }
} }

View file

@ -2,7 +2,9 @@ package cn.super12138.todo.views.progress
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cn.super12138.todo.logic.Repository import cn.super12138.todo.logic.Repository
import kotlinx.coroutines.launch
class ProgressFragmentViewModel : ViewModel() { class ProgressFragmentViewModel : ViewModel() {
val totalCount: MutableLiveData<Int> = MutableLiveData() val totalCount: MutableLiveData<Int> = MutableLiveData()
@ -10,13 +12,14 @@ class ProgressFragmentViewModel : ViewModel() {
val progress: MutableLiveData<Int> = MutableLiveData() val progress: MutableLiveData<Int> = MutableLiveData()
fun updateProgress() { fun updateProgress() {
val progressData = Repository.getCompleteTotalCount() viewModelScope.launch {
val total = progressData.total val total = Repository.getAll().size
val complete = progressData.complete val complete = Repository.getAllComplete().size
val calcProgress = (complete.toDouble() / total.toDouble()) * 100
progress.value = calcProgress.toInt() val calcProgress = (complete.toDouble() / total.toDouble()) * 100
totalCount.value = total progress.postValue(calcProgress.toInt())
completeCount.value = complete completeCount.postValue(complete)
totalCount.postValue(total)
}
} }
} }

View file

@ -1,6 +1,5 @@
package cn.super12138.todo.views.todo package cn.super12138.todo.views.todo
import android.content.ContentValues
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,9 +10,13 @@ import androidx.lifecycle.ViewModelStoreOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.logic.Repository import cn.super12138.todo.logic.Repository
import cn.super12138.todo.logic.dao.ToDoRoom
import cn.super12138.todo.logic.model.ToDo import cn.super12138.todo.logic.model.ToDo
import cn.super12138.todo.views.progress.ProgressFragmentViewModel import cn.super12138.todo.views.progress.ProgressFragmentViewModel
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class ToDoAdapter(val todoList: MutableList<ToDo>, val viewModelStoreOwner: ViewModelStoreOwner) : class ToDoAdapter(val todoList: MutableList<ToDo>, val viewModelStoreOwner: ViewModelStoreOwner) :
RecyclerView.Adapter<ToDoAdapter.ViewHolder>() { RecyclerView.Adapter<ToDoAdapter.ViewHolder>() {
@ -47,12 +50,10 @@ class ToDoAdapter(val todoList: MutableList<ToDo>, val viewModelStoreOwner: View
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, todoList.size) notifyItemRangeChanged(position, todoList.size)
val newState = ContentValues().apply { GlobalScope.launch {
put("state", true) Repository.updateStateByUUID(todo.uuid)
progressViewModel.updateProgress()
} }
Repository.updateData(todo.uuid, newState)
progressViewModel.updateProgress()
// 设置空项目提示可见性 // 设置空项目提示可见性
if (todoList.isEmpty()) { if (todoList.isEmpty()) {
@ -73,16 +74,11 @@ class ToDoAdapter(val todoList: MutableList<ToDo>, val viewModelStoreOwner: View
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, todoList.size) notifyItemRangeChanged(position, todoList.size)
val tempTaskInfo = ContentValues().apply { GlobalScope.launch {
put("uuid", todo.uuid) Repository.deleteByUUID(todo.uuid)
put("state", false) progressViewModel.updateProgress()
put("subject", todo.subject)
put("context", todo.context)
} }
Repository.deleteData(false, todo.uuid)
progressViewModel.updateProgress()
// 设置空项目提示可见性 // 设置空项目提示可见性
if (todoList.isEmpty()) { if (todoList.isEmpty()) {
@ -99,8 +95,18 @@ class ToDoAdapter(val todoList: MutableList<ToDo>, val viewModelStoreOwner: View
todoList.add(ToDo(todo.uuid, todo.context, todo.subject)) todoList.add(ToDo(todo.uuid, todo.context, todo.subject))
todoViewModel.refreshData.value = 1 todoViewModel.refreshData.value = 1
Repository.insertData(tempTaskInfo)
progressViewModel.updateProgress() GlobalScope.launch {
Repository.insert(
ToDoRoom(
todo.uuid,
0,
todo.subject,
todo.context
)
)
progressViewModel.updateProgress()
}
} }
.show() .show()
} }

View file

@ -1,6 +1,5 @@
package cn.super12138.todo.views.todo package cn.super12138.todo.views.todo
import android.content.ContentValues
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -8,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cn.super12138.todo.R import cn.super12138.todo.R
@ -15,9 +15,11 @@ import cn.super12138.todo.ToDoApplication
import cn.super12138.todo.databinding.DialogAddTodoBinding import cn.super12138.todo.databinding.DialogAddTodoBinding
import cn.super12138.todo.databinding.FragmentTodoBinding import cn.super12138.todo.databinding.FragmentTodoBinding
import cn.super12138.todo.logic.Repository import cn.super12138.todo.logic.Repository
import cn.super12138.todo.logic.dao.ToDoRoom
import cn.super12138.todo.logic.model.ToDo import cn.super12138.todo.logic.model.ToDo
import cn.super12138.todo.views.progress.ProgressFragmentViewModel import cn.super12138.todo.views.progress.ProgressFragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
class ToDoFragment : Fragment() { class ToDoFragment : Fragment() {
@ -38,11 +40,6 @@ class ToDoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val todos = Repository.getAllData()
for (todo in todos) {
todoList.add(ToDo(todo.uuid, todo.context, todo.subject))
}
/*ViewCompat.setOnApplyWindowInsetsListener(binding.addItem) { view, windowInsets -> /*ViewCompat.setOnApplyWindowInsetsListener(binding.addItem) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<ViewGroup.MarginLayoutParams> { view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -63,6 +60,20 @@ class ToDoFragment : Fragment() {
val todoViewModel = val todoViewModel =
ViewModelProvider(requireActivity()).get(ToDoFragmentViewModel::class.java) ViewModelProvider(requireActivity()).get(ToDoFragmentViewModel::class.java)
lifecycleScope.launch {
val todos = Repository.getAllUncomplete()
var count = 0
for (todo in todos) {
todoList.add(ToDo(todo.uuid, todo.context, todo.subject))
count++
}
if (count == 0) {
todoViewModel.emptyTipVis.value = View.VISIBLE
} else {
todoViewModel.emptyTipVis.value = View.GONE
}
}
if (todoList.size == 0) { if (todoList.size == 0) {
todoViewModel.emptyTipVis.value = View.VISIBLE todoViewModel.emptyTipVis.value = View.VISIBLE
} }
@ -99,18 +110,19 @@ class ToDoFragment : Fragment() {
ToDo(randomUUID, todoContext, todoSubject) ToDo(randomUUID, todoContext, todoSubject)
) )
// 添加到数据库 lifecycleScope.launch {
val todoData = ContentValues().apply { Repository.insert(
put("uuid", randomUUID) ToDoRoom(
put("state", 0) randomUUID,
put("subject", todoSubject) 0,
put("context", todoContext) todoSubject,
todoContext
)
)
progressViewModel.updateProgress()
} }
Repository.insertData(todoData)
binding.todoList.adapter?.notifyItemInserted(todoList.size + 1) binding.todoList.adapter?.notifyItemInserted(todoList.size + 1)
progressViewModel.updateProgress()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
@ -124,11 +136,12 @@ class ToDoFragment : Fragment() {
.setMessage(R.string.delete_confirm) .setMessage(R.string.delete_confirm)
.setPositiveButton(R.string.ok) { dialog, which -> .setPositiveButton(R.string.ok) { dialog, which ->
todoList.clear() todoList.clear()
Repository.deleteData(true, null) lifecycleScope.launch {
Repository.deleteAll()
progressViewModel.updateProgress()
}
binding.todoList.adapter?.notifyItemRangeRemoved(0, todoList.size + 1) binding.todoList.adapter?.notifyItemRangeRemoved(0, todoList.size + 1)
progressViewModel.updateProgress()
todoViewModel.emptyTipVis.value = View.VISIBLE todoViewModel.emptyTipVis.value = View.VISIBLE
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)

View file

@ -2,4 +2,5 @@
plugins { plugins {
id("com.android.application") version "8.2.1" apply false id("com.android.application") version "8.2.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false
} }