12/08/2018, 16:38

Sử dụng Android Architecture Components áp dụng vào Firebase Realtime Database (Phần 1)

Năm nay tại Google I/O 2017, đội phát triển Android của Google đã thông báo rằng Android Architecture Components của họ đã ra phiên bản chính thức stable 1.0, thì Android Architecture Components là gì, nó chính là thứ cung cấp các thư viện giúp cho việc thiết kế phần mềm một cách mạnh mẽ, có khả ...

Năm nay tại Google I/O 2017, đội phát triển Android của Google đã thông báo rằng Android Architecture Components của họ đã ra phiên bản chính thức stable 1.0, thì Android Architecture Components là gì, nó chính là thứ cung cấp các thư viện giúp cho việc thiết kế phần mềm một cách mạnh mẽ, có khả năng kiểm thử cao, và dễ dàng maintain. Trong số các công cụ mà AAC (Android Architecture Components) đưa ra thì tôi đặc biệt ấn tượng với cách mà nó giúp bạn quản lí vòng đời của các Activity và Fragment- một lỗi băn khoăn chung của các lập trình viên Android. Trong loạt bài này, tôi sẽ cùng các bạn nghiên cứu các mà cách thư viện có thể làm việc cùng với nhau và áp dụng nó cho Firebase Realtime Database SDK như thế nào. Cách mà những client app đọc dữ liệu từ RealTime Database là thông qua các listener, từ đó mà thực hiện việc update dữ liệu. Điều đó cho phép bạn dễ dàng kiễm soát UI và luôn hiển thi dữ liệu mới nhất. Ứng dụng Android sử dụng Realtime Database thường xuyên bắt lắng nghe từ onStart() và ngừng lắng nghe ở onStop() . Điều đó chắc chắng rằng nó chỉ nhận sự thay đổi khi mà Activity hoặc Fragment còn đang hiển thị trên màn hình. Thử tưởng tượng bạn có 1 Activity đang hiển thị Ticker và giá gần đây nhất của nó từ database. Thì Activity của bạn sẽ như sau:

public class MainActivity extends AppCompatActivity {
    private static final String LOG_TAG = "MainActivity";

    private final DatabaseReference ref =
        FirebaseDatabase.getInstance().getReference("/hotstock");

    private TextView tvTicker;
    private TextView tvPrice;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        tvTicker = findViewById(R.id.ticker);
        tvPrice = findViewById(R.id.price);
    }

    @Override
    protected void onStart() {
        super.onStart();
        ref.addValueEventListener(listener);
    }

    @Override
    protected void onStop() {
        ref.removeEventListener(listener);
        super.onStop();
    }

    private ValueEventListener listener = new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            // update the UI here with values in the snapshot
            String ticker = dataSnapshot.child("ticker").getValue(String.class);
            tvTicker.setText(ticker);
            Float price = dataSnapshot.child("price").getValue(Float.class);
            tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price));
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
            // handle any errors
            Log.e(LOG_TAG, "Database error", databaseError.toException());
        }
    };
}

Trông có vẻ ổn phải không nào. Một database lắng nghe và nhận sự cập nhật to giá của sticker tại /hotstock trong database, và giá trị của nó được hiển thị ở 2 TextView. Đối với những trường hợp đơn giản thì các làm như trên sẽ không có vấn đề gì, nhưng nếu ứng dụng của bạn trở lên phức tạp hơn thì ngay tức khắc bạn sẽ thấy ngay 2 vấn đề cần lưu ý:

1. Boilerplate

Có rất nhiều boilerplate cho việc định nghĩa DatabaseReference tại một ví trí trong database và quản lí nó tại onStart()onStop() . Càng nhiều các listenter tham gia thì càng nhiều boilerplate code hỗn loạn. Và loại bỏ tất cả các listener đã được thêm vào trước đó sẽ dẫn đến leak bộ nhớ, chỉ cần một sai lầm cũng có thể khiến bạn mất đi tiền bạc và hiệu suất của hệ thống.

2. Khó khăn cho việc test và đọc

