Android Architecture Component – Phần 1: Tìm Hiểu Về ViewModel

Posted by

Chào mừng các bạn đã đến với bài viết về Android Architecture Component – Phần 1. Đây là bài viết trong các bài bổ sung cho chương trình học Android của Yellow Code Books.

Trong sự kiện Google I/O 2017 vừa qua (trước cả cái sự kiện Google I/O 2018 của năm nay nhé), nhóm Android Framework của Google có trình bày một kiến trúc mới cho các lập trình viên Android, kiến trúc mới này được gọi với cái tên Architechture Components. Kiến trúc bao gồm một tập hợp nhiều thư viện khác nhau, nhằm mang đến cho chúng ta, các lập trình viên Android, có được các công cụ cần thiết để thiết kế ra các ứng dụng mạnh mẽ, ổn định và dễ dàng bảo trì nhất.

Do có rất nhiều kiến thức liên quan đến kiến trúc mới này của Android, nên mình sẽ chia chúng ra làm nhiều phần để dễ trình bày và tiếp cận. Phần đầu tiên trong chuỗi bài viết này sẽ dẫn bạn đến với một khái niệm có tên ViewModel.

Các bài viết trong phần này vừa thích hợp cho các bạn đã biết về Android và muốn tìm hiểu thêm về kiến trúc mới này, và còn thích hợp cho cả các bạn đang chập chững làm quen với Android nữa, để các bạn có thể tiếp cận ngay từ ban đầu cách sử dụng các công cụ hiệu quả trong lĩnh vực lập trình Android này. Source code trong bài viết được mình thể hiện bằng Kotlin thay vì Java quen thuộc, nhưng nếu bạn chưa từng bao giờ sử dụng Kotlin thì cũng hãy yên tâm vì mình sẽ cố gắng trình bày source code một cách đơn giản, để ai cũng có thể hiểu, và mình cũng sẽ giải nghĩa một chút các đoạn code nếu sử dụng các phương thức Kotlin nào đó mà lại quá lạ lẫm với các bạn Java nhé.

ViewModel Là Gì?

Như mình có nói đến trên đây, ViewModel là một phần trong Android Architechture Component. ViewModel thực chất chỉ là một lớp được cung cấp bởi hệ thống, nó được tạo ra nhằm mục đích giúp bạn quản lý các dữ liệu của UI, đảm bảo các dữ liệu này luôn được bảo toàn trong quá trình sống của UI đó.

Vậy thì tại sao lại cần sự đảm bảo và bảo toàn dữ liệu này? Chúng ta cùng đến với câu hỏi tiếp theo.

Tại Sao Lại Sử Dụng ViewModel?

Như bạn cũng biết, một trong những bug kinh điển nhất của Android, đó là việc Activity sẽ bị hủy và khởi tạo lại (callback onDestroy() được gọi trước và onCreate() lại được gọi lại sau đó) khi người dùng xoay màn hình. Hiện tượng khởi tạo này của UI được gọi là Configuration Change. Điều này làm cho các dữ liệu hiển thị lên UI của ứng dụng bị reset lại, và hiển nhiên người dùng sẽ thấy mọi thông tin của họ đã cất công nhập vào bỗng biến mất, ngay sau khi màn hình thiết bị xoay ngang/dọc.

Có một số cách để khắc phục bug trên đây. Đầu tiên có thể có bạn đã biết, đó là bạn có thể sử dụng callback onSaveInstanceState() để lưu trữ dữ liệu rồi sau đó khôi phục lại nó ở lần gọi onCreate() kế tiếp. Cách thứ hai đó là khai báo thuộc tính android:configChanges trong thẻ Activity của Manifest, để tránh việc hệ thống hủy và khởi tạo lại Activity đó, khi đó bạn có thể bảo toàn được dữ liệu, hoặc có thể thiết lập lại dữ liệu này trong phương thức onConfigurationChanged(). Tuy nhiên cả hai kỹ thuật này cũng chỉ hỗ trợ bạn lưu trữ các dữ liệu nhỏ, nếu dữ liệu cần lưu trữ và khôi phục lại quá lớn, nó có thể làm chậm ứng dụng. Mặt khác, kỹ thuật thứ hai trên đây còn có một nguy hiểm tiền ẩn nữa, đó là việc ngăn không cho Activity khởi tạo lại sẽ làm mất đi khả năng hỗ trợ Alternative Resource của hệ thống.

