Kotlin Coroutine

Mobile Dec 22, 2020

Chào các bạn, hôm nay mình sẽ giới thiệu cho mọi người một chủ đề đang rất nổi trong lập trình Android là Kotlin coroutines. Như chúng ta được biết Kotlin là một ngôn ngữ được ra mắt bởi JetBrains năm 2011, được phát hành vào năm 2014 và được Google support cho lập trình Android năm 2017. Để giải quyết khó khăn cho việc viết mã code bất đồng bộ (asynchronous code) Jetbrains đã giới thiệu Kotlin Coroutines.

1. Đầu tiên chúng ta tìm hiểu Asynchronous Programing (Lập trình bất đồng bộ trong Android là như thế nào )

- Trong Android chia làm 2 luồng chính gồm Main Thread (hay còn gọi là UI Thread)  và Background Thread. Trong đó Main Thread là luồng xử lý những tác vụ liên quan đến giao diện người dùng như hiển thị ảnh, hiển thị text, hiển thị progress bar,... Tuy nhiên nếu ta xử lý những tác vụ tốn nhiều thời gian như đọc file, request network trên main thread sẽ dẫn đến việc app bị đơ, và gặp hiện tượng ứng dụng không phản hồi.  Nguyên nhân là do trên main thread phải đợi tác vụ tốn nhiều thời gian đó xử lý xong mới thực hiện những yêu cầu tiếp theo. Để xử lý việc này chúng ta sẽ thực hiện những công việc như đọc file, request network trên 1 luồng khác gọi là background thread và cập nhật kết quả trả về trên main thread.





















Một số công cụ hỗ trợ cho lập trình bất đồng bộ có thể kể đến như AsyncTask, RxAndroid,...

* Giới thiệu về AsyncTask (Là 1 công cụ có sẵn trên Android, bạn không cần phải import bất cứ thư viện gì )

Ví dụ chúng ta xử lý 1 yêu cầu get api từ 1 url


Đoạn code trên giúp ta xử lý công việc trên. Đầu tiên chúng ta khai báo một class extend AsyncTask với 3 tham số truyền vào như trên hình là String, Integer, String. Trong đó giá trị đầu tiên là tham số chúng ta truyền vào trong hàm execute , giá trị thứ 2 (Integer) là giá trị chúng ta sử dụng trong hàm progress update và giá trị cuối cùng là kết quả trả về trong hàm doInBackground và được sử dụng trong hàm onPostExecute. Ở đây 3 hàm onPreExecute, onProgressUpdate và onPostExecute đều được xử lý trên main thread và doInBackGround được xử lý trên background thread.


Ưu điểm của việc xử lý AsyncTask là chúng ta không cần sử dụng thêm thư viện, code dễ đọc, dễ hiểu. Tuy nhiên trong bài toán kết hợp 2 công việc chạy song song, việc xử lý sẽ phức tạp làm dài đoạn code của chúng ta. Một điều quan trọng nữa là từ Android 11 trở đi, AsyncTask đã bị deprecated

* Sử dụng RxAndroid

Đã từ lâu các developer android đã coi RX là thư viện không thể thiếu trong mỗi project của mình với nhiều operator hỗ trợ trong việc xử lý đa luồng.



Đoạn code trên minh họa việc get api từ url cùng với những tác vụ update lên main thread như showProgress trong quá trình xử lý hay hideProgress và showResult khi hoàn thành xong. Rõ ràng là code ngắn và đẹp hơn rất nhiều so với asynctask. Không những vậy Rx cũng hỗ trợ việc xử lý song song hay kết hợp 2 công việc chạy song song với nhau với những operation như merge hay zip. Tuy vậy với một người bắt đầu tìm hiểu về Rx thì sẽ mất thời gian để hiểu hết những operation của nó.

Do đó trong bài viết này mình sẽ giới thiệu đến mọi người công cụ đang rất nổi trong thời gian gần đây là Kotlin Coroutines.

KOTLIN COROUTINES :

Coroutines nghĩa là kết hợp các công việc với nhau, nó không phải là một khái niệm mới và được áp dụng trong nhiều ngôn ngữ khác như Golang,..





Theo như documents https://kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html

Coroutines là một light-weight thread. Giống như thread, coroutine cũng có thể chạy tuần tự, song song hay kết hợp với nhau. Khác biệt lớn nhất giữa coroutine với thread là chúng rất "rẻ" (ngốn ít ram hơn) . Chúng ta có thể tạo ra hàng nghìn coroutine và cho chúng hoạt động mà không gặp trở ngại gì so với việc tạo ra hàng nghìn thread.

Cấu trúc 1 đoạn code sử dụng Kotlin coroutines


