前言
在空閑的時候,就要寫代碼來鞏固以下自己的知識體系。所以呢,使用Room和WorkManager在Android架構組件下,實現一個查看Task列表,左滑右滑刪除item,新建附帶提醒功能的Task的App。
本文會牽涉以下知識點
- Android架構組件
- Jetpack - Room
- Jetpack - WorkManager
- Kotlin Coroutines
- Recyclerview 自定義左滑右滑事件的實現
本文會從系統架構到詳細代碼,一步一步進行介紹,敬請期待...
截圖
架構組件
下圖為我們的系統架構組件圖,為Google推薦的一種實現
下面來解釋一下
- Entity: 實體類,帶注釋的類,在Room中充當與數據庫的一個表
- SQLite:使用封裝好了的Room充當持久性庫,創建并維護此數據庫
- Dao: 數據訪問對象。SQL查詢到該函數的映射,使用DAO時,您將調用方法,而Room負責其余的工作。
- Room數據庫 :底層還是SQLite的實現,數據庫使用DAO向SQLite數據庫發出查詢。
- Repository:存儲庫,主要用于管理多個數據源,通常充作ViewModel和數據獲取的橋梁。
- ViewModel:充當存儲庫(數據)和UI之間的通信中心。UI不再需要擔心數據的來源。ViewModel不會因為activity或者fragment的生命周期而丟失。
- LiveData:以觀察到的數據持有者類。始終保存/緩存最新版本的數據,并在數據更改時通知其觀察者。LiveData知道生命周期。UI組件僅觀察相關數據,而不會停止或繼續觀察。LiveData自動管理所有這些,因為它在觀察的同時知道相關生命周期狀態的變化。
下面是TodoApp的系統框架圖
每個封閉框(SQLite數據庫除外)都代表我們將創建的每一個類
創建程序
- 打開Android Studio,然后單擊Start a new Android Studio project
- 在“創建新項目”窗口中,選擇Empty Activity ,然后單擊Next。
- 在下一個界面,將應用命名為TodoApp,然后點擊Finish。
更新Gradle文件
- 打開build.gradle (Moudle:app)
- 在頂部使用kapt注釋處理器和kotlin的ext函數
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
- 在android節點添加packagingOptions,防止出現警告
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"
//在電腦上用瀏覽器調試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'
}
創建實體類
我們的實體類是Task,任務,我們需要哪些字段呢?
首先,我們肯定需要任務的名稱name,然后需要任務的描述desc,然后我們用一個boolean來標志是否需要提醒,同時,用Date日期類記錄提醒時間,然后,我們需要一個界面Image的顏色color,最后,我們需要一個每一個任務對應的workmanager_id,這個id主要是刪除item的時候,WorkManager結束任務用的,這個后面再細說,此處不作過多描述。
@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
每個實體都需要一個主鍵。我們設定一個Long值作為主鍵,初始值為0,并讓他自增長(autoGenerate = true)
- @ColumnInfo(name = "name")
如果希望表中的列名與成員變量的名稱不同,則指定該列名。這將列命名為name。
- TypeConverters
因為Room數據庫只能保存基礎類型(Int,String,Boolean,Float等),對于一些obj,則需要轉換,我們定義了一個DateConverter轉換,保存數據庫的時候,把Date轉成long,取值的時候,再把Long轉成Date。代碼如下
class DateConverter {
@TypeConverter
fun revertDate(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun converterDate(date: Date?): Long? {
return date?.time
}
}
創建Dao
什么是Dao?
Dao是數據庫訪問對象,指定SQL查詢語句和它調用的方法關聯,例如Query,Insert,Delete,Update等。
DAO必須是接口或抽象類
Room可以使用協程,在方法名前面加suspend修飾符
怎么使用Dao?
我們接下來就編寫一個Dao,來實現對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,插入執行,無須寫SQL語句,同樣無須寫SQL語句的還有Delete,Update
- onConflict = OnConflictStrategy.IGNORE:如果所選的onConflict策略與列表中已有的Task完全相同,則會忽略該Task
- fun deleteAll()聲明一個刪除所有Task的方法
- remove(task: Task)聲明一個刪除單個Task的方法
- fun getAllTask(): LiveData<List<Task>> 一個返回LiveData包含所有Task的集合對象,外部通過監聽這個對象,實現布局的刷新...
- @Query("SELECT * from task_table "):查詢返回所有Task列表,可以拓展插入一些升序降序或者過濾的查詢語句
LiveData
數據更改時,通常需要采取一些措施,例如在UI中顯示更新的數據。這意味著您必須觀察數據,以便在數據更改時可以做出反應。
根據數據的存儲方式,這可能很棘手。觀察應用程序多個組件之間的數據更改可以在組件之間創建明確的,嚴格的依賴路徑。這使測試和調試變得非常困難。
LiveData,用于數據觀察的生命周期庫類可解決此問題。LiveData在方法描述中使用類型的返回值,然后Room會生成所有必要的代碼來更新LiveData數據庫。
在TaskDao中,返回LiveData包含所有Task的集合對象,然后后面的MainActivity我們監聽它
@Query("SELECT * from task_table")
fun getAllTask(): LiveData<List<Task>>
Room database
什么是Room database
- Room是SQLite數據庫的頂層調用。
- Room的工作任務類似于以前SQlite的SQLiteOpenHelper
- Room使用DAO向其數據庫增刪改查操作
- Room的SQL語句在編譯中會檢查該語法
怎么使用Room database
Room數據庫類必須是抽象類,并且是繼承自RoomDatabase,一般是以單例模式的方式存在。
現在我們就來構建一個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,否則,創建database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TaskRoomDatabase::class.java,
"task_database"
)
// 如果沒有遷移數據庫,則擦除并重建而不是遷移。
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
我們看一下以上代碼
- 使用@Database注解,標明這個類是數據庫類,然后指定它的實體類(可以設置多個)還有版本號。
- TaskRoomDatabase 通過它的抽象對象TaskDao獲取對象進行操作
- 數據庫一般是單例模式,防止同時打開多個數據庫實例
儲存庫Repository
在最常見的示例中,存儲庫實現了用于確定是從網絡中獲取數據還是使用本地數據庫中緩存的結果的邏輯。
TaskRepository的實現如下
// 在構造器中聲明Dao的私有屬性,通過Dao而不是整個數據庫,因為只需要訪問Dao
class TaskRepository(private val taskDao: TaskDao) {
// Room在單獨的線程上執行所有查詢
// 觀察到的LiveData將在數據更改時通知觀察者。
val allWords: LiveData<List<Task>> = taskDao.getAllTask()
fun insert(task: Task) {
taskDao.insert(task)
}
fun remove(task: Task) {
taskDao.remove(task)
}
}
注意,
- DAO作為TaskRepository的構造函數,無須用到數據庫實例,安全。
- 通過LiveData從Room 獲取Task列表進行初始化。Room在單獨的線程上執行查詢Task操作,LiveData當數據更改時,觀察者將在主線程上通知觀察者。
- 存儲庫旨在在不同的數據源之間進行中介。在這個TodoApp中,只有Room一個數據源,因此存儲庫不會做很多事情。有關更復雜的實現,可以看我寫的一個例子
ViewModel
什么是什么是ViewModel?
ViewModel提供數據給UI,能在activity和fragment周期改變的時候保存。一般是連接Repository和Activity/Fragment的中間樞紐,還可以使用它共享數據。
ViewModel把數據和UI分開,可以更好地遵循單一職責原則。
一般ViewModel會搭配LiveData一起使用,LiveData搭配ViewModel的好處有很多:
- 將觀察者放在數據上(不用輪詢更改),并且僅在數據實際更改時才更新UI。
- ViewModel分割了儲存庫和UI
- 更高可測試性
viewModelScope
在Kotlin,所有協程都在內運行CoroutineScope。scope通過job來控制協程的生命周期.,當scope中的job取消時,它也會一起取消在該scope作用域范圍內啟動的所有協程。
AndroidX lifecycle-viewmodel-ktx庫添加了viewModelScope類的擴展功能ViewModel,可以在其作用域下進行工作
下面,看一下TaskViewModel的實現
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val repository: TaskRepository
// 使用LiveData并緩存getAllTask返回的內容有幾個好處:
// - 每當Room數據庫有更新的時候通知觀察者,而不是輪詢更新
// 數據變化適時更新UI。
// - 存儲庫通過ViewModel與UI完全隔離。
val allWords: LiveData<List<Task>>
init {
val taskDao = TaskRoomDatabase.getDatabase(application, viewModelScope).taskDao()
repository = TaskRepository(taskDao)
allWords = repository.allWords
}
/**
* 啟動新的協程以非阻塞方式插入數據
*/
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)這協程方法操作數據庫。避免了主線程被阻塞。
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設置每一個item顯示,根據Task的hasReminder和time值,顯示列表item,然后再MainActivity中,設置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)
}
}
連接數據
在MainActivity,創建一個成員變量ViewModel
private lateinit var wordViewModel: TaskViewModel
然后我們要實例化它,然后獲取了TaskViewModel對象之后,就可以監聽Room中Task列表變化。代碼如下
wordViewModel = ViewModelProvider(this).get(TaskViewModel::class.java)
// 在getAllTask返回的LiveData上添加觀察者。
// 當觀察到的數據更改并且Acticity處于前臺時,將觸發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)
}
}
})
當監聽列表數據不為空時,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,一個控制是否需要提醒功能的開關SwitchMaterial,點擊文本框newTodoDateEditText彈出一個日期選擇器DatePickerDialog,點擊文本框newToDoDateTimeReminderTextView彈出一個時間選擇器TimePickerDialog,點擊保存按鈕,如果輸入框文本為空,則提示需要輸入,否則,創建Task成功,然后退出本Activity,在MainActivity顯示剛剛加入的Task。
在AddTaskActivity我們同樣需要使用TaskViewModel,讓它執行插入操作。
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的關鍵代碼,現在,我們已經完成了對于Task的增刪查操作。已經掌握了Room結合Android架構組件開發的流程。現在我們使用Jetpack的另一個組件---WorkManager,令這個程序更有趣一些。
對Task帶有提醒功能的WorkManager
現在,我們使用WorkManager,對一些有提醒的任務進行系統的提醒(Notification)
- 第一步,在build.gradle(Module:app)中添加對workmanager的支持
//workManager
def work_version = "2.3.4"
implementation "androidx.work:work-runtime-ktx:$work_version"
- 第二步,新建一個繼承Worker的任務類,我們命名為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()
}
- 實現sendNotification方法,發送系統通知
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())
}
- 在創建任務的時候,如果選擇了提醒時間,那么需要創建一個發送系統通知的work,我們在AddTaskActivity執行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()
}
我們為每一個帶有提醒時間的Task添加OneTimeWorkRequest。在實體類Task中添加一個字段work_manager_uuid保存OneTimeWorkRequest,方便執行列表左滑右滑的時候刪除item時候,同時使用cancelWorkById()把對應的任務取消,下面的代碼就是MainActivity中item左滑右滑的回調監聽。
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))
}
}
})
計算出現在時間和創建Task那個提醒時間的delay差值,使用
OneTimeWorkRequest.Builder(NotifyWork::class.java)
.setInitialDelay(delay, TimeUnit.MILLISECONDS).setInputData(data).build()
來建議一個任務,然后beginWith(notificationWork).enqueue()來把任務交給WorkManager。
這樣就實現了當提醒時間到達的時候,系統就會打開一個通知。完成這個提醒功能。
注意:用Google Nexus 6P和小米9分別測試該功能。在殺死app的情況下,前者依舊能夠收到系統的通知。但是小米不可以,國產的部分ROM已經對WorkManager失去作用。
總結
以上就是基于Android架構組件用Room和WorkManager實現的一個簡單TODO APP,基本能掌握ROOM和WorkManager的基礎用法,同時對Kotlin的語法有進一步加深理解。