Làm thế nào để Data Binding trong Fragment Android

0
36
Bài này thuộc phần 4 của 4 phần trong series Hướng dẫn toàn tập Fragment trong Android

Data Binding trong fragment android

Hướng dẫn toàn tập về cách sử dụng Fragment trong Android

Part 4: Data Binding

Ở hai phần trước chúng ta đã tìm hiểu kĩ về vòng đời của Fragment và cách để có thể tạo và add fragment vào Activity. Sau khi chúng ta đã có đầy đủ công cụ trong tay, bước tiếp theo là làm thế nào để các fragment có thể comunicate với nhau được? Chúng ta tiếp tục cùng tìm hiểu nhé

1. Data Binding

Khi bạn xem qua source code của All The Rages project mà các bạn đã download ở phần trước. Bạn sẽ thấy một điều như:

  • Có một file DataBindingAdapters
  • Cấu hình bật dataBinding trong gradle
dataBinding {
  enabled = true
}
  • Cấu hình dữ liệu trong xml layout file
<layout xmlns:android="http://schemas.android.com/apk/res/android">

  <data>

    <variable
      name="comic"
      type="com.raywenderlich.alltherages.Comic" />
 </data>

  ...
</layout>
  • Và một lớp Comic data

Nếu bạn chưa từng sử dụng Data Binding bao giờ thì mình tin chắc mặt bạn sẽ thế này… haha (Giống hệt mình ngày xưa….)

android_fragments_003_canine_coding-650x488

Bình tâm lại nhé, mình sẽ lần lượt giải thích từng phần!

1.1 Data Binding là gì?

Thông thường, nếu bạn muốn đặt giá trị cho một thuộc tính trong layout, bạn sẽ làm như bên dưới(tất nhiên là code này viết trong fragment hoặc Activity nào đó):

programmer.name = "a purr programmer"
view.findViewById<TextView>(R.id.name).setText(programmer.name)

Vấn đề là nếu bạn thay đổi giá trị name cho programmer, bạn sẽ cần phải gọi hàm setText thêm lần nữa để TextView cập nhật. Hãy tưởng tượng có một công cụ có thể liên kết một biến từ fragment và activity tới View và cho phép thay đổi biến đó trực tiếp từ View thay vì phải cập nhật trong fragment hay activity. Đó là chính là DataBinding.

Trong  All The Rages project, Data Binding được bật trong build.gradle như mình đã nói ở trên. Lớp dữ liệu chứa dữ liệu mà chúng ta muốn sử dụng trong fragment và hiển thị trong View. Trường data chứa các biến bao gồm các tùy chọn nametype xác định loại và tên của biến liên kết trong fragment. Dữ liệu này được sử dụng trong View bằng cách sử dụng {@} notation

Ví dụ, Dưới đây sẽ thiết lập một text field với giá trị được lấy từ trường tên của biến comic

tools:text="@{comic.name}"

Xong lý thuyết!

1.2 Làm sao để Data Binding?

Bây giờ chúng ta tiến hành bind các biến từ fragment vào trong View. Đây là lúc ma thuật của data binding phát huy! Bất cứ khi nào một view có trường data, framework sẽ tự động tạo ra một binding object. Tên của binding object được tạo ra bằng cách chuyển đổi tên của file layout sang tên chuẩn class.

Ví dụ: một file layout có binding data  là recycler_item_rage_comic.xmlthì sẽ có binding object tương ứng là RecyclerItemRageComicBinding.

override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
  //1
  val recyclerItemRageComicBinding = RecyclerItemRageComicBinding.inflate(layoutInflater,
    viewGroup, false)
  //2
  val comic = Comic(imageRmesIds[position], names[position], descriptions[position],
    urls[position])
  recyclerItemRageComicBinding.comic = comic

Sau đó, bạn có thể inflate một view thông qua hàm inflater trên binding object và thiết lập các thuộc tính thông qua các cơ chế truy cập chuẩn( Tức là gán trực tiếp thuộc tính của đối tượng qua toán tử ‘=’)

Data binding theo mô hình Model-View-ViewModel (MVVM). Mô hình MVVM bao gồm ba thành phần:

  • A View: Chứa các layout
  • A Model: Chứa các lớp dữ liệu
  • A View Model/Binder: Các file binding được tự động tạo ra

Các bạn muốn tìm hiểu thêm về kiến trúc ứng dụng thì có thể tham khảo một bài viết rất chi tiết trên VNTALKING về một mô hình rất phổ biến MVP tại đây.

