Sử dụng Room với Kotlin

Room được giới thiệu lần đầu tiên trong sự kiện thường niên Google I/O năm 2017 do Google tổ chức. Thực chất đây chỉ là 1 thư viện wrapper của sqlite nhằm tăng cường sức mạnh cho embedded database này đồng thời thích hợp với các pattern hiện đại. Và đây cũng là lí do mà tác giả MANIJSHRESTHA đã đưa ra bài viết này để thể hiện cách hiểu và cách dùng theo quan điểm cá nhân của ông.

Và trong sự kiên Gooogle IO lần này, Google cũng mang tới một niềm vui lớn cho các nhà phát triển ứng dụng android là họ chính thức hỗ trợ ngôn ngữ Kotlin, một ngôn ngữ script giúp phát triển ứng dụng cực nhanh, source code clear và dễ maintain hơn java rất nhiều.

Tác giả cũng áp dụng thư viện Room trong project sample sử dụng Kotlin luôn.

Trong bài viết này, tác giả sẽ hướng dẫn các bạn làm thế nào để bắt đầu làm việc với Room và ngôn ngữ Kotlin.

Tôi bắt đầu project với việc thiết lập Dagger 2. Chúng ta sẽ áp dụng Room trong project Kotlin có sử dụng Dagger2, sau đó sẽ tiến hành tích hợp với RxJava2.

Tôi cũng sử dụng một ví dụ từ google samples, "ToDoList" project, cho phép người dùng tạo các task công việc. Để bắt đầu bạn hãy include các thư viện cần thiết.

Including Room

Thêm vào file build.gradle các dependency cho room sau (phiên bản mới nhất hiện tại là 1.0.3-alpha)

   dependencies {
   ....
    compile "android.arch.persistence.room:runtime:1.0.3-alpha1"
    kapt "android.arch.persistence.room:compiler:1.0.3-alpha1"
    ...
    }

Defining Entities

Sao đó bạn tạo 1 entity tên là Task để lưu trữ data với cấu trúc đơn giản là gồm có 1 trường id, description (nội dung công việc của task), flag (trạng thái của task đã hoàn thành hay chưa)

import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey

@Entity(tableName = "task")
data class Task(@ColumnInfo(name = "completed_flag") var completed: Boolean = false,
           @ColumnInfo(name = "task_desciption") var description: String) {
    @ColumnInfo(name = "id")
    @PrimaryKey(autoGenerate = true) var id: Long = 0
}

Nếu bạn chú ý bạn sẽ thấy điều quan trọng nhất ở đây là modifier "data class". Chúng ta thêm annotation này để Room hiểu và dùng nó.

@Entity(tableName = “task”) đúng như cái tên, annotation này dùng để định nghĩa tên của table trong database. Nếu bạn không định nghĩa thì mặc định tên class chính là tên của table.

@ColumnInfo annotation dùng để định nghĩa tên của column trong bảng, nếu không định nghĩa thì Room sẽ dùng tên của biến làm tên của column. như ở ví dụ này thì tên của column khác tên của filed bao gồm cả ký tự “_”.

@PrimaryKey(autoGenerate = true) được dùng cho để định nghĩa khoá chính cho bảng. Trong trường hợp này thì id sẽ tự động tăng 1 giá trị khi bạn insert thêm 1 row mới. 1 bảng phải có ít nhất 1 khoá chính.

Defining Dao

Dao trong Room thực hiện rất nhiều chức năng để tương tác với database. Chúng ta chỉ cần định nghĩa 1 interface với các method có annotation là các query tương ứng để thực hiện truy vấn đến database. Room sẽ compile các lệnh này trước khi build app để chúng ta có thể sử dụng được. Điều này khá giống với cơ chế của thư viện Retrofit. Nào chúng ta cùng xem nhé

@Dao interface TaskDao {

    @Query("select * from task")
    fun getAllTasks(): List<Task>

    @Query("select * from task where id = :p0")
    fun findTaskById(id: Long): Task

    @Insert(onConflict = REPLACE)
    fun insertTask(task: Task)

    @Update(onConflict = REPLACE)
    fun updateTask(task: Task)

    @Delete
    fun deleteTask(task: Task)
}

Như bạn thấy ở trên, chúng ta có 1 interface được đánh dấu với annotation @Dao, và trong này chứ rất nhiều chức năng với các annotation khác nhau.

Hãy xem 1 trong số chúng, method "fun getAllTasks: List", method này sẽ trả về 1 danh sách tất cả các task có trong database của chúng ta. Chức năng này được đánh dấu với annotation @Query. Câu truy vấn này sẽ được biên dịch khi compile. Nếu câu truy vấn sai cú pháp thì tiến trình build sẽ bị lỗi. Và nếu build thành công thì chắc chắn là nó đã hoạt động.

Giờ hãy nhìn 1 chức năng phức tạp hơn 1 chút, "fun findTaskById(id:Long): Task". Chức năng này được đánh dấu với annotation @Query(“select * from task where id = :p0”).

Và cho đến thời điểm hiện tại thì ngôn ngữ kotlin convert vẫn còn có vài vấn đề như các biến bị chuyển thành p0, arg0... đọc rất khó hiểu. Những vấn đề như vậy sẽ sớm được cải thiện thôi.

Defining Database

Chúng ta cần 1 định nghĩa database bằng cách tạo 1 abstract class kế thừa từ RoomDatabase.

