preface

In your spare time, write code to reinforce your body of knowledge. Therefore, use Room and WorkManager to implement an App that checks the Task list, deletes the item left and right, and creates a Task with a reminder function under Android architecture components.

This article will cover the following points

  • Android Architecture Components
  • Jetpack – Room
  • Jetpack – WorkManager
  • Kotlin Coroutines
  • Recyclerview custom left slip right slip event implementation

This article will be introduced step by step from system architecture to detailed code, stay tuned…

screenshots

Architectural components

Below is a diagram of the components of our system architecture, one of the implementations recommended by Google

Here’s an explanation

  • Entity: Entity class, annotated class that acts as a table with the database in Room
  • SQLite: Create and maintain this database using the encapsulated Room as a persistence library
  • Dao: Data access object. SQL queries map to the function, and with DAO, you call the methods, while Room does the rest.
  • Room database: The underlying implementation of SQLite, the database uses DAO to issue queries to the SQLite database.
  • Repository: A Repository used to manage multiple data sources, often serving as a bridge between ViewModel and data retrieval.
  • ViewModel: Acts as a communication center between the repository (data) and the UI. The UI no longer needs to worry about the source of the data. Viewmodels are not lost to the activity or fragment life cycle.
  • LiveData: The observed data holder class. Always save/cache the latest version of the data and notify its observers when the data changes. LiveData knows the life cycle. The UI component only observes relevant data and does not stop or continue observing. LiveData automatically manages all of this because it is aware of changes in the associated lifecycle state as it watches.

Here is the system framework for TodoApp

Each closed box (with the exception of the SQLite database) represents each class we will create

Create a program

  1. Open Android Studio, then click Start a New Android Studio Project
  2. In the Create New Project window, selectEmpty ActivityAnd then clickNext.
  3. On the next screen, name the app TodoApp and clickFinish.

Update Gradle files

  1. Open build. Gradle (Moudle: app)
  2. Use the Kapt annotation handler and Kotlin’s Ext function at the top
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
Copy the code
  1. Add packagingOptions to the Android node to prevent warnings
android {
    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
Copy the code
  1. Add the following code at the end of the code Dependencies block
// 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-run-time KTX :$work_version" 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"Copy the code
  1. Open build.gradle (Project: TodoApp) without adding the following code at the end
Ext {roomVersion = '2.2.5' archLifecycleVersion = '2.2.0' coreTestingVersion = '2.1.0' materialVersion = '1.1.0' Coroutines = '1.3.4'}Copy the code

Creating an entity Class

Our entity class is Task, Task, what fields do we need?

First, we definitely need the task name name, then we need the task description desc, then we use a Boolean to indicate whether we need to be reminded, and also use the Date class to record the reminding time, then we need a color of the interface Image color, finally, We need a workManager_ID for each task. This id is used by the WorkManager to terminate a task when an item is deleted. This id will be described later and not described here.

@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,// if @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 = "" }Copy the code

Let’s see what these annotations do

  • @Entity(tableName = “task_table”)

Each @Entity class represents an SQLite table. Annotate your class declaration to indicate that it is an Entity. If you want the table name to be different from the class name, you can specify the table name, for example, “task_table”.

  • @PrimaryKey

Each entity needs a primary key. We set a Long as the primary key, start with 0, and let it autoGenerate = true

  • @ColumnInfo(name = “name”)

Specify the column name if you want the column name in the table to be different from the name of the member variable. This names the column name.

  • TypeConverters

The Room database can only store basic types (Int,String,Boolean,Float, etc.). For obj, conversion is required. We define a DateConverter conversion. Convert Long to Date. The following code

class DateConverter { @TypeConverter fun revertDate(value: Long?) : Date? { return value? .let { Date(it) } } @TypeConverter fun converterDate(date: Date?) : Long? { return date? .time } }Copy the code

Create Dao

What is a Dao?

Dao is the database access object, specify the SQL Query statement and it calls the method, such as Query, Insert, Delete, Update, etc.

Daos must be interfaces or abstract classes

Room can use coroutines with the suspend modifier in front of the method name

How do I use Dao?

We will write a Dao to implement Task add, delete, change and query. The following code

@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)

}
Copy the code