Nhìn đoạn code khá dễ hiểu và tuần tự phải không? Sau đây chúng ta sẽ bắt đầu đi tìm hiểu nó.

(Lưu ý là kotlin coroutines được jetbrain hỗ trợ luôn từ bản kotlin 1.3 trở lên do đó bạn không cần phải import bất cứ thư viện gì)

  1. Suspend function

Đầu tiên chúng ta hãy so sánh giữa suspend và blocking

  • Block : Lấy ví dụ trong hình ảnh dưới đây Function B chỉ được gọi sau khi Main Function được xử lý xong và tất cả chúng được thực thi trên main thread. Điều này có nghĩa là các dòng lệnh phía sau sẽ chỉ được thực hiện khi dòng lệnh phía trước thực thi xong.

Điều này có một nhược điểm lớn với những công việc xử lý mất nhiều thời gian như đọc file, get network sẽ xảy đến tình trạng main thread bị đơ một lúc chỉ đến khi công việc đó hoàn thành xong.

  • Suspend : Function B khi được khởi tạo được tạm ngừng (suspend) trên main thread nhưng chúng ta có thể tiếp tục sử dụng nó trên bất kỳ luồng nào khác.

Một suspend function chỉ được gọi từ một suspend function khác hoặc từ 1 coroutine scope.

Lấy ví dụ chúng ta có 1 hàm lấy thông tin user từ network. Mọi người đừng quan tâm vào những thứ như CoroutineScope, Dispatchers, async, await hay launch. Lát nữa mình sẽ giải thích rõ. Chúng ta hãy tập trung vào suspend.

private suspend fun fetchUser(): User = CoroutineScope(Dispatchers.IO).async {

return@async User()

}.await()

Ok giờ mình có hàm fetchUser trả về 1 user, việc tiếp theo mình muốn làm là muốn sử dụng user này để hiển thị lên main thread. Giả sử mình sẽ viết function hiển thị user là showUser. Điều bắt buộc là bạn phải khai báo hàm showUser với từ khóa suspend như sau:

private suspend fun showUser() {

val user = fetchUser()

Toast.makeText(this, "User", Toast.LENGTH_SHORT).show()

}

Chắc chắn nếu bạn khai báo showUser mà không có suspend, Android studio sẽ hiện cảnh báo đỏ để bạn hiểu.

Được rồi bây giờ chúng ta đã có function showUser (cũng là suspend function) . vậy ta gọi showUser ở đâu. Dù sao đích đến cuối cùng của hàm suspend cũng cần phải viết trong hàm onCreate. Ta sử dụng Coroutine Scope

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

CoroutineScope(Dispatchers.Main).launch {

showUser()

}

}

Coroutine Scope giúp chúng ta định nghĩa vòng đời của một Kotlin coroutines

  1. Coroutine Context

Đây được coi là phần core của mỗi coroutine. Nơi mà bạn chỉ định threadpool xử lý, quản lý vòng đời hay xử lý exception.

Về cơ bản Coroutine Context =(Dispatchers + Job + Exception)

  • Dispatchers : Xác định thread nào mà coroutine chạy trên đó. Chúng ta có các loại dispatcher sau

- Dispatchers.Main : Chạy trên main Thread , xử lý những nhiệm vụ liên quan đến view

- Dispatchers.IO : Chạy trên background thread, xử lý những nhiệm vụ như đọc ghi file, kết nối internet.

- Dispatchers.Default : Chạy trên background thread, xử lý những công việc tiêu tốn CPU như sắp xếp list, parse json,..

Nếu các bạn đã làm quen với Rx thì thấy Dispatchers này cũng tương tự với AndroidScheduler.mainThread, Schedulers.IO hay Schedulers.computation.

  • Job : Quản lý lifecycle của coroutine

Hãy thử tưởng tượng nếu bạn đang ở 1 màn hình và có truy cập network để lấy dữ liệu nhưng do mạng kém, quá trình xử lý lâu, bạn chán phải chờ đợi và muốn di chuyển sang màn hình khác. Câu hỏi đặt ra là function get network của bạn có hủy sau khi view của bạn bị hủy không.

Câu trả lời đương nhiên là không, nó sẽ chạy trên background thread trả về kết quả để cập nhật lên view .Nhưng vấn đề ở đây là view của bạn đã bị destroy sau khi bạn di chuyển rồi -> app sẽ dẫn đến bị crash. Điều này đòi hỏi bạn phải xử lý hủy function get network sau khi view bị destroy. Job trong kotlin coroutine hỗ trợ chúng ta giải quyết việc này.

Bạn có thể khai báo job trong coroutine context bằng toán tử + (plus) như sau

val job =Job()

CoroutineScope(Dispatchers.Main + job).launch {

//action

}

