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

Posted by

Chào mừng các bạn đã đến với bài viết về Android Architecture Component – Phần 2. Đâ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.

Ở nội dung của bài hôm trước, mình đã cố gắng trình bày rõ ràng nhất cho các bạn hiểu về viên gạch đầu tiên trong hệ thống kiến trúc mới này của Android, viên gạch đầu tiên này có cái tên ViewModel.

Hôm nay chúng ta tiếp tục nói đến một hỗ trợ tuyệt vời khác của ViewModel, đó là việc chia sẻ dữ liệu giữa các Fragment. Và nói thêm một thành phần mới trong Android Architecture Component, giúp việc chia sẻ dữ liệu này được cập nhật theo thời gian thực giữa các Fragment, thành phần mới này có tên là LiveData.

Trước hết chúng ta cùng nhau nói về LiveData.

LiveData Là Gì?

Như ai cũng biết rằng, LiveData cũng như ViewModel đều là một phần trong bộ Android Architecture Component. LiveData là một lớp, nó dùng để truyền tải các thông điệp về dữ liệu, dựa trên mô hình của Observer.

Nếu bạn nào còn chưa rõ về khái niệm Observer, thì đây chính là một mô hình trong số các mô hình mẫu khác (các mô hình mẫu đều được gọi chung với cái tên Design Pattern). Mô hình Observer này được xây dựng với ý tưởng sẽ có một đối tượng trung tâm (còn được gọi là Subject), đối tượng trung tâm này nắm danh sách các đối tượng quan sát khác (đối tượng quan sát chính là các Observer). Để rồi khi có bất kỳ thay đổi nào với trạng thái của đối tượng trung tâm đó, nó sẽ thông báo ngay cho các đối tượng quan sát được biết.

Observer này rất phổ biến trong thực tế. Ví dụ một tình huống là bạn và nhiều bạn khác hôm nay không muốn đi học, các bạn đều cử ra một bạn hơi siêng hơn một tí xíu đến lớp thay các bạn và với lời nhắn rằng “nếu thầy có điểm danh, mày hãy gọi cho bọn tao, bọn tao sẽ có mặt trong 5 phút”. Vậy thôi, cái bạn được cử đi học thay đó chính là một Subject. Còn các bạn trốn học chính là các Observer. Trạng thái mà bạn Subject đang nắm giữ chính là tín hiệu điểm danh trong lớp. Nếu thực sự có sự điểm danh, Subject sẽ thông báo ngay cho các Observer và các thành viên này sẽ hành động theo tín hiệu mà Subject vừa thông báo.

Vậy thì, với việc LiveData cũng dựa trên mô hình của Observer, nó cũng sẽ có các Subject, và các Subject này cũng sẽ có trách nhiệm thông báo các sự thay đổi trạng thái đến các đối tượng quan sát khác. Cụ thể hơn, các Subject khi này là các ViewModel, còn các đối tượng giám sát khi này là các Thành phần của ứng dụng (đó là các Activity, Fragment, hay Service).

Nhưng nếu LiveData cũng chính là Observer, thì tại sao lại không dùng Observer luôn cho rồi. Tất nhiên Google đã đưa ra LiveData tức là có dụng ý của họ. LiveData hơn hẳn Observer truyền thống ở chỗ nó hiểu rõ vòng đời của các Thành phần của ứng dụng, những tín hiệu mà Subject của LiveData truyền đến chỉ khi nào vòng đời của các Thành phần quan sát đó đang ở trong trạng thái hoạt động mà thôi.

Khi Nào Thì Cần Sử Dụng LiveData?

Dựa trên những ý trên đây, bạn cũng có thể tưởng tượng rằng, khi xây dựng các ứng dụng Android, chúng ta sẽ phải đụng chạm rất nhiều đến các tình huống mà Observer mang đến, tức là UI của ứng dụng sẽ cần được thông báo việc thay đổi của dữ liệu mà nó đang hiển thị, để mà có sự chỉnh sửa và cập nhật đúng đắn nhất.

Điều này gắn liền với khái niệm “thời gian thực”. Có nghĩa là người dùng sẽ mong muốn nhìn thấy ngay tức khắc sự thay đổi trên giao diện của ứng dụng, ngay khi ở đâu đó, dữ liệu liên quan đến giao diện này cũng bị thay đổi.

