Kiến thức cơ bản về Kotlin cho Android 06.2: Coroutine và Room

Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Một trong những ưu tiên hàng đầu để tạo trải nghiệm người dùng hoàn hảo cho ứng dụng của bạn là đảm bảo giao diện người dùng luôn phản hồi và chạy mượt mà. Một cách để cải thiện hiệu suất giao diện người dùng là di chuyển các tác vụ diễn ra trong thời gian dài (chẳng hạn như các thao tác với cơ sở dữ liệu) vào nền.

Trong lớp học lập trình này, bạn sẽ triển khai phần dành cho người dùng của ứng dụng TrackMySleepQuality bằng cách sử dụng coroutine Kotlin để thực hiện các thao tác cơ sở dữ liệu bên ngoài luồng chính.

Kiến thức bạn cần có

Bạn cần thông thạo:

  • Xây dựng giao diện người dùng (UI) cơ bản bằng cách sử dụng một hoạt động, các mảnh, khung hiển thị và trình xử lý lượt nhấp.
  • Điều hướng giữa các mảnh và sử dụng safeArgs để truyền dữ liệu đơn giản giữa các mảnh.
  • Mô hình hiển thị, nhà máy mô hình hiển thị, các phép biến đổi và LiveData.
  • Cách tạo cơ sở dữ liệu Room, tạo DAO và xác định các thực thể.
  • Bạn nên tìm hiểu về các khái niệm về luồng và xử lý đa tiến trình.

Kiến thức bạn sẽ học được

  • Cách hoạt động của luồng trong Android.
  • Cách sử dụng coroutine Kotlin để di chuyển các thao tác cơ sở dữ liệu ra khỏi luồng chính.
  • Cách hiển thị dữ liệu được định dạng trong TextView.

Bạn sẽ thực hiện

  • Mở rộng ứng dụng TrackMySleepQuality để thu thập, lưu trữ và hiển thị dữ liệu trong và từ cơ sở dữ liệu.
  • Sử dụng coroutine để chạy các thao tác dài hạn với cơ sở dữ liệu ở chế độ nền.
  • Sử dụng LiveData để kích hoạt thao tác điều hướng và hiển thị một thanh thông báo nhanh.
  • Sử dụng LiveData để bật và tắt các nút.

Trong lớp học lập trình này, bạn sẽ tạo mô hình hiển thị, các coroutine và phần hiển thị dữ liệu của ứng dụng TrackMySleepQuality.

Ứng dụng này có 2 màn hình, được biểu thị bằng các mảnh, như minh hoạ trong hình bên dưới.

Màn hình đầu tiên (xuất hiện ở bên trái) có các nút để bắt đầu và dừng theo dõi. Màn hình này cho thấy tất cả dữ liệu về giấc ngủ của người dùng. Nút Xoá sẽ xoá vĩnh viễn tất cả dữ liệu mà ứng dụng đã thu thập cho người dùng.

Màn hình thứ hai (ở bên phải) là màn hình chọn mức đánh giá chất lượng giấc ngủ. Trong ứng dụng, điểm xếp hạng được biểu thị bằng số. Để phục vụ mục đích phát triển, ứng dụng này cho thấy cả biểu tượng khuôn mặt và giá trị tương đương bằng số của biểu tượng đó.

Luồng người dùng như sau:

  • Người dùng mở ứng dụng và thấy màn hình theo dõi giấc ngủ.
  • Người dùng nhấn vào nút Bắt đầu. Thao tác này sẽ ghi lại thời gian bắt đầu và hiển thị thời gian đó. Nút Start (Bắt đầu) bị vô hiệu hoá và nút Stop (Dừng) được bật.
  • Người dùng nhấn vào nút Dừng. Thao tác này sẽ ghi lại thời gian kết thúc và mở màn hình chất lượng giấc ngủ.
  • Người dùng chọn một biểu tượng chất lượng giấc ngủ. Màn hình đóng lại và màn hình theo dõi sẽ hiển thị thời gian kết thúc giấc ngủ và chất lượng giấc ngủ. Nút Stop (Dừng) bị tắt và nút Start (Bắt đầu) được bật. Ứng dụng đã sẵn sàng cho một đêm khác.
  • Nút Xoá sẽ bật bất cứ khi nào có dữ liệu trong cơ sở dữ liệu. Khi người dùng nhấn vào nút Xoá, tất cả dữ liệu của họ sẽ bị xoá mà không có cách nào khôi phục được. Không có thông báo "Bạn có chắc chắn không?".