Let’s take a look at some of the code above

  • TaskDao is an interface; Because we mentioned above that daOs must be interfaces or abstract classes.
  • Use @dao to indicate that this interface is the Dao of Room
  • Insert (task: task), which declares a method for inserting a new task
  • @insert, Insert execution, do not need to write SQL statements, also do not need to write SQL statements are Delete,Update
  • OnConflict = OnConflictStrategy. IGNORE: if selected onConflict strategy with the list for the same Task, will IGNORE this Task
  • Fun deleteAll() declares a method to deleteAll tasks
  • Remove (task: task) declares a method for removing a single task
  • Fun getAllTask(): LiveData a set object that returns LiveData containing all tasks.
  • @query (“SELECT * from task_table “) : Returns a list of tasks that can be inserted in ascending or descending order or filtered statements

LiveData

When data changes, you usually need to take some action, such as displaying the updated data in the UI. This means that you must observe the data so that you can react when it changes.

Depending on how the data is stored, this can be tricky. Observing changes in data between multiple components of an application creates clear, rigid dependency paths between components. This makes testing and debugging very difficult.

LiveData, a lifecycle library class for data observation, solves this problem. LiveData uses the return value of the type in the method description, and Then Room generates all the necessary code to update the LiveData database.

In the TaskDao, we return the LiveData collection object that contains all the tasks, and then we listen for the MainActivity

@Query("SELECT * from task_table")
fun getAllTask(): LiveData<List<Task>>
Copy the code

Room database

What is Room Database

  • Room is a top-level call to the SQLite database.
  • Room’s job is similar to SQlite’s SQLiteOpenHelper
  • Room uses DAO to add, delete, change and query operations to its database
  • Room SQL statements check this syntax during compilation

How to use Room Database

Room database classes must be abstract and inherit from RoomDatabase, typically in a singleton pattern.

Now let’s build a TaskRoomDatabase with the following code

@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 {// If INSTANCE is null, return INSTANCE; otherwise, create database return INSTANCE? : synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, TaskRoomDatabase::class.java, "Task_database") // If the database has not been migrated, erase and rebuild instead of migrating. .fallbackToDestructiveMigration() .build() INSTANCE = instance instance } } } }Copy the code

Let’s take a look at the code above

  • Use the @Database annotation to indicate that this class is a Database class, and then specify its entity class (you can set more than one) and version number.
  • TaskRoomDatabase operates by retrieving objects from its abstract object TaskDao
  • Databases are typically singleton, preventing multiple database instances from being opened at the same time

The Repository Repository

In the most common example, the repository implements the logic to determine whether to fetch data from the network or use results cached in a local database.

TaskRepository is implemented as follows

// Declare the Dao's private attributes in the constructor, through the Dao instead of the entire database, since only Dao Class TaskRepository(private Val taskDao: TaskDao) {// Room executes all queries on a separate thread // Observed LiveData will notify the observer when the data changes. val allWords: LiveData<List<Task>> = taskDao.getAllTask() fun insert(task: Task) { taskDao.insert(task) } fun remove(task: Task) { taskDao.remove(task) } }Copy the code

Pay attention to,

  • The DAO, as a TaskRepository constructor, does not need to use the database instance.
  • Get the Task list from Room via LiveData for initialization. Room performs query Task operations on a separate thread, LiveData notifies the observer on the main thread when data changes.
  • Repositories are designed to mediate between different data sources. In this TodoApp, there is only one data source, Room, so the repository doesn’t do much. For a more complex implementation, see an example I wrote

ViewModel

What is what is a ViewModel?

The ViewModel provides data to the UI that can be stored when the activity and fragment cycles change. It is typically a hub between Repository and Activity/Fragment, and can be used to share data.

The ViewModel better follows the single responsibility principle by separating the data from the UI.

In general, ViewModel will be used with LiveData. The benefits of LiveData with ViewModel are many:

  • Place the observer on the data (without polling for changes) and update the UI only when the data actually changes.
  • The ViewModel separates the repository from the UI
  • Higher testability

viewModelScope

At Kotlin, all coroutines run the CoroutineScope inside. Scope controls the life cycle of coroutines through job. When the job in scope is cancelled, it also cancels all coroutines started within the scope.

AndroidX Lifecycle – ViewModel-kTX library adds the viewModel extension of the viewModelScope class to work in its scope

Next, take a look at the TaskViewModel implementation

class TaskViewModel(application: Application) : AndroidViewModel(application) { private val repository: TaskRepository // Using LiveData and caching the content returned by getAllTask has several benefits: // - Notifies the observer whenever there is an update to the Room database, rather than polling for updates. // - The repository is completely isolated from the UI through the ViewModel. val allWords: LiveData<List<Task>> init { val taskDao = TaskRoomDatabase.getDatabase(application, ViewModelScope).taskDao() repository = TaskRepository(taskDao) allWords = repository.allwords} /** * Starts a new coroutine to insert data in a non-blocking manner */ 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() } } }Copy the code

We use the coroutine method viewModelscope.launch (dispatchers.io) to operate the database. The main thread is prevented from being blocked.

Task list XML layout

  1. First add the layout information for task item task_list_item.xml
<? The 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>Copy the code

Then add a RecyclerView and toDoEmptyView to the layout of activity_main.xml, and add a fab button to AddTaskActivity to create a new 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>Copy the code

RecyclerView and 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]) } }Copy the code