Tất nhiên trước khi LiveData ra đời thì chúng ta cũng đã có khá nhiều cách thức để có thể cập nhật dữ liệu cho UI như thế này. Mình có thể kể vắn tắt thôi, như sử dụng Interface và lắng nghe sự kiện từ nó, hay bạn có thể sử dụng EventBus, hay có thể sử dụng cả BroadcastReceiver, hoặc xây dựng luôn Observer,… Dù cho bạn đã biết rõ những cách thức mình đã liệt kê này hay không, thì bạn vẫn cứ “mở lòng” hết cho LiveData này nhé, bạn sẽ thấy sử dụng nó khá là dễ dàng.

Sử Dụng LiveData Để Cập Nhật Dữ Liệu Theo Thời Gian Thực Cho UI

Nào, chúng ta sẽ cùng xây dựng một ứng dụng giả lập cho nhu cầu muốn cập nhật dữ liệu theo thời gian thực này. Ứng dụng của chúng ta có một Activity, nhưng có 3 Fragment, mỗi Fragment sẽ cùng nhau hiển thị  số lượng huy chương của người dùng như sau.

LiveData - Màn hình thực hành

3 Fragment ở màn hình trên bao gồm. Fragment màu trắng mình đặt tên là ControlFragment, Fragment này sẽ chứa đựng các nút điều khiển, giúp người dùng tăng/giảm số lượng các huy chương Vàng/Bạc/Đồng. Fragment màu xanh phía trên mình đặt tên là SummaryFragment, Fragment này sẽ luôn biết được ControlFragment tăng/giảm số lượng huy chương như thế nào, rồi cộng tổng chúng lại để hiển thị tổng số huy chương. Fragment còn lại nằm bên phải màn hình, có tên DetailFragment, làm nhiệm vụ hiển thị chính xác từng loại huy chương mà ControlFragment đang điều chỉnh.

Mong muốn của ứng dụng này là, khi người dùng tăng/giảm số lượng từng loại huy chương ở ControlFragment, thì các thông tin về huy chương này cũng được cập nhật tức thời đến với SummaryFragmentDetailFragment.

LiveData - Cụ thể các Fragment bên trong ứng dụng thực hành

Kịch bản là như thế, giờ bạn hãy tạo mới một project để xây dựng ứng dụng như trên. 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é.

Chuẩn Bị Sẵn Giao Diện Ban Đầu

Sau khi tạo mới project Android, bạn có thể tiếp tục tiến hành code sẵn các giao diện như trên trước khi gắn ViewModelLiveData cho chúng. Mình sẽ để lên đây các code mẫu ban đầu này, bạn có thể tìm thấy chúng ở link đến Github mình để ở cuối bài học.

Trước hết đây là lớp SummaryFragment.kt.

class SummaryFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_summary, container, false)
    }
}

Và giao diện của Fragment này, file fragment_summary.xml.

<?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="wrap_content"
    android:background="@android:color/holo_blue_bright"
    tools:background="@android:color/holo_blue_light"
    tools:context=".ui.SummaryFragment">

    <!-- TODO: Update blank fragment layout -->

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:src="@mipmap/ic_launcher_round"
        app:layout_constraintDimensionRatio="w,16:19"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvNumberOfMedal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="16dp"
        android:text="@string/number_of_medal_label"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />
</android.support.constraint.ConstraintLayout>

Đây là lớp DetailFragment.kt.

class DetailFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_detail, container, false)
    }
}

Và giao diện của nó, fragment_detail.xml.

<?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"
    android:background="@android:color/holo_blue_dark"
    tools:context=".ui.DetailFragment">

    <!-- TODO: Update blank fragment layout -->

    <TextView
        android:id="@+id/tvDetailGoldNumber"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        android:text="@string/number_of_gold_label"
        android:textAllCaps="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvDetailSilverNumber"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        android:text="@string/number_of_silver_label"
        android:textAllCaps="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvDetailGoldNumber" />

    <TextView
        android:id="@+id/tvDetailBronzeNumber"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        android:text="@string/number_of_bronze_label"
        android:textAllCaps="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvDetailSilverNumber" />
</android.support.constraint.ConstraintLayout>

Rồi đến lớp ControlFragment.kt.

class ControlFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_control, container, false)
    }
}

Và giao diện fragment_control.xml.

