Giới thiệu
Trước khi đi vào các khái niệm thực tế, trước tiên chúng ta hãy hiểu vấn đề mà chúng ta sẽ giải quyết. Quản lý trạng thái (State management) là một trong những khía cạnh quan trọng của bất kỳ nền tảng nào khi đang ở giai đoạn phát triển sản phẩm.
Giả sử bạn có màn hình thanh toán với nhiều cổng thanh toán: visa, ví momo, zalo pay . Người dùng có thể thực hiện thanh toán bằng bất kỳ cổng nào. Sau khi thanh toán, sẽ có một cửa sổ bật lên xác nhận cho biết trạng thái mua hàng.
Vấn đề này tưởng chừng như đơn giản nhưng nó ngày càng phức tạp theo thời gian với nhiều cổng có nhiều trạng thái. Điều này đã được giải quyết bởi các sealed classes .
Tuy nhiên thì không phải mọi vấn đề đều có độ phức tạp như nhau, một request network đơn giản có thể chỉ có hai trạng thái như thành công hoặc thất bại. Đó là lúc chúng ta cần giải pháp đơn giản hơn, tốt nhất là thứ gì đã có từ chính ngôn ngữ. Đó là những gì chúng ta sẽ tìm hiểu trong bài viết này.
Túm cái váy lại :
Problem: Tìm một giải pháp đơn giản để quản lý state mà không tạo ra những module mới
Solution: Sử dụng runCatching và its khả năng của nó với Result sealed class trong bộ thư viện chuẩn của Kotlin.
Điều kiện tiên quyết
Trước khi đi xa hơn, mình thực sự khuyên bạn nên có một chút kiến thức về các sealed classes và coroutines. Vấn đề và giải pháp trong bài viết này dựa trên chúng. Nếu chưa biết từ đầu thì bạn có thể tham khảo 2 bài viết này hay bất cứ bài viết nào trên mạng :
- “How to Use Kotlin Sealed Classes for State Management”
- “Kotlin Coroutines, From the Basic to the Advanced”
Quay trở lại với vấn đề ban đầu,
Bây giờ, hãy xem một ví dụ đơn giản về cách sử dụng các sealed class. Đây là ví dụ để duy trì các trạng thái khi một network request được khởi tạo. Ở đây có hai trạng thái: Loading và Error.
Loading
: Trạng thái này trả về một giá trị boolean nếu quá trình tải thực sự xuất hiện - nếu không, nó sẽ bị ẩn.
Error
: Trạng thái này trả về một thông báo lỗi dưới dạng một string. Khi lỗi được kích hoạt, quá trình tải sẽ bị ẩn và cửa sổ bật lên có thông báo lỗi sẽ xuất hiện.
sealed class NetworkStatus { data class Loading(var loading : Boolean) : NetworkStatus() data class Error(var errorMsg : String) : NetworkStatus()
}
Bây giờ, ở phía bên kia, chúng ta phải sử dụng câu lệnh when trong Kotlin để xử lý các trạng thái khác nhau:
when (networkStatus) { is NetworkStatus.Loading -> { loadingStatus(networkStatus.loading) } is NetworkStatus.Error ->{ hideLoading() showError(networkStatus.errorMsg) }
}
Tất nhiên nhiều khi sẽ còn phức tạp hơn nữa :
sealed class PrchaseResult { sealed class GoogplayBilling: PrchaseResult(){ data class Sucess(val purchseToken : String) : GoogplayBilling() data class Fail(val error : String) : GoogplayBilling() object Retry : GoogplayBilling() sealed class PurchaseAcknowledgement : GoogplayBilling(){ object Sucess : PurchaseAcknowledgement() data class Fail(val errorCode : Int) : PurchaseAcknowledgement() } } sealed class Stripe: PrchaseResult(){ data class Sucess(val purchseToken : String) : Stripe() data class Fail(val error : String) : Stripe() object Retry : Stripe() sealed class PurchaseAcknowledgement : Stripe(){ object Sucess : PurchaseAcknowledgement() data class Fail(val errorCode : Int) : PurchaseAcknowledgement() } } sealed class PayYouMoney: PrchaseResult(){ data class Sucess(val purchseToken : String) : PayYouMoney() data class Fail(val error : String) : PayYouMoney() object Retry : PayYouMoney() sealed class PurchaseAcknowledgement : PayYouMoney(){ object Sucess : PurchaseAcknowledgement() data class Fail(val errorCode : Int) : PurchaseAcknowledgement() } } }
Đó là cách mà ta sẽ xử lý khi gặp mấy case thanh toán rắc rối ở trên. Tất nhiên là các project lớn sẽ vậy, vậy còn các dự án nhỏ , việc tạo ra qua nhiều thứ như vậy là không cần thiết khi ta đã có những thứ dưới đây:
Result & runCatching
Trước khi đi sâu vào giải pháp cho vấn đề bên trên, hãy dành một chút thời gian và tìm hiểu về những gì chúng ta đang sử dụng tại đây.
Result
Result
không gì khác ngoài một generic sealed class mà chúng ta thường tạo trong các dự án của mình để duy trì trạng thái. Nhưng Result đi kèm với bộ thư viện tiêu chuẩn, nghĩa là chúng ta không cần phải viết các lớp tùy chỉnh của riêng mình, rất tiện lợi phải không! Không cần phải thêm nếm gì cả. Đừng phát minh ra cái bánh xe trong khi chúng ta có thể sử dụng nó luôn rồi . Nếu bạn muốn tìm hiểu sâu về Result, hãy nhấp vào đây. Về cơ bản, nó phục vụ như một sealed class bằng cách cung cấp các lệnh gọi lại onSuccess
và onFailure
.
runCatching
runCatching
gọi khối chức năng được chỉ định và trả về kết quả được đóng gói của nó nếu lệnh gọi thành công, bắt bất kỳ ngoại lệ Throwable
nào được ném ra khỏi quá trình thực thi và đóng gói nó như là một failure
. Xem ví dụ sau :
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R> runCatching(block: () -> R): Result<R> { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) }
}
Solution
Bây giờ chúng ta đã có sẵn các chức năng tiện dụng, đã đến lúc đưa chúng vào hoạt động. Vấn đề của chúng ta là thực hiện một network request với quản lý trạng thái đơn giản bằng cách sử dụng Result
và runCatching
.
Chúng ta biết rằng runCatching
lấy một hàm chức năng Kotlin làm đầu vào và trả về kết quả của nó ở định dạng Kết quả dựa trên trạng thái thành công của nó. Trong trường hợp của chúng ta là một network request. Hãy xem:
fun simpleFunctionInViewmodel(param1 : String, param2 : Int) { viewModelScope.launch { val result = kotlin.runCatching { simpleRepo.simpleNwtorkRequest(param1, param2) }.onSuccess { successData: String -> }.onFailure { exception: Throwable -> } }
}
Trông có đơn giản không nào , không có try/ catch để xử lý lỗi, không có các lớp tùy chỉnh quản lý trạng thái thủ công. runCatching and Result cung cấp nhiều hơn một giải pháp đơn giản mà bạn sẽ tìm hiểu trong phần tiếp theo.
Extra Benefits
fold
Một trong những thứ thích nhất khi sử dụng Result là sử dụng fold
inline function , hàm này đóng gói sucess và failure dưới dạng các tham số và cho chúng ta cơ hội có kiểu trả về nhất quán. Hãy xem phiên bản thực thi mã khác trong phần giải pháp:
fun simpleFunctionInViewmodel(param1 : String, param2 : Int) { viewModelScope.launch { val result = kotlin.runCatching { simpleRepo.simpleNwtorkRequest(param1, param2) }.fold( onSuccess = { successData: String -> }, onFailure = { exception: Throwable -> } ) }
}
recover
recover
cho phép bạn xử lý lỗi và khôi phục từ đó với giá trị dự phòng của cùng một kiểu dữ liệu. Xem ví dụ sau.
fun simpleFunctionInViewmodel(param1 : String, param2 : Int) { viewModelScope.launch { val result = kotlin.runCatching { simpleRepo.simpleNwtorkRequest(param1, param2) } .onSuccess { } .onFailure { } .recover { error: Throwable -> "COMMON_STATUS" } }
}
Ngoài những chức năng này, có một số chức năng thú vị và tiện dụng hơn như map
, mapCatching
, getOrDefault
, getOrElse
và hơn thế nữa. Bạn có thể tham khảo tất cả về chúng từ đây.
runCatching như một chức năng mở rộng
Hàm runCatching có khả năng hoạt động như một hàm mở rộng chung cùng với hàm nội tuyến bình thường. Hãy xem cú pháp:
@InlineOnly
@SinceKotlin("1.3")
public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) }
}
Với khả năng này, chúng ta có thể gọi trực tiếp runCatching theo request mà không cần phải thực hiện riêng lẻ:
fun simpleFunctionInViewmodel(param1 : String, param2 : Int) { viewModelScope.launch { val result = simpleRepo.simpleNetworkRequest(param1, param2).runCatching { } .recover { error: Throwable -> "COMMON_STATUS" } }
}
Bài viết lần này được dịch từ https://medium.com/android-dev-hacks/simple-state-management-in-kotlin-6d1d5e41e4e8. Hy vọng mọi người hứng thú
Xin cảm ơn!