Ứng dụng này sử dụng một cấu trúc đơn giản, như minh hoạ dưới đây trong bối cảnh của cấu trúc đầy đủ. Ứng dụng chỉ sử dụng các thành phần sau:

  • Bộ điều khiển giao diện người dùng
  • Xem mô hình và LiveData
  • Cơ sở dữ liệu Room

Trong nhiệm vụ này, bạn sử dụng TextView để hiển thị dữ liệu theo dõi giấc ngủ đã định dạng. (Đây không phải là giao diện cuối cùng. Bạn sẽ tìm hiểu một cách hiệu quả hơn trong một lớp học lập trình khác.)

Bạn có thể tiếp tục với ứng dụng TrackMySleepQuality mà bạn đã tạo trong lớp học lập trình trước hoặc tải ứng dụng khởi đầu cho lớp học lập trình này xuống.

Bước 1: Tải xuống và chạy ứng dụng khởi động

  1. Tải ứng dụng TrackMySleepQuality-Coroutines-Starter xuống từ GitHub.
  2. Tạo và chạy ứng dụng. Ứng dụng sẽ hiển thị giao diện người dùng cho mảnh SleepTrackerFragment nhưng không có dữ liệu. Các nút không phản hồi khi nhấn.

Bước 2: Kiểm tra mã

Đoạn mã khởi đầu cho lớp học lập trình này cũng là đoạn mã giải pháp cho lớp học lập trình 6.1 Tạo cơ sở dữ liệu Room.

  1. Mở res/layout/activity_main.xml. Bố cục này chứa mảnh nav_host_fragment. Ngoài ra, hãy lưu ý thẻ <merge>.

    Bạn có thể dùng thẻ merge để loại bỏ các bố cục thừa khi đưa bố cục vào, và bạn nên dùng thẻ này. Ví dụ về bố cục dư thừa là ConstraintLayout > LinearLayout > TextView, trong đó hệ thống có thể loại bỏ LinearLayout. Loại tối ưu hoá này có thể đơn giản hoá hệ phân cấp khung hiển thị và cải thiện hiệu suất ứng dụng.
  2. Trong thư mục navigation, hãy mở navigation.xml. Bạn có thể thấy 2 mảnh và các thao tác điều hướng kết nối chúng.
  3. Trong thư mục layout, hãy nhấp đúp vào mảnh trình theo dõi giấc ngủ để xem bố cục XML của mảnh đó. Lưu ý những điều sau:
  • Dữ liệu bố cục được gói trong một phần tử <layout> để bật tính năng liên kết dữ liệu.
  • ConstraintLayout và các khung hiển thị khác được sắp xếp bên trong phần tử <layout>.
  • Tệp có thẻ giữ chỗ <data>.

Ứng dụng khởi đầu cũng cung cấp kích thước, màu sắc và kiểu dáng cho giao diện người dùng. Ứng dụng này chứa một cơ sở dữ liệu Room, một DAO và một thực thể SleepNight. Nếu bạn chưa hoàn thành lớp học lập trình trước đó, hãy nhớ tự mình khám phá những khía cạnh này của mã.

Giờ đây, bạn đã có cơ sở dữ liệu và giao diện người dùng, bạn cần thu thập dữ liệu, thêm dữ liệu vào cơ sở dữ liệu và hiển thị dữ liệu. Tất cả công việc này đều được thực hiện trong mô hình hiển thị. Mô hình hiển thị của trình theo dõi giấc ngủ sẽ xử lý các lượt nhấp vào nút, tương tác với cơ sở dữ liệu thông qua DAO và cung cấp dữ liệu cho giao diện người dùng thông qua LiveData. Mọi thao tác về cơ sở dữ liệu sẽ phải chạy trên luồng giao diện người dùng chính và bạn sẽ thực hiện việc này bằng cách sử dụng coroutine.

