Blog

  • Accessibility in Android and Usage

    1. Accessibility là gì

    • Điện thoại di động đã và đang trở thành một vật bất ly thân với mỗi người chúng ta. Tuy nhiên trong nhiều trường hợp, mọi người sẽ cảm thấy khó khăn trong việc sử dụng điện thoại di động. Điều này bao gồm một người bị mù bẩm sinh hoặc mất kỹ năng vận động trong một tai nạn. Điều này cũng bao gồm cả những người không thể sử dụng tay vì họ đang bế một đứa trẻ. Bạn có thể gặp khó khăn khi sử dụng điện thoại khi đeo găng tay khi trời lạnh. Có thể bạn gặp khó khăn trong việc phân biệt các mục trên màn hình khi trời sáng.
    • Trong những trường hợp này, thứ họ cần chính là những hỗ trợ từ những chiếc điện thoại thông minh. Accessibility từ đó được sinh ra để hỗ trợ chúng ta.
    • Dịch vụ trợ năng (Accessibility) là một tính năng của Android Framework được thiết kế để cung cấp phản hồi điều hướng thay thế cho người dùng thay mặt cho các ứng dụng được cài đặt trên thiết bị Android. Dịch vụ trợ năng có thể thay mặt ứng dụng giao tiếp với người dùng, chẳng hạn như bằng cách chuyển đổi văn bản thành giọng nói hoặc cung cấp phản hồi xúc giác khi người dùng di chuột trên một khu vực quan trọng của màn hình. Phòng học mã này chỉ cho bạn cách tạo một dịch vụ trợ năng rất đơn giản.

    2. Ứng dụng của Accessibility trong Android

    • Switch Access: cho phép các người dùng android bị hạn chế vận động tương tác với điện thoại qua một hoặc nhiều nút.
    • Voice Access (beta): cho phép các người dùng Android bị hạn chế vận động điều khiển thiết bị bằng cử chỉ giọng nói.
    • Talkback: một trình đọc màn hình thường được người khiếm thị hoặc người mù sử dụng.

    3. Hướng dẫn cài đặt Accessibility Service

    Cách cài đặt Accessibility Service trong project Android

    • Accessibility yêu cầu các điện thoại chạy chúng phải có phiên bản từ Android 7 trở lên
    • Cùng xem chúng ta cần những gì trong file AndroidManifest của service này
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <service
                android:name=".HelperService"
                android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true">
                <intent-filter>
                    <action android:name="android.accessibilityservice.AccessibilityService"/>
                </intent-filter>
                <meta-data
                    android:name="android.accessibilityservice"
                    android:resource="@xml/helper_service"/>
            </service>
        </application>
    </manifest>
    • Để có thể chạy service này ta cần thêm quyền android:permission=”android.permission.BIND_ACCESSIBILITY_SERVICE”
    • Sau đó ta thêm file metadata vào service: @xml/helper_service
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
        android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="flagDefault"
        android:canPerformGestures="true"
        android:canRetrieveWindowContent="true" />
    • Để thực hiện thao tác vuốt, android:canPerformGesture được đặt thành true
    • Để truy cập nội dung cửa sổ, android:canRetrieveWindowContent được đặt thành true.

    Sau đó, để triển khai các chức năng của AccessibilityService, chúng ta phải tạo một Service kế thừa AccessibilityService

    public class HelperService extends AccessibilityService {
        @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
        }
        @Override
        public void onInterrupt() {
        }
    }
    • Chúng ta sẽ tạo một view để hiển thị các nút bấm chức năng hỗ trợ trong service này
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:gravity="start"
            android:id="@+id/power"
            android:text="@string/power"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/volume_up"
            android:text="@string/volume_up"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/volume_down"
            android:text="@string/volume_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/swipe_down"
            android:text="@string/swipe_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/swipe_right"
            android:text="@string/swipe_right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    • Kết quả chúng ta sẽ có được một view như sau:
    • Trong hàm onServiceConnected() của HelperService, chúng ta có thể khởi tạo một giao diện sử dụng quyền WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY – thứ sẽ giúp chúng ta vẽ lên trên màn hình.
        @Override
        protected void onServiceConnected() {
            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
            mLayout = new FrameLayout(this);
            WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
            lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
            lp.format = PixelFormat.TRANSLUCENT;
            lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            lp.gravity = Gravity.TOP;
            LayoutInflater inflater = LayoutInflater.from(this);
            inflater.inflate(R.layout.action_bar, mLayout);
        }

    Thiết lập các chức năng cho AccessibilityService

    • Từ các quá trình cài đặt trên chúng ta đã có giao diện cho service của mình, giờ là lúc chúng ta thêm các chức năng cho các nút bấm này
    • Ví dụ về chức năng tắt nguồn
        private void configurePowerButton() {
            Button powerButton = mLayout.findViewById(R.id.power);
            powerButton.setOnClickListener(view -> performGlobalAction(GLOBAL_ACTION_POWER_DIALOG));
        }
    • Ví dụ về chức năng tăng âm lượng:
        private void configureVolumeButtonUp() {
            Button volumeUpButton = mLayout.findViewById(R.id.volume_up);
            volumeUpButton.setOnClickListener(view -> {
                AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
                audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
                        AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
            });
        }
    • Ví dụ về chức năng vuốt màn hình:
        private void configureSwipeButtonDown() {
            Button swipeButton = (Button) mLayout.findViewById(R.id.swipe_down);
            swipeButton.setOnClickListener(view -> {
                Path swipePath = new Path();
                swipePath.moveTo(100, 1000);
                swipePath.lineTo(100, 100);
                GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
                gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 500));
                dispatchGesture(gestureBuilder.build(), null, null);
            });
        }

    4. Chạy thử trên thiết bị thật

    • Để chạy service này trên thiết bị thật, chúng ta phải chỉnh sửa phần configurations của mục run, chọn Launch OptionsNothing
    • Khi service đã được chạy thành công từ Android Studio, chúng ta phải vào phần Settings -> Additional settings -> Accessibility, chọn đến phần Downloaded Apps chúng ta sẽ thấy tên service của chúng ta (HelperService), ấn chọn vào service và bật service lên. Giao diện các chức năng hỗ trợ của chúng ta sẽ hiển thị lên.
    • Demo trên thiết bị thật

    5. Những hiểm nguy từ Accessibility Service

    • Accessibility Service được sinh ra với một mục đích rất tốt, đó là hộ trỡ những người khuyết tật hoặc những người hạn chế, khó trong các vận động. Tuy nhiên với các khả năng có thể đọc được các dữ liệu trên màn hình, điều khiển điện thoại,… nếu như chúng được sử dụng vào mục đích xấu thì sẽ rất dễ gây nguy hiểm, rủi ro bảo mật thông tin đến cho chủ sở hữu điện thoại.

    Để phòng tránh những rủi ro tiềm ẩn này thì chúng ta nên có những biện pháp phòng tránh như là:

    • Chỉ cài đặt các ứng dụng trên các chợ chính chủ của các hệ điều hành. Accessibility Service được đánh giá là một quyền nguy hiểm. Vậy nên việc kiểm duyệt các ứng dụng này diễn ra rất nghiêm ngặt.
    • Không tải/ cài app từ các nguồn không chính thống, trên các đường link lạ
    • Với các ứng dụng có yêu cầu quyền này, hãy đọc kỹ các điều khoản dịch vụ.

    Author: LamNT59
    References: https://codelabs.developers.google.com/codelabs/developing-android-a11y-service#0
    Github Demo: https://github.com/lamdev99/HelperService

  • SCD (Slowly Changing Dimension) là gì? Các loại SCD và ví dụ cụ thể

    SCD (Slowly Changing Dimension) là gì? Các loại SCD và ví dụ cụ thể

    Xin chào mọi người, ở bài viết trước mình đã chia sẻ về ETL testing và trong quá trình test ETL mình có nhắc đến việc chúng ta sẽ đi 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? Vậy cách ghi/thay đổi dữ liệu đó là gì nhỉ? Hôm nay chúng ta sẽ đi tìm hiểu về Slowly Changing Dimension (SCD) nhé!


    1. Slowly Changing Dimension (SCD) là gì?

    image

    SCD (Slowly Changing Dimension) có thể hiểu một cách đơn giản nhất đó là: so sánh dữ liệu nguồn với dữ liệu bảng đích hiện có bằng cách sử dụng Khóa nghiệp vụ – Business key (Khóa duy nhất – Unique Key).

    Nếu không có bản ghi nào khớp thì sẽ coi là Bản ghi mới hoặc Nếu bản ghi khớp thì sẽ so sánh các thuộc tính với các thuộc tính đã thay đổi nếu dữ liệu có vẻ được cập nhật thì nó cập nhật bản ghi hoặc nếu không thì nó để nguyên như không thay đổi.

    Slowly Changing Dimension sẽ kiểm tra các thuộc tính cho ba trường hợp: Bản ghi mới, đã thay đổi hoặc chưa thay đổi.

    2. Tại sao cần Slowly Changing Dimension?

    Trong thế giới Datawarehouse, đôi khi việc theo dõi sự thay đổi kích thước theo thời gian là rất quan trọng. Điều này giúp chúng ta theo dõi dữ liệu tốt hơn và cũng tạo ra các sản phẩm hiệu quả tùy thuộc vào các trường hợp sử dụng.

    3. Tại sao lại gọi là Slowly Changing Dimension?

    Slowly Chaning Dimension – gọi như trên có nghĩa là chúng ta sẽ phải sử dụng thành phần này chỉ cho các bảng không được cập nhật thường xuyên.

    Lưu ý: Không áp dụng cho bảng thường xuyên thay đổi, chỉ áp dụng trên các bảng kích thước (Dimension table) thay đổi chậm.

    4. Các loại SCD – Ví dụ về Slowly Changing Dimensions trong data warehouse (High-Level)

    Các loại SCD (High-Level)

    Sau đây là các loại SCD, mỗi loại đều có một số ưu điểm và nhược điểm riêng.

    1. Type – 0 Giữ lại dữ liệu gốc
    2. Type – 1 Ghi đè lên dữ liệu hiện có
    3. Type – 2 Thêm các bản ghi mới trên cùng một bảng
    4. Type – 3 Thêm cột mới trên cùng một bảng
    5. Type – 4 Sử dụng bảng lịch sử
    6. Type – 6 Phương pháp kết hợp (Loại 1 + Loại 2 + Loại 3)

    image

    SCD type 1 (Ghi đè)

    Loại hành động SCD đầu tiên có thể thực hiện được là ghi đè. Ở đây, các giá trị kích thước được ghi đè bởi các giá trị mới.

    Ví dụ: Khách hàng Nguyễn Văn A chuyển từ HCM đến Hà Nội, thì thành phố của anh ấy sẽ được cập nhật với giá trị mới nhất, tức là Hà Nội

    Original Record – Dữ liệu gốc

    Cust_ID Name City
    1001 Nguyễn Văn A HCM
    1002 Nguyễn Văn B Nam Định

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name City
    1001 Nguyễn Văn A Hà Nội
    1002 Nguyễn Văn B Nam Định

    Trong ví dụ trên, khách hàng đã di chuyển từ nơi này sang nơi khác và địa chỉ gần đây đã được ghi đè lên các bản ghi hiện có.

    Nhược điểm: chúng ta không thể truy xuất thông tin địa chỉ trước đây của anh ấy từ tình huống này.

    SCD type 2

    • Thêm bản ghi mới
    • Chúng ta có thể nắm bắt thay đổi thuộc tính bằng cách thêm một cột mới làm khóa thay thế (VD: IsActive)

    Khi giá trị của bản ghi hiện tại thay đổi, bản ghi hiện tại được đánh dấu là không hoạt động (inactive – 0) và bản ghi mới được insert vào.

    Kết quả, sẽ có 2 bản ghi được liên kết với Nguyễn Văn A trong bảng được cập nhật, nhưng chỉ có phiên bản mới nhất được đánh dấu là hoạt động (active – 1).

    Original Record – Dữ liệu gốc

    Cust_ID Name City IsActive
    1001 Nguyễn Văn A HCM 1
    1002 Nguyễn Văn B Nam Định 1

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name City IsActive
    1001 Nguyễn Văn A HCM 0
    1001 Nguyễn Văn A Hà Nội 1
    1002 Nguyễn Văn B Nam Định 1

    Ưu điểm: thỏa mãn điểm trừ trước đó theo dõi dữ liệu lịch sử bằng cách tạo mục nhập mới trên cùng một bảng.

    Nhược điểm: mặc dù nó nắm bắt dữ liệu lịch sử, nhưng nó có thể dẫn đến hoạt động tốn kém ở phía cơ sở dữ liệu.

    SCD Type 3 (Thêm cột giá trị trước đó)

    Loại SCD phổ biến thứ ba là thêm một cột giá trị trước đó. Ở đây, các phiên bản trước và hiện tại được duy trì trong một hàng.

    Hạn chế của phương pháp này là nó sẽ chỉ có hiện tại/trước đó chứ không phải toàn bộ lịch sử

    Original Record – Dữ liệu gốc

    Cust_ID Name City
    1001 Nguyễn Văn A HCM
    1002 Nguyễn Văn B Nam Định

    Updated Record – Dữ liệu được thay đổi

    Cust_ID Name Current City Previous City
    1001 Nguyễn Văn A Hà Nội HCM
    1002 Nguyễn Văn B Nam Định

    SCD Type 4: Thêm bảng mới (Bảng lịch sử)

    • Sử dụng bảng Lịch sử
    • Trong cách tiếp cận này, bảng lịch sử riêng biệt được tạo ra để theo dõi các thay đổi.
    • Bảng chính sẽ chỉ có dữ liệu mới nhất
    • Ưu điểm: Phản hồi nhanh hơn đối với các truy vấn yêu cầu dữ liệu mới nhất. Dễ quản lý và viết mã, thuận lợi cho các thuộc tính có tính biến động cao hoặc được sử dụng thường xuyên ở kích thước rất lớn.
    • Nhược điểm: Đôi khi tổng hợp/tham gia giữa dữ liệu hoạt động và lịch sử có thể mất thời gian và trở nên phức tạp

    Customer Table

    Cust_ID Name City
    1001 Nguyễn Văn A Hà Nội
    1002 Nguyễn Văn B Nam Định

    Customer History Table

    Cust_ID Name City Last_updated_date
    1001 Nguyễn Văn A HCM 11-03-2023
    1001 Nguyễn Văn A Hà Nam 11-05-2023
    1001 Nguyễn Văn A Bắc Ninh 11-06-2023

    SCD Type 6

    Phương pháp kết hợp (Loại 1 + Loại 2 + Loại 3)

    • Ưu điểm: Mọi bản chụp thay đổi dữ liệu đều có trong cùng một bảng
    • Nhược điểm: Phức tạp, khó quản lý, bảng lớn.
    Cust_ID Name City EffectiveFrom EffectiveTo IsActive
    1001 Nguyễn Văn A HCM 11-03-2023 11-05-2023 0
    1001 Nguyễn Văn A Hà Nam 11-05-2023 11-06-2023 0
    1001 Nguyễn Văn A Bắc Ninh 11-06-2023 03-07-2023 0
    1001 Nguyễn Văn A Hà Nội 03-07-2023 1
    1002 Nguyễn Văn B Nam Định 22-02-2023 1

    Ở ví dụ trên, dữ liệu lịch sử được theo dõi trong hàng mới được xác định bởi các trường ngày bắt đầu và ngày kết thúc. Ngoài ra, chúng ta có thể có trường cờ isActive để xác định bản ghi hiện tại từ danh sách. Chúng ta có thể duy trì lịch sử của tất cả các thay đổi đồng thời cập nhật giá trị hiện tại trên các bản ghi hiện có.


    Trên đây là những kiến thức về SCD (Slowly Changing Dimension), Các loại SCD và kịch bản kiểm thử, bài viết cũng khá dài rồi, nếu có cơ hội, bài viết tới mình sẽ chia sẻ chi tiết hơn về SCD type 2. Mong rằng bài viết này sẽ giúp ích được cho mọi người trong quá trình tìm hiểu về SCD. 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ài liệu tham khảo:

    https://www.expressanalytics.com/blog/what-is-a-slowly-changing-dimension-and-the-logic-in-implementation/

    https://www.learnmsbitutorials.net/slowly-changing-dimensions-ssis.php

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

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

  • IBInspectable and IBDesignable in Swift

    IBInspectable and IBDesignable in Swift

    Xin chào mọi người, bài viết này mình xin giới thiệu với các bạn về IBInspectable và IBDesignable trong swift.

    IBInspectable

    Khi các bạn thực hiện code UI bằng Interface builder của Xcode, nó sẽ hiển thị cho các bạn một số các thuộc tính cơ bản để các bạn có thể chỉnh sửa. Hình dưới đây là Atributes Inspector của UIView.

    Inspectable-and-IBDesignable

    Có bao giờ bạn muốn thêm các thuộc tính của một UI trong tab Attributes Inspector chưa? Nếu bạn có ý định này thì xin chúc mừng. IBInspectable sẽ giúp bạn làm được việc này.

    IBInspectable giúp cho bạn có thể thêm được rất nhiều các thuộc tính vào tab Attributes Inspector từ đó giúp các bạn dễ dàng chỉnh sửa nó trên Interface builder của Xcode một cách dễ dàng.

    Vậy để sử dụng IBInspectable thêm các thuộc tính vào Interface builder của xCode chúng ta làm như sau:

    Ở đây mình sẽ làm một ví dụ để thêm thuộc tính cho UIView

    Như bạn đã biết thì UIView trên Interface builder không có các thuộc tính như cornerRadius(bo góc), borderColor(màu viền), borderWidth(độ rộng viền)… Vậy trong ví dụ này mình sẽ thêm các thuộc tính này vào Attributes inspector của UIView.

    Đầu tiên mình tạo một class CommonView kế thừa lại UIView như sau:

    class CommonView: UIView {
        // thêm thuộc tính để bo góc cho View
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        // thêm thuộc tính để đặt độ dày của viền cho View
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        // thêm thuộc tính để sửa màu viền cho View
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    Để sử dụng CommonView thì chúng ta mở file Storyboard hoặc file xib lên và kéo một UIView vào, sau đó đổi class từ UIView(mặc định) sang CommonView, vậy là xong.

    Kết quả chúng ta sẽ được như sau:

    Inspectable-and-IBDesignable
    Các thuộc tính Corner Radius, Border Witdh, Border color đã được thêm vào Attributes Inspector băng thuộc tính @IBInspectable

    Vậy là chúng ta đã thêm được các thuộc tính vào Attributes Inspector của Xcode, tuy nhiên chúng ta cần phải build app lên thì mới thấy sự thay đổi. Sao nó không thay đổi ngay khi chúng ta sửa giá trị như các thuộc tính khác? Vì một mình IBInspectable thì không làm được vì vậy các nhà phát triển của Apple mới đẻ ra IBDesignable để làm việc này.

    IBDesignable

    IBDesignable cho phép chúng ta xem trực tiếp các thay đổi của view trong storyboard hoặc trong file xib mà không cần phải run ứng dụng.

    Để sử dụng IBDesignable thì chúng ta chỉ cần thêm @IBDesignable vào đằng trước class mà chúng ta muốn và override lại func prepareForInterfaceBuilder() để nó update giá trị và hiển thị lên trên Interface builder, trong ví dụ này mình để nó ở trước class CommonView của mình như sau:

    @IBDesignable
    class CommonView: UIView {
        // set giá trị để hiển thị cho Interface builder
        override func prepareForInterfaceBuilder() {
            setupView()
        }
        // setup view
        private func setupView() {
            self.layer.cornerRadius = cornerRadius
            self.layer.borderWidth = borderWidth
            self.layer.borderColor = borderColor.cgColor
        }
    
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    CHÚ Ý: Bạn cần phải override lại func prepareForInterfaceBuilder() và set lại các thuộc tính để nó có thể update giá trị cho interface builder.

    Bây giờ chúng ta chỉ cần kéo UIView vào là nó sẽ tự apply các thuộc tính và khi sửa tại Attributes inspector thì nó sẽ được update ngay mà không cần phải build ứng dụng để kiểm tra lại UI.

    Kết quả chúng ta được như hình dưới đây:

    IBInspectable and IBDesignable uiview

    Trong trường hợp các bạn muốn làm common và không cho sửa thuộc tính nào trên interface builder thì bạn chỉ cần bỏ IBInspectable của thuộc tính đó đi là được.

    Tổng kết

    Vậy là mình đã giới thiệu cho các bạn một phương pháp để thực hiện làm common rất hiệu quả và tiết kiệm thời gian khi làm ứng dụng di động trên iOS. Từ ví dụ common view này chúng ta có thể phát triên cho các common khác như UILabel, UIButton … Mình hi vọng bài viết sẽ giúp ích cho các bạn trong quá trình học hỏi và phát triển ứng dụng iOS.