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.
Đầ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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ActivityA extends AppCompatActivity { final int left = R.id.left; FragmentA mFA; void onCreate(Bundle b) { super.onCreate(b); // contentView @layout/activity_a được trình bày bên dưới setContentView(R.layout.activity_a); // mFA sẽ là Fragment chứa màu sắc mFA = new FragmentA(); getSupportFragmentManager().beginTransaction().add(left, mFA, "FA").commit(); } } |
Layout của nó được định nghĩa rất đơn giản như bên dưới:
1 2 3 4 5 6 7 8 9 10 11 12 |
<LinearLayout xmlns:android="..." android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:id="@+id/left" /> </LinearLayout> |
Còn FragmentA thì được định nghĩa như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class FragmentA extends Fragment implements AdapterView.OnItemClickListener { public static final int[] COLOURS = { Color.parseColor("#F44336"), Color.parseColor("#FF9800"), Color.parseColor("#FFEB3B"), Color.parseColor("#4CAF50"), Color.parseColor("#2196F3"), Color.parseColor("#3F51B5"), Color.parseColor("#9C27B0") } public static final String[] COL_NAMES = { "Đỏ", "Cam", "Vàng", "Lục", "Lam", "Chàm", "Tím" } private OnListViewItemClickListener mListener; public LeftFragment() { } void onAttach(Context c) { } View onCreateView(LayoutInflater inf, ViewGroup pa, Bundle b) { View contentView = inf.inflate(R.layout.fragment_a, pa, false); ListView lv = (ListView) contentView.findViewById(R.id.list); lv.setOnItemClickListener(this); ColorAdapter a = new ColorAdapter(); lv.setAdapter(a); return contentView; } void onItemClick(AdapterView a, View v, int position, long id) { } // Tạm để đây và tôi sẽ giải thích sau. public interface OnListViewItemClickListener { } } |
@layout/fragment_a chỉ đơn giản là chứa một ListView như bên dưới:
1 2 3 4 5 6 7 8 9 10 11 |
<LinearLayout xmlns:android="..." android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/list /> </LinearLayout> |
ColorAdapter có nội dung như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class ColorAdapter extends BaseAdapter { public ColorAdapter() { } public int getCount() { return FragmentA.COL_NAMES.length; } public Object getItem(int position) { return FragmentA.COLOURS[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false); } TextView text = (TextView) convertView; text.setText(FragmentA.COL_NAMES[position]); text.setTextColor(FragmentA.COLOURS[position]); return convertView; } } |
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 FragmentA và FragmentA.OnListViewItemClickListener dư lầy:
1 2 3 4 5 6 7 8 9 10 11 |
// LeftFragment OnListViewItemClickListener mListener; void onItemClick(AdapterView a, View v, int position, long id) { mListener.onItemClick(position); } public interface OnListViewItemClickListener { void onItemClick(int position); } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ActivityA extends AppCompatActivity implements FragmentA.OnListViewItemClickListener { final int left = R.id.leftpanel; FragmentA mFA; void onCreate(Bundle b) { ... } void onItemClick(int position) { Toast.makeText(this, FragmentA.COL_NAMES[position], 0).show(); } } |
Đế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.
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?
1 2 3 4 5 6 7 |
// LeftFragment OnListViewItemClickListener mListener; // NULLPOINTEREXCEPTION ở dòng này! Tên mListener bị NULL! Đang NULL làm sao gọi method được??????????? void onItemClick(AdapterView a, View v, int position, long id) { mListener.onItemClick(position); } |
Đơ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.
1 2 3 |
void onAttach(Context c) { mListener = (OnListViewItemClickListener) c; } |
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.
1 2 3 4 5 6 7 8 |
void onItemClick(int position) { if (handset) { // Chuyển sang ActivityB chứa FragmentB } else { // Do FragmentB nằm trên Activity A rồi nên Activity A sẽ trực tiếp "làm ăn" với nó } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<--! @layout/activity_b !--> <LinearLayout xmlns:android="..." android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/frb /> </LinearLayout> |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<LinearLayout xmlns:android="..." android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:id="@+id/left" /> <FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:id="@+id/right" /> </LinearLayout> |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<--! @layout/fragment_b !--> <LinearLayout xmlns:android="..." android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="20dp"> <View android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/col /> </LinearLayout> |
Tôi code FragmentB trước, sử dụng newInstance cho dễ. Sau đó là ActivityB.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class FragmentB extends Fragment { public FragmentB() { } public static FragmentB newInstance(int col) { FragmentB fb = new FragmentB(); Bundle args = new Bundle(); args.putInt("Colour"); fb.setArguments(args); return fb; } protected View onCreateView(LayoutInflater inf, ViewGroup pa, Bundle b) { View contentView = inf.inflate(R.layout.fragment_b, pa, false); Bundle args = getArguments(); int color = args.getInt("Colour"); contentView.findViewById(R.id.col).setBackgroundColor(colour); return contentView; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ActivityB extends AppCompatActivity { void onCreate(Bundle b) { super.onCreate(b); setContentView(R.layout.activity_b); Intent receiver = getIntent(); int colour = receiver.getExtras().getInt("Colour"); FragmentB fb = FragmentB.newInstance(colour); getSupportFragmentManager().beginTransaction().add(R.id.frb, fb).commit(); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class ActivityA extends AppCompatActivity implements FragmentA.OnListViewItemClickListener { final int left = R.id.left; final int right = R.id.right; boolean isHandset = false; FragmentA mFA; FragmentB mFB; void onCreate(Bundle b) { super.onCreate(b); // contentView @layout/activity_a được trình bày bên dưới setContentView(R.layout.activity_a); // Kiểm tra màn hình đang chạy là handset hay tablet isHandset = findViewById(right) == null; // Nếu không có View nào có id là R.id.right // mFA sẽ là Fragment chứa màu sắc mFA = new FragmentA(); getSupportFragmentManager().beginTransaction().add(left, mFA, "FA").commit(); } void onItemClick(int position) { int colour = FragmentA.COLOURS[position]; if (isHandset) { // Nếu là handset thì gọi ActivityB Intent sender = new Intent(this, ActivityB.class); sender.putExtra("Color", colour); startActivity(sender); } else { // Không thì cập nhật FragmentB if (mFB != null) mFB = null; mFB = FragmentB.newInstance(colour); getSupportFragmentManager().beginTransaction().replace(right, mFB).commit(); } } } |
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.
Sao không bỏ code bạn lên github chia sẻ để dễ đọc bạn
Mình chưa hiểu đoạn lắm bạn ơi
void onAttach(Context c) {
mListener = (OnListViewItemClickListener) c;
}