Bước 1: Thêm SleepTrackerViewModel

  1. Trong gói sleeptracker, hãy mở SleepTrackerViewModel.kt.
  2. Kiểm tra lớp SleepTrackerViewModel. Lớp này được cung cấp cho bạn trong ứng dụng khởi động và cũng xuất hiện bên dưới. Xin lưu ý rằng lớp này mở rộng AndroidViewModel(). Lớp này giống với ViewModel, nhưng lấy ngữ cảnh ứng dụng làm tham số và cung cấp tham số đó dưới dạng một thuộc tính. Bạn sẽ cần thông tin này sau.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Bước 2: Thêm SleepTrackerViewModelFactory

  1. Trong gói sleeptracker, hãy mở SleepTrackerViewModelFactory.kt.
  2. Kiểm tra mã được cung cấp cho bạn cho nhà máy, mã này xuất hiện bên dưới:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Lưu ý những điều sau:

  • SleepTrackerViewModelFactory được cung cấp sẽ lấy cùng đối số với ViewModel và mở rộng ViewModelProvider.Factory.
  • Bên trong nhà máy, mã này sẽ ghi đè create(), nhận mọi loại lớp làm đối số và trả về ViewModel.
  • Trong phần nội dung của create(), mã sẽ kiểm tra xem có lớp SleepTrackerViewModel hay không và nếu có, mã sẽ trả về một phiên bản của lớp đó. Nếu không, mã sẽ gửi một ngoại lệ.

Bước 3: Cập nhật SleepTrackerFragment

  1. Trong SleepTrackerFragment, hãy lấy thông tin tham chiếu đến ngữ cảnh ứng dụng. Đặt tham chiếu trong onCreateView(), bên dưới binding. Bạn cần có một tham chiếu đến ứng dụng mà mảnh này được đính kèm để truyền vào trình cung cấp nhà máy mô hình hiển thị.

    Hàm requireNotNull Kotlin sẽ gửi một IllegalArgumentException nếu giá trịnull.
val application = requireNotNull(this.activity).application
  1. Bạn cần có một thông tin tham chiếu đến nguồn dữ liệu thông qua thông tin tham chiếu đến DAO. Trong onCreateView(), trước return, hãy xác định một dataSource. Để lấy thông tin tham chiếu đến DAO của cơ sở dữ liệu, hãy dùng SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Trong onCreateView(), trước return, hãy tạo một thực thể của viewModelFactory. Bạn cần truyền dataSourceapplication.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Giờ đây, bạn đã có một nhà máy, hãy tham chiếu đến SleepTrackerViewModel. Tham số SleepTrackerViewModel::class.java đề cập đến lớp Java thời gian chạy của đối tượng này.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Mã hoàn tất của bạn sẽ có dạng như sau:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Sau đây là phương thức onCreateView() cho đến thời điểm hiện tại:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Bước 4: Thêm tính năng liên kết dữ liệu cho mô hình hiển thị

Sau khi thiết lập ViewModel cơ bản, bạn cần hoàn tất việc thiết lập tính năng liên kết dữ liệu trong SleepTrackerFragment để kết nối ViewModel với giao diện người dùng.


Trong tệp bố cục fragment_sleep_tracker.xml:

  1. Bên trong khối <data>, hãy tạo một <variable> tham chiếu đến lớp SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

Trong SleepTrackerFragment:

  1. Đặt hoạt động hiện tại làm chủ sở hữu vòng đời của hoạt động liên kết. Thêm mã này vào bên trong phương thức onCreateView(), trước câu lệnh return:
binding.setLifecycleOwner(this)
  1. Chỉ định biến liên kết sleepTrackerViewModel cho sleepTrackerViewModel. Đặt mã này vào bên trong onCreateView(), bên dưới mã tạo SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Có thể bạn sẽ thấy lỗi vì bạn phải tạo lại đối tượng liên kết. Dọn dẹp và tạo lại dự án để loại bỏ lỗi.
  2. Cuối cùng, như mọi khi, hãy đảm bảo mã của bạn được tạo và chạy mà không có lỗi.

Trong Kotlin, coroutine là cách xử lý các tác vụ chạy trong thời gian dài một cách hiệu quả và gọn gàng. Coroutine Kotlin cho phép bạn chuyển đổi mã dựa trên lệnh gọi lại thành mã tuần tự. Mã được viết tuần tự thường dễ đọc hơn và thậm chí có thể sử dụng các tính năng ngôn ngữ như ngoại lệ. Rốt cục thì coroutine và lệnh gọi lại hoạt động giống hệt nhau: chờ cho đến khi có kết quả từ một thao tác dài hạn rồi tiếp tục thực thi.