<?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=".ui.ControlFragment">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/gold_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnGoldMinus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/minus_button_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

    <Button
        android:id="@+id/btnGoldPlus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="@string/plus_button_label"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

    <TextView
        android:id="@+id/tvMainGoldNumber"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        app:layout_constraintBaseline_toBaselineOf="@+id/btnGoldMinus"
        app:layout_constraintEnd_toStartOf="@+id/btnGoldPlus"
        app:layout_constraintStart_toEndOf="@+id/btnGoldMinus" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="16dp"
        android:text="@string/silver_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnGoldMinus" />

    <Button
        android:id="@+id/btnSilverMinus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/minus_button_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <Button
        android:id="@+id/btnSilverPlus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="@string/plus_button_label"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <TextView
        android:id="@+id/tvMainSilverNumber"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        app:layout_constraintBaseline_toBaselineOf="@+id/btnSilverMinus"
        app:layout_constraintEnd_toStartOf="@+id/btnSilverPlus"
        app:layout_constraintStart_toEndOf="@+id/btnSilverMinus" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="16dp"
        android:text="@string/bronze_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnSilverMinus" />

    <Button
        android:id="@+id/btnBronzeMinus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/minus_button_label"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView4" />

    <Button
        android:id="@+id/btnBronzePlus"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="@string/plus_button_label"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView4" />

    <TextView
        android:id="@+id/tvMainBronzeNumber"
        android:layout_width="40dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:gravity="center_horizontal"
        app:layout_constraintBaseline_toBaselineOf="@+id/btnBronzeMinus"
        app:layout_constraintEnd_toStartOf="@+id/btnBronzePlus"
        app:layout_constraintStart_toEndOf="@+id/btnBronzeMinus" />

</android.support.constraint.ConstraintLayout>

Cuối cùng, tuy hơi dài nhưng mình hi vọng là bạn có thể tự xây dựng các giao diện như thế này. Chúng ta còn cần phải ráp 3 Fragment này vào giao diện của MainActivity. Và đây chính là nội dung của activity_main.xml.

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

    <fragment
        android:id="@+id/fragment"
        android:name="com.livedata_sample.yellow.ui.SummaryFragment"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <fragment
        android:id="@+id/fragment2"
        android:name="com.livedata_sample.yellow.ui.DetailFragment"
        android:layout_width="128dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/fragment" />

    <fragment
        android:id="@+id/fragment3"
        android:name="com.livedata_sample.yellow.ui.ControlFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/fragment2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/fragment" />
</android.support.constraint.ConstraintLayout>

Hiện tại nội dung của lớp ActivityMain.kt mình vẫn chưa chỉnh sửa gì, vẫn là mặc định khi vừa mới tạo project, nên mình không đưa code lên đây.

Xây Dựng ViewModel Để Chia Sẻ Dữ Liệu Giữa Các Fragment

Bước này đây chúng ta chưa quan tâm đến việc cập nhật dữ liệu thời gian thực là như thế nào. Trước hết chúng ta cần phải làm sao để các Fragment có thể dùng chung dữ liệu huy chương với nhau. Cách hay nhất, và phù hợp với kiến thức về Android Architecture Component nhất, đó là dùng đến ViewModel.

Nếu bạn có thắc mắc tại sao ViewModel lại có thể dùng để lưu trữ dữ liệu dùng chung giữa các Fragment, thì có thể đọc kỹ lại mục này của Phần 1 nhé.

Và cũng như Phần 1 có nói, chúng ta nên xem trước hướng dẫn này từ Google để biết cách cấu hình cho build.gradle sao cho có thể sử dụng được ViewModel và LiveData cho ứng dụng hôm nay. Theo như link thì chúng ta nên thêm dòng sau vào build.gradle ở cấp độ module.

dependencies {
    def lifecycle_version = "1.1.1"

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:support-v4:27.1.1'
    implementation "android.arch.lifecycle:extensions:$lifecycle_version" // ViewModel and LiveData
    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'
}

Tiếp theo, vì chúng ta muốn chia sẻ các giá trị Vàng/Bạc/Đồng, vậy chúng ta sẽ khai báo chúng bên trong ViewModel của chúng ta, mình đặt tên ViewModel này là MedalViewModel.

class MedalViewModel : ViewModel() {

    var numberOfGoldMedal = 0
    var numberOfSilverMedal = 0
    var numberOfBronzeMedal = 0
}

