Android

Tương tác giữa Fragment với Activity và với Fragment khác trong Android

Việc đặt Fragment vào Activity thì dễ rồi, nhưng ứng dụng là để cho người dùng tương tác. Và nếu chỉ add và remove Fragment “cho vui” mà không làm thì khác thì chắc không ai gắn bó với ứng dụng của bạn cả. Lần này thì tôi sẽ hướng dẫn các bạn thực hiện tối ưu hóa giao diện ứng dụng Android cho máy tính bảng và tương tác giữa Fragment-Activity và Fragment-Fragment trong cùng một Activity, thông qua một ví dụ nho nhỏ.

Ý tưởng của tôi là sẽ làm một ứng dụng hiển thị màu sắc theo hướng như hình bên dưới. Fragment A sẽ chứa một ListView chứa danh sách các màu sắc, khi tôi chọn một màu trên đó thì Fragment B chứa một View hình chữ nhật sẽ được tô (fill) màu vừa được chọn. Trên handset thì sẽ có hai activity, còn trên tablet sẽ chỉ có một activity chứa hai fragment.

android-fragments

Đầu tiên, tôi code ActivityA. Dù là handsets hay tablets thì code cũng phải giống nhau, ít nhất là tới thời điểm bạn đọc tới nửa bài viết này rồi, giống nhau như bên dưới:

Layout của nó được định nghĩa rất đơn giản như bên dưới:

Còn FragmentA thì được định nghĩa như sau:

@layout/fragment_a chỉ đơn giản là chứa một ListView như bên dưới:

ColorAdapter có nội dung như sau:

Quay trở lại FragmentA, hiện tại tôi muốn khi click vào một item trên ListView, thì ứng dụng sẽ cho ra một Toast báo cái màu tôi đang chọn. Sẽ là không có gì phức tạp nếu tôi cho Toast trongFragmentA. Nhưng bây giờ, tôi muốn cho Toast trong Activity thì sao? (Phần này là Fragment-Activity communication)

Đơn giản là tôi sẽ dùng interface OnListViewItemClickListener đã định nghĩa trong FragmentA và cho ActivityA implements cái interface đó mà thôi. Và tôi sẽ điều chỉnh nội dung hàm onItemClick của FragmentAFragmentA.OnListViewItemClickListener dư lầy:

Còn về ActivityA, sau khi implements cái interface thì tôi chỉ cần code hàm Toast trong onItemClick(int position) vừa phải implements xong:

Đến đây, bạn cho chạy thử project nào. 1, 2, 3… Khi click vào một item trên ListView của FragmentA thì… Ối làng nước ơi, cái bảng bên dưới lại hiện ra nữa rồi.

Unfortunately-app-has-stopped

Kiểm tra lại phần Logcat, bạn sẽ thấy NullPointerException ở dòng này và nguyên nhân là mListener đang null. Vì sao vậy?

Đơn giản là bạn chưa gán giá trị khác null cho mListener thì nó vẫn null chứ sao. Vậy gán nó bằng cách nào? Thực ra có nhiều cách lắm, nhưng làm theo cách bài bản lí thuyết hàn lâm nhất, thì bạn sẽ làm trong onAttach(Context c). Biến Context c mặc dù mang tiếng là Context nhưng thực chất nó chính là cái Activity chứa Fragment đang thao tác, và onAttach nghĩa là onAttachToActivity. Trong trường hợp này, nó chính là nguyên cả cái ActivityA. Và vì ActivityA đang implements cái FragmentA.OnListViewItemClickListener nên biến Context c đang kiêm nhiệm cả hai thứ. Do đó, ta có thể type cast cái Context c thành mListener. Được không? Được chứ! Hãy chờ xem.