Các coroutine có những thuộc tính sau:

  • Coroutine không đồng bộ và không chặn.
  • Coroutine sử dụng các hàm tạm ngưng để tạo mã không đồng bộ theo tuần tự.

Coroutine không đồng bộ.

Coroutine chạy độc lập với các bước thực thi chính của chương trình. Việc này có thể diễn ra song song hoặc trên một bộ xử lý riêng. Cũng có thể là trong khi phần còn lại của ứng dụng đang chờ dữ liệu đầu vào, bạn sẽ lén lút xử lý một chút. Một trong những khía cạnh quan trọng của async là bạn không thể mong đợi kết quả có sẵn cho đến khi bạn đợi kết quả một cách rõ ràng.

Ví dụ: giả sử bạn có một câu hỏi cần nghiên cứu và bạn yêu cầu đồng nghiệp tìm câu trả lời. Họ sẽ rời đi và làm việc đó, giống như họ đang làm việc "không đồng bộ" và "trên một luồng riêng biệt". Bạn có thể tiếp tục làm những việc khác không phụ thuộc vào câu trả lời, cho đến khi đồng nghiệp quay lại và cho bạn biết câu trả lời.

Coroutine không chặn.

Không chặn có nghĩa là một coroutine không chặn luồng chính hoặc luồng giao diện người dùng. Vì vậy, với các coroutine, người dùng luôn có trải nghiệm mượt mà nhất có thể, vì tương tác trên giao diện người dùng luôn được ưu tiên.

Coroutine sử dụng các hàm tạm ngưng để tạo mã không đồng bộ theo tuần tự.

Từ khoá suspend là cách Kotlin đánh dấu một hàm hoặc loại hàm là có sẵn cho các coroutine. Khi một coroutine gọi một hàm được đánh dấu bằng suspend, thay vì chặn cho đến khi hàm đó trả về như một lệnh gọi hàm thông thường, coroutine sẽ tạm ngưng việc thực thi cho đến khi kết quả sẵn sàng. Sau đó, coroutine sẽ tiếp tục từ nơi dừng lại, kèm theo kết quả.

Trong khi coroutine bị tạm ngưng và chờ kết quả, hệ thống sẽ bỏ chặn luồng mà coroutine đó đang chạy. Bằng cách đó, các hàm hoặc coroutine khác có thể chạy.

Từ khoá suspend không chỉ định luồng mà mã chạy trên đó. Một hàm tạm ngưng có thể chạy trên một luồng trong nền hoặc trên luồng chính.

Để sử dụng coroutine trong Kotlin, bạn cần có 3 thứ:

  • Một công việc
  • Người điều phối
  • Một phạm vi

Job: Về cơ bản, job là mọi thứ có thể bị huỷ. Mỗi coroutine đều có một tác vụ và bạn có thể dùng tác vụ đó để huỷ coroutine. Bạn có thể sắp xếp các tác vụ theo hệ phân cấp mẹ con. Việc huỷ một tác vụ mẹ sẽ huỷ ngay lập tức tất cả các tác vụ con của tác vụ đó, thuận tiện hơn nhiều so với việc huỷ từng coroutine theo cách thủ công.

Trình điều phối: Trình điều phối gửi các coroutine để chạy trên nhiều luồng. Ví dụ: Dispatcher.Main chạy các tác vụ trên luồng chính và Dispatcher.IO giảm tải các tác vụ I/O chặn sang một nhóm luồng được chia sẻ.

Phạm vi: Phạm vi của một coroutine xác định ngữ cảnh mà coroutine đó chạy. Phạm vi kết hợp thông tin về tác vụ và trình điều phối của một coroutine. Các phạm vi theo dõi coroutine. Khi khởi chạy một coroutine, coroutine đó sẽ nằm "trong một phạm vi", tức là bạn đã chỉ định phạm vi nào sẽ theo dõi coroutine đó.