Nào, hãy để ý kỹ, nếu bạn chưa hiểu rõ về ViewModel, hãy xem lại bài trước. Còn bạn đã rõ rồi, thì xem mình dùng MedalViewModel để chứa dữ liệu các huy chương như sau. Đầu tiên là chỉnh sửa ở lớp ControlFragment.kt.

class ControlFragment : Fragment(), View.OnClickListener {

    lateinit var mMedalViewModel: MedalViewModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        val root = inflater.inflate(R.layout.fragment_control, container, false)

        mMedalViewModel = ViewModelProviders.of(activity!!).get(MedalViewModel::class.java)

        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        displayMedal()

        btnGoldMinus.setOnClickListener(this)
        btnGoldPlus.setOnClickListener(this)
        btnSilverMinus.setOnClickListener(this)
        btnSilverPlus.setOnClickListener(this)
        btnBronzeMinus.setOnClickListener(this)
        btnBronzePlus.setOnClickListener(this)
    }

    fun displayMedal() {
        tvMainGoldNumber.text = mMedalViewModel?.numberOfGoldMedal.toString()
        tvMainSilverNumber.text = mMedalViewModel?.numberOfSilverMedal.toString()
        tvMainBronzeNumber.text = mMedalViewModel?.numberOfBronzeMedal.toString()
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            btnGoldMinus.id -> {
                mMedalViewModel?.numberOfGoldMedal = mMedalViewModel?.numberOfGoldMedal?.minus(1)
                displayMedal()
            }
            btnGoldPlus.id -> {
                mMedalViewModel?.numberOfGoldMedal = mMedalViewModel?.numberOfGoldMedal?.plus(1)
                displayMedal()
            }
            btnSilverMinus.id -> {
                mMedalViewModel?.numberOfSilverMedal = mMedalViewModel?.numberOfSilverMedal?.minus(1)
                displayMedal()
            }
            btnSilverPlus.id -> {
                mMedalViewModel?.numberOfSilverMedal = mMedalViewModel?.numberOfSilverMedal?.plus(1)
                displayMedal()
            }
            btnBronzeMinus.id -> {
                mMedalViewModel?.numberOfBronzeMedal = mMedalViewModel?.numberOfBronzeMedal?.minus(1)
                displayMedal()
            }
            btnBronzePlus.id -> {
                mMedalViewModel?.numberOfBronzeMedal = mMedalViewModel?.numberOfBronzeMedal?.plus(1)
                displayMedal()
            }
        }
    }
}

Có lẽ code thay đổi ở ControlFragment trên đây không làm khó bạn. Chúng chẳng qua chỉ là khai báo mMedalViewModel chính là một ViewModelmMedalViewModel này sẽ bảo toàn dữ liệu từ Activity chứa nó thông qua đối số của phương thức of(). Sau đó là các phương thức hiển thị các con số này lên các TextView. Rồi đến các sự kiện click lên các Button +/- đối với từng loại huy chương.

Thừa thắng xông lên, chúng ta làm tương tự với SummaryFragment. Với Fragment này chúng ta cũng khai báo một ViewModel. Theo lý thuyết, thì dữ liệu từ ViewModel này đã được bảo toàn theo sự thay đổi từ ControlFragment bởi tham số của phương thức of() khi này cũng chính là Activity chứa SummaryFragmentControlFragment.

class SummaryFragment : Fragment() {

    lateinit var mMedalViewModel: MedalViewModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        var root =  inflater.inflate(R.layout.fragment_summary, container, false)

        mMedalViewModel = ViewModelProviders.of(activity!!).get(MedalViewModel::class.java)

        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        displayMedal()
    }

    fun displayMedal() {
        var totalMedal: Int = mMedalViewModel?.numberOfGoldMedal?.plus(mMedalViewModel?.numberOfSilverMedal!!).plus(mMedalViewModel?.numberOfBronzeMedal!!)
        tvNumberOfMedal.text = activity?.getString(R.string.number_of_medal_label, totalMedal)
    }
}

Chúng ta cũng code tương tự vậy cho DetailFragment.

class DetailFragment : Fragment() {

    lateinit var mMedalViewModel: MedalViewModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        var root = inflater.inflate(R.layout.fragment_detail, container, false)

