前言
在空閑的時候州泊,就要寫代碼來鞏固以下自己的知識體系丧蘸。所以呢,使用Room和WorkManager在Android架構(gòu)組件下遥皂,實現(xiàn)一個查看Task列表力喷,左滑右滑刪除item,新建附帶提醒功能的Task的App演训。
本文會牽涉以下知識點
- Android架構(gòu)組件
- Jetpack - Room
- Jetpack - WorkManager
- Kotlin Coroutines
- Recyclerview 自定義左滑右滑事件的實現(xiàn)
本文會從系統(tǒng)架構(gòu)到詳細代碼弟孟,一步一步進行介紹,敬請期待...
截圖
架構(gòu)組件
下圖為我們的系統(tǒng)架構(gòu)組件圖样悟,為Google推薦的一種實現(xiàn)
下面來解釋一下
- Entity: 實體類拂募,帶注釋的類,在Room中充當與數(shù)據(jù)庫的一個表
- SQLite:使用封裝好了的Room充當持久性庫窟她,創(chuàng)建并維護此數(shù)據(jù)庫
- Dao: 數(shù)據(jù)訪問對象陈症。SQL查詢到該函數(shù)的映射,使用DAO時震糖,您將調(diào)用方法录肯,而Room負責其余的工作。
- Room數(shù)據(jù)庫 :底層還是SQLite的實現(xiàn)吊说,數(shù)據(jù)庫使用DAO向SQLite數(shù)據(jù)庫發(fā)出查詢论咏。
- Repository:存儲庫,主要用于管理多個數(shù)據(jù)源颁井,通常充作ViewModel和數(shù)據(jù)獲取的橋梁厅贪。
- ViewModel:充當存儲庫(數(shù)據(jù))和UI之間的通信中心。UI不再需要擔心數(shù)據(jù)的來源蚤蔓。ViewModel不會因為activity或者fragment的生命周期而丟失。
- LiveData:以觀察到的數(shù)據(jù)持有者類糊余。始終保存/緩存最新版本的數(shù)據(jù)秀又,并在數(shù)據(jù)更改時通知其觀察者单寂。LiveData知道生命周期。UI組件僅觀察相關(guān)數(shù)據(jù)吐辙,而不會停止或繼續(xù)觀察宣决。LiveData自動管理所有這些,因為它在觀察的同時知道相關(guān)生命周期狀態(tài)的變化昏苏。
下面是TodoApp的系統(tǒng)框架圖
每個封閉框(SQLite數(shù)據(jù)庫除外)都代表我們將創(chuàng)建的每一個類
創(chuàng)建程序
- 打開Android Studio尊沸,然后單擊Start a new Android Studio project
- 在“創(chuàng)建新項目”窗口中,選擇Empty Activity 贤惯,然后單擊Next洼专。
- 在下一個界面,將應(yīng)用命名為TodoApp孵构,然后點擊Finish屁商。
更新Gradle文件
- 打開build.gradle (Moudle:app)
- 在頂部使用kapt注釋處理器和kotlin的ext函數(shù)
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
- 在android節(jié)點添加packagingOptions,防止出現(xiàn)警告
android {
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- 在代碼dependencies塊的末尾添加以下代碼
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
//workManager
def work_version = "2.3.4"
implementation "androidx.work:work-runtime-ktx:$work_version"
//在電腦上用瀏覽器調(diào)試room颈墅,能夠可視化增刪改查
def version_debug_database = "1.0.6"
debugImplementation "com.amitshekhar.android:debug-db:$version_debug_database"
debugImplementation "com.amitshekhar.android:debug-db-encrypt:$version_debug_database"
- 打開build.gradle (Project:TodoApp),在最末未添加以下代碼
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
創(chuàng)建實體類
我們的實體類是Task,任務(wù)蜡镶,我們需要哪些字段呢?
首先恤筛,我們肯定需要任務(wù)的名稱name官还,然后需要任務(wù)的描述desc,然后我們用一個boolean來標志是否需要提醒毒坛,同時望伦,用Date日期類記錄提醒時間,然后粘驰,我們需要一個界面Image的顏色color屡谐,最后,我們需要一個每一個任務(wù)對應(yīng)的workmanager_id,這個id主要是刪除item的時候蝌数,WorkManager結(jié)束任務(wù)用的愕掏,這個后面再細說,此處不作過多描述顶伞。
@Entity(tableName = "task_table")
@TypeConverters(DateConverter::class)
data class Task(
@ColumnInfo(name = "name")
var name: String,
@ColumnInfo(name = "desc")
val desc: String,
@ColumnInfo(name = "time")
val time: Date?,
@ColumnInfo(name = "hasReminder")
val hasReminder: Boolean,//是否有提醒
@ColumnInfo(name = "color")
val color: Int
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var id: Long = 0
@ColumnInfo(name = "work_manager_uuid")
var work_manager_uuid: String = ""
}
我們來看看這些注解的作用
- @Entity(tableName = "task_table")
每個@Entity類代表一個SQLite表饵撑。注釋您的類聲明以表明它是一個Entity。如果希望表名與類名不同唆貌,則可以指定表名滑潘,例如命名為“task_table”。
- @PrimaryKey
每個實體都需要一個主鍵锨咙。我們設(shè)定一個Long值作為主鍵语卤,初始值為0,并讓他自增長(autoGenerate = true)
- @ColumnInfo(name = "name")
如果希望表中的列名與成員變量的名稱不同,則指定該列名粹舵。這將列命名為name钮孵。
- TypeConverters
因為Room數(shù)據(jù)庫只能保存基礎(chǔ)類型(Int,String,Boolean,Float等),對于一些obj眼滤,則需要轉(zhuǎn)換巴席,我們定義了一個DateConverter轉(zhuǎn)換,保存數(shù)據(jù)庫的時候诅需,把Date轉(zhuǎn)成long漾唉,取值的時候,再把Long轉(zhuǎn)成Date堰塌。代碼如下
class DateConverter {
@TypeConverter
fun revertDate(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun converterDate(date: Date?): Long? {
return date?.time
}
}
創(chuàng)建Dao
什么是Dao赵刑?
Dao是數(shù)據(jù)庫訪問對象,指定SQL查詢語句和它調(diào)用的方法關(guān)聯(lián)蔫仙,例如Query,Insert,Delete,Update等料睛。
DAO必須是接口或抽象類
Room可以使用協(xié)程,在方法名前面加suspend修飾符
怎么使用Dao?
我們接下來就編寫一個Dao摇邦,來實現(xiàn)對Task增刪改查恤煞。代碼如下
@Dao
interface TaskDao {
@Query("SELECT * from task_table")
fun getAllTask(): LiveData<List<Task>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(task: Task)
@Query("DELETE FROM task_table")
fun deleteAll()
@Delete
fun remove(task: Task)
}
我們看一下上面的代碼的一些解說
- TaskDao是一個接口;因為我們上面提過DAO必須是接口或抽象類施籍。
- 用@Dao來標志這個接口是作為Room的Dao
- insert(task: Task)居扒,聲明插入一個新Task的方法
- @Insert,插入執(zhí)行丑慎,無須寫SQL語句喜喂,同樣無須寫SQL語句的還有Delete,Update
- onConflict = OnConflictStrategy.IGNORE:如果所選的onConflict策略與列表中已有的Task完全相同,則會忽略該Task
- fun deleteAll()聲明一個刪除所有Task的方法
- remove(task: Task)聲明一個刪除單個Task的方法
- fun getAllTask(): LiveData<List<Task>> 一個返回LiveData包含所有Task的集合對象竿裂,外部通過監(jiān)聽這個對象玉吁,實現(xiàn)布局的刷新...
- @Query("SELECT * from task_table "):查詢返回所有Task列表,可以拓展插入一些升序降序或者過濾的查詢語句
LiveData
數(shù)據(jù)更改時腻异,通常需要采取一些措施进副,例如在UI中顯示更新的數(shù)據(jù)。這意味著您必須觀察數(shù)據(jù)悔常,以便在數(shù)據(jù)更改時可以做出反應(yīng)影斑。
根據(jù)數(shù)據(jù)的存儲方式,這可能很棘手机打。觀察應(yīng)用程序多個組件之間的數(shù)據(jù)更改可以在組件之間創(chuàng)建明確的矫户,嚴格的依賴路徑。這使測試和調(diào)試變得非常困難残邀。
LiveData皆辽,用于數(shù)據(jù)觀察的生命周期庫類可解決此問題柑蛇。LiveData在方法描述中使用類型的返回值,然后Room會生成所有必要的代碼來更新LiveData數(shù)據(jù)庫驱闷。
在TaskDao中唯蝶,返回LiveData包含所有Task的集合對象,然后后面的MainActivity我們監(jiān)聽它
@Query("SELECT * from task_table")
fun getAllTask(): LiveData<List<Task>>
Room database
什么是Room database
- Room是SQLite數(shù)據(jù)庫的頂層調(diào)用遗嗽。
- Room的工作任務(wù)類似于以前SQlite的SQLiteOpenHelper
- Room使用DAO向其數(shù)據(jù)庫增刪改查操作
- Room的SQL語句在編譯中會檢查該語法
怎么使用Room database
Room數(shù)據(jù)庫類必須是抽象類,并且是繼承自RoomDatabase鼓蜒,一般是以單例模式的方式存在痹换。
現(xiàn)在我們就來構(gòu)建一個TaskRoomDatabase,代碼如下
@Database(entities = [Task::class], version = 1)
abstract class TaskRoomDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
@Volatile
private var INSTANCE: TaskRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): TaskRoomDatabase {
// 如果INSTANCE為null都弹,返回此INSTANCE娇豫,否則,創(chuàng)建database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TaskRoomDatabase::class.java,
"task_database"
)
// 如果沒有遷移數(shù)據(jù)庫畅厢,則擦除并重建而不是遷移冯痢。
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
我們看一下以上代碼
- 使用@Database注解,標明這個類是數(shù)據(jù)庫類框杜,然后指定它的實體類(可以設(shè)置多個)還有版本號浦楣。
- TaskRoomDatabase 通過它的抽象對象TaskDao獲取對象進行操作
- 數(shù)據(jù)庫一般是單例模式,防止同時打開多個數(shù)據(jù)庫實例
儲存庫Repository
在最常見的示例中咪辱,存儲庫實現(xiàn)了用于確定是從網(wǎng)絡(luò)中獲取數(shù)據(jù)還是使用本地數(shù)據(jù)庫中緩存的結(jié)果的邏輯振劳。
TaskRepository的實現(xiàn)如下
// 在構(gòu)造器中聲明Dao的私有屬性,通過Dao而不是整個數(shù)據(jù)庫油狂,因為只需要訪問Dao
class TaskRepository(private val taskDao: TaskDao) {
// Room在單獨的線程上執(zhí)行所有查詢
// 觀察到的LiveData將在數(shù)據(jù)更改時通知觀察者历恐。
val allWords: LiveData<List<Task>> = taskDao.getAllTask()
fun insert(task: Task) {
taskDao.insert(task)
}
fun remove(task: Task) {
taskDao.remove(task)
}
}
注意,
- DAO作為TaskRepository的構(gòu)造函數(shù)专筷,無須用到數(shù)據(jù)庫實例弱贼,安全。
- 通過LiveData從Room 獲取Task列表進行初始化磷蛹。Room在單獨的線程上執(zhí)行查詢Task操作吮旅,LiveData當數(shù)據(jù)更改時,觀察者將在主線程上通知觀察者弦聂。
- 存儲庫旨在在不同的數(shù)據(jù)源之間進行中介鸟辅。在這個TodoApp中,只有Room一個數(shù)據(jù)源,因此存儲庫不會做很多事情莺葫。有關(guān)更復(fù)雜的實現(xiàn)匪凉,可以看我寫的一個例子
ViewModel
什么是什么是ViewModel?
ViewModel提供數(shù)據(jù)給UI,能在activity和fragment周期改變的時候保存捺檬。一般是連接Repository和Activity/Fragment的中間樞紐再层,還可以使用它共享數(shù)據(jù)。
ViewModel把數(shù)據(jù)和UI分開,可以更好地遵循單一職責原則聂受。
一般ViewModel會搭配LiveData一起使用蒿秦,LiveData搭配ViewModel的好處有很多:
- 將觀察者放在數(shù)據(jù)上(不用輪詢更改),并且僅在數(shù)據(jù)實際更改時才更新UI蛋济。
- ViewModel分割了儲存庫和UI
- 更高可測試性
viewModelScope
在Kotlin棍鳖,所有協(xié)程都在內(nèi)運行CoroutineScope。scope通過job來控制協(xié)程的生命周期.碗旅,當scope中的job取消時渡处,它也會一起取消在該scope作用域范圍內(nèi)啟動的所有協(xié)程。
AndroidX lifecycle-viewmodel-ktx庫添加了viewModelScope類的擴展功能ViewModel祟辟,可以在其作用域下進行工作
下面医瘫,看一下TaskViewModel的實現(xiàn)
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val repository: TaskRepository
// 使用LiveData并緩存getAllTask返回的內(nèi)容有幾個好處:
// - 每當Room數(shù)據(jù)庫有更新的時候通知觀察者,而不是輪詢更新
// 數(shù)據(jù)變化適時更新UI旧困。
// - 存儲庫通過ViewModel與UI完全隔離醇份。
val allWords: LiveData<List<Task>>
init {
val taskDao = TaskRoomDatabase.getDatabase(application, viewModelScope).taskDao()
repository = TaskRepository(taskDao)
allWords = repository.allWords
}
/**
* 啟動新的協(xié)程以非阻塞方式插入數(shù)據(jù)
*/
fun insert(task: Task) = viewModelScope.launch(Dispatchers.IO) {
try {
repository.insert(task)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun remove(task: Task) = viewModelScope.launch(Dispatchers.IO) {
try {
repository.remove(task)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
我們使用 viewModelScope.launch(Dispatchers.IO)這協(xié)程方法操作數(shù)據(jù)庫遭赂。避免了主線程被阻塞丢氢。
Task列表xml布局
- 首先添加task item的布局信息task_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/listItemLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:background="@android:color/white"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/toDoListItemColorImageView"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginLeft="16dp"
android:gravity="center" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_marginLeft="16dp"
android:layout_weight="5"
android:gravity="center">
<TextView
android:id="@+id/toDoListItemTextview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:gravity="start|bottom"
android:lines="1"
android:text="Clean your room"
android:textColor="@color/secondary_text"
android:textSize="16sp"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/todoListItemTimeTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toDoListItemTextview"
android:gravity="start|center"
android:text="27 Sept 2015, 22:30"
android:textColor="?attr/colorAccent"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>
然后在MainActivity中的布局activity_main.xml,加入RecyclerView,和空布局toDoEmptyView念赶,另外還有一個fab按鈕拗盒,點擊進入AddTaskActivity新建Task
<androidx.constraintlayout.widget.ConstraintLayout 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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F0F1F9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/task_list_item" />
<LinearLayout
android:id="@+id/toDoEmptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="gone">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/empty_view_bg" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:paddingBottom="8dp"
android:text="@string/no_todo_data"
android:textColor="@color/secondary_text"
android:textSize="16sp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_task"
android:src="@drawable/ic_baseline_add_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerView和Adapter
class TaskListAdapter internal constructor(
private val context: Context
) : RecyclerView.Adapter<TaskListAdapter.ViewHolder>(),
ItemTouchHelperClass.ItemTouchHelperAdapter {
interface OnItemEventListener {
fun onItemRemoved(task: Task)
fun onItemClick(task: Task)
}
fun setOnItemEventListener(listener: OnItemEventListener) {
this.listener = listener
}
private lateinit var listener: OnItemEventListener
private var tasks = emptyList<Task>() // Cached copy of words
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val taskItemView: TextView = itemView.findViewById(R.id.toDoListItemTextview)
val mTimeTextView: TextView = itemView.findViewById(R.id.todoListItemTimeTextView)
val mColorImageView: ImageView = itemView.findViewById(R.id.toDoListItemColorImageView)
val rootView: LinearLayout = itemView.findViewById(R.id.listItemLinearLayout)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView =
LayoutInflater.from(parent.context).inflate(R.layout.task_list_item, parent, false)
return ViewHolder(itemView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val current = tasks[position]
if (current.hasReminder && current.time != null) {
holder.taskItemView.maxLines = 1
holder.mTimeTextView.visibility = View.VISIBLE
} else {
holder.taskItemView.maxLines = 2
holder.mTimeTextView.visibility = View.GONE
}
holder.taskItemView.text = current.name
val myDrawable = TextDrawable.builder().beginConfig()
.textColor(Color.WHITE)
.useFont(Typeface.DEFAULT)
.toUpperCase()
.endConfig()
.buildRound(current.name.substring(0, 1), current.color)
holder.mColorImageView.setImageDrawable(myDrawable)
current.time?.let { time ->
holder.mTimeTextView.text = if (is24HourFormat(context)) TimeUtils.formatDate(
DATE_TIME_FORMAT_24_HOUR,
time
) else TimeUtils.formatDate(DATE_TIME_FORMAT_12_HOUR, time)
var nowDate = Date()
var reminderDate = current.time
holder.mTimeTextView.setTextColor(
if (reminderDate.before(nowDate)) ContextCompat.getColor(
context,
R.color.grey600
) else ContextCompat.getColor(context, R.color.colorAccent)
)
}
holder.rootView.setOnClickListener {
listener.onItemClick(current)
}
}
internal fun setTasks(tasks: List<Task>) {
this.tasks = tasks
notifyDataSetChanged()
}
override fun getItemCount() = tasks.size
override fun onItemMoved(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(tasks, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(tasks, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onItemRemoved(position: Int) {
listener.onItemRemoved(task = tasks[position])
}
}
Adapter中的onBindViewHolder設(shè)置每一個item顯示畔濒,根據(jù)Task的hasReminder和time值,顯示列表item锣咒,然后再MainActivity中侵状,設(shè)置Recyclerview和Adapter
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
val adapter = TaskListAdapter(this)
recyclerview.adapter = adapter
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerview.itemAnimator = DefaultItemAnimator()
recyclerview.setHasFixedSize(true)
val itemTouchHelperClass = ItemTouchHelperClass(adapter)
val itemTouchHelper = ItemTouchHelper(itemTouchHelperClass)
itemTouchHelper.attachToRecyclerView(recyclerview)
}
}
連接數(shù)據(jù)
在MainActivity,創(chuàng)建一個成員變量ViewModel
private lateinit var wordViewModel: TaskViewModel
然后我們要實例化它毅整,然后獲取了TaskViewModel對象之后趣兄,就可以監(jiān)聽Room中Task列表變化。代碼如下
wordViewModel = ViewModelProvider(this).get(TaskViewModel::class.java)
// 在getAllTask返回的LiveData上添加觀察者悼嫉。
// 當觀察到的數(shù)據(jù)更改并且Acticity處于前臺時艇潭,將觸發(fā)onChanged()方法。
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let {
if (it.isEmpty()) {
toDoEmptyView.visibility = View.VISIBLE
recyclerview.visibility = View.GONE
} else {
toDoEmptyView.visibility = View.GONE
recyclerview.visibility = View.VISIBLE
adapter.setTasks(it)
}
}
})
當監(jiān)聽列表數(shù)據(jù)不為空時戏蔑,recyclerview顯示蹋凝,toDoEmptyView隱藏,否則总棵,toDoEmptyView顯示鳍寂,recyclerview隱藏。然后運行程序情龄,如下圖所示
<html>
<img src="http://lbz-blog.test.upcdn.net/post/todoapp_empty.jpg" width = "180" height = "390" border="1" />
</html>
添加Task
新建一個AddTaskActivity迄汛,頁面布局activity_add_task.xml**如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<EditText
android:id="@+id/edit_task"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/big_padding"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_task"
android:inputType="textAutoComplete"
android:minHeight="@dimen/min_height"
android:textSize="18sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/big_padding"
android:orientation="horizontal">
<ImageView
android:id="@+id/alarmTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_baseline_add_alarm_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/alarmTv"
android:text="@string/remind_me"
android:textColor="@color/secondary_text"
android:textSize="18sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true" />
</RelativeLayout>
<LinearLayout
android:visibility="gone"
android:id="@+id/toDoEnterDateLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/big_padding"
android:animateLayoutChanges="true"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top">
<EditText
android:textColor="@color/secondary_text"
android:text="今天"
android:id="@+id/newTodoDateEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.5"
android:editable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center"
android:textIsSelectable="false" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".2"
android:gravity="center"
android:padding="4dp"
android:text="\@"
android:textColor="?attr/colorAccent" />
<EditText
android:textColor="@color/secondary_text"
android:text="下午1:00"
android:id="@+id/newTodoTimeEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:editable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center"
android:textIsSelectable="false" />
</LinearLayout>
<TextView
android:layout_marginTop="10dp"
android:id="@+id/newToDoDateTimeReminderTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/remind_date_and_time"
android:textColor="@color/secondary_text"
android:textSize="14sp" />
</LinearLayout>
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/big_padding"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:textColor="@color/buttonLabel" />
</LinearLayout>
一個輸入Task Name的文本輸入框edit_task,一個控制是否需要提醒功能的開關(guān)SwitchMaterial捍壤,點擊文本框newTodoDateEditText彈出一個日期選擇器DatePickerDialog,點擊文本框newToDoDateTimeReminderTextView彈出一個時間選擇器TimePickerDialog鞍爱,點擊保存按鈕鹃觉,如果輸入框文本為空,則提示需要輸入睹逃,否則盗扇,創(chuàng)建Task成功,然后退出本Activity沉填,在MainActivity顯示剛剛加入的Task粱玲。
在AddTaskActivity我們同樣需要使用TaskViewModel,讓它執(zhí)行插入操作拜轨。
class AddTaskActivity : AppCompatActivity() {
private lateinit var wordViewModel: TaskViewModel
public override fun onCreate(savedInstanceState: Bundle?) {
wordViewModel = ViewModelProvider(this).get(TaskViewModel::class.java)
button_save.setOnClickListener {
saveTask()
}
switch_btn.setOnCheckedChangeListener { _, isChecked ->
toDoEnterDateLinearLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
}
newTodoDateEditText.setOnClickListener {
openDataSelectDialog()
}
newTodoTimeEditText.setOnClickListener {
openTimeSelectDialog()
}
}
private fun saveTask() {
if (!TextUtils.isEmpty(edit_task.text)) {
val name = edit_task.text.toString()
val task = Task(
name,
"",
mUserReminderDate,
switch_btn.isChecked,
ColorGenerator.MATERIAL.randomColor
)
wordViewModel.insert(task)
if (switch_btn.isChecked) {
createNotifyWork(task)
}
finish()
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG
).show()
}
}
}
以上,為AddTaskActivity的關(guān)鍵代碼允青,現(xiàn)在橄碾,我們已經(jīng)完成了對于Task的增刪查操作。已經(jīng)掌握了Room結(jié)合Android架構(gòu)組件開發(fā)的流程〉唢保現(xiàn)在我們使用Jetpack的另一個組件---WorkManager,令這個程序更有趣一些法牲。
對Task帶有提醒功能的WorkManager
現(xiàn)在,我們使用WorkManager琼掠,對一些有提醒的任務(wù)進行系統(tǒng)的提醒(Notification)
- 第一步拒垃,在build.gradle(Module:app)中添加對workmanager的支持
//workManager
def work_version = "2.3.4"
implementation "androidx.work:work-runtime-ktx:$work_version"
- 第二步,新建一個繼承Worker的任務(wù)類瓷蛙,我們命名為NotifyWork,并重寫doWork()方法
override fun doWork(): Result {
val id = inputData.getInt(NOTIFICATION_ID, 0)
val title = inputData.getString(TASK_TITLE) ?: "Title"
sendNotification(id, title)
return Result.success()
}
- 實現(xiàn)sendNotification方法悼瓮,發(fā)送系統(tǒng)通知
private fun sendNotification(id: Int, title: String) {
val intent = Intent(applicationContext, AddTaskActivity::class.java)
intent.flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(NOTIFICATION_ID, id)
val notificationManager =
applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val subtitleNotification = "點擊可進入Task詳情"
val pendingIntent = getActivity(applicationContext, 0, intent, 0)
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title).setContentText(subtitleNotification)
.setDefaults(DEFAULT_ALL).setContentIntent(pendingIntent).setAutoCancel(true)
notification.priority = PRIORITY_MAX
if (SDK_INT >= O) {
notification.setChannelId(NOTIFICATION_CHANNEL)
val ringtoneManager = getDefaultUri(TYPE_NOTIFICATION)
val audioAttributes = AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE)
.setContentType(CONTENT_TYPE_SONIFICATION).build()
val channel =
NotificationChannel(NOTIFICATION_CHANNEL, NOTIFICATION_NAME, IMPORTANCE_HIGH)
channel.enableLights(true)
channel.lightColor = RED
channel.enableVibration(true)
channel.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
channel.setSound(ringtoneManager, audioAttributes)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(id, notification.build())
}
- 在創(chuàng)建任務(wù)的時候,如果選擇了提醒時間艰猬,那么需要創(chuàng)建一個發(fā)送系統(tǒng)通知的work横堡,我們在AddTaskActivity執(zhí)行SaveTask()的時候,補充如下
private fun saveTask() {
if (!TextUtils.isEmpty(edit_task.text)) {
...
wordViewModel.insert(task)
if (switch_btn.isChecked) {
createNotifyWork(task)
}
finish()
}
...
}
private fun createNotifyWork(task: Task) {
val customTime = mUserReminderDate.time
val currentTime = currentTimeMillis()
if (customTime > currentTime) {
val data = Data.Builder().putInt(NOTIFICATION_ID, (0 until 100000).random())
.putString(TASK_TITLE, task.name).build()
val delay = customTime - currentTime
scheduleNotification(delay, data,task)
}
}
private fun scheduleNotification(delay: Long, data: Data,task: Task) {
val notificationWork = OneTimeWorkRequest.Builder(NotifyWork::class.java)
.setInitialDelay(delay, TimeUnit.MILLISECONDS).setInputData(data).build()
task.work_manager_uuid = notificationWork.id.toString()
wordViewModel.updateWorkIdByName(notificationWork.id.toString(),task.name)
val instanceWorkManager = WorkManager.getInstance(this)
instanceWorkManager.beginWith(notificationWork).enqueue()
}
我們?yōu)槊恳粋€帶有提醒時間的Task添加OneTimeWorkRequest冠桃。在實體類Task中添加一個字段work_manager_uuid保存OneTimeWorkRequest命贴,方便執(zhí)行列表左滑右滑的時候刪除item時候,同時使用cancelWorkById()把對應(yīng)的任務(wù)取消食听,下面的代碼就是MainActivity中item左滑右滑的回調(diào)監(jiān)聽胸蛛。
adapter.setOnItemEventListener(object : TaskListAdapter.OnItemEventListener {
override fun onItemRemoved(task: Task) {
Toast.makeText(baseContext, "刪除" + task.name + "成功", Toast.LENGTH_SHORT).show()
wordViewModel.remove(task)
if (!TextUtils.isEmpty(task.work_manager_uuid)) {
WorkManager.getInstance (this@MainActivity)
.cancelWorkById(UUID.fromString(task.work_manager_uuid))
}
}
})
計算出現(xiàn)在時間和創(chuàng)建Task那個提醒時間的delay差值,使用
OneTimeWorkRequest.Builder(NotifyWork::class.java)
.setInitialDelay(delay, TimeUnit.MILLISECONDS).setInputData(data).build()
來建議一個任務(wù)樱报,然后beginWith(notificationWork).enqueue()來把任務(wù)交給WorkManager葬项。
這樣就實現(xiàn)了當提醒時間到達的時候,系統(tǒng)就會打開一個通知迹蛤。完成這個提醒功能玷室。
注意:用Google Nexus 6P和小米9分別測試該功能零蓉。在殺死app的情況下,前者依舊能夠收到系統(tǒng)的通知穷缤。但是小米不可以敌蜂,國產(chǎn)的部分ROM已經(jīng)對WorkManager失去作用。
總結(jié)
以上就是基于Android架構(gòu)組件用Room和WorkManager實現(xiàn)的一個簡單TODO APP津肛,基本能掌握ROOM和WorkManager的基礎(chǔ)用法章喉,同時對Kotlin的語法有進一步加深理解。