The Adapter onBindViewHolder sets the display of each item, depending on the Task’s hasReminder and time values, to display the list item, and then the MainActivity sets the Recyclerview and 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)
    }
}
Copy the code

Connection data

In MainActivity, create a member variable, ViewModel

    private lateinit var wordViewModel: TaskViewModel

Copy the code

Then we instantiate it, and once we’ve got the TaskViewModel object, we can listen for changes to the Task list in the Room. The following code

WordViewModel = ViewModelProvider (this). The get (TaskViewModel: : class. Java) / / on getAllTask return LiveData add observer. // The onChanged () method is triggered when observed data changes and acElasticity is to the foreground. 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) } } })Copy the code

When the listener list data is not empty, recyclerView display, toDoEmptyView hidden, otherwise, toDoEmptyView display, RecyclerView hidden. Then run the program, as shown below

Add a Task

Create a new AddTaskActivity page with the following layout for 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" android:gravity="top"> <EditText android:textColor="@color/secondary_text" Android :text=" today" android:id="@+id/newTodoDateEditText" android:layout_width="0dp" android:layout_height="wrap_content" Android: layout_weight = "1.5" android: the 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 PM "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>Copy the code

A text input box for Task Name edit_task, a switch that controls whether a reminder function is required SwitchMaterial, click the text box newTodoDateEditText to pop up a DatePickerDialog, Click the text box newToDoDateTimeReminderTextView popup selector TimePickerDialog a time, click save button, if the input text box is empty, is prompted for input, otherwise, to create the Task successfully, and then out of this Activity, The newly added Task is displayed in MainActivity.

In AddTaskActivity we also need to use the TaskViewModel to perform the insert.

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() } } }Copy the code

This is the key code for AddTaskActivity. Now we have completed adding and deleting tasks. I have mastered the development process of Room combined with Android architecture components. Now let’s use another component of Jetpack, WorkManager, to make the program more interesting.

WorkManager with reminders for tasks

Now, let’s use WorkManager to systematically notify tasks that have reminders.

  1. First, add support for WorkManager in build.gradle(Module:app)
//workManager def work_version = "2.3.4" implementation "androidx.work: work-run-time KTX :$work_version"Copy the code
  1. Step 2, create an inheritanceWorkerTask class, which we named asNotifyWork, and rewrite **doWorkH * * e () method
override fun doWork(): Result { val id = inputData.getInt(NOTIFICATION_ID, 0) val title = inputData.getString(TASK_TITLE) ? : "Title" sendNotification(id, title) return Result.success() }Copy the code
  1. Implement sendNotification method to send system notification
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 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()) }Copy the code
  1. Create a work that sends system notifications if you select a reminder time when creating a task. Add the following when SaveTask() is executed by AddTaskActivity
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() }Copy the code

We added OneTimeWorkRequest for each Task with a reminder time. CancelWorkById (); cancelWorkById() cancelWorkById(); cancelWorkById(); The following code is a callback listening for items to slide left and right in MainActivity.

adapter.setOnItemEventListener(object : TaskListAdapter.OnItemEventListener { override fun onItemRemoved(task: Task) {toast.maketext (baseContext, "delete" + task.name + "success ", 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)) } } })Copy the code

Calculate the delay difference between the time at which the Task was created and the time at which the Task was created

OneTimeWorkRequest.Builder(NotifyWork::class.java)
            .setInitialDelay(delay, TimeUnit.MILLISECONDS).setInputData(data).build()
Copy the code

To suggest a task and beginWith(notificationWork).enqueue() to hand the task to the WorkManager.

This enables the system to open a notification when the time is up. Complete the reminder function.

Note: Test this feature on Google Nexus 6P and Mi 9 respectively. In the case of killing the app, the former can still receive a notification from the system. However, Xiaomi cannot. Some domestic ROM has lost its effect on WorkManager.

conclusion

The above is a simple TODO APP based on Android architecture components using Room and WorkManager. I can basically master the basic usage of Room and WorkManager, and have a further understanding of Kotlin’s syntax.

Project address: github.com/laibinzhi/T…