Bạn muốn người dùng có thể tương tác với dữ liệu về giấc ngủ theo những cách sau:

  • Khi người dùng nhấn vào nút Start (Bắt đầu), ứng dụng sẽ tạo một đêm ngủ mới và lưu trữ đêm ngủ đó trong cơ sở dữ liệu.
  • Khi người dùng nhấn vào nút Dừng, ứng dụng sẽ cập nhật thời gian kết thúc cho đêm.
  • Khi người dùng nhấn vào nút Clear (Xoá), ứng dụng sẽ xoá dữ liệu trong cơ sở dữ liệu.

Các thao tác với cơ sở dữ liệu này có thể mất nhiều thời gian nên sẽ chạy trên một luồng riêng.

Bước 1: Thiết lập các coroutine cho các thao tác trên cơ sở dữ liệu

Khi người dùng nhấn vào nút Start (Bắt đầu) trong ứng dụng Sleep Tracker, bạn muốn gọi một hàm trong SleepTrackerViewModel để tạo một thực thể mới của SleepNight và lưu trữ thực thể đó trong cơ sở dữ liệu.

Khi bạn nhấn vào một trong các nút này, một thao tác cơ sở dữ liệu sẽ được kích hoạt, chẳng hạn như tạo hoặc cập nhật một SleepNight. Vì lý do này và những lý do khác, bạn sử dụng các coroutine để triển khai trình xử lý lượt nhấp cho các nút của ứng dụng.

  1. Mở tệp build.gradle ở cấp ứng dụng và tìm các phần phụ thuộc cho các coroutine. Để sử dụng coroutine, bạn cần những phần phụ thuộc này (đã được thêm cho bạn).

    $coroutine_version được xác định trong tệp build.gradle của dự án dưới dạng coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Mở tệp SleepTrackerViewModel.
  2. Trong phần nội dung của lớp, hãy xác định viewModelJob và gán cho lớp này một thực thể của Job. viewModelJob này cho phép bạn huỷ tất cả các coroutine do mô hình hiển thị này khởi động khi mô hình hiển thị không còn được dùng nữa và bị huỷ. Bằng cách này, bạn sẽ không gặp phải trường hợp các coroutine không có nơi nào để quay lại.
private var viewModelJob = Job()
  1. Ở cuối phần nội dung của lớp, hãy ghi đè onCleared() và huỷ tất cả các coroutine. Khi ViewModel bị huỷ, onCleared() sẽ được gọi.
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Ngay bên dưới định nghĩa của viewModelJob, hãy xác định một uiScope cho các coroutine. Phạm vi xác định luồng mà coroutine sẽ chạy và phạm vi cũng cần biết về công việc. Để lấy một phạm vi, hãy yêu cầu một thực thể của CoroutineScope, rồi truyền vào một điều phối viên và một công việc.

Việc sử dụng Dispatchers.Main có nghĩa là các coroutine được khởi chạy trong uiScope sẽ chạy trên luồng chính. Điều này hợp lý đối với nhiều coroutine do ViewModel khởi chạy, vì sau khi thực hiện một số quy trình xử lý, các coroutine này sẽ dẫn đến việc cập nhật giao diện người dùng.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Bên dưới định nghĩa của uiScope, hãy xác định một biến có tên là tonight để lưu giữ đêm hiện tại. Tạo biến MutableLiveData vì bạn cần có thể quan sát dữ liệu và thay đổi dữ liệu đó.
private var tonight = MutableLiveData<SleepNight?>()
  1. Để khởi chạy biến tonight càng sớm càng tốt, hãy tạo một khối init bên dưới định nghĩa của tonight và gọi initializeTonight(). Bạn sẽ xác định initializeTonight() trong bước tiếp theo.
init {
   initializeTonight()
}
  1. Bên dưới khối init, hãy triển khai initializeTonight(). Trong uiScope, hãy chạy một coroutine. Bên trong, hãy lấy giá trị cho tonight từ cơ sở dữ liệu bằng cách gọi getTonightFromDatabase() rồi chỉ định giá trị cho tonight.value. Bạn sẽ xác định getTonightFromDatabase() trong bước tiếp theo.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Triển khai getTonightFromDatabase(). Xác định hàm này là một hàm private suspend trả về SleepNight có giá trị rỗng, nếu không có SleepNight nào đang bắt đầu. Việc này sẽ khiến bạn gặp lỗi vì hàm phải trả về một giá trị.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. Bên trong phần nội dung hàm của getTonightFromDatabase(), hãy trả về kết quả từ một coroutine chạy trong ngữ cảnh Dispatchers.IO. Sử dụng trình điều phối I/O, vì việc lấy dữ liệu từ cơ sở dữ liệu là một thao tác I/O và không liên quan gì đến giao diện người dùng.
  return withContext(Dispatchers.IO) {}
  1. Bên trong khối trả về, hãy để coroutine lấy đêm nay (đêm mới nhất) từ cơ sở dữ liệu. Nếu thời gian bắt đầu và thời gian kết thúc không giống nhau, tức là đêm đã kết thúc, hãy trả về null. Nếu không, hãy trả về đêm.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

