Month: June 2023

  • Làm cách nào để thực hiện cuộc gọi, gọi FaceTime và gửi SMS trong ứng dụng sử dụng Swift?

    Làm cách nào để thực hiện cuộc gọi, gọi FaceTime và gửi SMS trong ứng dụng sử dụng Swift?

    Hiện nay hầu hết các ứng dụng di động đều có tính năng liên lạc nhằm mục đích giúp người sử dụng dễ dàng liên hệ được với bộ phận chăm sóc khách hàng. Để làm được việc này thì Apple có cung cấp một URL Scheme để thực hiện việc này.

    URL Scheme Mail

    Để có thể sử dụng được tính năng gửi mail thông qua app của Apple chúng ta cần cấu hình dự án cho phép sử dụng URL schemes “mailto” như sau:

    Mở file info.plist và thêm Queried URL Shemes -> add item và đặt trường value với giá trị là mailto

    Để thực hiện được việc gửi mail chúng ta thực hiện đoạn code như sau:

      func mail(to: String, cc: String = "", subject: String = "", body: String = "") {
            
            var mailURLString: String = "mailto:"
            
            // add to
            mailURLString += to
            
            // add cc
            mailURLString += "?cc=\(cc)"
            
            // add subject
            if let subjectEncode = subject.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
                mailURLString += "&subject=\(subjectEncode)"
            }
            
            // add content
            if let bodyEncode = body.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
                mailURLString += "&body=\(bodyEncode)"
            }
            
            // check url
            if let mailURL = URL(string: mailURLString), UIApplication.shared.canOpenURL(mailURL) {
                // open app mail with url
                UIApplication.shared.open(mailURL)
            }
        }

    Bây giờ bất kể chỗ nào chúng ta dùng để gửi mail đều có thể call func này để thực hiện việc gửi mail ví dụ như sau:

    mail(to: "[email protected]", cc: "[email protected]", subject: "Techover.io", body: "New post")

    URL Scheme Phone

    Tương tự như Mail việc gọi điện app cũng có 1 scheme cho phép thực hiện việc này.

        func tel(to: String) {
            if let telURL = URL(string: "tel:\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    URL Scheme FaceTime

        func faceTime(to: String) {
            if let telURL = URL(string: "facetime-audio://\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    URL Scheme SMS

        func sms(to: String) {
            if let telURL = URL(string: "sms:\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    Ngoài ra chúng ta còn có thêm các Apple URL Scheme khác như MAP Links , iTunes Links, YouTube Links … Anh em có thể tham khảo thêm tài liệu của Apple ở link sau: https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html#//apple_ref/doc/uid/TP40007899-CH1-SW1

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

  • Tìm hiểu về ETL Testing, ETL (Extract, Transform, and Load) Process

    Tìm hiểu về ETL Testing, ETL (Extract, Transform, and Load) Process

    Xin chào mọi người, chắc hẳn các bạn đã từng nghe qua thuật ngữ ETL và tự hỏi rằng ETL là gì? Đối với tester muốn test ETL cần phải làm gì? Quy trình ETL ra sao? Tại sao phải ETL nhỉ? Và test ETL khác gì so với test app, test web? Trước đây, mình cũng thế, đặt ra hàng vạn câu hỏi vì sao rồi đi tìm hiểu, sau quá trình học và có chút kinh nghiệm thực tế, hôm nay mình xin giới thiệu đôi chút kiến thức về ETL testing, tổng quan về quy trình ETL và test ETL khác gì so với test app, web.

    1. Khái niệm

    1.1. ETL Thử nghiệm/Kiểm thử ETL là gì?

    Kiểm thử ETL là quá trình kiểm thử được thực hiện để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác, là việc xác minh dữ liệu ở các giai đoạn trung gian đang được sử dụng giữa nguồn và đích. ETL là từ viết tắt của Extract-Transform-Load.

    1.2. Kiểm tra kho dữ liệu (data warehouse) là gì?

    Kiểm tra kho dữ liệu là một phương pháp kiểm tra trong đó dữ liệu bên trong kho dữ liệu được kiểm tra tính toàn vẹn, độ tin cậy, độ chính xác và tính nhất quán để tuân thủ khung dữ liệu của công ty. Mục đích chính của thử nghiệm kho dữ liệu là để đảm bảo rằng dữ liệu được tích hợp bên trong kho dữ liệu đủ tin cậy để một công ty đưa ra quyết định.

    1.3. ETL là gì? ETL hoạt động như nào?

    ETL là viết tắt của Extract-Transform-Load, là một quy trình trích xuất dữ liệu từ các hệ thống nguồn khác nhau, sau đó chuyển đổi dữ liệu (như áp dụng phép tính, phép nối, v.v.) Và cuối cùng tải dữ liệu vào hệ thống Kho dữ liệu. Trích xuất, chuyển đổi và tải (ETL) hoạt động bằng cách di chuyển dữ liệu từ hệ thống gốc đến hệ thống đích trong các chu kỳ định kỳ. Quy trình ETL hoạt động theo ba bước:

    • Extract: Trích xuất dữ liệu có liên quan từ cơ sở dữ liệu nguồn
    • Transform: Chuyển đổi dữ liệu để phù hợp hơn cho việc phân tích
    • Load: Tải dữ liệu vào cơ sở dữ liệu đích

    image

    1.4 Tại sao chúng ta phải ETL dữ liệu?

    Nếu chúng ta vẫn để nguyên các dữ liệu trên các database của các dữ liệu nguồn, chúng ta vẫn làm được các báo cáo phân tích, … Vậy tại sao chúng ta phải ETL dữ liệu làm gì?

    Như đã nói trên, bạn dùng ETL dữ liệu để chuyển mục đích, và tối ưu hóa mục đích sử dụng dữ liệu của các phần mềm từ ghi nhận các nghiệp vụ phát sinh hàng ngày, sang mục đích khai thác, vận hành, và phân tích các dữ liệu này để các nhà quản trị tìm ra các cơ may phát triển, các hoạt động kinh doanh mới đề vận hành doanh nghiệp – và đây chính là mục đích của ETL, và là nguyên nhân bạn cần công cụ này – chuyển đổi công năng sử dụng dữ liệu để cung cấp cho nhà quản trị.

    2. Quy trình kiểm thử ETL

    Tương tự như các Quy trình kiểm thử khác, ETL cũng trả qua các giai đoạn khác nhau. Các giai đoạn của quá trình kiểm thử ETL như sau:

    image

    3. ETL Tools

    Trên thị trường, có rất nhiều tools ETL, nhưng dưới đây là vài tool nổi bật nhất mọi người hay dùng:

    • Marklogic: https://www.marklogic.com/product/getting-started/
    • Oracle: https://www.oracle.com/index.html
    • Amazon redshift: https://aws.amazon.com/redshift/?Nc2=h_m1

    4. Mình đã sử dụng AWS trong ETL testing như thế nào?

    Như ở trên, chúng ta đã hiểu, ETL testing là kiểm tra để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác. Lý thuyết là vậy, còn thực hành sẽ như nào nhỉ?

    Thật khó để mình có thể chia sẻ hết kinh nghiệm trong quá trình tìm hiểu, học và kiểm thử ETL trong bài viết này, nhưng mình sẽ lấy 1 Ví dụ để mô tả một cách dễ hiểu nhất những gì 1 tester cần làm trong quá trình kiểm thử ETL. Từ đó, các bạn dễ hình dung, hiểu hơn về ETL testing và có thể áp dụng trong tương lai.

    Ví dụ: Dưới đây là luồng di chuyển dữ liệu từ hệ thống nguồn (Stream data source) đến hệ thống đích (S3 Data Lake Target) trên AWS. Tester sẽ cần làm gì để test dữ liệu từ Source lên S3?

    image

    • B1. Bạn sẽ cần chuẩn bị dữ liệu thô (VD: file csv, file parquet, …) để up lên source.
    • B2. Vậy làm cách nào để cho data chạy từ source lên hệ thống đích được nhỉ? Bạn sẽ cần phải khởi chạy ETL job.
    • B3. Sau khi chạy, chúng ta sẽ kiểm tra job ETL đã chạy thành công chưa?
    • B4. Sau khi job chạy thành công, kiểm tra hệ thống đích (VD: S3 Data Lake Target) có tạo bảng như mong đợi?
    • B5. Kiểm tra dữ liệu bảng đích. VD: Số lượng bản ghi, số lượng column, tên column, data type từng bản ghi, data trong từng column, … Kiểm tra, theo dõi cách ghi/thay đổi dữ liệu trong bảng đích đã đúng yêu cầu hay chưa.

    5. ETL testing giống và khác gì so với test mobile, test web?

    5.1. Giống nhau:

    • Trước hết, để test bất kỳ cái gì chúng ta đều phải đọc và hiểu tài liệu đặc tả. Lên kế hoạch kiểm thử và estimate thời gian kiểm thử.
    • Thiết kế test case, đảm bảo test đủ các trường hợp có thể xảy ra.
    • Chuẩn bị data test, môi trường test, …
    • Mục đích cuối cùng đều là đảm bảo chất lượng sản phẩm, đảm bảo đầu ra đúng với nhu cầu khách hàng.
    • ….

    5.2. Khác nhau:

    Vậy ETL testing có gì khác biệt so với test web và mobile? Dưới đây là một vài điểm khác biệt mà mình thấy được trong quá trình làm việc với ETL:

    • Test app, web để kiểm tra giao diện (UI), tương tác và trải nghiệm người dùng (UX) hay các chức năng, giá trị hiển thị, … thì chúng ta sẽ cần so sánh đúng với yêu cầu đặc tả (SRS)/mong muốn của khách hàng. Tức là chúng ta đã có sẵn yêu cầu đầu ra, việc cần làm là kiểm tra tính đúng đắn so với yêu cầu đó.
    • Test ETL thì chúng ta cần có kiến thức về SQL. Vì thực tế luôn có những chuyển đổi dữ liệu (transform) phức tạp, hoặc transform data từ nhiều nguồn, nhiều khoảng thời gian, … nên để tìm ra được output expect (kết quả đầu ra) là điều không dễ dàng. Do đó, chúng ta cần viết script SQL chuẩn, đúng với tài liệu để có được kết quả đầu ra, từ đó mới có thể so sánh và kiểm tra dữ liệu.

    Kết luận

    • ETL là viết tắt của Trích xuất, Chuyển đổi và Tải (Extract, Transform and Load)

    • ETL cung cấp phương pháp di chuyển dữ liệu từ nhiều nguồn khác nhau vào kho dữ liệu.

    • Trong bước trích xuất đầu tiên, dữ liệu được trích xuất từ hệ thống nguồn vào khu vực tổ chức.

    • Trong bước chuyển đổi, dữ liệu được trích xuất từ nguồn được làm sạch và chuyển đổi.

    • Tải dữ liệu vào kho dữ liệu đích là bước cuối cùng của quy trình ETL.

    Trên đây là những chia sẻ của mình về ETL testing, mong rằng bài viết này sẽ giúp ích được cho các bạn. Nếu mọi người có thắc mắc hay câu hỏi gì đừng ngần ngại comment và cùng nhau giải đáp nhé!

    Hẹn gặp lại mọi người trong bài viết tiếp theo.

    Tác giả bài viết

    HanhTM2

    Tài liệu tham khảo:

    Https://www.guru99.com/etl-extract-load-process.html

    Https://aws.amazon.com/vi/what-is/etl/