Đừng bỏ qua:  MVP: Kiến trúc tốt nhất cho ứng dụng Android

2. Communicating giữa Fragment với Activity

Mặc dù các fragment được gắn liền với một activity, nhưng chúng không thể call qua lại lẫn nhau một cách “chính thống” được.
Ví dụ trong All the Rages project, bạn sẽ cần RageComicListFragment khai báo cho MainActivity biết khi nào người dùng đã thực hiện một lựa chọn để RageComicDetailsFragment có thể hiển thị các lựa chọn đó. Vẫn hơi trừa tượng phải không? Vậy chúng ta tìm hiểu code nhé
Để bắt đầu, mở RageComicListFragment.kt và thêm interface sau đây ở phía cuối của lớp:

interface OnRageComicSelected {
  fun onRageComicSelected(comic: Comic)
}

Điều này định nghĩa một listener interface để activity có thể bắt sự kiện của các fragment. Các activity sẽ implement interface  này và fragment sẽ gọi hàm onRageComicSelected()khi một mục được chọn bởi người dùng và chuyển lựa chọn đó đến activity.

2.1 Sử dụng interface để communicate giữa fragment và activity như thế nào?

Thêm trường mới bên dưới các trường hiện có trong RageComicListFragment:

private lateinit var listener: OnRageComicSelected

Trong onAttach() thêm đoạn sau vào super.onAttach(context)

if (context is OnRageComicSelected) {
  listener = context
} else {
  throw ClassCastException(context.toString() + " must implement OnRageComicSelected.")
}

Phần này sẽ khởi tạo tham chiếu cho listener. Bạn hãy đợi đến khi onAttach()hoàn thành để chắc chắn rằng fragment đã đính kèm. Sau đó bạn cần xác định activity nào sẽ implement OnRageComicSelected interface thông qua từ khóa instanceof

Nếu không hãy bắn Exception vì bạn không thể proceed. Nếu có hãy thiết lập activity như một listener cho RageComicListFragment.

Trong phương thức onBindViewHolder()ở RageComicAdapter, thêm đoạn code sau vào cuối cùng.

viewHolder.itemView.setOnClickListener { listener.onRageComicSelected(comic) }

Thêm hàm View.OnClickListenercho từng Rage Comic để gọi lệnh callback tới listener (hay chính là activity như đã nói ở trên) để chuyển tới lựa chọn mà người dùng đã chọn

Mở MainActivity.kt và thêm phần định nghĩa implement interface như sau:

class MainActivity : AppCompatActivity(), RageComicListFragment.OnRageComicSelected {

Bạn sẽ nhận được một thông báo lỗi từ IDE, yêu cầu bạn phải chuyển MainActivity thành abstract classs hoặc implement hàm OnRageComicSelected(comic: Comic). Đừng bận tâm, bạn sẽ giải quyết nó sau!

Đoạn code này xác định rằng MainActivity là một implementation của OnRageComicSelected interface.(Nếu bạn còn mơ hồ về khái niệm interface và implementation thì mình khuyên nên tìm hiểu thêm về tính đa hình trong lập trình hướng đối tượng. Tương lai gần mình sẽ viết bài về chủ đề này, chờ nhé!)

Bây giờ bạn sẽ chỉ hiển thị một Toast để xác minh đoạn code trên có hoạt động hay không!? Thêm đoạn code sau để thử:

import android.widget.Toast

Thêm các phương thức sau vào onCreate()

override fun onRageComicSelected(comic: Comic) {
  Toast.makeText(this, "Hey, you selected " + comic.name + "!",
      Toast.LENGTH_SHORT).show()
}

Thông báo lỗi từ IDE cũng biến mất. Bạn thử build và run sau đó chọn Rage Comics bạn sẽ nhận được kết quả như hình bên dưới(Toast được show khi chọn bất kì hình Comic nào)

android_fragments_015_app_selected_item

Đừng bỏ qua:  6 lí do bạn không nên tự học code một mình

3. Các tham số của Fragment và Transactions

Hiện tại RageComicDetailsFragment hiển thị một ảnh và một string mà mình hardcode trước đó.  Nhưng giả sử nếu bạn muốn nó hiển thị giá trị mà người dùng touch vào mỗi ảnh trong màn hình AllRageComic thì sao?

Đầu tiên, thay thế toàn bộ view trong fragment_rage_comic_details.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="comic"
            type="com.raywenderlich.alltherages.Comic" />
    </data>