        mMedalViewModel = ViewModelProviders.of(activity!!).get(MedalViewModel::class.java)

        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        displayMedal()
    }

    fun displayMedal() {
        tvDetailGoldNumber.text = activity?.getString(R.string.number_of_gold_label, mMedalViewModel?.numberOfGoldMedal)
        tvDetailSilverNumber.text = activity?.getString(R.string.number_of_silver_label, mMedalViewModel?.numberOfSilverMedal)
        tvDetailBronzeNumber.text = activity?.getString(R.string.number_of_bronze_label, mMedalViewModel?.numberOfBronzeMedal)
    }
}

Đến lúc này đây, khi thực thi ứng dụng, bạn đã có thể nhấn vào các nút +/- ở ControlFragment để tăng/giảm các huy chương tương ứng. Nhưng bạn thấy, các giá trị ở các Fragment khác vẫn không đổi!?! Đó là vì tuy ViewHolderControlFragment đã được thay đổi giá trị, nhưng chúng chưa có cơ chế thông báo cho các thành phần quan sát khác. Nếu bạn thử nghiệm bằng cách xoay màn hình, bạn sẽ thấy dữ liệu khi này mới được cập nhật đều cho các Fragment. Chúng ta cùng đến bước tiếp theo để hoàn thành việc cập nhật dữ liệu thời gian thực cho các Fragment nhé.

Xây Dựng LiveData Để Cập Nhật Dữ Liệu Thời Gian Thực Cho Các Fragment

Bạn hãy đảm bảo rằng đã hiểu được cách sử dụng ViewModel trên kia, trước khi đi tiếp phần này. Thực ra phần này mới là kiến thức chính của bài hôm nay.

Trước hết, để các dữ liệu có thể cập nhật thời gian thực đến các Observer, chúng ta hãy biến chúng thành các Subject. Các Subject mà chúng ta cần quan tâm đó chính là các giá trị của các huy chương. Vậy hãy đến MedalViewModel.kt để “bao” các giá trị numberOfGoldMedalnumberOfSilverMedal, và numberOfBronzeMedal bằng các LiveData, bạn xem sự thay đổi của code ở MedalViewModel.kt như sau nhé.

class MedalViewModel : ViewModel() {

    var numberOfGoldMedal: MutableLiveData<Int> = MutableLiveData()
    var numberOfSilverMedal: MutableLiveData<Int> = MutableLiveData()
    var numberOfBronzeMedal: MutableLiveData<Int> = MutableLiveData()

    init {
        numberOfGoldMedal.value = 0
        numberOfSilverMedal.value = 0
        numberOfBronzeMedal.value = 0
    }
}

Mình nói sơ qua một chút. Thay đổi đầu tiên, đó là các thuộc tính giúp lưu giữ các giá trị Vàng/Bạc/Đồng sẽ không còn là kiểu Int nữa, thay vào đó nó phải là kiểu MutableLiveData. MutableLiveData cũng là LiveData mà thôi, nhưng nó được bổ sung việc cho phép cập nhật sửa chữa giá trị cho các LiveData. Khi này MutableLiveData chính là các lớp bảo trợ cho việc thay đổi giá trị kiểu Int, giúp các thuộc tính khi này trở thành các Subject trong mô hình Observer.

Các đoạn code trong khối init giúp khởi tạo giá trị ban đầu cho các Subject này là 0.

Với việc thay đổi này, thì ở bên ngoài khi sử dụng đến các giá trị Vàng/Bạc/Đồng bên trong MedalViewModel, sẽ không sử dụng trực tiếp đến giá trị của chúng nữa mà phải thông qua phương thức getValue() (với Kotlin chỉ cần viết value). Như vậy ở ControlFragment.kt, lớp này không phải là một Observer, tức là nó không cần quan sát sự thay đổi trạng thái của các thuộc tính Subject, vì nó chỉ thay đổi các giá trị của các thuộc tính  này mà thôi, nên sự chỉnh sửa chỉ ở các dòng sau.

class ControlFragment : Fragment(), View.OnClickListener {

    // ... code cũ không đổi gì trong đoạn này