Với việc viết code trên thì quả thật thấy đơn giản, nhưng chỉ đơn giản cho việc bạn viết còn khi bạn muốn viết unit test cho logic của chúng thì bạn phải mò từng dòng một, vì tất cả mọi thứ bạn đang nhồi nhét vào 1 đối tượng đó là Activity điều đó khiến cho việc khó đọc code và quản lí chúng.

Android Architecture Components sẽ giúp được gì để giải quyết bài toán bên trên?

Hãy cùng đào bới đống thư viện mà Architecture Components cung cấp, bạn sẽ tìm thấy 2 class đặc thù và hữu dụng để giải quyết bài toán bên trên đó là ViewModelLiveData. Nếu bạn chưa có hiểu biết gì về ViewModel và LiveData thì bạn hãy dành 1 chút thời gian để đọc về nó nhé ViewModel và LiveData . Tôi cũng sẽ Extending LiveData , LifeCyclerOwner (Activity hoặc Fragment) sẽ quản lí chúng.

Extending LiveData đối với Firebase Realtime Database

LiveData là một observable có chứa dữ liệu. Nó tuân theo vòng đời của ứng dụng Android các thành phần như Activity, Fragment hoặc Service và chỉ thông báo cho các compoment mà nó đang ở trạng thái vòng đời đang hoạt động. Tôi sẽ sử dụng nó ở đây để lắng nghe sự thay đổi của database Query hoặc DatabaseReference, và thông báo đến nơi lắng nghe sự thay đổi ở Activity sau đó cập nhật UI. Những thông báo đó đến từ DataSnapshot objects. Đây là cách mà tôi mở rộng LiveData

public class FirebaseQueryLiveData extends LiveData<DataSnapshot> {
    private static final String LOG_TAG = "FirebaseQueryLiveData";

    private final Query query;
    private final MyValueEventListener listener = new MyValueEventListener();

    public FirebaseQueryLiveData(Query query) {
        this.query = query;
    }

    public FirebaseQueryLiveData(DatabaseReference ref) {
        this.query = ref;
    }

    @Override
    protected void onActive() {
        Log.d(LOG_TAG, "onActive");
        query.addValueEventListener(listener);
    }

    @Override
    protected void onInactive() {
        Log.d(LOG_TAG, "onInactive");
        query.removeEventListener(listener);
    }

    private class MyValueEventListener implements ValueEventListener {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            setValue(dataSnapshot);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
            Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException());
        }
    }
}

Với FirebaseQueryLiveData, bất cứ khi nào dữ liệu từ Query đưa ra thay đổi, thì MyValueEventListener sẽ trigger theo DataSnapShot mới, và nó sẽ thông báo cho những nơi lắng nghe nó bằng việc sử dụng setValue() trong LiveData . Thông báo của MyValueEventListener được quản lí ở OnActive()onInActive(). Vì thế bất cứ khi nào Activity hoặc Fragment có chứa LiveData object đang on screen (trong trạng thái STARTED hoặc RESUMED) , LiveData object ở trạng thái "active" và database listener sẽ được thêm vào. Chiến thắng lớn nhất và LiveData mang lại cho chúng ta là có thể quản lí được database listener theo trang thái của Activity liên kết. Sẽ không còn khả năng leak memory nữa bởi vì FirebaseQueryLiveData biết chính xác khi nào và cách thiết lập và hủy bỏ công việc của nó. Chúng ta có thể tái sử dụng class này cho tất cả loại Firebase query. Giờ đây chúng ta có LiveData object, chúng ta có thể đọc và phân tán thay đổi đến database, chúng ta cần một ViewModel object để móc nối đến Activity. Hãy cùng xem cách làm nhé.

Implement ViewModel để quản lí FirebaseQueryLiveData

ViewModel chứa LiveData object để sử dụng trong Activity. Bởi vì ViewModel còn sống sót ngày cả khi rơi vào Activity configuration changes (Khi user xoay thiết bị), LiveData sẽ được tiếp tục sử dụng một cách bình thường. Vòng đời của ViewModel đối với Acitvity mà nó nằm trên đó được biểu thị như sau Ở đây ViewModel implement cho FirebaseQueryLiveData lắng nghe tại /hotstock trong Realtime Database:

public class HotStockViewModel extends ViewModel {
    private static final DatabaseReference HOT_STOCK_REF =
        FirebaseDatabase.getInstance().getReference("/hotstock");

    private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);

    @NonNull
    public LiveData<DataSnapshot> getDataSnapshotLiveData() {
        return liveData;
    }
}

Sử dụng LiveData và ViewModel cùng nhau trong Activity

Sau khi đã implement LiveData và ViewModel thì chúng ta sẽ kết hợp chúng trong Activity nhé:

public class MainActivity extends AppCompatActivity {
    private TextView tvTicker;
    private TextView tvPrice;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        tvTicker = findViewById(R.id.ticker);
        tvPrice = findViewById(R.id.price);

        // Obtain a new or prior instance of HotStockViewModel from the
        // ViewModelProviders utility class.
        HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class);

        LiveData<DataSnapshot> liveData = viewModel.getDataSnapshotLiveData();

        liveData.observe(this, new Observer<DataSnapshot>() {
            @Override
            public void onChanged(@Nullable DataSnapshot dataSnapshot) {
                if (dataSnapshot != null) {
                    // update the UI here with values in the snapshot
                    String ticker = dataSnapshot.child("ticker").getValue(String.class);
                    tvTicker.setText(ticker);
                    Float price = dataSnapshot.child("price").getValue(Float.class);
                    tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price));
                }
            }
        });
    }
}

Với chỉ khoảng 20 dòng code bạn có thể dễ dàng đọc và quản lí. Tại hàm OnCreate() khởi tạo một HotStockViewModel bằng đoạn code

HotStockViewModel viewModel = ViewModelProviders.of(this).get(HotStockViewModel.class);

ViewModelProviders là một class tiện tích từ Architecture Components, nó quản lí ViewModel instance theo vòng đời của component. Với 1 instance của HotStockViewModel, những thay đổi từ LiveData sẽ được lắng nghe và thay đổi trên UI mỗi khi database thay đổi dữ liệu.

Như vậy, đâu là ưu điểm mà cách này mang lại?

  • Sẽ không có cơ hội cho việc quên việc xóa sự lằng nghe cho ValueEventListener trên DatabaseReference, đó là nguyên nhân dẫn đến leak memory. LiveData object có vòng đời của nó, vì thế nếu có nhiều chỗ lắng nghe nó thì nó cũng sẽ tự động xóa lắng nghe đến database nếu k có nơi nào lắng nghe nó (inactive)
  • Nếu rơi vào configuration change trong Activity, nó sẽ ngay lập tức lấy dữ liệu gần nhất từ DataSnapshot từ LiveData sử dụng ưu tiên cho Activity, nó sẽ không chờ bắn thay đổi về từ database.Cho bên bạn không cần thiết phải dùng đến onSaveInstanceState() với LiveData.
  • Tăng khả năng test và dễ đọc khi tách class riêng biệt. Việc tách class riêng biệt nhằm mục đích :
  1. MainAcivity để đáp ứng cho việc vẽ UI
  2. ViewModel nắm dữ data trong UI
  3. FirebaseQueryLiveData duy trì các dữ liệu, cho phép app lắng nghe sự thay đổi

Làm cách nào để cải thiện hơn không?

  • Nếu bạn nhìn vào Activity implement, bạn có thể thấy phần lớn nó làm việc với Firebase Realtime Database đã được di chuyển vào FirebaseQueryLiveData, ngoại trừ DataSnapshot. Có một ý tưởng đó là tối muốn xóa mọi tham chiếu đến Realtime Database từ Activity, như vậy sẽ không cần quan tâm data thực sự đến từ đâu. Điều đó là quan trọng nếu bạn đã từng muốn di chuyển đến Firestore - Activity sẽ không phải thay đổi nhiều *Có một vấn đề khác là trong thực tế mỗi khi rơi vào configuration change thì việc remove và add lại listener sẽ được thực hiện và bên cạnh đó còn phải fetch lại data về, tôi không muốn như vậy vì nó tiêu tốn dữ liệu di động.

Trong bài viết tiếp theo tôi sẽ đưa ra hướng giải quyết cho 2 vấn đề. Hãy cùng chờ đợi tiếp phần 2 nhé.

0