Tag: android

  • Giúp background của View co giãn tối ưu với 9-patch image trong Android

    Giúp background của View co giãn tối ưu với 9-patch image trong Android

    Đã bao giờ bạn phải xử lý một Dialog, Button hoặc 1 cái CardView design đặc biệt tràn đầy graphical art các kiểu con đà điểu chưa? Có thể bạn nhìn file design một lúc và export nguyên cái ảnh ra để rồi nhận ra nếu nhiều chữ hơn hoặc màn hình bé hơn to hơn -> ảnh vỡ, code của bạn tèo ?

    pain...
    không chỉ ở làng Mưa, mà trong file design cũng có pain…

    Và rồi bạn đành phải work around, tự cắt các thành phần của một design ra nhiều phần nhỏ và dùng ImageView ghép lại theo định nghĩa phần có thể giãn và phần không cần giãn. Well, code bạn vẫn chạy ổn, nhưng chưa phải tối ưu.

    Để tạo background cho những component mang tính graphical như vậy, chúng ta đã 9-patch image hỗ trợ.

    9-patch Image là gì cơ?

    9-patch image là drawable dạng ảnh png đặc biệt có thể co giãn theo một định nghĩa nằm trong file này. Loại drawable này thường được sử dụng để làm background cho các View, Android OS sẽ xử lý background dạng 9-patch co giãn theo định nghĩa của file thay vì giãn đều như drawable bình thường. File 9-patch sẽ có đuôi 9.png.
    Btw, 9-patch là 9 mảnh vá đó ? (trích của anh Nguyen Van Toan)

    Cấu trúc của ảnh 9-patch

    Như ảnh bạn có thể thấy 9-patch image được chia làm 2 thành phần:

    • Stretchable area
    • Fill area (Padding box)

    Stretchable area là phần mà ảnh có thể được giãn nếu cần thiết.
    Fill area là phần mà content của bạn được phép nằm trong đó (Vì thế nên còn gọi là Padding box đó hehe).

    Ngoài ra thì 9-patch image còn hỗ trợ Optical Bounds. Optical bounds trên 9-patch image sẽ hiển thị bằng đường kẻ màu đỏ.

    Như bạn thấy, khi định nghĩa chuẩn, View của bạn sẽ được co giãn một cách tối ưu.

    Nghe hay phết, vậy tạo 9-patch như thế nào z bro?

    Cách tạo 9-patch image

    Để tạo ra 9-patch image, Android Studio đã có sẵn tool support đầy đủ, bạn không cần tải thêm bất cứ thứ gì khác. Ngoài ra thì cũng có cách khác là sử dụng Simple nine-patch generator trong bộ tools online của Roman Nurik.

    Tạo bằng Android Studio

    Bước 1: Chúng ta click chuột phải vào ảnh PNG gốc, chọn Create 9-Patch file…

    Bước 2: bắt đầu tuỳ chỉnh các phẩn Stretchable area và Fill area cho hợp lý

    Bước 3: Sử dụng file 9-patch như một drawable bình thường (không cần ghi đuôi .9)

    Tạo bằng Simple nine-patch generator

    Để hỗ trợ các density khác nhau của các device Android khác nhau, chúng ta cần tạo đủ các resources như drawable-ldpi, drawable-mdpi, drawable-hdpi, drawable-xhdpi… nên nếu tạo bằng tay từng cái một có thể sẽ là 1 đống việc. Nên chúng ta dùng Tool!

    Bước 1: Mở trang web Simple nine-patch generator chọn Select image và chọn đúng loại desity của source ảnh bạn chọn.

    Bước 2: Chỉnh Stretchable area và Fill area cho hợp lý. Nếu bạn sử dụng Optical bounds thì nó sẽ là viền đỏ trên file 9-Patch.

    Bước 3: Tải ảnh về khi đã edit xong, chúng ta sẽ click vào icon download ở góc phải trên cùng để tải về.

    Bước 4: Sử dụng file 9-patch như một drawable bình thường (không cần ghi đuôi .9)

    Việc tạo bằng tool sẽ giúp chúng ta tinh chỉnh dễ dàng hơn, đỡ phải take time tạo từng 9-patch image khác nhau cho các density mà chưa chắc đã có độ nhất quán tuyệt đối. Nhưng bên cạnh đó lại có một nhược điểm là bạn chỉ có thể tạo một Stretchable area trong một drawable. Android studio thì lại giúp bạn tuỳ ý tạo các Stretchable area khác nhau.

    Summary

    Để styling trong Android thì muôn vàn cách, 9-patch chỉ là một trong số cách đó. Trên đây chỉ là một vài ví dụ cơ bản về việc sử dụng 9-patch. Mong rằng bài viết đã giúp bạn có thêm một kiến thức mới – phép thuật mới cho chặng đường của một phù thủy Android ??

  • Delegate trong Kotlin

    Delegate trong Kotlin

    Delegate là gì nhỉ? Tại sao lại là Delegate? Đúng như cái tên, Delegate là một design pattern mà bạn ủy quyền xử lý logic của Class hiện tại cho một Object/Class khác. Delegate thường được sử dụng để tách logic code theo việc của nó (separate concerns) hoặc common hóa một đoạn logic.
    Bài viết này sẽ giúp bạn tìm hiểu cơ bản về Delegate trong Kotlin và cách sử dụng khái niệm này. Trong Kotlin, có 2 cách để sử dụng Delegate
    – Interface/Class Delegation
    – Delegate Properties

    Delegate là cách bạn cho phép một object khác xử lý một logic cho object hiện tại

    Interface/Class Delegation

    Với cách thứ nhất, chúng ta sẽ sử dụng một interface làm abstract cho một object và truyền object đó vào phần khai báo implement thông qua keyword by. Bằng cách này, các abstract methods (method của interface) sẽ chạy code của delegating object!

    interface CameraOptimization {
        fun optimize()
    }
    
    object XiaomiDevicesOptimization : CameraOptimization {
        override fun optimize() {
            TODO("do something for Xiaomi devices")
        }
    
    }
    
    object DefaultDevicesOptimization : CameraOptimization {
        override fun optimize() {
            TODO("do something for others devices")
        }
    }
    
    class CameraManager(optimization: CameraOptimization) : CameraOptimization by optimization {
        fun cameraFocus() {
            //todo: focus camera
        }
    }

    Đây là cách setup cơ bản của Interface/Class Delegation. Như các bạn thấy thì implementation của method optimize() không trực tiếp xuất hiện ở trong class CameraManager mà sẽ được delegate đến object truyền vào bằng keyword by.

    class CameraActivity : BaseActivity() {
        private lateinit var _cameraManager: CameraManager
    
        private fun initView() {
            val vendor = android.os.Build.MANUFACTURER
            val config = when {
                vendor.equals("Xiaomi", ignoreCase = true) -> XiaomiDevicesOptimization
                else -> DefaultDevicesOptimization
            }
            _cameraManager = CameraManager(config)
            _cameraManager.optimize()
            _cameraManager.cameraFocus()
        }
    }


    Khi method optimize() được gọi, nó sẽ delegate đến Config của XiaomiDevicesOptimization hoặc DefaultDevicesOptimization tùy theo device đó là gì. Với cách tiếp cận này logic của CameraManager vẫn có khả năng tối ưu mà không cần phải quan tâm rằng nó sinh ra cho vendor cụ thể nào cả. Đồng thời cũng tăng khả năng mở rộng của Class này hơn. Nếu app của bạn quyết định support optimize thêm cả anh zai Samsung cũng oke luôn, code thêm 1 class và 1 dòng duy nhất.

    Delegate Properties

    Chắc bạn đã từng sử dụng rất nhiều lần lazy trong kotlin rồi đúng không? Delegate đó :v Delegate Properties là việc bạn implement operator getValue (có thể thêm cả setValue luôn nếu bạn muốn nó set được cả value) của một class. Hoặc một cách khác tường mình hơn là implement interface ReadWriteProperty/ReadOnlyProperty. Class đó sẽ trở thành Delegate. Vẫn là keyword by, chúng ta khai báo một biến với Delegate thông qua by.

    class IntNaturalSet : ReadWriteProperty<Any, Int> {
        private var _value: Int = 0
        override fun getValue(thisRef: Any, property: KProperty<*>): Int {
            return _value
        }
    
        override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
            _value = if(value < 0) 0 else value
        }
    }

    Trong ví dụ đơn giản này chúng ta đã ủy quyền getter setter của biến cho class IntNaturalSet can thiệp và xử lý logic. Bên cạnh việc ủy quyển xử lý logic getter (setter) thì Delegate cũng có thể access đến Class chứa biến được delegate thông qua param thisRef(Chính là generic T trong ReadWriteProperty/ReadOnlyProperty). Có thể là built-in delegate cho một Type hoặc ép kiểu thisRef trong logic của getter setter để có thể sử dụng param này.

    Ứng dụng của Delegate Properties rất rộng, chúng ta có thể custom cho logic in/out data như shared preferences, cache…
    Trong Androidx/Kotlin cơ bản cũng có vài delegate như lazy, viewModels, activityViewModels…

    Summary

    Delegate là một phương pháp khá hay trong lập trình giúp chúng ta tối ưu logic source code. Một source base có thể sẽ clean hơn nếu rút gọn các common logic hay boilerplate code. Một class có thể tăng tính mở rộng trong tương lai. Một project có thể sẽ triển khai nhanh hơn nhờ những common và khả năng scalable tốt. Lần tới nếu như bạn gặp phải một vấn đề có thể xử lý bằng Delegate, cứ thử xem sao nhé!
    Hi vọng bài viết giúp bạn có thêm chút kiến thức trong chặng đường của một Engineer :3!

  • 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

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

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

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

    Chào mọi người, hôm nay mình xin chia sẻ về một chủ đề khá thông dụng trong Android, đó là Context. Mục đích của bài này đâu đấy các bạn mới tiếp cận với Android có thể hiểu đúng bản chất của Context, các loại Context và quan trọng hơn là cách cân nhắc sử dụng chúng trong từng trường hợp một cách hợp lý và chính xác nhất, tránh gây ra những lỗi không mong muốn.

    1. Khái niệm

    Mình xin trích dẫn một đoạn trong Official Document Android về Context

    Mình xin trích dẫn một đoạn trong Official Document Android về Context

    Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.

    Các bạn cũng có thể hiểu cơ bản thế này

    Context là trạng thái của ứng dụng tại một thời điểm nhất định.

    Context là 1 lớp cơ bản chứa hầu hết các thông tin về môi trường ứng dụng của android, tức là mọi thao tác, tương tác với hệ điều hành đều phải thông qua lớp này.

    Context là 1 abstract class, nó cung cấp cho các lớp triển khai các phương thức truy cập vào tài nguyên của ứng dụng và hệ thống. Ví dụ như nó có thể khởi tạo và chạy các activities, broadcast, các intents….

    Nào giờ chúng ta xem xét 3 functions hay được sử dụng nhiều nhất để truy xuất Context:

    • getContext() — trả về Context được liên kết với Activity được gọi.
    • getApplicationContext() — trả về Context được liên kết với Application chứa tất cả các Activity đang chạy bên trong nó.
    • getBaseContext() — có liên quan đến ContextWrapper, được tạo xung quanh Context hiện có và cho phép thay đổi hành vi của nó. Với getBaseContext() chúng ta có thể lấy Context hiện có bên trong lớp ContextWrapper.

    Trong số này nổi bật hơn cả là getContext()getApplicationContext(). Liên quan đến getBaseContext(), có một lưu ý nhỏ là tránh sử dụng loại Context này – lớp này được triển khai khi một class extends từ ContextWrapper. Mà lớp này lại có khoảng 40 lớp con trực tiếp và không trực tiếp. Vì vậy, nên gọi trực tiếp đến getContext, Activity, Fragment… để tránh gây ra memory leak.

    2. Các loại truy cập Context

    2.1. getContext()

    Trong getContext(), Context được gắn với một Activity và vòng đời của nó. Chúng ta có thể hình dung Context là layer đứng sau Activity và nó sẽ tồn tại chừng nào Activity còn tồn tại. Thời điểm Activity chết, Context cũng vậy.

    Dưới đây là danh sách các chức năng mà Activity’s Context cung cấp cho chúng ta:

    Load Resource Values,
    Layout Inflation,
    Start an Activity,
    Show a Dialog,
    Start a Service,
    Bind to a Service,
    Send a Broadcast,
    Register BroadcastReceiver.
    

    2.2. getApplicationContext()

    Trong getApplicationContext(), Context của chúng ta được gắn với ứng dụng và vòng đời của nó. Chúng ta có thể coi nó như một lớp đằng sau toàn bộ ứng dụng. Miễn là người dùng không tắt ứng dụng, thì nó vẫn tồn tại.

    Bây giờ bạn có thể tự hỏi, đâu là sự khác biệt giữa getContext() và getApplicationContext(). Sự khác biệt là Context của ứng dụng không liên quan đến giao diện người dùng. Điều đó có nghĩa là, chúng ta không nên sử dụng nó để Inflate một Layout, start một Activity cũng như Dialog. Về phần còn lại của các chức năng từ Context của Activity, chúng cũng có sẵn trong Application Context. Vì vậy, danh sách các chức năng cho Application Context bao gồm như sau:

    Load Resource Values,
    Start a Service,
    Bind to a Service,
    Send a Broadcast,
    Register BroadcastReceiver.
    

    3. Cách sử dụng hợp lý Context trong Android

    Việc sử dụng Context phù hợp trong Android là rất quan trọng để đảm bảo ứng dụng của bạn hoạt động bình thường và tránh các sự cố như memoryleak hoặc sự cố tài nguyên. Dưới đây là một số nguyên tắc giúp bạn sử dụng ngữ cảnh phù hợp:

    • Chọn Context cụ thể theo ngữ cảnh: Chọn Context cung cấp phạm vi cần thiết cho thao tác bạn đang thực hiện. Ví dụ: nếu bạn đang tạo thành phần giao diện người dùng trong một Activity, hãy sử dụng Activity Context thay vì Application Context.
    • Lưu ý đến Lifespan của Context: Xem xét vòng đời của thành phần yêu cầu Context. Đối với các hoạt động tồn tại trong thời gian ngắn, hãy sử dụng Context phù hợp với Lifespan của Component. Ví dụ: nếu bạn cần Context trong phương thức onReceive() của BroadcastReceiver, hãy sử dụng tham số Context được cung cấp thay vì Context tồn tại lâu dài như Application Context.
    • Tránh sử dụng Context hoạt động ngoài vòng đời của nó: Hãy thận trọng khi sử dụng Context hoạt động bên ngoài vòng đời của Activity, vì nó có thể dẫn đến memoryleak. Chẳng hạn, nếu bạn lưu trữ tham chiếu đến ngữ Activity Context và sử dụng nó sau khi Activity đã bị hủy, thì điều đó có thể gây ra sự cố. Trong những trường hợp như vậy, hãy chuyển sang sử dụng Application Context để thay thế.
    • Lưu ý về bộ nhớ: Tránh lưu trữ các tham chiếu lâu dài đến các Context một cách không cần thiết, vì chúng có thể giữ một tham chiếu đến toàn bộ Activity và Application và ngăn các tài nguyên bị thu gom rác. Nếu bạn cần một Context trong một Component tồn tại lâu dài, chẳng hạn như một singleton, hãy ưu tiên sử dụng Application Context.
    • Cân nhắc sử dụng Dependency Injection: Việc sử dụng các Dependency Injection như Dagger, Hilt hoặc Koin có thể đơn giản hóa việc quản lý Context bằng cách cung cấp ngữ cảnh phù hợp khi cần. Nó giúp tách rời các thành phần và đảm bảo Context chính xác được đưa vào dựa trên phạm vi và yêu cầu của hoạt động.

    4. Kết luận

    Việc lựa chọn Context phụ thuộc vào các yêu cầu cụ thể của mã của bạn và thành phần bạn đang xử lý. Điều quan trọng là chọn Context phù hợp để tránh memoryleak, sự cố tài nguyên hoặc sự không nhất quán trong ứng dụng của bạn. Theo nguyên tắc chung, hãy cố gắng sử dụng bối cảnh hạn chế nhất có thể để hoàn thành nhiệm vụ hiện tại, đảm bảo rằng nó có phạm vi và Lifespan cần thiết cho hoạt động.

    Mong bài viết này sẽ giúp ích ít nhiều cho các bạn. Hẹn gặp lại mọi người trong bài viết tiếp theo.

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

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

    Chào mọi người, trong lập trình Android, khi gặp một yêu cầu liên quan đến delay hoặc dừng một task vụ sau một khoảng thời gian chờ nhất định, chúng ta thường nghĩ đến 2 phương án là Thread.sleep() và kotlinx.coroutines.delay().

    Vậy 2 cơ chế này có gì khác nhau và khi nào mình nên sử dụng chúng. Hôm nay mình sẽ xin chia sẻ tới mọi người ý hiểu của mình.

    1. Khái niệm

    kotlinx.coroutines.delay() và Thread.sleep() đều là cơ chế giới thiệu độ trễ hoặc tạm dừng trong quá trình thực thi chương trình, nhưng chúng được sử dụng trong các ngữ cảnh khác nhau và có các đặc điểm khác nhau.

    1.1. kotlinx.coroutines.delay()

    kotlinx.coroutines.delay() là một suspending function được cung cấp bởi thư viện Kotlin coroutines. Nó thường được sử dụng trong các asynchronous programming khi làm việc với các coroutine, nơi bạn muốn tạm dừng việc thực thi một coroutine mà không chặn luồng chính.

    1.2. Thread.sleep()

    Thread.sleep() là một static method được cung cấp bởi lớp Thread trong Java (cũng có thể sử dụng được trong Kotlin) và được sử dụng để tạm dừng quá trình thực thi của luồng hiện tại.

    Thread.sleep() không chỉ tạm dừng việc thực thi một coroutine mà còn chặn toàn bộ luồng, nghĩa là các luồng khác trong ứng dụng sẽ không thể tiếp tục công việc của chúng trong suốt thời gian Sleep.

    2. Ví dụ triển khai

    2.1. kotlinx.coroutines.delay()

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

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

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

    Tiếp đó là Job 0 -> Job 1 -> Job 2, 3 jobs đều khởi động cùng một lúc và bị delay với thời gian tương ứng trong từng job.

    Tiếp theo, task có delay() time ngắn nhất thì chạy xong trước, theo sau là các công việc tiếp theo hoàn thành. Độ trễ dài nhất là 2s.

    Kết quả là:

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

    or

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

    Nhìn vào kết quả, tại sao là 2.1s mà không phải là delay() time dài nhất 2.0s. Vì các bạn có thể thấy khoảng time từ khi Main Job bắt đầu đến khi delay của Job1 là 0.01s. Có một số lần chạy, khoảng time đó quá nhỏ nên kết quả có thể là 2s.

    2.2. Thread.sleep() on Dispatchers.Main

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

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

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

    Tiếp đó là Job 0 -> Job 1. Job sẽ bị suspended. Tuy nhiên Thread.sleep(2000) chạy ở Job 1, nó sẽ block Main Thread trong 2s. Job2 trong thời gian dó không thực thi gì cả.

    Sau 2s, D sễ được in ra, tiếp đó là E của Job2. Job 2 sẽ bị suspend. Vì Main Job và Job 1 có suspend time nhỏ hơn 2s, vậy nên sau đó nó sẽ được in ra ngay lập tức. Lúc này Job 0 sẽ được chạy trước vì có time delay bé hơn.

    Cuối cùng là 0.5s, Job2 sẽ tiếp tục chạy và hoàn thành in ra F.

    Kết quả là: G->A->C->D->E->B->H->F->2.5s

    Timestamp #1 (sau 0 second)

    • Main job và Job 0 start and suspended.

    • Job 1 start và blocks Thread

    Timestamp #2 (sau 2 seconds)

    • Job 1 hoàn thành

    • Job 2 start and suspended.

    • Job 0 và Main Job tiếp tục và hoàn thành.

    Timestamp #3 (after 0.5 seconds)

    • Job 3 tiếp tục và hoàn thành.

    Khoảng thời gian tổng dao động trong 2.5s.

    2.3. Thread.sleep() on Dispatchers.Default/IO or Dispatchers.Default/Default

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

    Kết quả là:

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

    Kết quả khá tương tự với ví dụ sử dụng kotlinx.coroutines.delay()

    Khi Dispatchers.Default hoặc Dispatchers.IO được sử dụng, nó được hỗ trợ bởi một nhóm luồng. Mỗi lần chúng ta gọi launch{}, một worker thread khác được created/used.

    Ví dụ, đây là các Worker thread đang được sử dụng:

    Main Job – DefaultDispatcher-worker-1

    Job 0 – DefaultDispatcher-worker-2

    Job 1 – DefaultDispatcher-worker-3

    Job 2 – DefaultDispatcher-worker-4

    Để xem luồng nào hiện đang chạy, bạn có thể sử dụng println("Run ${Thread.currentThread().name}")

    Vì vậy, Thread.sleep() thực sự chặn luồng đó, nhưng chỉ chặn DefaultDispatcher-worker-3. Các công việc khác vẫn có thể được tiếp tục chạy vì chúng nằm trên các luồng khác nhau.

    Timestamp #1 (after 0 second)

    • Main Job, Job 0, Job 1 và Job 2 start.

    • Main Job, Job 0 và Job2 bị suspended.

    • Job 1 bị block trên Thread của nó, ở đây là DefaultDispatcher-worker-3.

    Timestamp #2 (after 0.5 second)

    • Job 2 tiếp tục và hoàn thành

    Timestamp #3 (after 1 second)

    • Job 0 tiếp tục và hoàn thành

    Timestamp #4 (after 1.5 seconds)

    • Main Job tiếp tục và hoàn thành

    Timestamp #5 (after 2 seconds)

    • Job 1 tiếp tục và hoàn thành

    Bởi vì mỗi công việc chạy trên một luồng khác nhau, công việc có thể được bắt đầu vào những thời điểm khác nhau. Vì vậy, đầu ra của A, C, E, G có thể là ngẫu nhiên. Như vậy, bạn thấy trình tự các task có thể khác với trình tự trong Exampe 1 ở phần trên.

    3. Conclusion

    Thread.sleep() chặn luồng gọi nó còn kotlinx.coroutines.delay() thì không.

    Chỉ nên Thread.sleep() để kiểm tra xem tôi đã đặt đúng tác vụ chạy dài vào chuỗi nền chưa.

    Cuối cùng, kotlinx.coroutines.delay() là phương pháp được khuyến nghị để tạo độ trễ trong một coroutine mà không chặn luồng giao diện người dùng.

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

  • Android Room Database Tips

    Android Room Database Tips

    Chào mọi người, chắc hẳn trong chúng ta nếu triển khai database của Android trong thời điểm hiện tại, chúng ta sẽ nghĩ ngay đến việc sử dụng Room. Chính vì vậy, hôm nay mình xin chia sẻ một số Tips nhỏ trong việc sử dụng Room đến mọi người.

    1. Thiết lập ràng buộc giữa các Entities thông qua ForeignKey

    Mặc dù Room không hỗ trợ trực tiếp ràng buộc giữa các mối quan hệ, nhưng nó cho phép bạn xác định các ràng buộc Foreign keys giữa các Entities.

    Thông qua annotation @ForeignKey, một phần trong bộ annotation của @Entity, để cho phép sử dụng các tính năng khóa ngoại của SQLite. No không những giúp thể hiện được tốt hơn mối quan hệ giữa các Entities, đảm bảo đúng thiết kế mà còn thực thi các ràng buộc trên các bảng để đảm bảo mối quan hệ hợp lệ khi bạn sửa đổi cơ sở dữ liệu.

    Cụ thể chúng ta sẽ cùng đến một ví dụ cụ thể:

    image

    Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonIdDogId cùng với PersonId được sử dụng như là một foreign key.

    @Entity(tableName = “dog”,
            foreignKeys = arrayOf(
                ForeignKey(entity = Person::class,
                           parentColumns = arrayOf(“personId),
                           childColumns = arrayOf("owner"))))
    
    data class Dog(@PrimaryKey val dogId: String,
                  val name: String,
                  val owner: String)
    

    Theo tùy chọn, bạn có thể tuỳ chọn hành động sẽ được thực hiện khi đối tượng Parent Entity bị xóa hoặc cập nhật trong cơ sở dữ liệu.

    Bạn có thể chọn một trong các tùy chọn sau: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT hoặc CASCADE, tương tự sử dụng như trong SQLite.

    2. Tạo mối quan hệ Relation trong Room Database

    Vẫn là mối quan hệ 1-n ở ví dụ trước. Bây giờ mình muốn truy vấn thực hiện việc lấy dữ liệu của các Person và toàn bộ Dogs tương ứng kèm theo.

    image

    Cách thông thường để thực hiện, chúng ta sẽ cần thực hiện 2 truy vấn: một truy vấn để lấy danh sách tất cả Person và một truy vấn khác để lấy danh sách Dog dựa trên Id của Person. Cụ thể:

    @Query(“SELECT * FROM Person”)
    public List<Person> getPersons();
    
    @Query(“SELECT * FROM dog where owner = :personId”)
    public List<Dog> getDogsForPersons(String personId);
    

    Sau đây, mình sẽ triển khai theo cách thực hiện tạo mỗi quan hệ Relation giữa 2 đối tượng Person và Dog thông qua annotation @Relation

    class PersonAndDogs {
       @Embedded
       var person: Person? = null
       @Relation(parentColumn = “personId”,
                 entityColumn = “owner”)
       var dogs: List<Dog> = arrayListOf()
    }
    

    Trong DAO, chúng tôi chỉ thực hiện một truy vấn duy nhất và Rôm sẽ truy vấn cả bảng Person và Dog, tiếp đó xử lý mapping đối tượng.

    @Transaction
    @Query(“SELECT * FROM Person”)
    List<PersonAndDogs> getPersonAnDogs();
    

    3. Thực hiện câu lệnh trong một Transaction

    Khi một câu lệnh trong Room gắn với @Transaction, nó sẽ đảm bảo rằng tất cả các hoạt động cơ sở dữ liệu mà bạn đang thực hiện trong phương thức đó sẽ được chạy bên trong một transaction.

    Transaction sẽ thất bại khi một Exception trong một trong những truy vấn trong Transaction đó xảy ra.

    @Dao
    abstract class UserDao {
        
        @Transaction
        open fun updateData(users: List<User>) {
            deleteAllUsers()
            insertAll(users)
        }
    
        @Insert
        abstract fun insertAll(users: List<User>)
    
        @Query("DELETE FROM Users")
        abstract fun deleteAllUsers()
    }
    

    Bạn cũng có thể sử dụng @Transaction cho các phương thức @Query có câu lệnh chọn, trong các trường hợp sau:

    • Khi kết quả của truy vấn khá lớn. Bằng cách truy vấn cơ sở dữ liệu trong một giao dịch, bạn đảm bảo rằng nếu kết quả truy vấn không vừa với single cursor window, thì nó sẽ không bị hỏng do những thay đổi trong cơ sở dữ liệu giữa các lần cursor window swaps.

    • Khi kết quả của truy vấn là POJO với các trường @Relation. Các trường là các truy vấn riêng biệt nên việc chạy chúng trong một Transation sẽ đảm bảo kết quả nhất quán giữa các truy vấn.

    4. Tối ưu hoá đối tượng truy vấn

    Khi truy vấn cơ sở dữ liệu, bạn nên cân nhắc rằng có sử dụng tất cả các fields bạn trả về trong truy vấn của mình không?

    Quan tâm đến dung lượng bộ nhớ mà ứng dụng của bạn sử dụng và chỉ tải tập hợp con các trường mà bạn sẽ sử dụng. Điều này cũng sẽ cải thiện tốc độ truy vấn của bạn bằng cách giảm IO cost.

    Room sẽ thực hiện ánh xạ giữa các Columns và Objects cho bạn.

    Cùng xem một ví dụ sau:

    @Entity(tableName = "users")
    data class User(@PrimaryKey
                    val id: String,
                    val userName: String,
                    val firstName: String, 
                    val lastName: String,
                    val email: String,
                    val dateOfBirth: Date, 
                    val registrationDate: Date)
    

    Trên một số trường hợp cụ thể, các bạn không cần hiển thị tất cả thông tin này. Vì vậy, thay vào đó, chúng ta có thể tạo một đối tượng UserMinimal chỉ chứa dữ liệu cần thiết.

    data class UserMinimal(val userId: String,
                           val firstName: String, 
                           val lastName: String)
    

    Trong lớp DAO, bạn chỉ cần xác định truy vấn như sau.

    @Dao
    interface UserDao {
        @Query(“SELECT userId, firstName, lastName FROM Users)
        fun getUsersMinimal(): List<UserMinimal>
    }
    

    5. Khởi tạo trước dữ liệu cho Room Database

    Có rất nhiều trường hợp cụ thể mà bạn muốn thiết lập một bộ dữ liệu Default vào cơ sở dữ liệu trước. Khi đó bạn nên cân nhắc đến việc chèn dữ liệu ngay sau khi Room được khởi tạo.

    Cụ thể, bạn có thể cân nhắc sử dụng RoomDatabase#Callback! Gọi phương thức addCallback khi xây dựng RoomDatabase của bạn và ghi đè onCreate hoặc onOpen.

    • onCreate will be called when the database is created for the first time, after the tables have been created.
    • onOpen is called when the database was opened.
       companion object {
    
            @Volatile private var INSTANCE: DataDatabase? = null
    
            fun getInstance(context: Context): DataDatabase =
                    INSTANCE ?: synchronized(this) {
                        INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                    }
    
            private fun buildDatabase(context: Context) =
                    Room.databaseBuilder(context.applicationContext,
                            DataDatabase::class.java, "Test.db")
                            // prepopulate the database after onCreate was called
                            .addCallback(object : Callback() {
                                override fun onCreate(db: SupportSQLiteDatabase) {
                                    super.onCreate(db)
                                    // insert the data on the IO Thread
                                    ioThread {
                                        getInstance(context).dataDao().insertData(PRE_POPULATE_DATA)
                                    }
                                }
                            })
                            .build()
    
            val PRE_POPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
        }
    
    private val IO_EXECUTOR = Executors.newSingleThreadExecutor()
    
    /**
     * Utility method to run blocks on a dedicated background thread, used for io/database work.
     */
    fun ioThread(f : () -> Unit) {
        IO_EXECUTOR.execute(f)
    }
    

    Note: Các bạn chú ý thực hiện việc Insert default data trên một Worker Thread.

    Trên đây là một số Tips nhỏ trong việc sử dụng và triển khai Room Database. Mong là ít nhiều sẽ giúp ích cho mọi người. Cảm ơn mọi người đã theo dõi.

  • Flutter vs React Native vs Native: So sánh chi tiết hiệu năng

    Hãy so sánh hiệu năng FPS, CPU, Memory và GPU của các công cụ phát triển thiết bị di động phổ biến.

    Câu chuyện đằng sau việc nghiên cứu

    inVerita và nhóm phát triển mobile của mình liên tục nghiên cứu hiệu năng của các giải pháp mobile đa nền tảng hiện có để trả lời câu hỏi công nghệ nào tốt nhất Flutter hoặc React Native (hoặc Native) cho sản phẩm của bạn, đó là cách Flutter vs React Native vs Native Part I nổi lên. Điều đó gây ra nhiều tranh cãi vì người ta nói rằng không sử dụng React Native để thực hiện phép tính (perform multiple calculations) hàng ngày – có thể đúng như vậy – nhưng trong trường hợp này, các task nặng của CPU được ứng dụng Flutter hoặc Native thực hiện tốt hơn.

    Đó là lý do tại sao trong bài viết này, chúng tôi quyết định nghiên cứu hiệu năng của UI có tác động lớn hơn nhiều đến daily user của mobile app.

    Việc đo lường hiệu năng UI rất phức tạp và yêu cầu kỹ sư triển khai cùng chức năng theo cùng một cách trên mọi nền tảng. Chúng tôi đã sử dụng GameBench, công cụ kiểm tra toàn cầu để đảm bảo sự khách quan (nó không thay đổi sự thật là chúng tôi thực sự yêu thích Flutter ở nhiều khía cạnh 🙂 và vẫn chạy rất nhiều dự án React Native và Native). GameBench có rất nhiều không gian để cải tiến, nhưng chúng tôi đã cố gắng đưa mọi ứng dụng vào một môi trường single testing với sự trợ giúp của nó, đó là mục tiêu của chúng tôi.

    Source code mở vì vậy hãy thử nghiệm và chia sẻ suy nghĩ của bạn với chúng tôi nếu bạn muốn. UI animation chủ yếu sử dụng các công cụ khác nhau trên các nền tảng khác nhau, vì vậy chúng tôi thu hẹp mọi thứ vào các thư viện được hỗ trợ bởi mọi nền tảng (trừ một trường hợp) hoặc ít nhất chúng tôi đã làm mọi thứ để hoàn thành điều đó. Kết quả test có thể khác nhau và tùy thuộc vào phương pháp triển khai, chúng tôi tin rằng bạn có thể đẩy bộ tool đến giới hạn mà nó vượt trội so với các con số của chúng tôi. Bây giờ, chúng ta hãy xem xét các trường hợp.

    Thông tin thiết bị phần cứng:

    Đối với mục đích thử nghiệm của chúng tôi, chúng tôi đã sử dụng một chiếc Xiaomi Redmi Note 5 và iPhone 6s giá cả phải chăng.

    Repo link:

    Source code

    Use case 1 — List view benchmarking

    Chúng tôi đã triển khai cùng một UI trên cả Android và iOS ,sử dụng Native, React Native và Flutter. Chúng tôi cũng tự động hóa tốc độ scroll bằng cách sử dụng RecyclerView.SmoothScroller trên Android. Trên iOS và React Native, chúng tôi đã sử dụng cách tiếp cận với timer và lập trình scroll đến vị trí. Trên Flutter, chúng tôi đã sử dụng ScrollController để scroll qua danh sách một cách trơn tru. Trong mỗi trường hợp, chúng tôi có 1000 phần tử trong list view và cùng một thời gian scroll để đến element cuối cùng. Trong mỗi trường hợp này, chúng tôi đã sử dụng hình ảnh trong bộ nhớ đệm (image caching) với các lib khác nhau trên mỗi nền tảng. Xem thông tin chi tiết trong source code.

    iOS

    • Tải và lưu hình ảnh vào bộ nhớ đệm – Nuke

    Android

    • Tải và lưu hình ảnh vào bộ nhớ đệm – Glide

    React Native

    Android — GPU tests results are not supported by the benchmark (unfortunately, with the devices we have, and we have many:)) )

    Kết quả kiểm tra Android – GPU không được hỗ trợ bởi benchmark

    Kết quả kiểm tra
    1. Tất cả các thử nghiệm đều cho thấy FPS xấp xỉ như nhau.
    2. Android Native sử dụng một nửa memory so với Flutter và React Native.
    3. React Native yêu cầu khai thác CPU nhiều nhất. Lý do là việc sử dụng JSBridge giữa mã JS và Native kích động sự lãng phí tài nguyên khi serialization và deserialization.
    4. Về khai thác pin, Android Native có kết quả tốt nhất. React-native đang tụt hậu so với cả Android và Flutter. Chạy các animation liên tục sẽ tiêu tốn nhiều pin hơn trên React Native.

    Test trên iPhone 6s

    Kết quả kiểm tra
    1. FPS: Kết quả của React Native kém hơn so với Flutter và Swift. Lý do là không thể sử dụng biên dịch (compilation) IoT trên iOS.
    2. Memory: Flutter gần như khớp với nguyên bản (native) về mức tiêu thụ Memory nhưng vẫn nặng hơn trên CPU. React Native thua xa Flutter và native trong thử nghiệm này.
    3. Sự khác biệt giữa Flutter và Swift. Flutter đang tích cực sử dụng CPU khi iOS Native đang tích cực sử dụng GPU. Đối chiếu trong Flutter làm tăng tải trên CPU.

    Use case 2 — Heavy animations test

    Ngày nay hầu hết các điện thoại chạy trên Android và iOS đều có phần cứng rất mạnh. Trong hầu hết các trường hợp sử dụng các ứng dụng kinh doanh, có thể thấy không có sự sụt giảm số khung hình/giây nào. Đó là lý do tại sao chúng tôi quyết định thực hiện một số thử nghiệm với animation nặng. Đủ nặng để giảm số khung hình/giây. Chúng tôi đã sử dụng animation animated vector với Lottie trên Android, iOS, React Native và sử dụng các animation tương tự để sử dụng với Flare on Flutter.

    Thử nghiệm animation với Lottie cho Android, iOS, React Native và Flare cho Flutter.

    Lottie cho Android

    Android

    Kết quả kiểm tra
    1. Android và React Native có những điểm tương đồng về hiệu năng của chúng. Đó là điều hiển nhiên vì Lottie cho React Native sử dụng phương tiện Native (16–19% CPU, 30–29 FPS).
    2. Kết quả của Flutter là một bất ngờ, mặc dù nó có một chút trục trặc trong một performance. (12% CPU và 9 FPS).
    3. Android yêu cầu ít bộ nhớ nhất (205 Mb); React Native cần 280 Mb và Flutter cần 266 Mb.
    4. Khởi động lại app. Theo chỉ số này, Flutter là người dẫn đầu (2 giây). Đối với Android Native và React Native, mất khoảng 4 giây.

    Chúng tôi phát hiện ra rằng việc xóa một animation cụ thể khỏi lưới (grid) sẽ tăng FPS lên đến 40% trên Flutter. Chúng tôi cho rằng Flare nặng hơn và không được tối ưu hóa cho loại task này, đó là lý do tại sao Flutter lại bị sụt FPS như vậy.

    IOS

    Kết quả kiểm tra
    1. Kết quả của iOS và React Native trong bài kiểm tra này gần giống như Lottie đối với React Native.
    2. Flare và Flutter sẽ không ngừng khiến bạn ngạc nhiên. Flare chắc chắn có một con đường để đi 😀
    3. iOS Native yêu cầu ít bộ nhớ nhất (48 Mb). React Native cần 135 Mb và Flutter cần 117 Mb.
    4. Khởi động cold app. Theo chỉ số này, Flutter là người dẫn đầu (2 giây). Đối với iOS và React Native, mất khoảng 10 giây.

    Lưu ý: chúng tôi đã sử dụng một thư viện khác cho trường hợp này với Flutter nặng hơn nhiều so với những thư viện đã sử dụng cho các nền tảng khác và nó có thể là lý do khiến fps giảm.

    Use case 3 — Kiểm tra animation thậm chí còn nặng hơn với các rotation, scaling và fade.

    Trong thử nghiệm này, chúng tôi đã so sánh hiệu năng trong khi tạo animation cho 200 hình ảnh. Các animation xoay tỷ lệ và mờ dần được thực hiện cùng một lúc.

    200 hình ảnh

    Android

    Kết quả kiểm tra
    1. Native cho thấy hiệu năng cao nhất và tiêu thụ bộ nhớ hiệu quả nhất.
    2. Flutter cho thấy hiệu năng vừa đủ để làm việc thoải mái nhưng chi phí bộ nhớ cao hơn gấp đôi so với Native.
    3. React Native đã cho thấy hiệu năng thấp trong trường hợp này.

    IOS

    Kết quả kiểm tra
    1. iPhone 6s đủ mạnh để không giảm fps trong cả 3 trường hợp.
    2. Native sử dụng ít tài nguyên hơn và GPU được sử dụng gần hết.
    3. React Native chủ yếu sử dụng CPU để hiển thị trong khi Flutter sử dụng GPU.
    4. React Native đã sử dụng nhiều bộ nhớ hơn một chút.

    Tóm lại

    Đối với các ứng dụng thông thường có animation nhỏ và vẻ ngoài lấp lánh, công nghệ không thành vấn đề. Nhưng nếu bạn sẽ thực hiện một số animation nặng, hãy nhớ rằng shiny Native có sức mạnh hiệu năng cao nhất để làm điều đó. Tiếp theo, hãy đến với Flutter và React Native. Chúng tôi chắc chắn không khuyên bạn nên sử dụng React Native trong một hoạt động quá nặng về CPU, trong khi Flutter rất phù hợp cho các task như vậy từ cả quan điểm CPU và Memory.

    Công cụ bạn chọn tùy thuộc vào sản phẩm và business case cụ thể của bạn. Trong trường hợp bạn đang tìm cách phát triển MVP một nền tảng – hãy sử dụng các phương tiện gốc, nhưng hãy nhớ rằng các ứng dụng Flutter có thể được xây dựng cho cả môi trường mobile, web, desktop và có vẻ như Flutter có thể trở thành Vua phát triển đa nền tảng trong tương lai không xa, vì hiện tại Flutter đã tạo ra một cuộc cạnh tranh cho các công cụ phát triển native, đặc biệt nếu ngân sách phát triển không hạn chế mà bạn vẫn đang tìm kiếm hiệu năng tốt cho ứng dụng của mình trên các nền tảng khác nhau.

    Chúng tôi phải đối mặt với thực tế là có thể có nhiều yếu tố ảnh hưởng đến việc triển khai và benchmark của từng công nghệ và nhiều người trong số các bạn có thể là chuyên gia thực sự của một nền tảng cụ thể có thể khai thác nhiều hơn nữa bộ tool yêu thích. Chúng tôi đã cố gắng giải thích bằng cách tạo ra một môi trường duy nhất cho mỗi ứng dụng để thử nghiệm và một bộ công cụ duy nhất để đo lường hiệu năng và tôi hy vọng bạn thích kết quả này.

    Bài viết này được dịch từ đây.