Author: phongpn3

  • Observable/ Observer trong RxJava (Rx in Android Part 2)

    Observable/ Observer trong RxJava (Rx in Android Part 2)

    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().

    val singleObserver: SingleObserver<String> = object : SingleObserver<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}")
        }
    }
    

    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.

  • Tổng quan Rx trong Android (Rx in Android Part 1)

    Tổng quan Rx trong Android (Rx in Android Part 1)

    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.

    image

    1. 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 đó.

    2. 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.

    3. 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.

    4. 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).

    5. 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.

    6. 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 ?

  • Coroutine và Bài toán nhiều Task vụ

    Coroutine và Bài toán nhiều Task vụ

    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)
    }
    
      viewModel?.data1?.collect { dataResponse1 ->
             repository.getAPIcall2()         
    }
    
    viewModel?.data1?.collect { dataResponse2 ->
             repository.getAPIcall3()         
    }
    

    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.

    suspend fun fetchData() =       
        coroutineScope {
            val mergedResponse = listOf(   
                async { getAPIcall1() },  
                async { getAPIcall2() } 
            )
            mergedResponse.awaitAll()        
        }
    

    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.

    repository.getData1()
    .zip(repository.getData2()) { data1, data2 ->
        return@zip data1 + data2
    }
    .flowOn(Dispatchers.IO)
    .catch { e ->
      ..
    }
    .collect { it ->
        handleSuccessResponse(..)
    }
    

    Tổng kết

    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.

  • Context trong Android và những lưu ý

    Context trong Android và những lưu ý

    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()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.

  • Thread.sleep() and kotlinx.coroutines.delay()

    Thread.sleep() and kotlinx.coroutines.delay()

    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.

    2. Ví dụ triển khai

    2.1. kotlinx.coroutines.delay()

    fun main(args: Array<String>) {
        runBlocking {
                run()
            }
        }
    }
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        delay(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
       }
    }
    

    Luồng chạy sẽ như nhau:

    Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

    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.

    2.2. Thread.sleep() on Dispatchers.Main

    fun main(args: Array<String>) {
        runBlocking {
                run()
            }
        }
    }
    
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        Thread.sleep(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
        }
    
    }
    
    

    Luồng chạy sẽ như nhau:

    Main Job sẽ chạy và sẽ bị tạm dừng bởi delay.

    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

    fun main(args: Array<String>) {
        withContext(Dispatchers.Default) {
            run()
        }
    }
    
    
    suspend fun run() {
        coroutineScope {
            val timeInMillis = measureTimeMillis {
                val mainJob = launch {
                    //Job 0
                    launch {
                        print("A->")
                        delay(1000)
                        print("B->")
                    }
                    //Job 1
                    launch {
                        print("C->")
                        Thread.sleep(2000)
                        print("D->")
                    }
                    //Job 2
                    launch {
                        print("E->")
                        delay(500)
                        print("F->")
                    }
    
                    //Main job
                    print("G->")
                    delay(1500)
                    print("H->")
                }
    
                mainJob.join()
            }
    
            val timeInSeconds =
                String.format("%.1f", timeInMillis/1000f)
            print("${timeInSeconds}s")
        }
    
    }
    
    

    Kết quả là:

    G->A->C->E->F->B->H->D->2.0s

    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.

    Cảm ơn các bạn đã theo dõi.

  • Android Room Database Tips

    Android Room Database Tips

    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ể:

    image

    Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonIdDogId 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.

    image

    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.

  • ANDROID BLUETOOTH CLASSIC – PART 4

    ANDROID BLUETOOTH CLASSIC – PART 4

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Transfer Bluetooth Data.

    Sau khi bạn đã kết nối thành công với thiết bị Bluetooth, mỗi thiết bị có một BluetoothSocket được kết nối. Sau đó bạn có thể chia sẻ thông tin giữa các thiết bị bằng cách thông qua đối tượng BluetoothSocket, về cơ bản các bước đọc ghi dữ, để truyền dữ liệu như sau:

    • Get InputStream và OutputStream xử lý truyền qua Socket bằng cách sử dụng getInputStream() và getOutputStream() tương ứng.
    • Đọc và ghi dữ liệu vào các luồng sử dụng read(byte[]) và write(byte[]).

    Mình sẽ tạo một ví dụ minh hoạ, với ứng dụng Chat giữa client và server bằng Bluetooth Classic.

    Đầu tiên chúng ta tạo một Object chứa phương thức đọc, ghi. Xử lý Read, Write thông qua 2 object InputStream và OutputStream.

    private val bluetoothAdapter: BluetoothAdapter = adapter
    private val handler: Handler = mHandler
    
    private lateinit var bluetoothSending: BluetoothSending
    
    inner class BluetoothSending(bluetoothSocket: BluetoothSocket?) : Thread() {
    
        private val inputStream: InputStream?
        private val outputStream: OutputStream?
    
        init {
            var tempIn: InputStream? = null
            var tempOut: OutputStream? = null
            try {
                tempIn = bluetoothSocket?.inputStream
                tempOut = bluetoothSocket?.outputStream
            } catch (e: IOException) {
                e.printStackTrace()
            }
            inputStream = tempIn
            outputStream = tempOut
        }
    
        override fun run() {
            val buffer = ByteArray(1024)
            var bytes: Int
    
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    bytes = inputStream?.read(buffer)!!
                    handler.obtainMessage(Contstants.STATE_MESSAGE_RECEIVED, bytes, -1, buffer)
                        .sendToTarget()
                } catch (e: IOException) {
                    Log.e("PhongPN", "Bluetooth Reading Data Error", e)
                    e.printStackTrace()
                }
            }
        }
    
        fun write(bytes: ByteArray?) {
            try {
                outputStream?.write(bytes)
            } catch (e: IOException) {
                Log.e("PhongPN", "Bluetooth Writing Data Error", e)
                e.printStackTrace()
            }
        }
    }
    

    Tiếp đến chúng ta tạo một Server Socket. Lắng nghe các kết nối đến nó.

    inner class ServerClass : Thread() {
    
        private var serverSocket: BluetoothServerSocket? = null
    
        init {
            try {
                serverSocket =
                    bluetoothAdapter.listenUsingRfcommWithServiceRecord(
                        "PhongPN Transfer Data Bluetooth Classic", UUID.fromString(
                            Contstants.UUID
                        )
                    )
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not listen RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            var socket: BluetoothSocket? = null
            while (socket == null) {
                try {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTING
                    handler.sendMessage(message)
                    socket = serverSocket?.accept()
                } catch (e: IOException) {
                    e.printStackTrace()
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTION_FAILED
                    handler.sendMessage(message)
                }
                if (socket != null) {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTED
                    handler.sendMessage(message)
                    bluetoothSending = BluetoothSending(socket)
                    bluetoothSending.start()
                    break
                }
            }
        }
    
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Cuối cùng, chúng ta tạo một Client tương tác và trao đổi dữ liệu với Server Socket.

    inner class ClientClass(device: BluetoothDevice) : Thread() {
    
        private var socket: BluetoothSocket? = null
    
        init {
            try {
                socket = device.createRfcommSocketToServiceRecord(UUID.fromString(Contstants.UUID))
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not create RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            try {
                socket?.connect()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTED
                handler.sendMessage(message)
                bluetoothSending = BluetoothSending(socket)
                bluetoothSending.start()
            } catch (e: IOException) {
                e.printStackTrace()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTION_FAILED
                handler.sendMessage(message)
            }
        }
    
        fun cancel() {
            try {
                socket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Chúng ta xử lý send data tới nơi hiển thị dữ liệu (View) qua đối tượng Handle.

    private val handler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(message: Message) {
            when (message.what) {
                Contstants.STATE_LISTENING -> binding.txtStatus.text = "Listening"
                Contstants.STATE_CONNECTING -> binding.txtStatus.text = "Connecting"
                Contstants.STATE_CONNECTED -> binding.txtStatus.text = "Connected"
                Contstants.STATE_CONNECTION_FAILED -> binding.txtStatus.text = "Connection Failed"
                Contstants.STATE_MESSAGE_RECEIVED -> {
                    val readBuff = message.obj as ByteArray
                    val tempMsg = String(readBuff, 0, message.arg1)
                    binding.txtReceivedMessage.text = tempMsg
                }
            }
        }
    }
    

    Các bạn chú ý là phương thức close() của luồng cho phép bạn chấm dứt kết nối bất kỳ lúc nào bằng cách đóng BluetoothSocket. Các nên gọi phương thức này khi bạn sử dụng xong kết nối Bluetooth. Vì thế trong 2 class client vs server mình luôn có 2 hàm đó để sẵn sàng trong việc đóng kết nối.

    Trên đây là chuỗi bài viết của mình liên quan đến Bluetooth Classic trong Android. Mong đâu đấy chút chia sẻ của mình có thế giúp mọi người có cái nhìn sơ qua về Bluetooth Classic, cũng như là có thể ít nhiều giúp các bạn nếu có gặp phải bài toán liên quan đến nó trong tương lai.

    Cảm ơn mọi người đã theo dõi. Hẹn gặp lại trong các bài viết sắp tới.

  • ANDROID BLUETOOTH CLASSIC – PART 3

    ANDROID BLUETOOTH CLASSIC – PART 3

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Connect giữa các thiết bị Bluetooth.

    Để tạo kết nối Bluetooth giữa 2 thiết bị chúng ta cần thiết lập lập kết nối giống như việc kết nối giữa máy chủ (Server) và máy khách (Client). Chúng giao tiếp với nhau thông qua RFCOMM sockets, cụ thể thông qua đối tượng BluetoothSocket Socket trong Android. Cơ chế như nhau, máy khách (Client) cung cấp Socket information khi nó mở một RFCOMM Channel đến máy chủ (Server), máy chủ (Server) nhận Socket information khi kết nối đến được Accept. Server và Client được coi là đã kết nối với nhau khi chúng có một BluetoothSocket được kết nối trên cùng một kênh RFCOMM.

    Có 2 kết nối mà bạn cần quan tâm khi tạo Connect trong Bluetooth Classic

    • Connect như là một Client
    • Connect như là một Server

    Với từng bài toán, từng requirement cụ thể thì ta sẽ sử dụng chúng theo cách phù hợp.

    1. Connect như là một Client

      Cách connect đầu tiên này thường dùng cho bài toán kết nối với một thiết bị Bluetooth có sẵn. Từ đó kết nối, điều khiển, transfer dữ liệu với thiết bị đó. Đầu tiên bạn phải có được một đối tượng BluetoothDevice đại diện cho thiết bị Bluetooth. Đây là công đoạn bạn Scan (Discovery) và lấy thông thiên của thiết bị. Sau đó, bạn phải sử dụng BluetoothDevice để có được BluetoothSocket và bắt đầu kết nối.

      Các bước cụ thể như sau:

      1. Tạo một BluetoothSocket (RFCOMM Sockets) từ BluetoothDevice bằng cách gọi hàm createRfcommSocketToServiceRecord(UUID). Các bạn có hiểu UUID là một mã string, kiểu như một Port Number. Mã này của Client truyền vào hàm này, phải khớp với mã UUID mà máy chủ cấp. Thường với bài toán kết nối với thiết bị Bluetooth, thì thiết bị Bluetooth có cung cấp một chuỗi UUID chuẩn để ứng dụng có thể fix cứng dưới code và gửi lên khi tạo connect.

      2. Gọi BluetoothSocket.connect(). Sau khi gọi hàm, hệ thống sẽ thực hiện tra cứu SDP để tìm thiết bị từ xa có UUID phù hợp. Nếu tìm kiếm và khớp thành công và thiết bị từ xa chấp nhận kết nối, nó sẽ chia sẻ kênh RFCOMM để sử dụng trong quá trình kết nối và trả về phương thức connect(). Nếu kết nối không thành công hoặc nếu phương thức connect() timeout (sau khoảng 12 giây), thì phương thức này sẽ đưa ra một IOException. Bạn có thể tham khảo mã code sau

        class BluetoothClassicConnection(device: BluetoothDevice): Thread() {
        
        private val bluetoothSocket: BluetoothSocket? by lazy {
            device.createRfcommSocketToServiceRecord(UUID)
        }
        
        override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            if (BluetoothAdapter.getDefaultAdapter()?.isDiscovering == true) {
                BluetoothAdapter.getDefaultAdapter()?.cancelDiscovery()
            }
        
            try {
                bluetoothSocket?.let { socket ->
                    socket.connect()
        
                    // Handle Transfer Data 
                }
            } catch (e: IOException) {
                try {
                    bluetoothSocket?.close()
                } catch (e: IOException) {
                    Log.e(TAG, "Could not close the client socket", e)
                }
                Log.e(TAG, "Could not connect", e)
            }
        }
        
        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                bluetoothSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
        
        companion object {
            private const val UUID = "UUID code provide from Server Site"
         }
        }
        

      Note:

      • Việc duy trì kết nối Bluetooth Classic khá tốn tài nguyên của máy, đặc biệt như là PIN. Các bạn cần lưu ý đóng socket khi không cần dùng nữa, bằng cách gọi close() của đối tượng BluetoothSocket. Sau khi gọi Socket sẽ ngay lực tức được đóng lại và giải phóng tất cả các tài nguyên nội bộ có liên quan.
      • Chú ý đến việc huỷ Discovery() trước khi kết nối để đảm bảo việc connect được diễn ra thành công.
    2. Connect như một Server

      Với loại connect này được thực hiện nếu bạn muốn connect 2 thiết bị devices với nhau. Một thiết bị trong 2 thiết bị đó phải hoạt động như một Server bằng cách nắm giữ BluetoothServerSocket. Cụ thể BluetoothServerSocket lắng nghe các kết nối, sử dụng để thu nhận kết nối bluetoothSocket (RFCOMM sockets). Khi kết nối được thiết lập, máy chủ socket không còn cần thiết nữa và có thể đóng lại thông qua hàm close().

      Các bước cụ thể như sau:

      1. Lấy thông tin BluetoothServerSocket bằng cách gọi hàm listenUsingRfcommWithServiceRecord(String, UUID).

      2. Bắt đầu lắng nghe các yêu cầu kết nối bằng cách gọi accept(). Khi thành công, accept() trả về BluetoothSocket được kết nối và sau đó chúng ta sẽ sử dụng nó để trao đổi dữ liệu với thiết bị kết nối.

        class BluetoothClassicServerConnection: Thread() {
        private val serverSocket: BluetoothServerSocket? by lazy {
            BluetoothAdapter.getDefaultAdapter()?.listenUsingInsecureRfcommWithServiceRecord(NAME, UUID)
        }
        
        private var bluetoothSocket: BluetoothSocket? = null
        
        override fun run() {
            // Keep listening until exception occurs or a socket is returned.
            var shouldLoop = true
        
            while (shouldLoop) {
                val socket: BluetoothSocket? = try {
                    bluetoothSocket = serverSocket?.accept()
                } catch (e: IOException) {
                    Log.e(TAG, "Socket's accept() method failed", e)
                    shouldLoop = false
                    null
                }
                socket?.also {
                    
                    // Handle Transfer Data  
        
                    serverSocket?.close()
                    shouldLoop = false
                }
            }
        }
        
        // Closes the connect socket and causes the thread to finish.
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
        
        companion object {
            private const val NAME = "The name we provide the service with when it is added to the SDP (Service Discovery Protocol)"
            private const val UUID = "UUID code provide from Server Site"
          }
        }
        

      Note:

      • Khi call accept() trả về BluetoothSocket, Socket đã được kết nối. Vì vậy, không nên gọi connect(), giống như mình làm ở phía Client.

    Vậy là mình cũng vừa trình bày xong phần liên quan đến Connect Bluetooth Classic trong Android. Các loại và các trường hợp sử dụng nó. Bài tiếp theo mình sẽ trình bày về việc Transfer Data (Read and Write) của Bluetooth Classic.

    Hẹn gặp lại các bạn trong bài viết sắp tới.

  • Android Bluetooth Classic – Part 2

    Android Bluetooth Classic – Part 2

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

        val requestCode = 1;
        val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
           putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 500)
        }
        startActivityForResult(discoverableIntent, requestCode)
    

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.

  • Android Bluetooth Classic

    Android Bluetooth Classic

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

        val requestCode = 1;
        val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
           putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 500)
        }
        startActivityForResult(discoverableIntent, requestCode)
    

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.