Hàm tạm ngưng getTonightFromDatabase() hoàn chỉnh sẽ có dạng như sau. Sẽ không còn lỗi nào nữa.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Bước 2: Thêm trình xử lý lượt nhấp cho nút Bắt đầu

Bây giờ, bạn có thể triển khai onStartTracking(), trình xử lý lượt nhấp cho nút Start (Bắt đầu). Bạn cần tạo một SleepNight mới, chèn SleepNight đó vào cơ sở dữ liệu và chỉ định SleepNight đó cho tonight. Cấu trúc của onStartTracking() sẽ rất giống với initializeTonight().

  1. Bắt đầu bằng định nghĩa hàm cho onStartTracking(). Bạn có thể đặt trình xử lý lượt nhấp ở trên onCleared() trong tệp SleepTrackerViewModel.
fun onStartTracking() {}
  1. Bên trong onStartTracking(), hãy chạy một coroutine trong uiScope, vì bạn cần kết quả này để tiếp tục và cập nhật giao diện người dùng.
uiScope.launch {}
  1. Trong quá trình khởi chạy coroutine, hãy tạo một SleepNight mới. Coroutine này sẽ ghi lại thời gian hiện tại làm thời gian bắt đầu.
        val newNight = SleepNight()
  1. Vẫn bên trong lệnh khởi chạy coroutine, hãy gọi insert() để chèn newNight vào cơ sở dữ liệu. Bạn sẽ thấy lỗi vì bạn chưa xác định hàm tạm ngưng insert() này. (Đây không phải là hàm DAO có cùng tên.)
       insert(newNight)
  1. Ngoài ra, trong quá trình khởi chạy coroutine, hãy cập nhật tonight.
       tonight.value = getTonightFromDatabase()
  1. Bên dưới onStartTracking(), hãy xác định insert() là một hàm private suspend lấy SleepNight làm đối số.