Để hiểu rõ hơn về bug mà mình nêu ra trên đây, mình mời các bạn hãy nhanh chóng cùng tạo một project. Project này có giao diện đơn giản chỉ với một TextView và hai Button. TextView giúp hiển thị một con số và các Button giúp tăng/giảm con số đó tương ứng, thế thôi. Giao diện sẽ trông như sau.

Android Architecture Component - Giao diện mẫu

Nếu bạn là người mới tiếp cận Android, hãy đọc bài này để biết cách thức tạo mới một project Android nhé.

Source code cho phần giao diện trên (file activity_main.xml) như sau (mình collapse lại cho nó ngắn gọn bài viết). Bạn cũng có thể xem được hoàn chỉnh giao diện này bằng cách click vào link mình để ở cuối bài.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageButton
        android:id="@+id/btnUp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@android:drawable/arrow_up_float" />

    <TextView
        android:id="@+id/tvCount"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="24dp"
        android:gravity="center_horizontal"
        android:text="0"
        android:textSize="32sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnUp" />

    <ImageButton
        android:id="@+id/btnDown"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvCount"
        app:srcCompat="@android:drawable/arrow_down_float" />
</android.support.constraint.ConstraintLayout>

Còn đây là source code cho logic ở MainActivity.kt.

class MainActivity : AppCompatActivity(), View.OnClickListener {

    var mCounter = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        displayCount()

        btnUp.setOnClickListener(this)
        btnDown.setOnClickListener(this)
    }

    fun displayCount() {
        tvCount.text = mCounter.toString()
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            btnUp.id -> {
                mCounter++
                displayCount()
            }

            btnDown.id -> {
                mCounter--
                displayCount()
            }
        }
    }
}

Vậy với project ví dụ này thì chuyện gì sẽ xảy ra? Bạn có thể thấy nếu như bạn nhấn các nút trên/dưới của TextView để thay đổi giá trị cho nó bao nhiêu đi nữa, nhưng nếu bạn xoay màn hình, mọi thứ sẽ bị reset, tức là TextView lại hiển thị giá trị 0. Bạn có thể xem mô tả bởi ảnh động bên dưới. Đó là vấn đề mà chúng ta cần phải giải quyết trong bài hôm nay. Và cũng trả lời cho câu hỏi tại sao này.

Android Architecture Component - Mô phỏng bug khi chưa dùng ViewModel

Khi Nào Thì Sử Dụng ViewModel?

Ngoài việc lưu trữ và đảm bảo tính toàn vẹn của dữ liệu khi xoay màn hình như minh họa trên đây. Thì ViewModel còn dùng để bảo toàn dữ liệu cho một số trường hợp nào đó mà UI (Activity hay Fragment) bị reset lại giống như là người dùng vừa xoay màn hình vậy. Ngoài ra thì ViewModel còn dùng như mà một dữ liệu dùng chung giữa các Fragment, hay dùng với cơ sở dữ liệu khi có sự kết hợp của LiveData mà mình sẽ nói đến ở phần tiếp theo.

Quay lại với mong muốn được bảo toàn dữ liệu cho UI, chúng ta cùng xem qua sơ đồ về thời gian sống của ViewModel trong UI mà nó được chỉ định để bảo vệ dữ liệu như sau.

Android Architecture Component - ViewModel scope

Theo như sơ đồ trên, thì bạn có thể nhìn thấy “đời sống” của một ViewModel (thanh màu xanh lá cây bên phải) sẽ bắt đầu khi Activity created, trải qua các trạng thái onCreate, onStart, onResume (các trạng thái này cũng chính là các callback tương ứng của Activity). Sau đó dù cho Activity rotated (xoay màn hình), với các trạng thái onPause, onStop, onDestroy, onCreate, onStart, onResume, mà ViewModel vẫn còn sống. Cho đến khi finish(), thì ở các trạng thái onPause, onStop, onDestroy sau lần gọi đó vẫn chứng kiến ViewModel còn hoạt động, cho đến khi kết thúc sự kiện onDestroy thì ViewModel mới kết thúc sứ mệnh bảo vệ dữ liệu cho Activity này mà thôi.

Bạn có thể thấy sơ đồ trên mô tả các trạng thái của một Activity, nhưng bạn nên nhớ ViewModel cũng sẽ hỗ trợ cho Fragment tương tự như vậy.

Sử Dụng ViewModel Để Bảo Toàn Dữ Liệu Cho UI Như Thế Nào?