@Database(entities = arrayOf(Task::class), version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {

abstract fun taskDao(): TaskDao
}

Class này phải khái báo với annotation @Database và tất cả các bảng của nó, đương nhiên cả phiên bản nữa. Bạn để ý kỹ hơn sẽ thấy khái báo “exportSchema” đc set false. Nếu không thì mặc định set true thì khi compile code bạn sẽ gặp warning như dưới:

warning: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide room.schemaLocation annotation processor argument OR set exportSchema to false. Class này có cấu trức giống như 1 thành phần của dagger, và thực hiện các method mà chúng ta định nghĩa ở trên, được gọi là "TaskDao"

Configuring in Dagger

Giống như dagger, chúng ta sẽ build room database, chúng ta muốn nó là singleton để nhúng vào những class cần làm việc, hãy khai báo như sau:

@Module class AppModule(private val context: Context) {

    @Provides fun providesAppContext() = context

    @Provides fun providesAppDatabase(context: Context): AppDatabase =
            Room.databaseBuilder(context, AppDatabase::class.java, "my-todo-db").allowMainThreadQueries().build()

    @Provides fun providesToDoDao(database: AppDatabase) = database.taskDao()
}

Room database sử dụng application context, chúng ta trỏ tới abstract class ở trên và file database mà chúng ta muốn.

Chúng ta gọi method dưới đây để các câu query có thể đc được trong main thread.

.allowMainThreadQueries() Nếu chúng ta không gọi lệnh này thì có 1 exception xảy ra và thông báo chúng ta không thể truy xuất vào database trên main thread.

Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.

Trong phần tiếp theo chúng ta sẽ tiếp tục với việc nhúng RxJava, đồng thời cũng sử dụng dagger để cung cấp TaskDao.

Getting Entities in Presenter

Ở ví dụ này chúng ta sử dụng pattern MVP. bạn hãy xem cách chúng ta lấy dữ liệu và hiển thị trên 1 recycler view như thế nào.

class ToDoPresenter @Inject constructor(val taskDao: TaskDao) {

    var tasks = ArrayList<Task>()

    var presentation: ToDoPresentation? = null

    fun onCreate(toDoPresentation: ToDoPresentation) {
        presentation = toDoPresentation
        loadTasks()
    }

    fun onDestroy() {
        presentation = null
    }

    fun loadTasks() {
        tasks.clear()
        tasks.addAll(taskDao.getAllTasks())
        presentation?.showTasks(tasks)
    }

    fun addNewTask(taskDescription: String) {
        val newTask = Task(description = taskDescription)
        tasks.add(newTask)
        taskDao.insertTask(newTask)
        (tasks.size - 1).let {
            presentation?.taskAddedAt(it)
            presentation?.scrollTo(it)
        }
    }
}

Ok, vậy là xong rồi đúng không các bạn

Using Room with RxJava/RxAndroid with Kotlin

Đến phần tiếp theo chúng ta sẽ sử dụng dependency RxJava/RxAndroid. Bạn hãy thêm vào build.gradle như sau:

compile "io.reactivex.rxjava2:rxjava:2.1.0" compile "io.reactivex.rxjava2:rxandroid:2.0.1"

Để hoạt động được chúng ta cần thêm 1 dependency nữa làm cầu nối

compile "android.arch.persistence.room:rxjava2:1.0.0-alpha3"

Từ giờ chúng ta đã có thể sử dụng đc Room với RxJava

@Query("select * from task") fun getAllTasks(): Flowable<List<Task>> Chúng ta có thể remove “allowMainThreadQueries()” đi được rồi. Và module của chúng ta sẽ đơn gỉan chỉ còn:

@Provides fun providesAppDatabase(context: Context): AppDatabase = Room.databaseBuilder(context, AppDatabase::class.java, "my-todo-db").build()

Chúng ta cũng cần chỉnh lại presenter 1 chút. Dưới đây là toàn bộ source code của presenter.

package com.manijshrestha.todolist.ui

import com.manijshrestha.todolist.data.Task
import com.manijshrestha.todolist.data.TaskDao
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject

class ToDoPresenter @Inject constructor(val taskDao: TaskDao) {

    val compositeDisposable = CompositeDisposable()
    var tasks = ArrayList<Task>()

    var presentation: ToDoPresentation? = null

    fun onCreate(toDoPresentation: ToDoPresentation) {
        presentation = toDoPresentation
        loadTasks()
    }

    fun onDestroy() {
        compositeDisposable.dispose()
        presentation = null
    }

    fun loadTasks() {
        compositeDisposable.add(taskDao.getAllTasks()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    tasks.clear()
                    tasks.addAll(it)
                    (tasks.size - 1).takeIf { it >= 0 }?.let {
                        presentation?.taskAddedAt(it)
                        presentation?.scrollTo(it)
                    }
                }))

        presentation?.showTasks(tasks)
    }

    fun addNewTask(taskDescription: String) {
        val newTask = Task(description = taskDescription)
        compositeDisposable.add(Observable.fromCallable { taskDao.insertTask(newTask) }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe())
    }
}

Thật tuyệt, đến đây là bạn đã biết cách dùng Room, Dagger, RxJava sử dụng Kotlin rồi đấy.

Source code cho ví dụ bạn có thể tham khảo thêm ở đây https://github.com/manijshrestha/ToDoList

Translated and Edited from https://manijshrestha.wordpress.com/2017/06/03/using-room-with-kotlin/