Sau đó trong onDestroy chúng ta hủy coroutine bằng cách gọi job.cancel(). Điều này đảm bảo công việc bạn đang thực thi trên coroutine bị hủy bỏ

override fun onDestroy() {

super.onDestroy()

job.cancel()

}

Tuy nhiên nếu bạn muốn ứng dụng của mình có 1 function thực hiện chạy xuyên suốt theo vòng đời của app có thể sử dụng GlobalScope. Điểm khác biệt của GlobalScope như cái tên của nó - Global ( toàn cầu ) là nó sẽ không tự hủy (cancel được ) chúng ta không thể gọi job. cancel() như với những scope thông thường. Cách khai báo

GlobalScope.launch {

//action

}

Ngoài ra job có method là join() với mục đích là đợi coroutine chạy xong task của mình rồi mới chạy tiếp .

  • Exception Handler

Trong Coroutine nếu muốn xử lý exception được ném ra chúng ta sử dụng CoroutineExceptionHandler

val exceptionHandler = CoroutineExceptionHandler { _, exception ->

handleException(exception)

}

CoroutineScope(Dispatchers.Main + job + exceptionHandler).launch {

}

Khi một coroutine con xảy ra exception thì các coroutine khác cũng sẽ bị stop. Nếu chúng ta muốn khi một coroutine xảy ra exception, các coroutine khác vẫn tiếp tục chạy thì nên sử dụng SupervisorJob() thay vì Job()

  1. Coroutine Builder

Việc khởi tạo một coroutine ta dùng launch{} và async{} .

Trong đó launch thì không trả về giá trị còn async thì trả về một Deferred Instance của một object có thể là String , Integer hay Object bất kỳ bạn định nghĩa. Và để get giá trị này bạn cần sử dụng hàm await() .


Bạn hãy nhìn đoạn code sử dụng async() trong hình vẽ trên. Ở đây mình sử dụng 2 api để get 2 user từ server. Tất nhiên mình thực thi nó trên background thread với thread pool là Dispatchers.IO và sau khi thực thi xong sẽ được hiển thị trên main thread. Nhìn code khá là tuần tự từ trên xuống dưới nhưng thực tế là hai function fetchFirstUser và fetchSecondUser được thực hiện song song với nhau và khi cả 2 cùng hoàn thành sẽ được hiển thị trên main thread .

Một thứ nữa mà mình muốn giới thiệu với các bạn là withContext()

withContext là 1 suspend function do đó chúng ta chỉ có thể gọi nó từ 1 suspend fuction hay từ 1 coroutine scope

suspend fun fetchUser(): User = withContext(Dispatchers.IO) {

//make net work call

//return user

return@withContext User()

}

Ta có thể hiểu được là withContext =async{}.await(). Điểm khác biệt lớn nhất là withContext sẽ chạy các đoạn code tuần tự mà không song song như async. Lấy ví dụ với 2 đoạn code dưới đây .


Bạn có thể thấy đoạn code bên trái là sử dụng withContext để lấy thông tin user còn đoạn code bên phải sử dụng async. Tất nhiên với cách sử dụng withContext firstUser sẽ được thực thi trước, sau khi hoàn thành mới tiếp tục thực thi lấy thông tin secondUser .

Một lưu ý khi sử dụng withContext là bạn hãy dùng nó khi chỉ trả về 1 kết quả duy nhất còn async là khi bạn muốn kết hợp nhiều kết quả trả về với nhau.

4. Sử dụng Coroutine trong ViewModel

Chúng ta được biết hiện nay mô hình MVVM (Model + View + ViewModel) là một mô hình phổ biến được sử dụng trong lập trình android, ở đó viewModel sẽ xử lý logic của ứng dụng.

Để code đúng chuẩn mô hình ta cần sử dụng Coroutine trong ViewModel. Rất đơn giản ta khai báo 1 class extend ViewModel trong class đó ta sử dụng viewModelScope đã được khởi tạo sẵn .

Chúng ta sử dụng nó giống với ví dụ mình đã thực hiện ở trên

class MainViewModel : ViewModel() {

fun main(){

viewModelScope.launch (Dispatchers.IO){

//action handle

}

}

Được rồi bài giới thiệu đến đây của mình là kết thúc. Tất nhiên đây là lần đầu viết bài nên còn nhiều sai sót, rất mong nhận được ý kiến và phản hồi của mọi người. Sắp tới nếu có thời gian mình sẽ trình bày thêm về Flow và Channel trong Coroutines đây là một phần khá hay. Cảm ơn mọi người đã theo dõi bài viết của mình  

Tài liệu tham khảo :

Tác giả: Phí Văn Tuấn











Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.