Chúng ta đều đã biết source code chúng ta vừa tạo ra ở đầu bài viết sẽ gây ra bug làm reset dữ liệu của UI như thế nào rồi. Vậy bước này đây, với việc ứng dụng ViewModel, chúng ta sẽ cùng xem liệu vấn đề có được giải quyết hay không nhé.

Đầu tiên, theo như hướng dẫn này từ Google, chúng ta phải cấu hình cho build.gradle sao cho có thể sử dụng được các lớp liên quan đến ViewModel. Vậy bạn nên thêm dòng sau vào build.gradle ở cấp độ module.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'android.arch.lifecycle:extensions:1.1.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

Sau đó, chúng ta hãy đơn giản tạo một lớp kế thừa từ lớp ViewModel của hệ thống. Lớp mới này sẽ là lớp giúp chúng ta quản lý dữ liệu mà mình đã nói đến. Mình đặt cho nó cái tên CounterViewModel.kt.

class CounterViewModel : ViewModel() {

    var count = 0
}

Code của CounterViewModel rất đơn giản. Bởi vì lớp cha của nó là ViewModel đã làm đủ mọi cách để bảo toàn dữ liệu cho UI mà nó được chỉ định rồi. Nên bên trong CounterViewModel chỉ cần chúng ta chỉ định data nào cần được bảo toàn mà thôi. Trong trường hợp của ví dụ hôm nay chúng ta chỉ cần bảo toàn giá trị đếm, nên mình chỉ khai báo duy nhất một thuộc tính count là vậy.

Việc còn lại là chúng ta sẽ sử dụng ViewModel này như thế nào ở UI. Chúng ta cùng quay lại MainActivity.kt để thêm vào các đoạn code sau.

class MainActivity : AppCompatActivity(), View.OnClickListener {

    var mCounterViewModel: CounterViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mCounterViewModel = ViewModelProviders.of(this).get(CounterViewModel::class.java)

        displayCount()

        btnUp.setOnClickListener(this)
        btnDown.setOnClickListener(this)
    }

    fun displayCount() {
        tvCount.text = mCounterViewModel?.count.toString()
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            btnUp.id -> {
                mCounterViewModel?.count = mCounterViewModel?.count!! + 1
                displayCount()
            }

            btnDown.id -> {
                mCounterViewModel?.count = mCounterViewModel?.count!! - 1
                displayCount()
            }
        }
    }
}

Mình giải thích một chút code trên như sau. Đầu tiên, chúng ta không cần dùng đến biến mCounter để mà lưu trữ con số cho TextView nữa. Thay vào đó chúng ta dùng đến một đối tượng của CounterViewModel với tên mCounterViewModel.

Dòng khai báo tiếp theo mCounterViewModel = ViewModelProviders.of(this).get(CounterViewModel::class.java) giúp khởi tạo cho mCounterViewModel, giúp cho đối tượng này biết mà bảo toàn dữ liệu cho UI được chỉ định ở tham số truyền vào phương thức of(), và bảo toàn bằng cách chứa dữ liệu đó vào ViewModel được chỉ định ở tham số truyền vào phương thức get(). Có một điều thú vị của phương thức khởi tạo lên, đó là nếu bạn truyền vào cùng một UI cho tham số of(), bạn sẽ nhận được cùng một thể hiện của ViewModel, điều này giúp đảm bảo được rằng dù cho Activity bị reset lại, thì mCounterViewModel vẫn giữ nguyên, thế nên nó mới bảo toàn được dữ liệu. Mặt khác với ý này thì ViewModel còn được tận dụng để lưu trữ dữ liệu dùng chung giữa các Fragment nếu chúng truyền vào cùng một Activity cho tham số of() này, mà bài học sau chúng ta sẽ cùng nhau tìm hiểu.

Tiếp tục, với phương thức displayCount(), giờ đây bạn nên lấy dữ liệu từ mCounterViewModel, dữ liệu này đã được bảo toàn.

Còn với sự kiện onClick(), bạn cứ thoải mái tăng giảm biến count bên trong mCounterViewModel theo đúng yêu cầu của project. Thế là xong. Bạn cùng xem lại kết quả nào.

Android Architecture Component - Sau khi sử dụng ViewModel

Trên đây là phần 1 của Android Architecture Component. Cảm ơn các bạn đã đọc và ủng hộ blog. Các bạn hãy chờ đón phần 2 của bài viết nhé.

Download Source Code Mẫu

Bạn có thể download source code mẫu của bài này ở đây.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

 

Advertisements
No votes yet.
Please wait...

Gửi phản hồi