private suspend fun insert(night: SleepNight) {}
  1. Đối với nội dung của insert(), hãy khởi chạy một coroutine trong ngữ cảnh I/O và chèn đêm vào cơ sở dữ liệu bằng cách gọi insert() từ DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. Trong tệp bố cục fragment_sleep_tracker.xml, hãy thêm trình xử lý lượt nhấp cho onStartTracking() vào start_button bằng cách sử dụng tính năng liên kết dữ liệu mà bạn đã thiết lập trước đó. Ký hiệu hàm @{() -> tạo một hàm lambda không nhận đối số và gọi trình xử lý lượt nhấp trong sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Tạo bản dựng và chạy ứng dụng của bạn. Nhấn vào nút Start (Bắt đầu). Thao tác này sẽ tạo dữ liệu, nhưng bạn chưa thể xem nội dung nào. Bạn sẽ khắc phục lỗi này trong bước tiếp theo.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Bước 3: Hiển thị dữ liệu

Trong SleepTrackerViewModel, biến nights tham chiếu đến LiveDatagetAllNights() trong DAO trả về LiveData.

Đây là một tính năng Room. Mỗi khi dữ liệu trong cơ sở dữ liệu thay đổi, LiveData nights sẽ được cập nhật để cho thấy dữ liệu mới nhất. Bạn không bao giờ cần đặt LiveData một cách rõ ràng hoặc cập nhật LiveData. Room cập nhật dữ liệu cho khớp với cơ sở dữ liệu.

Tuy nhiên, nếu bạn hiển thị nights trong một khung hiển thị văn bản, thì khung hiển thị đó sẽ cho thấy thông tin tham chiếu đối tượng. Để xem nội dung của đối tượng, hãy chuyển đổi dữ liệu thành một chuỗi có định dạng. Sử dụng một bản đồ Transformation được thực thi mỗi khi nights nhận được dữ liệu mới từ cơ sở dữ liệu.

  1. Mở tệp Util.kt rồi bỏ chú thích mã cho định nghĩa của formatNights() và các câu lệnh import được liên kết. Để huỷ chú thích mã trong Android Studio, hãy chọn tất cả mã được đánh dấu bằng // rồi nhấn Cmd+/ hoặc Control+/.
  2. Lưu ý rằng formatNights() trả về một loại Spanned, là một chuỗi được định dạng HTML.
  3. Mở strings.xml. Hãy lưu ý cách sử dụng CDATA để định dạng tài nguyên chuỗi nhằm hiển thị dữ liệu về giấc ngủ.
  4. Mở SleepTrackerViewModel. Trong lớp SleepTrackerViewModel, bên dưới định nghĩa của uiScope, hãy xác định một biến có tên là nights. Lấy tất cả các đêm từ cơ sở dữ liệu và chỉ định các đêm đó cho biến nights.
private val nights = database.getAllNights()
  1. Ngay bên dưới định nghĩa của nights, hãy thêm mã để chuyển đổi nights thành nightsString. Sử dụng hàm formatNights() từ Util.kt.

    Truyền nights vào hàm map() từ lớp Transformations. Để truy cập vào tài nguyên chuỗi, hãy xác định hàm ánh xạ là gọi formatNights(). Cung cấp nights và một đối tượng Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Mở tệp bố cục fragment_sleep_tracker.xml. Trong TextView, trong thuộc tính android:text, giờ đây, bạn có thể thay thế chuỗi tài nguyên bằng một tham chiếu đến nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Tạo lại mã và chạy ứng dụng. Giờ đây, tất cả dữ liệu về giấc ngủ của bạn cùng với thời gian bắt đầu sẽ hiển thị.
  2. Nhấn nút Bắt đầu thêm vài lần nữa để xem thêm dữ liệu.

Trong bước tiếp theo, bạn sẽ bật chức năng cho nút Stop (Dừng).

Bước 4: Thêm trình xử lý lượt nhấp cho nút Dừng

Sử dụng mẫu tương tự như trong bước trước, hãy triển khai trình xử lý lượt nhấp cho nút Stop (Dừng) trong SleepTrackerViewModel.

  1. Thêm onStopTracking() vào ViewModel. Chạy một coroutine trong uiScope. Nếu bạn chưa đặt thời gian kết thúc, hãy đặt endTimeMilli thành thời gian hiện tại của hệ thống và gọi update() bằng dữ liệu ban đêm.

    Trong Kotlin, cú pháp return@label chỉ định hàm mà câu lệnh này trả về, trong số một số hàm lồng nhau.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Triển khai update() bằng cách sử dụng cùng một mẫu mà bạn đã dùng để triển khai insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Để kết nối trình xử lý lượt nhấp với giao diện người dùng, hãy mở tệp bố cục fragment_sleep_tracker.xml rồi thêm trình xử lý lượt nhấp vào stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Tạo và chạy ứng dụng của bạn.
  2. Nhấn vào Bắt đầu, rồi nhấn vào Dừng. Bạn sẽ thấy thời gian bắt đầu, thời gian kết thúc, chất lượng giấc ngủ không có giá trị và thời gian ngủ.

Bước 5: Thêm trình xử lý lượt nhấp cho nút Xoá

  1. Tương tự, hãy triển khai onClear()clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Để kết nối trình xử lý lượt nhấp với giao diện người dùng, hãy mở fragment_sleep_tracker.xml rồi thêm trình xử lý lượt nhấp vào clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Tạo và chạy ứng dụng của bạn.
  2. Nhấn vào Xoá để xoá tất cả dữ liệu. Sau đó, hãy nhấn vào Bắt đầuDừng để tạo dữ liệu mới.

Dự án Android Studio: TrackMySleepQualityCoroutines

  • Sử dụng ViewModel, ViewModelFactory và tính năng liên kết dữ liệu để thiết lập cấu trúc giao diện người dùng cho ứng dụng.
  • Để đảm bảo giao diện người dùng hoạt động trơn tru, hãy sử dụng coroutine cho các thao tác dài hạn, chẳng hạn như tất cả thao tác với cơ sở dữ liệu.
  • Coroutine không đồng bộ và không chặn. Coroutine sử dụng các hàm suspend để tạo mã không đồng bộ theo tuần tự.
  • Khi một coroutine gọi một hàm được đánh dấu bằng suspend, thay vì chặn cho đến khi hàm đó trả về như một lệnh gọi hàm thông thường, hệ thống sẽ tạm ngưng thực thi cho đến khi kết quả sẵn sàng. Sau đó, nó tiếp tục từ nơi đã dừng lại với kết quả.
  • Sự khác biệt giữa chặntạm ngưng là nếu một luồng bị chặn, thì sẽ không có công việc nào khác diễn ra. Nếu luồng bị tạm ngưng, các công việc khác sẽ diễn ra cho đến khi có kết quả.

Để khởi chạy một coroutine, bạn cần có một tác vụ, một điều phối viên và một phạm vi:

  • Về cơ bản, job (công việc) là bất cứ thứ gì có thể bị huỷ. Mỗi coroutine đều có một tác vụ và bạn có thể dùng tác vụ để huỷ coroutine.
  • Trình điều phối gửi các coroutine để chạy trên nhiều luồng. Dispatcher.Main chạy các tác vụ trên luồng chính và Dispartcher.IO dùng để giảm tải các tác vụ I/O chặn đến một nhóm luồng được chia sẻ.
  • Phạm vi kết hợp thông tin, bao gồm cả tác vụ và trình điều phối, để xác định ngữ cảnh mà coroutine chạy. Các phạm vi theo dõi coroutine.

Để triển khai trình xử lý lượt nhấp kích hoạt các thao tác trên cơ sở dữ liệu, hãy làm theo mẫu sau:

  1. Chạy một coroutine trên luồng chính hoặc luồng giao diện người dùng, vì kết quả ảnh hưởng đến giao diện người dùng.
  2. Gọi một hàm tạm ngưng để thực hiện công việc chạy trong thời gian dài, nhờ đó bạn không chặn luồng giao diện người dùng trong khi chờ kết quả.
  3. Tác vụ chạy trong thời gian dài không liên quan đến giao diện người dùng, vì vậy, hãy chuyển sang ngữ cảnh I/O. Bằng cách đó, công việc có thể chạy trong một nhóm luồng được tối ưu hoá và dành riêng cho các loại thao tác này.
  4. Sau đó, hãy gọi hàm cơ sở dữ liệu để thực hiện công việc.

Sử dụng bản đồ Transformations để tạo một chuỗi từ đối tượng LiveData mỗi khi đối tượng thay đổi.

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Các tài liệu và bài viết khác:

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Trả lời những câu hỏi này

Câu hỏi 1

Đâu là lợi ích của coroutine:

  • Chúng không chặn
  • Chạy không đồng bộ.
  • Có thể chạy trên một luồng không phải là luồng chính.
  • Luôn giúp ứng dụng chạy nhanh hơn.
  • Có thể sử dụng các ngoại lệ.
  • Có thể viết và đọc như mã tuyến tính.

Câu hỏi 2

Hàm tạm ngưng là gì?

  • Một hàm thông thường được chú thích bằng từ khoá suspend.
  • Hàm có thể được gọi bên trong coroutine.
  • Khi một hàm tạm ngưng đang chạy, luồng lệnh gọi sẽ bị tạm ngưng.
  • Các hàm tạm ngưng phải luôn chạy ở chế độ nền.

Câu hỏi 3

Chặn và tạm ngưng một chuỗi có gì khác nhau? Đánh dấu tất cả những lựa chọn đúng.

  • Khi quá trình thực thi bị chặn, không thể thực thi công việc nào khác trên luồng bị chặn.
  • Khi quá trình thực thi bị tạm ngưng, chuỗi có thể thực hiện công việc khác trong khi chờ hoàn tất công việc giảm tải.
  • Việc tạm ngưng sẽ hiệu quả hơn vì các luồng có thể sẽ không chờ đợi mà không làm gì cả.
  • Cho dù bị chặn hay bị tạm ngưng, quá trình thực thi vẫn đang chờ kết quả của coroutine trước khi tiếp tục.

Chuyển sang bài học tiếp theo: 6.3 Sử dụng LiveData để kiểm soát trạng thái của nút

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.