Rồi, cho chạy lại. Và lần này nó hoạt động rồi. Oh yeah. Ngon và lành. Bây giờ, chúng ta tiếp tục đi vào phần triển khai ý tưởng ban đầu. Khi bạn click vào một item trên ListView của Fragment A, thì Android system sẽ chuyển sang Activity B chứa Fragment B trên handsets, còn với tablets thì do Fragment B nằm luôn trên Activity A nên Activity A sẽ trực tiếp yêu cầu Fragment B cập nhật dữ liệu. Fragment B sẽ gồm một View (có id là R.id.col như bên dưới) sẽ được tô đúng màu bạn vừa chọn trên Fragment A.

Trên handset, tôi làm một ActivityB sẽ có contentView là @layout/activity_b như bên dưới. FrameLayout có id là @+id/frb chính là nơi mà FragmentB sẽ “nhập” vào.

Còn trên tablet, tôi làm một contentView dành cho tablet của ActivityA. Tôi không overwrite cái đã có trong thư mục layout, mà tôi sẽ tạo một thư mục res mới là layout-large và làm một XML layout như bên dưới. Ở phần ActivityA.java thì tôi không cần làm gì khác, bởi khi chạy trên tablet thì R.layout.activity_a trong setContentView(R.layout.activity_a) sẽ tự trỏ vào phần layout mà tôi chuẩn bị làm. Do đó nếu bạn đang định làm một cái layout khác trong thư mục layout, rồi boolean isHandset để setContentView cho đúng thì không cần đâu. Tự nó sẽ làm cho bạn luôn.

Trong đó, cái FrameLayout có id là R.id.right sẽ chứa FragmentB, vốn có phần layout được định nghĩa như bên dưới. Như đã nói, phần View có id là R.id.col sẽ được tô đúng màu mà bạn chọn trên FragmentA, dù “Ép Ây” có nằm chung với “Ép Bi” trên cùng một Activity hay không.

Tôi code FragmentB trước, sử dụng newInstance cho dễ. Sau đó là ActivityB.

Như bạn thấy thì ActivityB, ngay sau khi nhận được intent thì sẽ gọi FragmentB “ngay và luôn” và yêu cầu nó rằng “FragmentB này, mày hiển thị cái màu có key là ‘Colour’ đi”. Còn đối với tablet, do cả hai Fragments A và B đều nằm trong ActivityA, nên sẽ xảy ra sự tương tác giữa chúng (Fragment-Fragment). Ở bài này, hai Fragment không tương tác trực tiếp với nhau, mà sự tương tác đó phải thông qua Activity, tức là đi theo hướng FragmentA -> ActivityA -> FragmentB. Vì vậy, quay lại ActivityA, tôi sẽ chỉ điều chỉnh lại code một chút như sau:

Trên đây là một ví dụ cực kì đơn giản, và cách làm thì tôi cố gắng làm theo hướng dẫn “chính chủ” của Android Developer Team At Google nên sẽ rất amateur và “dở ẹc” khi luôn phải replace fragment bên phải trên tablet mà không yêu cầu FragmentB tự đổi màu luôn. Tôi tin chắc là trong quá trình thao tác với Fragments, bạn sẽ từ từ tự “mò mẫm” ra những cách xử lí khác gọn hơn, tốt hơn, và tối ưu hơn. Chẳng hạn, bạn thấy là tôi yêu cầu ActivityA phải implements một nested interface của FragmentA, do đó, bạn có thể làm theo hướng tương tự là làm một nested interface trong ActivityA rồi cho FragmentB phải implements nó. Một lần nữa, bạn nên thực hiện nhiều ví dụ về Fragment để tích lũy thêm nhiều kinh nghiệm, từ đó phát hiện ra vô vàn các “đường tắt” hay ho và đơn giản về tương tác giữa Fragment-Activity và Fragment-Fragment. Happy coding.

2 thoughts on “Tương tác giữa Fragment với Activity và với Fragment khác trong Android”

  1. Mình chưa hiểu đoạn lắm bạn ơi
    void onAttach(Context c) {
    mListener = (OnListViewItemClickListener) c;
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.