    <ScrollView xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        tools:ignore="RtlHardcoded">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical">

            <TextView
                android:id="@+id/name"
                style="@style/TextAppearance.AppCompat.Title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="0dp"
                android:layout_marginTop="@dimen/rage_comic_name_margin_top"
                android:text="@{comic.name}" />

            <ImageView
                android:id="@+id/comic_image"
                android:layout_width="wrap_content"
                android:layout_height="@dimen/rage_comic_image_size"
                android:layout_marginBottom="@dimen/rage_comic_image_margin_vertical"
                android:layout_marginTop="@dimen/rage_comic_image_margin_vertical"
                android:adjustViewBounds="true"
                android:contentDescription="@null"
                android:scaleType="centerCrop"
                imageResource="@{comic.imageResId}" />

            <TextView
                android:id="@+id/description"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginBottom="@dimen/rage_comic_description_margin_bottom"
                android:layout_marginLeft="@dimen/rage_comic_description_margin_left"
                android:layout_marginRight="@dimen/rage_comic_description_margin_right"
                android:layout_marginTop="0dp"
                android:autoLink="web"
                android:text="@{comic.text}" />

        </LinearLayout>

    </ScrollView>
</layout>

Ở trên cùng, bạn sẽ thấy chúng ta đã thêm một biến cho Comic (trong thẻ data). Đoạn text cho tên và mô tả được bind với các biến có cùng tên trong đối tượng Comic

Đừng bỏ qua:  Tại sao hầu hết ứng dụng di động thất bại trong việc tăng lượt tải?

4. Cấu hình Binding Adapters

Vẫn ở  fragment_rage_comic_details.xml file. Trong ImageView bạn cần lưu ý một vài điểm sau:

imageResource="@{comic.imageResId}"

Nó tương ứng với một binding adapter mà bạn tạo trong DataBindingAdapters.kt file.

@BindingAdapter("android:src")
fun setImageResoruce(imageView: ImageView, resource: Int) {
  imageView.setImageResource(resource)
}

Một Binding Adapter cho phép chúng ta thực hiện các tác vụ mở rộng mà data binding mặc định không có. Trong trường hợp này, chúng ta muốn lưu resource integer (là đường dẫn của một resource nhưng được compiler mã hóa thành một số hexa kiểu như ID) cho ImageView. Nhưng data binding  mặc định không support để hiển thị một hình ảnh từ một ID. Để khắc phục điều đó, bạn cần phải tạo một BindingAdapter lấy một tham chiếu đến đối tượng mà nó được gọi từ cùng với một tham số.

4.1 Tìm hiểu vai trò của Arguments trong Fragment

OK. Xong phần layout XML. Chúng ta chuyển sang phần code trong RageComicDetailsFragment.kt

import java.io.Serializable

Thay thế newInstance() với đoạn code dưới đây:

private const val COMIC = "comic"

fun newInstance(comic: Comic): RageComicDetailsFragment {
  val args = Bundle()
  args.putSerializable(COMIC, comic as Serializable)
  val fragment = RageComicDetailsFragment()
  fragment.arguments = args
  return fragment
}

Một fragment có thể lấy thông số khởi tạo thông qua các đối số(Arguments). Arguments là một Bundle lưu trữ Arguments dưới dạng key-value giống như Bundle trong Activity.onSaveInstanceState

Như đoạn code trên thì bạn tạo arguments sau đó set các giá trị cần thiết vào arguments. Sau này bạn cần bất kì giá trị nào trong arguments thì chỉ cần reference đến arguments để lấy.

Như mình đã giải thích trong bài viết trước, khi một fragment được tạo lại, constructor không  tham số sẽ là constructor mặc định(kể cả bạn không viết lại trong code)

Bởi vì fragment có thể gọi lại tham số khỏi tạo từ chính arguments của nó. Vì vậy bạn có thể sử dụng chúng trong việc re-creation lại fragment. Đoạn code trên chúng ta đã lưu thông tin về Rage Comic được chọn trong RageComicDetailsFragment arguments.

Thêm đoạn code sau vào phần đầu của RageComicDetailsFragment.kt

import com.raywenderlich.alltherages.databinding.FragmentRageComicDetailsBinding

Thay thế toàn bộ nội dung trong onCreateView()như sau:

val fragmentRageComicDetailsBinding = FragmentRageComicDetailsBinding.inflate(inflater!!,
    container, false)

val comic = arguments.getSerializable(COMIC) as Comic
fragmentRageComicDetailsBinding.comic = comic
comic.text = String.format(getString(R.string.description_format), comic.description, comic.url)
return fragmentRageComicDetailsBinding.root

Bởi vì bạn muốn tự động nạp UI của RageComicDetailsFragment với comic được người dùng chọn. Bạn cần reference đến FragmentRageComicDetailsBinding trong fragment view trong hàm onCreateView. Tiếp theo, bạn bind  view comic với dữ liệu Comic được  truyền qua RageComicDetailsFragment.

4.2 Tại sao và khi nào sử dụng back stack cho Fragment

Cuối cùng, bạn cần tạo và hiển thị một RageComicDetailsFragment khi người dùng click vào một items, thay vì hiển thị một toast (Toast này mình chỉ làm với mục test thôi nhé). Mở MainActivity.kt và thay thế logic code bên trong onRageComicSelected như sau:

val detailsFragment =
    RageComicDetailsFragment.newInstance(comic)
supportFragmentManager.beginTransaction()
    .replace(R.id.root_layout, detailsFragment, "rageComicDetails")
    .addToBackStack(null)
    .commit()

Bạn sẽ thấy đoạn code này giống với đoạn code transaction đầu tiên khi thêm một list vào MainActivity, nhưng nó có vài điểm khác biệt:

  • Fragment được tạo ra và đồng thời có cả giá trị cần thiết thay vì chỉ là fragment rỗng
  • Bạn gọi replace() thay vì add(). Mục đích để thay thế fragment hiện tại trong container và sau đó thêm một fragment mới.
  • Đặc biệt là bạn hàm addToBackStack() của FragmentTransaction để add một instance của fragment vào stack. Giống như Activity, fragment cũng có thể quản lý theo stack. Bạn tham khảo hình bên dưới để thấy sự khác biệt nhé

android_fragments_d004_fragments_backstack-1-650x381

Vậy mục địch sử dụng addToBackStack ()để làm gì? Nó thêm fragment hiện tạo vào back stack để khi người dùng nhấn nút back trên thiết bị, nó trở về fragment trước đó mà không thoát khỏi Activity. Trong trường hợp này, nhấn nút Back sẽ đưa người dùng trở về danh sách đầy đủ comic.

Hàm add ()cho màn hình danh sách comic bỏ qua việc gọi addToBackStack (). Điều này có nghĩa là nếu người dùng nhấn nút Back từ màn hình danh sách comic, nó sẽ đưa người dùng ra khỏi ứng dụng.

Bây giờ, build and run và bạn sẽ thấy chi tiết về mỗi Rage khi bạn touch vào chúng:

android_fragments_017_app_showing_details-281x500

Vậy là chúng ta đã hoàn thành All The Rages app sử dụng fragment. Các bạn có thể download toàn bộ source của của project mẫu tại đây nhé

5. Tổng kết

Có rất nhiều thứ để học và làm với các fragment. Giống như bất kỳ loại công cụ hoặc tính năng nào, không phải cái gì mới cũng tốt, cũng đẹp. Hãy cân nhắc xem liệu fragment phù hợp với nhu cầu của ứng dụng của bạn hay không?!

Trong bài viết này, mình mới chỉ giới thiệu những kiến thức cơ bản về fragment. Để có thể trở thành master về fragment thì các bạn cần tìm hiểu thêm một số điểm mà liệt kê bên dưới nhé:

  • Sử dụng các fragment trong một ViewPager. Rất nhiều ứng dụng ( kể cả ứng Google Play) sử dụng cấu trúc nội dung có thể di chuyển được theo tab qua ViewPagers.
  • Sử dụng một DialogFragment mạnh và thuận lợi hơn thay vì một hộp thoại thông thường hoặc AlertDialog.
  • Cách fragment tương tác với các phần khác của Activity như app bar.

Mình hy vọng rằng bạn sẽ hứng thú với phần Tutorial Android Fragments này và nếu bạn có bất kỳ câu hỏi hoặc nhận xét nào, hãy comment bên dưới nhé.

Xem tiếp các bài trong Series
Phần trước: Hướng dẫn create và add fragment vào Activity trong Android

BÌNH LUẬN

Please enter your comment!
Please enter your name here