	fun displayMedal() {
        tvMainGoldNumber.text = mMedalViewModel?.numberOfGoldMedal?.value.toString()
        tvMainSilverNumber.text = mMedalViewModel?.numberOfSilverMedal.value.toString()
        tvMainBronzeNumber.text = mMedalViewModel?.numberOfBronzeMedal.value.toString()
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            btnGoldMinus.id -> {
                mMedalViewModel?.numberOfGoldMedal.value = mMedalViewModel?.numberOfGoldMedal?.value?.minus(1)
                displayMedal()
            }
            btnGoldPlus.id -> {
                mMedalViewModel?.numberOfGoldMedal.value = mMedalViewModel?.numberOfGoldMedal?.value?.plus(1)
                displayMedal()
            }
            btnSilverMinus.id -> {
                mMedalViewModel?.numberOfSilverMedal.value = mMedalViewModel?.numberOfSilverMedal?.value?.minus(1)
                displayMedal()
            }
            btnSilverPlus.id -> {
                mMedalViewModel?.numberOfSilverMedal.value = mMedalViewModel?.numberOfSilverMedal?.value?.plus(1)
                displayMedal()
            }
            btnBronzeMinus.id -> {
                mMedalViewModel?.numberOfBronzeMedal.value = mMedalViewModel?.numberOfBronzeMedal?.value?.minus(1)
                displayMedal()
            }
            btnBronzePlus.id -> {
                mMedalViewModel?.numberOfBronzeMedal.value = mMedalViewModel?.numberOfBronzeMedal?.value?.plus(1)
                displayMedal()
            }
        }
    }
}

Với SummaryFragment.kt thì chúng ta sẽ thay đổi nhiều hơn, ngoài việc thêm vào phương thức getValue() cho các giá trị bên trong MedalViewModel, thì chúng ta còn cần phải đăng ký Fragment này chính là một Observer, tức là nó sẽ quan sát sự thay đổi của các Subject mà nó quan tâm, khi có sự thay đổi, thì phương thức displayMedal() mới được gọi đến để thực hiện việc cập nhật giá trị mới lên UI. Chà lung tung nhỉ, mời bạn cùng xem code thay đổi của Fragment này.

class SummaryFragment : Fragment() {

    // ... code cũ không đổi gì trong đoạn này

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mMedalViewModel.numberOfGoldMedal.observe(activity!!, Observer<Int> { displayMedal() })
        mMedalViewModel.numberOfSilverMedal.observe(activity!!, Observer<Int> { displayMedal() })
        mMedalViewModel.numberOfBronzeMedal.observe(activity!!, Observer<Int> { displayMedal() })
    }

    fun displayMedal() {
        var totalMedal: Int = mMedalViewModel?.numberOfGoldMedal?.value!!.plus(mMedalViewModel?.numberOfSilverMedal?.value!!).plus(mMedalViewModel?.numberOfBronzeMedal?.value!!)
        tvNumberOfMedal.text = activity?.getString(R.string.number_of_medal_label, totalMedal)
    }
}

Nếu đến đây bạn đều đã hiểu chuyện gì xảy ra, thì xin chúc mừng bạn, bạn chỉ còn một thay đổi nhỏ tương tự đối với DetailFragment nữa mà thôi.

class DetailFragment : Fragment() {

    // ... code cũ không đổi gì trong đoạn này

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mMedalViewModel.numberOfGoldMedal.observe(activity!!, Observer<Int> { displayMedal() })
        mMedalViewModel.numberOfSilverMedal.observe(activity!!, Observer<Int> { displayMedal() })
        mMedalViewModel.numberOfBronzeMedal.observe(activity!!, Observer<Int> { displayMedal() })
    }

    fun displayMedal() {
        tvDetailGoldNumber.text = activity?.getString(R.string.number_of_gold_label, mMedalViewModel?.numberOfGoldMedal?.value)
        tvDetailSilverNumber.text = activity?.getString(R.string.number_of_silver_label, mMedalViewModel?.numberOfSilverMedal?.value)
        tvDetailBronzeNumber.text = activity?.getString(R.string.number_of_bronze_label, mMedalViewModel?.numberOfBronzeMedal?.value)
    }
}

Phù, cuối cùng cũng xong hết, mời bạn thực thi ứng dụng để kiểm tra lại xem việc cập nhật thời gian thực có hoạt động không nhé.

LiveData - Kết quả thực thi chương trình

Bạn vừa xem qua phần 2 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 3 của mình 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
Rating: 5.0/5. From 5 votes.
Please wait...

One comment

  1. Anh có thể làm một bài hướng dẫn sử dụng Android studio với gitlab trong làm việc với team được được không ạ. Em cảm ơn anh rất nhiều!!

Gửi phản hồi