Jetpack Room+WorkManager在Android架構(gòu)組件下的實戰(zhàn)

前言

在空閑的時候州泊,就要寫代碼來鞏固以下自己的知識體系丧蘸。所以呢,使用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)到詳細代碼弟孟,一步一步進行介紹,敬請期待...

截圖

image

image

架構(gòu)組件

下圖為我們的系統(tǒng)架構(gòu)組件圖样悟,為Google推薦的一種實現(xiàn)

image

下面來解釋一下

  • 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)框架圖


image

每個封閉框(SQLite數(shù)據(jù)庫除外)都代表我們將創(chuàng)建的每一個類

創(chuàng)建程序

  1. 打開Android Studio尊沸,然后單擊Start a new Android Studio project
  2. 在“創(chuàng)建新項目”窗口中,選擇Empty Activity 贤惯,然后單擊Next洼专。
  3. 在下一個界面,將應(yīng)用命名為TodoApp孵构,然后點擊Finish屁商。

更新Gradle文件

  1. 打開build.gradle (Moudle:app)
  2. 在頂部使用kapt注釋處理器和kotlin的ext函數(shù)
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
  1. 在android節(jié)點添加packagingOptions,防止出現(xiàn)警告
android {
    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. 在代碼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"
  1. 打開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

image

在最常見的示例中咪辱,存儲庫實現(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ù)。

image

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布局

  1. 首先添加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)

  1. 第一步拒垃,在build.gradle(Module:app)中添加對workmanager的支持
  //workManager
    def work_version = "2.3.4"
    implementation "androidx.work:work-runtime-ktx:$work_version"
  1. 第二步,新建一個繼承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()
    }
  1. 實現(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())
    }

  1. 在創(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的語法有進一步加深理解。

項目地址:https://github.com/laibinzhi/TodoApp

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末身坐,一起剝皮案震驚了整個濱河市秸脱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌部蛇,老刑警劉巖摊唇,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涯鲁,居然都是意外死亡巷查,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門抹腿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岛请,“玉大人,你說我怎么就攤上這事警绩〕绨埽” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵肩祥,是天一觀的道長后室。 經(jīng)常有香客問我,道長混狠,這世上最難降的妖魔是什么咧擂? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮檀蹋,結(jié)果婚禮上松申,老公的妹妹穿的比我還像新娘。我一直安慰自己俯逾,他們只是感情好贸桶,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著桌肴,像睡著了一般皇筛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坠七,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天水醋,我揣著相機與錄音旗笔,去河邊找鬼。 笑死拄踪,一個胖子當著我的面吹牛蝇恶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惶桐,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼撮弧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姚糊?” 一聲冷哼從身側(cè)響起贿衍,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎救恨,沒想到半個月后贸辈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡肠槽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年擎淤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片署浩。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扫尺,靈堂內(nèi)的尸體忽然破棺而出筋栋,到底是詐尸還是另有隱情,我是刑警寧澤正驻,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布弊攘,位于F島的核電站,受9級特大地震影響姑曙,放射性物質(zhì)發(fā)生泄漏襟交。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一伤靠、第九天 我趴在偏房一處隱蔽的房頂上張望捣域。 院中可真熱鬧,春花似錦宴合、人聲如沸焕梅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贞言。三九已至,卻和暖如春阀蒂,著一層夾襖步出監(jiān)牢的瞬間该窗,已是汗流浹背弟蚀。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酗失,地道東北人义钉。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像级零,于是被迫代替她去往敵國和親断医。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348