Đã bao giờ bạn phải xử lý một Dialog, Button hoặc 1 cái CardView design đặc biệt tràn đầy graphical art các kiểu con đà điểu chưa? Có thể bạn nhìn file design một lúc và export nguyên cái ảnh ra để rồi nhận ra nếu nhiều chữ hơn hoặc màn hình bé hơn to hơn -> ảnh vỡ, code của bạn tèo ?
không chỉ ở làng Mưa, mà trong file design cũng có pain…
Và rồi bạn đành phải work around, tự cắt các thành phần của một design ra nhiều phần nhỏ và dùng ImageView ghép lại theo định nghĩa phần có thể giãn và phần không cần giãn. Well, code bạn vẫn chạy ổn, nhưng chưa phải tối ưu.
Để tạo background cho những component mang tính graphical như vậy, chúng ta đã 9-patch image hỗ trợ.
9-patch Image là gì cơ?
9-patch image là drawable dạng ảnh png đặc biệt có thể co giãn theo một định nghĩa nằm trong file này. Loại drawable này thường được sử dụng để làm background cho các View, Android OS sẽ xử lý background dạng 9-patch co giãn theo định nghĩacủa file thay vì giãn đều như drawable bình thường. File 9-patch sẽ có đuôi 9.png. Btw, 9-patch là 9 mảnh vá đó ? (trích của anh Nguyen Van Toan)
Cấu trúc của ảnh 9-patch
Như ảnh bạn có thể thấy 9-patch image được chia làm 2 thành phần:
Stretchable area
Fill area (Padding box)
Stretchable area là phần mà ảnh có thể được giãn nếu cần thiết. Fill area là phần mà content của bạn được phép nằm trong đó (Vì thế nên còn gọi là Padding box đó hehe).
Ngoài ra thì 9-patch image còn hỗ trợ Optical Bounds. Optical bounds trên 9-patch image sẽ hiển thị bằng đường kẻ màu đỏ.
Như bạn thấy, khi định nghĩa chuẩn, View của bạn sẽ được co giãn một cách tối ưu.
Nghe hay phết, vậy tạo 9-patch như thế nào z bro?
Cách tạo 9-patch image
Để tạo ra 9-patch image, Android Studio đã có sẵn tool support đầy đủ, bạn không cần tải thêm bất cứ thứ gì khác. Ngoài ra thì cũng có cách khác là sử dụng Simple nine-patch generator trong bộ tools online của Roman Nurik.
Tạo bằng Android Studio
Bước 1: Chúng ta click chuột phải vào ảnh PNG gốc, chọn Create 9-Patch file…
Bước 2: bắt đầu tuỳ chỉnh các phẩn Stretchable area và Fill area cho hợp lý
Bước 3: Sử dụng file 9-patch như một drawable bình thường (không cần ghi đuôi .9)
Tạo bằng Simple nine-patch generator
Để hỗ trợ các density khác nhau của các device Android khác nhau, chúng ta cần tạo đủ các resources như drawable-ldpi, drawable-mdpi, drawable-hdpi, drawable-xhdpi… nên nếu tạo bằng tay từng cái một có thể sẽ là 1 đống việc. Nên chúng ta dùng Tool!
Bước 1: Mở trang web Simple nine-patch generator chọn Select image và chọn đúng loại desity của source ảnh bạn chọn.
Bước 2: Chỉnh Stretchable area và Fill area cho hợp lý. Nếu bạn sử dụng Optical bounds thì nó sẽ là viền đỏ trên file 9-Patch.
Bước 3: Tải ảnh về khi đã edit xong, chúng ta sẽ click vào icon download ở góc phải trên cùng để tải về.
Bước 4: Sử dụng file 9-patch như một drawable bình thường (không cần ghi đuôi .9)
Việc tạo bằng tool sẽ giúp chúng ta tinh chỉnh dễ dàng hơn, đỡ phải take time tạo từng 9-patch image khác nhau cho các density mà chưa chắc đã có độ nhất quán tuyệt đối. Nhưng bên cạnh đó lại có một nhược điểm là bạn chỉ có thể tạo một Stretchable area trong một drawable. Android studio thì lại giúp bạn tuỳ ý tạo các Stretchable area khác nhau.
Summary
Để styling trong Android thì muôn vàn cách, 9-patch chỉ là một trong số cách đó. Trên đây chỉ là một vài ví dụ cơ bản về việc sử dụng 9-patch. Mong rằng bài viết đã giúp bạn có thêm một kiến thức mới – phép thuật mới cho chặng đường của một phù thủy Android ??
Delegate là gì nhỉ? Tại sao lại là Delegate? Đúng như cái tên, Delegate là một design pattern mà bạn ủy quyền xử lý logic của Class hiện tại cho một Object/Class khác. Delegate thường được sử dụng để tách logic code theo việc của nó (separate concerns) hoặc common hóa một đoạn logic. Bài viết này sẽ giúp bạn tìm hiểu cơ bản về Delegate trong Kotlin và cách sử dụng khái niệm này. Trong Kotlin, có 2 cách để sử dụng Delegate – Interface/Class Delegation – Delegate Properties
Delegate là cách bạn cho phép một object khác xử lý một logic cho object hiện tại
Interface/Class Delegation
Với cách thứ nhất, chúng ta sẽ sử dụng một interface làm abstract cho một object và truyền object đó vào phần khai báo implement thông qua keyword by. Bằng cách này, các abstract methods (method của interface) sẽ chạy code của delegating object!
interface CameraOptimization {
fun optimize()
}
object XiaomiDevicesOptimization : CameraOptimization {
override fun optimize() {
TODO("do something for Xiaomi devices")
}
}
object DefaultDevicesOptimization : CameraOptimization {
override fun optimize() {
TODO("do something for others devices")
}
}
class CameraManager(optimization: CameraOptimization) : CameraOptimization by optimization {
fun cameraFocus() {
//todo: focus camera
}
}
Đây là cách setup cơ bản của Interface/Class Delegation. Như các bạn thấy thì implementation của method optimize() không trực tiếp xuất hiện ở trong class CameraManager mà sẽ được delegate đến object truyền vào bằng keyword by.
class CameraActivity : BaseActivity() {
private lateinit var _cameraManager: CameraManager
private fun initView() {
val vendor = android.os.Build.MANUFACTURER
val config = when {
vendor.equals("Xiaomi", ignoreCase = true) -> XiaomiDevicesOptimization
else -> DefaultDevicesOptimization
}
_cameraManager = CameraManager(config)
_cameraManager.optimize()
_cameraManager.cameraFocus()
}
}
Khi method optimize() được gọi, nó sẽ delegate đến Config của XiaomiDevicesOptimization hoặc DefaultDevicesOptimization tùy theo device đó là gì. Với cách tiếp cận này logic của CameraManager vẫn có khả năng tối ưu mà không cần phải quan tâm rằng nó sinh ra cho vendor cụ thể nào cả. Đồng thời cũng tăng khả năng mở rộng của Class này hơn. Nếu app của bạn quyết định support optimize thêm cả anh zai Samsung cũng oke luôn, code thêm 1 class và 1 dòng duy nhất.
Delegate Properties
Chắc bạn đã từng sử dụng rất nhiều lần lazy trong kotlin rồi đúng không? Delegate đó :v Delegate Properties là việc bạn implement operator getValue (có thể thêm cả setValue luôn nếu bạn muốn nó set được cả value) của một class. Hoặc một cách khác tường mình hơn là implement interface ReadWriteProperty/ReadOnlyProperty. Class đó sẽ trở thành Delegate. Vẫn là keyword by, chúng ta khai báo một biến với Delegate thông qua by.
class IntNaturalSet : ReadWriteProperty<Any, Int> {
private var _value: Int = 0
override fun getValue(thisRef: Any, property: KProperty<*>): Int {
return _value
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
_value = if(value < 0) 0 else value
}
}
Trong ví dụ đơn giản này chúng ta đã ủy quyền getter setter của biến cho class IntNaturalSet can thiệp và xử lý logic. Bên cạnh việc ủy quyển xử lý logic getter (setter) thì Delegate cũng có thể access đến Class chứa biến được delegate thông qua param thisRef(Chính là generic T trong ReadWriteProperty/ReadOnlyProperty). Có thể là built-in delegate cho một Type hoặc ép kiểu thisRef trong logic của getter setter để có thể sử dụng param này.
Ứng dụng của Delegate Properties rất rộng, chúng ta có thể custom cho logic in/out data như shared preferences, cache… Trong Androidx/Kotlin cơ bản cũng có vài delegate như lazy, viewModels, activityViewModels…
Summary
Delegate là một phương pháp khá hay trong lập trình giúp chúng ta tối ưu logic source code. Một source base có thể sẽ clean hơn nếu rút gọn các common logic hay boilerplate code. Một class có thể tăng tính mở rộng trong tương lai. Một project có thể sẽ triển khai nhanh hơn nhờ những common và khả năng scalable tốt. Lần tới nếu như bạn gặp phải một vấn đề có thể xử lý bằng Delegate, cứ thử xem sao nhé! Hi vọng bài viết giúp bạn có thêm chút kiến thức trong chặng đường của một Engineer :3!
Điện thoại di động đã và đang trở thành một vật bất ly thân với mỗi người chúng ta. Tuy nhiên trong nhiều trường hợp, mọi người sẽ cảm thấy khó khăn trong việc sử dụng điện thoại di động. Điều này bao gồm một người bị mù bẩm sinh hoặc mất kỹ năng vận động trong một tai nạn. Điều này cũng bao gồm cả những người không thể sử dụng tay vì họ đang bế một đứa trẻ. Bạn có thể gặp khó khăn khi sử dụng điện thoại khi đeo găng tay khi trời lạnh. Có thể bạn gặp khó khăn trong việc phân biệt các mục trên màn hình khi trời sáng.
Trong những trường hợp này, thứ họ cần chính là những hỗ trợ từ những chiếc điện thoại thông minh. Accessibility từ đó được sinh ra để hỗ trợ chúng ta.
Dịch vụ trợ năng (Accessibility) là một tính năng của Android Framework được thiết kế để cung cấp phản hồi điều hướng thay thế cho người dùng thay mặt cho các ứng dụng được cài đặt trên thiết bị Android. Dịch vụ trợ năng có thể thay mặt ứng dụng giao tiếp với người dùng, chẳng hạn như bằng cách chuyển đổi văn bản thành giọng nói hoặc cung cấp phản hồi xúc giác khi người dùng di chuột trên một khu vực quan trọng của màn hình. Phòng học mã này chỉ cho bạn cách tạo một dịch vụ trợ năng rất đơn giản.
2. Ứng dụng của Accessibility trong Android
Switch Access: cho phép các người dùng android bị hạn chế vận động tương tác với điện thoại qua một hoặc nhiều nút.
Voice Access (beta): cho phép các người dùng Android bị hạn chế vận động điều khiển thiết bị bằng cử chỉ giọng nói.
Talkback: một trình đọc màn hình thường được người khiếm thị hoặc người mù sử dụng.
3. Hướng dẫn cài đặt Accessibility Service
Cách cài đặt Accessibility Service trong project Android
Accessibility yêu cầu các điện thoại chạy chúng phải có phiên bản từ Android 7 trở lên
Cùng xem chúng ta cần những gì trong file AndroidManifest của service này
Để thực hiện thao tác vuốt, android:canPerformGesture được đặt thành true
Để truy cập nội dung cửa sổ, android:canRetrieveWindowContent được đặt thành true.
Sau đó, để triển khai các chức năng của AccessibilityService, chúng ta phải tạo một Service kế thừa AccessibilityService
public class HelperService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
}
Chúng ta sẽ tạo một view để hiển thị các nút bấm chức năng hỗ trợ trong service này
Trong hàm onServiceConnected() của HelperService, chúng ta có thể khởi tạo một giao diện sử dụng quyền WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY – thứ sẽ giúp chúng ta vẽ lên trên màn hình.
Để chạy service này trên thiết bị thật, chúng ta phải chỉnh sửa phần configurations của mục run, chọn Launch Options là Nothing
Khi service đã được chạy thành công từ Android Studio, chúng ta phải vào phần Settings -> Additional settings -> Accessibility, chọn đến phần Downloaded Apps chúng ta sẽ thấy tên service của chúng ta (HelperService), ấn chọn vào service và bật service lên. Giao diện các chức năng hỗ trợ của chúng ta sẽ hiển thị lên.
Demo trên thiết bị thật
5. Những hiểm nguy từ Accessibility Service
Accessibility Service được sinh ra với một mục đích rất tốt, đó là hộ trỡ những người khuyết tật hoặc những người hạn chế, khó trong các vận động. Tuy nhiên với các khả năng có thể đọc được các dữ liệu trên màn hình, điều khiển điện thoại,… nếu như chúng được sử dụng vào mục đích xấu thì sẽ rất dễ gây nguy hiểm, rủi ro bảo mật thông tin đến cho chủ sở hữu điện thoại.
Để phòng tránh những rủi ro tiềm ẩn này thì chúng ta nên có những biện pháp phòng tránh như là:
Chỉ cài đặt các ứng dụng trên các chợ chính chủ của các hệ điều hành. Accessibility Service được đánh giá là một quyền nguy hiểm. Vậy nên việc kiểm duyệt các ứng dụng này diễn ra rất nghiêm ngặt.
Không tải/ cài app từ các nguồn không chính thống, trên các đường link lạ
Với các ứng dụng có yêu cầu quyền này, hãy đọc kỹ các điều khoản dịch vụ.
Chào các bạn. Mình xin tiếp tục với chuỗi bài tìm hiểu Rx trong lập trình Android, cụ thể ở đây là RxJava. Hôm nay mình xin giới thiệu chi tiết hơn về 2 thành phần quan trọng, gần như là cốt lõi trong RxJava đó là Observable và Observer.
1. Observable
Observable trong RxJava là một thành phần quan trọng cho việc xử lý luồng dữ liệu trong phát triển ứng dụng Android. Observable đại diện cho một luồng dữ liệu có thể phát ra các sự kiện hoặc giá trị dữ liệu theo thời gian.
Trong RxJava, bạn có thể tạo một Observable từ các nguồn dữ liệu khác nhau như danh sách, tập hợp, sự kiện giao diện người dùng, kết quả truy vấn cơ sở dữ liệu, gọi API mạng, và nhiều nguồn dữ liệu khác.
Chúng ta sẽ có 5 loại Observable sau:
Observable
Single
Maybe
Flowable
Completable
Để tạo Observable trong RxJava, bạn có thể sử dụng các phương thức như:
Observable.create(): Tạo một Observable từ mã logic tùy chỉnh. Bạn có thể sử dụng các phương thức của Observer để phát ra các sự kiện hoặc giá trị dữ liệu.
Observable.just(): Tạo một Observable từ một hoặc nhiều giá trị cụ thể. Observable này sẽ phát ra các giá trị này và hoàn thành sau đó.
Observable.interval(): Tạo một Observable phát ra các số nguyên liên tục sau một khoảng thời gian nhất định.
Observable.fromIterable(): Tạo một Observable từ một danh sách, một tập hợp hoặc một iterable.
Observable.fromCallable(): Tạo một Observable từ một Callable, nơi bạn có thể thực hiện các tác vụ bất đồng bộ và trả về một giá trị.
Khi bạn đã tạo Observable, bạn có thể sử dụng các toán tử để biến đổi, lọc và xử lý dữ liệu trong Observable theo nhu cầu của bạn. Sau đó, bạn có thể đăng ký (Subscribe) một Observer với Observable để nhận và xử lý các sự kiện và giá trị được phát ra từ Observable.
Các Observable trong RxJava cho phép bạn xử lý dữ liệu một cách linh hoạt, thực hiện các tác vụ bất đồng bộ và tương tác với các thành phần Android khác trong việc phát triển ứng dụng Android.
2. Observer
Observer trong RxJava là một thành phần quan trọng để nhận và xử lý các sự kiện hoặc giá trị từ một Observable trong phát triển ứng dụng Android. Observer đăng ký (Subscribe) với một Observable để nhận thông báo về các sự kiện và giá trị được phát ra từ Observable đó.
Chúng ta sẽ có 5 loại Observer sau:
Observer
SingleObserver
MaybeObserver
CompletableObserver
Trong RxJava, bạn có thể tạo một Observer bằng cách triển khai đối tượng Observer<T>. Đối tượng này định nghĩa các phương thức mà bạn cần triển khai để xử lý các sự kiện và giá trị từ Observable.
Các phương thức chính trong giao diện Observer bao gồm:
onNext(T value): Phương thức này được gọi khi một giá trị mới được phát ra từ Observable. Bạn có thể định nghĩa các hành động xử lý khi nhận được giá trị này.
onError(Throwable throwable): Phương thức này được gọi khi có một lỗi xảy ra trong quá trình phát ra giá trị từ Observable. Bạn có thể xử lý và báo cáo lỗi trong phương thức này.
onComplete(): Phương thức này được gọi khi Observable hoàn thành việc phát ra các giá trị. Bạn có thể thực hiện các hành động dọn dẹp hoặc xử lý cuối cùng trong phương thức này.
Khi bạn đã triển khai giao diện Observer, bạn có thể đăng ký Observer với một Observable bằng cách sử dụng phương thức subscribe() trên Observable. Khi đăng ký thành công, Observer sẽ nhận các sự kiện và giá trị từ Observable và thực hiện các hành động xử lý tương ứng.
Ví dụ:
val observable: Observable<String> = Observable.just("Android", "RxJava", "RxAndroid");
val observer: Observer<String> = object : Observer<String> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {
// Handle when an error occurs during value generation
}
override fun onComplete() {
// Handle when Observable finishes emitting value
}
override fun onNext(t: String) {
// Handle value which receive from Observable
}
};
observable.subscribe(observer)
Trên đây là cách sử dụng Observer trong RxJava trong phát triển ứng dụng Android. Observer giúp bạn nhận và xử lý các sự kiện và giá trị từ Observable một cách linh hoạt và dễ dàng.
Sau đây mình sẽ nói về sự kết hợp và lấy ví dụ cho từng loại Observable và Observer với nhau.
3. Các loại và triển khai của Observable/ Observer
Như chúng ta đã đề cập ở trên có 5 loại Observable và 4 loại Observer. Bảng dưới đây sẽ mô tả sự tương ứng giữa Observable và Observer cũng như số emissions của từng loại
Observable
Observer
Nums of emissions
Observable
Observer
Multiple or None
Single
SingleObserver
One
Maybe
SingleObserver
One or None
Flowable
Observer
Multiple or None
Completable
CompletableObserver
None
3.1. Observable & Observer
Observable là một loại được sử dụng khá phổ biến. Nó có thể phát ra một hoặc nhiều items.
Mình sẽ triển khai 1 ví dụ minh hoạ sau:
Đầu tiên, chúng ta sẽ tạo một Observable:
val observableList = arrayListOf("RxJava", "RxAndroid", "Coroutine")
val observable: Observable<String> = Observable.create { emitter ->
// emit each item
for (item in observableList) {
Log.i("PhongPN3", "emitter: $item - ${Thread.currentThread().name}")
emitter.onNext(item)
}
// all items are emitted
emitter.onComplete()
}
Chúng ta sử dụng hàm onNext() để phát ra mỗi item. Khi nào hoàn thành quá trình emission, chúng ta sẽ dùng hàm onComplete().
Bước tiếp theo chúng ta định nghĩa Observer để handle các item được phát ra.
val observer: Observer<String> = object : Observer<String> {
override fun onSubscribe(d: Disposable) {
Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
}
override fun onNext(t: String) {
Log.i("PhongPN3", "onNext: $t - ${Thread.currentThread().name}")
}
override fun onError(e: Throwable) {
Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
}
override fun onComplete() {
Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
}
}
Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.
observable.subscribe(observer)
Kết quả sẽ là:
onSubscribe - main
emitter: RxJava - main
onNext: RxJava - main
emitter: RxAndroid - main
onNext: RxAndroid - main
emitter: Coroutine - main
onNext: Coroutine - main
onComplete - main
3.2. Single & SingleObserver
Single luôn luôn emit một item duy nhất hoặc ném ra một ngoại lệ nào đó.
val s = "RxJava"
val singleObservable: Single<String> = Single.create { emitter ->
emitter.onSuccess(s)
}
SingleObserver cũng sẽ khác với Observer bình thường, cụ thể nó sẽ không có hàm onNext() và onComple(), thay đó sẽ làm hàm onSuccess().
Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.
singleObservable.subscribe(singleObserver)
Kết quả sẽ là:
onSubscribe - main
onSuccess: RxJava - main
3.3. Maybe & MaybeObserver
Maybe là loại Observable mà có thể phát 1 item hoặc ko phát item nào cả (có 1 hoặc ko có gì). Với Maybe chúng ta sẽ sử dụng cho trường hợp giá trị muốn nhận là tùy biến có thể có hoặc ko.
Ví dụ chúng ta query note by Id trong database nó có thể có hoặc cũng có thể không.
val s = "RxJava"
val maybeObservable = Maybe.create { emitter: MaybeEmitter<String> ->
emitter.onSuccess(s)
}
Nếu muốn phát ra item, chúng ta sẽ sử dụng onSuccess, còn nếu ko muốn phát ra item thì chúng ta sẽ sử dụng onComplete. Đây chính là điểm khác nhau với Single observable.
val maybeObserver: MaybeObserver<String> = object : MaybeObserver<String> {
override fun onSubscribe(d: Disposable) {
Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
}
override fun onError(e: Throwable) {
Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
}
override fun onSuccess(t: String) {
Log.i("PhongPN3", "onSuccess: $t - ${Thread.currentThread().name}")
}
override fun onComplete() {
Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
}
}
Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.
maybeObservable.subscribe(maybeObserver)
Kết quả sẽ là:
onSubscribe - main
onSuccess: RxJava - main
3.4. Completable & CompletableObserver
Completable là loại Observable sẽ ko phát bất kỳ item nào mà nó chỉ thực thi một nhiệm vụ nào đó và thông báo nhiệm vụ hoàn thành hoặc chưa hoàn thành.
Khởi tạo Observable:
val completableObservable = Completable.create { emitter: CompletableEmitter ->
// do something
emitter.onComplete()
}
Định nghĩa Observer:
val completeObserver: CompletableObserver = object : CompletableObserver {
override fun onSubscribe(d: Disposable) {
Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
}
override fun onError(e: Throwable) {
Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
}
override fun onComplete() {
Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
}
}
Cuối cùng là subscribe việc lắng nghe dữ liệu từ Observable.
completableObservable.subscribe(completeObserver)
Kết quả sẽ là:
onSubscribe - main
onComplete - main
3.5. Flowable & SingleObsever
Được sử dụng khi một Observable tạo ra số lượng lớn các sự kiện / dữ liệu mà Observer có thể xử lý. Flowable có thể được sử dụng khi nguồn tạo ra rất nhiều sự kiện (theo nhiều tài liệu là khoảng 10k+ sự kiện) và Onserver không thể tiêu thụ tất cả. Flowable sử dụng phương pháp Backpressure để xử lý dữ liệu tránh lỗi MissingBackpressureException và OutOfMemoryError.
Ở ví dụ này, chúng ta sẽ tính tổng từ 1 đến 10, và kết quả sẽ được thông báo cho một SingleObserver.
val flowable = Flowable.range(1, 10)
val singleObserver: SingleObserver<Int> = object : SingleObserver<Int> {
override fun onSubscribe(d: Disposable) {
Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
}
override fun onError(e: Throwable) {
Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
}
override fun onSuccess(t: Int) {
Log.i("PhongPN3", "onSuccess: $t - ${Thread.currentThread().name}")
}
}
flowable.reduce(0) { sum: Int, item: Int ->
sum + item
}.subscribe(singleObserver)
Hàm reduce có tác dụng xử lý từng item mà flowable phát ra và trả về một giá trị là tổng của tất cả items.
Kết quả sẽ là:
onSubscribe - main
onSuccess: 55 - main
Lưu ý : Ở các ví dụ source code tham khảo, mình hay để lại Log để các bạn có thể tiện thử chạy và ra output giống kết quả mà mình trình bày.
Tổng kết
Trên đây là các loại và cách triển khai của các loại Observable và Observer tương ứng. Mình hy vọng bài viết phần nào giúp mọi người hiểu và nắm được cách sử dụng cơ bản nhất về 2 thành phần này RxJava.
Bài viết sắp tới mình sẽ tiếp tục với các Operator trong RxJava. Hẹn mọi người ở bài viết sắp tới.
Chào mọi người, hôm nay mình xin chia sẻ về chủ đề về Asynchronous Programming. Cụ thể là một là một thư viện khá phổ biến trong việc xử lý bất đồng bộ, giúp tối ưu quá trình xử lý các task vụ, đặc biệt là các task vụ nặng. Ở đây, mình nói đến đó là thư viện RxJava.
I. Reactive Programming
Đầu tiên mình phải nói đến thuật ngữ Reactive Programming, nền tảng tư tưởng để tạo ra RxJava. Vậy Reactive Programming là gì?
Reactive Programing mà một phương pháp lập trình tập trung vào các luồng dữ liệu không đồng bộ và quan sát sự thay đổi của các luồng dữ liệu không đồng bộ đó, khi có sự thay đổi sẽ có hành động xử lý phù hợp. Vì đây là luồng dữ liệu không đồng bộ nên các thành phần code cùng lúc chạy trên các thread khác nhau từ đó rút ngắn thời gian thực thi mà không làm block main thread.
Các thư viện phổ biến trong Reactive Programming trên Android bao gồm RxJava và RxAndroid, được phát triển dựa trên Reactive Extensions (Rx) của Microsoft.
Reactive Extension (ReactiveX hay RX) là một thư viện follow theo những quy tắc của Reactive Programming tức là nó soạn ra các chương trình bất đồng bộ và dựa trên sự kiện bằng cách sử dụng các chuỗi quan sát được.
Reactive Extension có sẵn bằng nhiều ngôn ngữ như C++ (RxCpp), C# (Rx.NET), Java (RxJava), Kotlin (RxKotlin) Swift (RxSwift), …
Lợi ích của việc sử dụng Reactive Programming trong Android bao gồm:
Phản hồi nhanh: Giúp ứng dụng phản ứng nhanh chóng với các sự kiện và thay đổi trong luồng dữ liệu.
Dễ quản lý: Các luồng dữ liệu được quản lý một cách rõ ràng và có thể sử dụng các toán tử để biến đổi và xử lý dữ liệu một cách dễ dàng.
Mã dễ đọc và bảo trì: Reactive Programming thường sử dụng các phép toán và trình tự xử lý dữ liệu rõ ràng, làm cho mã dễ đọc, hiểu và bảo trì hơn.
Tích hợp tốt: Reactive Programming có thể tích hợp với các thư viện và công nghệ khác nhau trong việc xử lý dữ liệu, như internet, cơ sở dữ liệu và các tác vụ bất đồng bộ.
II. RxJava/Rx Kotlin
RxJava cơ bản là một thư viện cung cấp các sự kiện không đồng bộ được phát triển dựa theo Observer Pattern. RxJava cho phép bạn tạo và quản lý các luồng dữ liệu (observable streams) và thực hiện các phép biến đổi, lọc, kết hợp và xử lý các sự kiện, nhận giá trị dữ liệu trong các luồng này thông qua Observer. Đặc biệt bạn có thể điều phối, xử lý chúng trên bất kì Thread mà bạn muốn.
Lợi ích của việc sử dụng RxJava trong Android bao gồm:
Xử lý bất đồng bộ dễ dàng: RxJava giúp xử lý các tác vụ bất đồng bộ một cách dễ dàng và gọn gàng, giúp tránh việc sử dụng các callback rườm rà và phức tạp.
Quản lý luồng dữ liệu: RxJava cho phép quản lý và xử lý luồng dữ liệu một cách rõ ràng, giúp tạo ra mã dễ đọc và hiểu hơn.
Tích hợp tốt với các thành phần khác: RxJava có khả năng tích hợp tốt với các thành phần khác trong Android như LiveData, ViewModel, Retrofit và Room, giúp xây dựng các ứng dụng Android mạnh mẽ và dễ bảo trì.
Rx Kotlin là tập hợp các phương thức bổ sung thêm (extension methods) của RxJava cho Kotlin. Sẽ có các phương thức giúp bạn dễ dàng tạo ra code reactive programming hơn, chuyển đổi, kết hợp các kiểu phát dữ liệu, …
III. RxAndroid
RxAndroid là một thư viện mở rộng của RxJava, được tối ưu hóa và đi kèm với các tính năng hỗ trợ cụ thể cho phát triển ứng dụng Android. Nó cung cấp các công cụ và khả năng bổ sung để sử dụng RxJava trong môi trường Android một cách tiện lợi.
RxAndroid giúp tương tác với giao diện người dùng (UI) trong quá trình sử dụng RxJava. Nó cung cấp các lớp trình trợ giúp cho việc lập trình phản ứng trong Android, bao gồm:
Schedulers: Cung cấp các Scheduler được tối ưu hóa cho Reactive Programming trong Android. Giúp chúng ta điều phối, phân chia, tối ưu hoá các hoạt động ở các Thread khác nhau. Ví dụ như MainThreadScheduler để thực thi các tác vụ trên luồng chính (UI thread).
AndroidObservable: Cung cấp các phương thức trợ giúp để tạo Observable từ các thành phần Android như giao diện người dùng, sự kiện chạm, thông báo hệ thống và vị trí GPS.
Binding APIs: Hỗ trợ tích hợp RxJava với các thư viện giao diện người dùng phổ biến như Data Binding và ButterKnife, giúp xử lý dữ liệu trong các thành phần giao diện người dùng một cách dễ dàng và linh hoạt.
Có một điều nhỏ các bạn có thể lưu ý trong khi triển khai set-up thư viện cho RxJava/RxAndroid cho Project Android đó là bạn hoàn toàn có thể chỉ khai báo Rx Android trong file build.gradle của app. App vẫn sẽ chạy bình thường vì Rx Android lúc đó sẽ tự pull Rx Java về. Nhưng thường phiên bản Rx Java ở đây là phiên bản cũ, ít được cập nhật vì nó phụ thuộc vào Rx Android mà nó cũng ít được cập nhật.
Nên mình có một recommend ở đây là khi khai báo sử dụng RxAndroid nên khai báo cả RxJava/Kotlin để luôn được cập nhật mới nhất.
IV. Các thành phần chính trong RxJava
Để tạo ra RxJava, cơ bản gồm 2 thành phần quan trọng nhất bao gồm Observable và Observer. Bên cạnh đó là một số thành phần đóng vào trò giúp triển khai, điều phối, thao tác dữ liệu, và kết nối, … từ đó tối ưu việc thực thi các task vụ bằng RxJava.
Dưới đây là một lược đồ minh hoạ cách triển khai của 1 RxJava, biểu thị các Observable và các phép biến đổi của các Observable.
Observable: Đại diện cho một luồng dữ liệu phát ra các sự kiện hoặc giá trị dữ liệu theo thời gian. Observable là nguồn dữ liệu và có khả năng phát ra các sự kiện và giá trị từ nguồn đó.
Observer: Là đối tượng nhận và xử lý các sự kiện hoặc giá trị từ một Observable. Observer sẽ đăng ký để nhận thông báo từ Observable và định nghĩa các hành động xử lý khi có sự kiện hoặc giá trị được phát ra.
Operators: Là các phép biến đổi và xử lý dữ liệu trên các Observable để tạo ra các luồng dữ liệu mới hoặc thực hiện các tác vụ xử lý. Các toán tử cho phép bạn biến đổi, lọc, kết hợp, nhóm dữ liệu và thực hiện các phép tính trên dữ liệu trong các luồng.
Scheduler: Được sử dụng để quy định luồng xử lý cho các sự kiện và tác vụ trong RxJava. Scheduler xác định xem liệu xử lý nên diễn ra trên luồng chính (UI thread) hay luồng nền (background thread).
Disposable: Đại diện cho việc đăng ký và hủy đăng ký của Observer với Observable. Disposable cho phép bạn quản lý vòng đời của quá trình đăng ký và giải phóng tài nguyên khi không cần thiết nữa.
Subject: Là một loại Observable đồng thời cũng là Observer, cho phép bạn phát và nhận sự kiện và giá trị dữ liệu như một Observable thông thường. Subject có thể được sử dụng để tạo sự tương tác giữa các Observable và Observer.
Mình sẽ giới thiệu từng thành phần này, cụ thể nó là gì, cách tạo ra và triển khai chúng ở bài tiếp theo ?
Chào mọi người. Trong xử lý lập trình bất đồng bộ, mọi người rất hay gặp phải tình huống xử lý nhiều task vụ cùng một lúc, hoặc các task vụ xử lý lần lượt, task vụ này phụ thuộc vào kết quả của các task vụ kia. Hôm nay mình xin chia sẻ một số cách để xử lý bài toán này bằng Coroutine.
Hãy xem xét một tình huống, trong đó Task_1 đang mong đợi một id thực hiện Task_2 với id được nhận từ Response của Task_1 và dựa trên phản hồi từ Task_2, các điều kiện và tham số của Task_3 sẽ bị thay đổi.
Task_1 → Task_2 with id -> Task_3
Làm thế nào để nhiều task song song phụ thuộc được thực hiện.
Thông thường, Task đầu tiên sẽ được thực hiện và sau khi nhận được phản hồi (Response), thao tác dữ liệu thì cuộc gọi tiếp theo sẽ được thực hiện.
viewModelScope.launch {
val data1Response:BaseResponse<Data1>?
try{
val call1 = repository.getAPIcall1()
}
catch (ex: Exception) {
ex.printStackTrace()
}
processData(data1Response)
}
Như cách triển khai trên với các task vụ phục thuộc vào nhau. Chúng ta cần phải Sync up trước khi thao tác với dữ liệu, cũng như xử lý với task vụ tiếp theo.
Với Couroutine có một số cách tiếp cận, xử lý tối ưu hơn để xử lý dữ liệu bất đồng bộ và đội kết quả để thực hiện Logic nghiệp vụ của bài toán.
Sau đây là một số cách tiếp cận.
1. Concurrent Approach with Wait Time async-await with Kotlin Coroutines
viewModelScope.launch {
val data1Response:BaseResponse<Data1>?
val data2Response: BaseResponse<Data2>?
val data3Response: BaseResponse<Data3>?
val call1 = async { repository.getAPIcall1()}
val call2 = async { repository.getAPIcall2()}
val call3 = async { repository.getAPIcall3() }
try {
data1Response = call1.await()
data2Response = call2.await()
data3Response = call3.await()
} catch (ex: Exception) {
ex.printStackTrace()
}
processData(data1Response, data2Response, data3Response)
}
Async sẽ mong đợi Response của task vụ. Tại đây, Task_1 sẽ cung cấp dữ liệu sẽ được đồng bộ hóa với Task_2, v.v. Sau đó, thao tác dữ liệu có thể được thực hiện với Response đã nhận và xử lý. Nhưng nó sẽ có một số thời gian chờ đợi giữa mỗi cuộc gọi.
Có một cách triển khai mà chúng ta có thể kết hợp gọi nhiều task vụ cùng tại một thời điểm.
2. Concurrent/ parallel Approach with thread switchingwithContext() will switch to seperate thread
Nó tương tự như async-await. Nhưng đâu đấy sẽ tiết kiệm chi phí hơn. Thay vì triển khai trên Main Thread, withcontext sẽ chuyển sang một Thread riêng và thực hiện tác vụ. Nó sẽ không có thời gian chờ như async-await.
Nếu runBlocking được thêm overlapping lên withContext(), nó sẽ đảo ngược tính chất bất đồng bộ và có thể hủy của Coroutines và chặn luồng. Cho đến khi nhiệm vụ cụ thể được hoàn thành.
Có 5 loại Dispatchers. IO, Main, Default, New Thread, Unconfined
Default Dispatcher: Đây là dispatcher mặc định trong hầu hết các hệ thống Coroutine. Nó sử dụng một pool thread cố định để thực thi các coroutine. Mặc dù nó hữu ích cho các hoạt động đơn giản, nhưng nó không phù hợp cho các hoạt động đòi hỏi nhiều tài nguyên hoặc chạy lâu.
IO Dispatcher: Dispatcher này được tối ưu hóa để thực thi các hoạt động I/O chậm, chẳng hạn như đọc/ghi dữ liệu từ đĩa hoặc mạng. Thay vì sử dụng pool thread cố định, IO Dispatcher thường sử dụng một số luồng I/O đặc biệt để tận dụng tối đa tài nguyên hệ thống.
Unconfined Dispatcher: Loại dispatcher này cho phép coroutine chạy trên bất kỳ luồng nào. Nó không liên kết với luồng nào cụ thể và cho phép coroutine chuyển đổi giữa các luồng trong quá trình thực thi. Điều này có thể hữu ích trong một số trường hợp đặc biệt, nhưng cũng có thể gây ra vấn đề về đồng bộ hóa và xử lý của task vụ.
New Thread Dispatcher: Dispatcher này tạo ra một luồng mới cho mỗi coroutine được thực thi. Điều này đảm bảo rằng mỗi coroutine sẽ chạy độc lập trên một luồng riêng biệt. Tuy nhiên, việc tạo và quản lý nhiều luồng có thể ảnh hưởng đến hiệu suất và tài nguyên hệ thống.
Main Dispathcher: Loại dispatcher đặc biệt được sử dụng để thực thi các coroutine trên luồng chính của ứng dụng. Main Dispatcher đảm bảo rằng các coroutine chạy trên luồng chính không bị chặn (block) trong quá trình thực thi, để đảm bảo khả năng phản hồi của giao diện người dùng.
viewModelScope.launch {
withContext(Dispatchers.Default) {
val apiResponse1 = api.getAPICall1()
val apiResponse2 = api.getAPICall2()
if (apiResponse1.isSuccessful() && apiResponse2.isSuccessful() { .. }
}
}
3. Parallel Approach with data merging
Cách tiếp cận thứ ba hơi khác một chút và nếu chúng ta muốn có hai task vụ độc lập và ghép chúng lại với nhau để có phản hồi mới, thì Zip Operator sẽ giúp chúng tôi xử lý song song chúng và đưa cho chúng ta kết quả mà chúng ta cần.
Trên đây mình đã giới thiệu 3 cách tiếp cận và triển khai, bạn có sử dụng cho từng bài toán, requirement cụ thể:
Khi chúng tôi muốn gọi nhiều Task vụ song song với thời gian chờ, thì cách tiếp cận async-await sẽ phù hợp.
Khi chúng ta muốn cách tiếp cận hiệu quả hơn với chuyển đổi các Thread, withcontext sẽ phù hợp
Và để ghép hai Response lại với nhau và thực hiện một số thao tác dữ liệu, phương pháp toán tử Zip là phù hợp.
Hy vọng bài viết này ít nhiều giúp các bạn về một số cách, một số lựa chọn xử lý với bài toàn xử lý nhiều task vụ, cụ thể ở đây là với Couroutine. Hẹn gặp mọi người ở bài viết sắp tới.
Chào mọi người, hôm nay mình xin chia sẻ về một chủ đề khá thông dụng trong Android, đó là Context. Mục đích của bài này đâu đấy các bạn mới tiếp cận với Android có thể hiểu đúng bản chất của Context, các loại Context và quan trọng hơn là cách cân nhắc sử dụng chúng trong từng trường hợp một cách hợp lý và chính xác nhất, tránh gây ra những lỗi không mong muốn.
1. Khái niệm
Mình xin trích dẫn một đoạn trong Official Document Android về Context
Mình xin trích dẫn một đoạn trong Official Document Android về Context
Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
Các bạn cũng có thể hiểu cơ bản thế này
Context là trạng thái của ứng dụng tại một thời điểm nhất định.
Context là 1 lớp cơ bản chứa hầu hết các thông tin về môi trường ứng dụng của android, tức là mọi thao tác, tương tác với hệ điều hành đều phải thông qua lớp này.
Context là 1 abstract class, nó cung cấp cho các lớp triển khai các phương thức truy cập vào tài nguyên của ứng dụng và hệ thống. Ví dụ như nó có thể khởi tạo và chạy các activities, broadcast, các intents….
Nào giờ chúng ta xem xét 3 functions hay được sử dụng nhiều nhất để truy xuất Context:
getContext() — trả về Context được liên kết với Activity được gọi.
getApplicationContext() — trả về Context được liên kết với Application chứa tất cả các Activity đang chạy bên trong nó.
getBaseContext() — có liên quan đến ContextWrapper, được tạo xung quanh Context hiện có và cho phép thay đổi hành vi của nó. Với getBaseContext() chúng ta có thể lấy Context hiện có bên trong lớp ContextWrapper.
Trong số này nổi bật hơn cả là getContext() và getApplicationContext().
Liên quan đến getBaseContext(), có một lưu ý nhỏ là tránh sử dụng loại Context này – lớp này được triển khai khi một class extends từ ContextWrapper. Mà lớp này lại có khoảng 40 lớp con trực tiếp và không trực tiếp. Vì vậy, nên gọi trực tiếp đến getContext, Activity, Fragment… để tránh gây ra memory leak.
2. Các loại truy cập Context
2.1. getContext()
Trong getContext(), Context được gắn với một Activity và vòng đời của nó. Chúng ta có thể hình dung Context là layer đứng sau Activity và nó sẽ tồn tại chừng nào Activity còn tồn tại. Thời điểm Activity chết, Context cũng vậy.
Dưới đây là danh sách các chức năng mà Activity’s Context cung cấp cho chúng ta:
Load Resource Values,
Layout Inflation,
Start an Activity,
Show a Dialog,
Start a Service,
Bind to a Service,
Send a Broadcast,
Register BroadcastReceiver.
2.2. getApplicationContext()
Trong getApplicationContext(), Context của chúng ta được gắn với ứng dụng và vòng đời của nó. Chúng ta có thể coi nó như một lớp đằng sau toàn bộ ứng dụng. Miễn là người dùng không tắt ứng dụng, thì nó vẫn tồn tại.
Bây giờ bạn có thể tự hỏi, đâu là sự khác biệt giữa getContext() và getApplicationContext(). Sự khác biệt là Context của ứng dụng không liên quan đến giao diện người dùng. Điều đó có nghĩa là, chúng ta không nên sử dụng nó để Inflate một Layout, start một Activity cũng như Dialog. Về phần còn lại của các chức năng từ Context của Activity, chúng cũng có sẵn trong Application Context. Vì vậy, danh sách các chức năng cho Application Context bao gồm như sau:
Load Resource Values,
Start a Service,
Bind to a Service,
Send a Broadcast,
Register BroadcastReceiver.
3. Cách sử dụng hợp lý Context trong Android
Việc sử dụng Context phù hợp trong Android là rất quan trọng để đảm bảo ứng dụng của bạn hoạt động bình thường và tránh các sự cố như memoryleak hoặc sự cố tài nguyên. Dưới đây là một số nguyên tắc giúp bạn sử dụng ngữ cảnh phù hợp:
Chọn Context cụ thể theo ngữ cảnh: Chọn Context cung cấp phạm vi cần thiết cho thao tác bạn đang thực hiện. Ví dụ: nếu bạn đang tạo thành phần giao diện người dùng trong một Activity, hãy sử dụng Activity Context thay vì Application Context.
Lưu ý đến Lifespan của Context: Xem xét vòng đời của thành phần yêu cầu Context. Đối với các hoạt động tồn tại trong thời gian ngắn, hãy sử dụng Context phù hợp với Lifespan của Component. Ví dụ: nếu bạn cần Context trong phương thức onReceive() của BroadcastReceiver, hãy sử dụng tham số Context được cung cấp thay vì Context tồn tại lâu dài như Application Context.
Tránh sử dụng Context hoạt động ngoài vòng đời của nó: Hãy thận trọng khi sử dụng Context hoạt động bên ngoài vòng đời của Activity, vì nó có thể dẫn đến memoryleak. Chẳng hạn, nếu bạn lưu trữ tham chiếu đến ngữ Activity Context và sử dụng nó sau khi Activity đã bị hủy, thì điều đó có thể gây ra sự cố. Trong những trường hợp như vậy, hãy chuyển sang sử dụng Application Context để thay thế.
Lưu ý về bộ nhớ: Tránh lưu trữ các tham chiếu lâu dài đến các Context một cách không cần thiết, vì chúng có thể giữ một tham chiếu đến toàn bộ Activity và Application và ngăn các tài nguyên bị thu gom rác. Nếu bạn cần một Context trong một Component tồn tại lâu dài, chẳng hạn như một singleton, hãy ưu tiên sử dụng Application Context.
Cân nhắc sử dụng Dependency Injection: Việc sử dụng các Dependency Injection như Dagger, Hilt hoặc Koin có thể đơn giản hóa việc quản lý Context bằng cách cung cấp ngữ cảnh phù hợp khi cần. Nó giúp tách rời các thành phần và đảm bảo Context chính xác được đưa vào dựa trên phạm vi và yêu cầu của hoạt động.
4. Kết luận
Việc lựa chọn Context phụ thuộc vào các yêu cầu cụ thể của mã của bạn và thành phần bạn đang xử lý. Điều quan trọng là chọn Context phù hợp để tránh memoryleak, sự cố tài nguyên hoặc sự không nhất quán trong ứng dụng của bạn. Theo nguyên tắc chung, hãy cố gắng sử dụng bối cảnh hạn chế nhất có thể để hoàn thành nhiệm vụ hiện tại, đảm bảo rằng nó có phạm vi và Lifespan cần thiết cho hoạt động.
Mong bài viết này sẽ giúp ích ít nhiều cho các bạn. Hẹn gặp lại mọi người trong bài viết tiếp theo.
Chào mọi người, trong lập trình Android, khi gặp một yêu cầu liên quan đến delay hoặc dừng một task vụ sau một khoảng thời gian chờ nhất định, chúng ta thường nghĩ đến 2 phương án là Thread.sleep() và kotlinx.coroutines.delay().
Vậy 2 cơ chế này có gì khác nhau và khi nào mình nên sử dụng chúng. Hôm nay mình sẽ xin chia sẻ tới mọi người ý hiểu của mình.
1. Khái niệm
kotlinx.coroutines.delay() và Thread.sleep() đều là cơ chế giới thiệu độ trễ hoặc tạm dừng trong quá trình thực thi chương trình, nhưng chúng được sử dụng trong các ngữ cảnh khác nhau và có các đặc điểm khác nhau.
1.1. kotlinx.coroutines.delay()
kotlinx.coroutines.delay() là một suspending function được cung cấp bởi thư viện Kotlin coroutines. Nó thường được sử dụng trong các asynchronous programming khi làm việc với các coroutine, nơi bạn muốn tạm dừng việc thực thi một coroutine mà không chặn luồng chính.
1.2. Thread.sleep()
Thread.sleep() là một static method được cung cấp bởi lớp Thread trong Java (cũng có thể sử dụng được trong Kotlin) và được sử dụng để tạm dừng quá trình thực thi của luồng hiện tại.
Thread.sleep() không chỉ tạm dừng việc thực thi một coroutine mà còn chặn toàn bộ luồng, nghĩa là các luồng khác trong ứng dụng sẽ không thể tiếp tục công việc của chúng trong suốt thời gian Sleep.
Tiếp đó là Job 0 -> Job 1 -> Job 2, 3 jobs đều khởi động cùng một lúc và bị delay với thời gian tương ứng trong từng job.
Tiếp theo, task có delay() time ngắn nhất thì chạy xong trước, theo sau là các công việc tiếp theo hoàn thành. Độ trễ dài nhất là 2s.
Kết quả là:
G->A->C->E->F->B->H->D->2.0s
or
G->A->C->E->F->B->H->D->2.1s
Nhìn vào kết quả, tại sao là 2.1s mà không phải là delay() time dài nhất 2.0s. Vì các bạn có thể thấy khoảng time từ khi Main Job bắt đầu đến khi delay của Job1 là 0.01s. Có một số lần chạy, khoảng time đó quá nhỏ nên kết quả có thể là 2s.
Tiếp đó là Job 0 -> Job 1. Job sẽ bị suspended. Tuy nhiên Thread.sleep(2000) chạy ở Job 1, nó sẽ block Main Thread trong 2s. Job2 trong thời gian dó không thực thi gì cả.
Sau 2s, D sễ được in ra, tiếp đó là E của Job2. Job 2 sẽ bị suspend. Vì Main Job và Job 1 có suspend time nhỏ hơn 2s, vậy nên sau đó nó sẽ được in ra ngay lập tức. Lúc này Job 0 sẽ được chạy trước vì có time delay bé hơn.
Cuối cùng là 0.5s, Job2 sẽ tiếp tục chạy và hoàn thành in ra F.
Kết quả là: G->A->C->D->E->B->H->F->2.5s
Timestamp #1 (sau 0 second)
Main job và Job 0 start and suspended.
Job 1 start và blocks Thread
Timestamp #2 (sau 2 seconds)
Job 1 hoàn thành
Job 2 start and suspended.
Job 0 và Main Job tiếp tục và hoàn thành.
Timestamp #3 (after 0.5 seconds)
Job 3 tiếp tục và hoàn thành.
Khoảng thời gian tổng dao động trong 2.5s.
2.3. Thread.sleep() on Dispatchers.Default/IO or Dispatchers.Default/Default
Kết quả khá tương tự với ví dụ sử dụng kotlinx.coroutines.delay()
Khi Dispatchers.Default hoặc Dispatchers.IO được sử dụng, nó được hỗ trợ bởi một nhóm luồng. Mỗi lần chúng ta gọi launch{}, một worker thread khác được created/used.
Ví dụ, đây là các Worker thread đang được sử dụng:
Main Job – DefaultDispatcher-worker-1
Job 0 – DefaultDispatcher-worker-2
Job 1 – DefaultDispatcher-worker-3
Job 2 – DefaultDispatcher-worker-4
Để xem luồng nào hiện đang chạy, bạn có thể sử dụng println("Run ${Thread.currentThread().name}")
Vì vậy, Thread.sleep() thực sự chặn luồng đó, nhưng chỉ chặn DefaultDispatcher-worker-3. Các công việc khác vẫn có thể được tiếp tục chạy vì chúng nằm trên các luồng khác nhau.
Timestamp #1 (after 0 second)
Main Job, Job 0, Job 1 và Job 2 start.
Main Job, Job 0 và Job2 bị suspended.
Job 1 bị block trên Thread của nó, ở đây là DefaultDispatcher-worker-3.
Timestamp #2 (after 0.5 second)
Job 2 tiếp tục và hoàn thành
Timestamp #3 (after 1 second)
Job 0 tiếp tục và hoàn thành
Timestamp #4 (after 1.5 seconds)
Main Job tiếp tục và hoàn thành
Timestamp #5 (after 2 seconds)
Job 1 tiếp tục và hoàn thành
Bởi vì mỗi công việc chạy trên một luồng khác nhau, công việc có thể được bắt đầu vào những thời điểm khác nhau. Vì vậy, đầu ra của A, C, E, G có thể là ngẫu nhiên. Như vậy, bạn thấy trình tự các task có thể khác với trình tự trong Exampe 1 ở phần trên.
3. Conclusion
Thread.sleep() chặn luồng gọi nó còn kotlinx.coroutines.delay() thì không.
Chỉ nên Thread.sleep() để kiểm tra xem tôi đã đặt đúng tác vụ chạy dài vào chuỗi nền chưa.
Cuối cùng, kotlinx.coroutines.delay() là phương pháp được khuyến nghị để tạo độ trễ trong một coroutine mà không chặn luồng giao diện người dùng.
Chào mọi người, chắc hẳn trong chúng ta nếu triển khai database của Android trong thời điểm hiện tại, chúng ta sẽ nghĩ ngay đến việc sử dụng Room. Chính vì vậy, hôm nay mình xin chia sẻ một số Tips nhỏ trong việc sử dụng Room đến mọi người.
1. Thiết lập ràng buộc giữa các Entities thông qua ForeignKey
Mặc dù Room không hỗ trợ trực tiếp ràng buộc giữa các mối quan hệ, nhưng nó cho phép bạn xác định các ràng buộc Foreign keys giữa các Entities.
Thông qua annotation @ForeignKey, một phần trong bộ annotation của @Entity, để cho phép sử dụng các tính năng khóa ngoại của SQLite. No không những giúp thể hiện được tốt hơn mối quan hệ giữa các Entities, đảm bảo đúng thiết kế mà còn thực thi các ràng buộc trên các bảng để đảm bảo mối quan hệ hợp lệ khi bạn sửa đổi cơ sở dữ liệu.
Cụ thể chúng ta sẽ cùng đến một ví dụ cụ thể:
Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonId và DogId cùng với PersonId được sử dụng như là một foreign key.
@Entity(tableName = “dog”,
foreignKeys = arrayOf(
ForeignKey(entity = Person::class,
parentColumns = arrayOf(“personId),
childColumns = arrayOf("owner"))))
data class Dog(@PrimaryKey val dogId: String,
val name: String,
val owner: String)
Theo tùy chọn, bạn có thể tuỳ chọn hành động sẽ được thực hiện khi đối tượng Parent Entity bị xóa hoặc cập nhật trong cơ sở dữ liệu.
Bạn có thể chọn một trong các tùy chọn sau: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT hoặc CASCADE, tương tự sử dụng như trong SQLite.
2. Tạo mối quan hệ Relation trong Room Database
Vẫn là mối quan hệ 1-n ở ví dụ trước. Bây giờ mình muốn truy vấn thực hiện việc lấy dữ liệu của các Person và toàn bộ Dogs tương ứng kèm theo.
Cách thông thường để thực hiện, chúng ta sẽ cần thực hiện 2 truy vấn: một truy vấn để lấy danh sách tất cả Person và một truy vấn khác để lấy danh sách Dog dựa trên Id của Person. Cụ thể:
@Query(“SELECT * FROM Person”)
public List<Person> getPersons();
@Query(“SELECT * FROM dog where owner = :personId”)
public List<Dog> getDogsForPersons(String personId);
Sau đây, mình sẽ triển khai theo cách thực hiện tạo mỗi quan hệ Relation giữa 2 đối tượng Person và Dog thông qua annotation @Relation
class PersonAndDogs {
@Embedded
var person: Person? = null
@Relation(parentColumn = “personId”,
entityColumn = “owner”)
var dogs: List<Dog> = arrayListOf()
}
Trong DAO, chúng tôi chỉ thực hiện một truy vấn duy nhất và Rôm sẽ truy vấn cả bảng Person và Dog, tiếp đó xử lý mapping đối tượng.
@Transaction
@Query(“SELECT * FROM Person”)
List<PersonAndDogs> getPersonAnDogs();
3. Thực hiện câu lệnh trong một Transaction
Khi một câu lệnh trong Room gắn với @Transaction, nó sẽ đảm bảo rằng tất cả các hoạt động cơ sở dữ liệu mà bạn đang thực hiện trong phương thức đó sẽ được chạy bên trong một transaction.
Transaction sẽ thất bại khi một Exception trong một trong những truy vấn trong Transaction đó xảy ra.
@Dao
abstract class UserDao {
@Transaction
open fun updateData(users: List<User>) {
deleteAllUsers()
insertAll(users)
}
@Insert
abstract fun insertAll(users: List<User>)
@Query("DELETE FROM Users")
abstract fun deleteAllUsers()
}
Bạn cũng có thể sử dụng @Transaction cho các phương thức @Query có câu lệnh chọn, trong các trường hợp sau:
Khi kết quả của truy vấn khá lớn. Bằng cách truy vấn cơ sở dữ liệu trong một giao dịch, bạn đảm bảo rằng nếu kết quả truy vấn không vừa với single cursor window, thì nó sẽ không bị hỏng do những thay đổi trong cơ sở dữ liệu giữa các lần cursor window swaps.
Khi kết quả của truy vấn là POJO với các trường @Relation. Các trường là các truy vấn riêng biệt nên việc chạy chúng trong một Transation sẽ đảm bảo kết quả nhất quán giữa các truy vấn.
4. Tối ưu hoá đối tượng truy vấn
Khi truy vấn cơ sở dữ liệu, bạn nên cân nhắc rằng có sử dụng tất cả các fields bạn trả về trong truy vấn của mình không?
Quan tâm đến dung lượng bộ nhớ mà ứng dụng của bạn sử dụng và chỉ tải tập hợp con các trường mà bạn sẽ sử dụng. Điều này cũng sẽ cải thiện tốc độ truy vấn của bạn bằng cách giảm IO cost.
Room sẽ thực hiện ánh xạ giữa các Columns và Objects cho bạn.
Cùng xem một ví dụ sau:
@Entity(tableName = "users")
data class User(@PrimaryKey
val id: String,
val userName: String,
val firstName: String,
val lastName: String,
val email: String,
val dateOfBirth: Date,
val registrationDate: Date)
Trên một số trường hợp cụ thể, các bạn không cần hiển thị tất cả thông tin này. Vì vậy, thay vào đó, chúng ta có thể tạo một đối tượng UserMinimal chỉ chứa dữ liệu cần thiết.
data class UserMinimal(val userId: String,
val firstName: String,
val lastName: String)
Trong lớp DAO, bạn chỉ cần xác định truy vấn như sau.
@Dao
interface UserDao {
@Query(“SELECT userId, firstName, lastName FROM Users)
fun getUsersMinimal(): List<UserMinimal>
}
5. Khởi tạo trước dữ liệu cho Room Database
Có rất nhiều trường hợp cụ thể mà bạn muốn thiết lập một bộ dữ liệu Default vào cơ sở dữ liệu trước. Khi đó bạn nên cân nhắc đến việc chèn dữ liệu ngay sau khi Room được khởi tạo.
Cụ thể, bạn có thể cân nhắc sử dụng RoomDatabase#Callback! Gọi phương thức addCallback khi xây dựng RoomDatabase của bạn và ghi đè onCreate hoặc onOpen.
onCreate will be called when the database is created for the first time, after the tables have been created.
onOpen is called when the database was opened.
companion object {
@Volatile private var INSTANCE: DataDatabase? = null
fun getInstance(context: Context): DataDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Test.db")
// prepopulate the database after onCreate was called
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// insert the data on the IO Thread
ioThread {
getInstance(context).dataDao().insertData(PRE_POPULATE_DATA)
}
}
})
.build()
val PRE_POPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
}
private val IO_EXECUTOR = Executors.newSingleThreadExecutor()
/**
* Utility method to run blocks on a dedicated background thread, used for io/database work.
*/
fun ioThread(f : () -> Unit) {
IO_EXECUTOR.execute(f)
}
Note: Các bạn chú ý thực hiện việc Insert default data trên một Worker Thread.
Trên đây là một số Tips nhỏ trong việc sử dụng và triển khai Room Database. Mong là ít nhiều sẽ giúp ích cho mọi người. Cảm ơn mọi người đã theo dõi.
Bản thân Flutter là một framework khá hoàn thiện đến mức việc tạo giao diện với Flutter trở nên dễ dàng và nhanh chóng đến mức bất ngờ! Điều này khiến lập trình viên Flutter cảm thấy việc lập trình mobile quá dễ (trừ khi họ là người đã từng nếm trái đắng từ khi code native). Mình cũng thấy dễ cho đến khi nhìn thấy giao diện ứa nước mắt mà designer gửi (đôi khi là kèm theo video animation siêu xịn…).
Thế bạn đã trang bị cho mình gì để ứng phó trước những design đẹp (hoặc k) nào? Bạn đã biết custom Slider trong Flutter chưa? Hay chỉ là những bé slider cũ mèn như này này này:
mặc địnhrange slidercupertino *hình ảnh chụp từ video youtube nên hơi mờ
Nhưng khi nhận được design thì trông như này
như này nèrồi như nàytrông cũng dễ?
Chắc ai nhận design cũng sẽ nghĩ: trông dễ phết, khó thì dùng thư viện (nhiều thư viện Slider quá mà). Có thể kể ra một số thư viện cho ai muốn dùng nè:
Đọc đến đây thì ai thích dùng thư viện có thể dừng lại và tham khảo 3 thư viện này. Đôi khi một số lại thấy thư viện thì quá là nặng thêm nhiều thứ lắt nhắt, rồi cho customize đủ thứ mà cái mình cần thì không có? Hoặc bạn muốn tự mình thử sức làm những slider này, thì tiếp tục đọc cách custom slider nào :3
Đầu tiên, chúng ta sẽ tìm hiểu về các thành phần của 1 slider và tên gọi của nó trong Flutter với thiết kế theo Material: (đoạn này cop trên document lười dịch nên nhờ bạn đọc tạm nhé)
The “thumb”, which is a shape that slides horizontally when the user drags it.
The “track”, which is the line that the slider thumb slides along.
The “value indicator”, which is a shape that pops up when the user is dragging the thumb to indicate the value being selected.
The “active” side of the slider is the side between the thumb and the minimum value.
The “inactive” side of the slider is the side between the thumb and the maximum value.
The “tick marks”, which are regularly spaced marks that are drawn when using discrete divisions.
The “value indicator”, which appears when the user is dragging the thumb to indicate the value being selected.
The “overlay”, which appears around the thumb, and is shown when the thumb is pressed, focused, or hovered. It is painted underneath the thumb, so it must extend beyond the bounds of the thumb itself to actually be visible.
Để thiết kế ra một Slider thì đầu tiên chúng ta sẽ xem đến các widget trực tiếp để tạo ra slider: Slider, RangeSlider, and CupertinoSlider(Flutter Widget of the Week) – YouTube. Tuy nhiên khi xem document (ờ lại là document, document của Flutter là tài liệu hữu ích nhất mà dev Flutter có thể tìm được bên cạnh source code open của nó) thì bạn phát hiện ra nó không có nhiều thứ liên quan đến giao diện cho lắm, ngoài việc cho phép đổi tí màu. Bạn bắt đầu tuyệt vọng.
một constructor của slidercó gì đó
và rồi cuối document, họ bảo, vẻ bề ngoài của nó thì dùng SliderThemeData từ widget SliderTheme hoặc một cái theme nào đó cha ông của nó mà có định nghĩa sliderTheme. Yeah XD
Bạn tiếp tục đi đến wiki của SliderThemeData xem tùy chỉnh được gì, ở đây nhé SliderThemeData class – material library – Dart API (flutter.dev). Có thể chỉnh được kha khá, hình tròn hình chữ nhật, bo góc (hoặc k), … bạn có thể dừng lại, đọc, và thử xem bạn có thể custom như nào (ảnh phía trên là 1 slider đã được 1 tác giả khác tạo ra nhờ tùy chỉnh các thuộc tính này).
Để rồi nhận ra rằng: nó không giống mấy cái design nhận được xíu nào 🙁
hình ảnh slider trong blog cuối bài
Thế làm sao để custom chúng nó như này? Hãy xem Flutter Team đã tạo ra Slider như nào đã nhé :3
ồ là một file hơn 3000 dòng code và rất nhiều tác giả
Nên cứ nghe theo thôi ha, họ bảo code mẫu rồi đấy, muốn custom thì xem mà học :3 họ bảo kế thừa mấy cái có sẵn này đi mà custom, nhưng custom sao thì họ không nói 🙁 nên t ở đây để giúp bạn đây hehe. Không dài dòng nữa, bắt đầu thôi :3
Khi bạn extend các lớp kể ở trên, bạn sẽ được trình gợi ý yêu cầu override hàm paint, đây là hàm để vẽ nên chúng nó. Trong hàm paint có một số thứ bạn cần chú ý: – Đầu tiên là PaintingContext context, ở đây bạn sẽ lấy được canvas ra và vẽ những gì bạn cần (context.canvas) – Tiếp theo là center với Thumb và parentBox với Track, bạn sẽ tìm được những điểm hữu dụng để vẽ – Tiếp theo là sliderTheme, từ đó bạn có thể lấy ra các thuộc tính khác mà bạn có thể truyền vào theme của slider (màu active, inactive…) – Cuối cùng, mn luôn luôn có thể tham khảo code open của Flutter xem họ làm như nào với các shape mặc định, hoặc tham khảo code của mình sau đây :3
// sử dụng
CustomSlider(
max: 100,
min: 10,
onValueChange: (_) { },
value: 30,
),
Mình đã custom cái slider này từ rất lâu rồi (nên code của nó mình không chắc là ổn lắm).
Và lí do mình viết bài này chủ yếu để nhớ kĩ hơn và muốn chia sẻ với mọi người khi mà chiều nay mình đã custom ra mấy cái extend SliderComponentShape, rồi ghép vào SliderTheme mà reload restart hoài UI không nhận :'( Mình rất bối rối muốn ném máy ra cửa sổ thì nhận ra nếu là RangeSlider thì cần extend RangeSliderTrackShape, RangeSliderThumbShape… (thêm Range cơ). Hóa ra do mình không đọc kĩ (tiện muốn kể chuyện InteractiveViewer ghê mà ai muốn nghe ib nhé :v)