Được chỉnh sửa ngày 07/02/2018.
Chào mừng các bạn đến với bài viết về Expand/Collapse ListView. Đây là bài viết trong các bài viết thực hành nhỏ cho chương trình học Android của Yellow Code Books.
Thời gian qua có nhiều bạn đã gởi mail hỏi mình làm cách nào xây dựng một Expand/Collapse ListView trong Android, đó là một ListView mà user có thể touch vào một item list để xổ ra các thành phần con của nó, và khi touch vào item list đó lần nữa sẽ thu các thành phần con của nó lại. Có bạn còn đưa hình như sau để mô tả rõ hơn ví dụ.
Bài hướng dẫn hôm nay chúng ta sẽ cùng nhau xây dựng một ListView như vậy.
Mình cũng xin nói trước là kiến thức của bài này sẽ dành cho bạn nào khá rành về Android, và rành về việc xây dựng một ListView như thế nào rồi. Do đó nếu bạn nào còn chưa có kiến thức xây dựng một ListView hay Android căn bản thì có thể đọc các bài học về Android ở Yellow Code Books. Bạn cũng có thể tham khảo thêm về ListView trong Android ở đây.
Và để bắt đầu bắt tay vào xây dựng Expand/Collapse ListView thì chúng ta cần phải biết đến một thành phần con của ListView có sẵn trong Android có tên là ExpandableListView. Kết quả của bài hôm nay sẽ là một chương trình có giao diện như hình sau, chương trình này sẽ hiển thị một danh sách các thương hiệu điện thoại Android, khi bạn touch vào một thương hiệu, sẽ xổ ra danh sách các thiết bị Android của thương hiệu đó. Và tất nhiên cũng có thể thu lại các thành phần con đó khi touch một lần nữa lên thương hiệu.
Tạo Mới Project
Đầu tiên nếu chưa mở Android Studio thì bạn hãy mở lên nhé. Sau khi đã mở Android Studio xong, nếu bạn đang ở màn hình Welcome (hình bên trái ở dưới), thì hãy chọn Start a new Android Studio project. Còn nếu bạn đang ở màn hình chính của Android Studio (hình bên phải ở dưới), hãy nhấn vào menu File > New > New Project….
Các bước tạo một project chắc bạn cũng biết rồi, hoặc bạn có thể xem Android Bài 3 để hiểu rõ hơn. Tuy nhiên có một lưu ý, để bắt đầu xây dựng project nhanh nhất có thể bạn nên chọn Empty Activity khi đến bước Add an Activity to Mobile như hình sau.
Tạo Dữ Liệu Cho Ứng Dụng
Như đã nói, ứng dụng của chúng ta giả lập một danh sách có thể xổ ra hoặc thu lại các thành phần con của nó. Trước khi xây dựng giao diện như vậy cho ứng dụng, chúng ta cần phải tạo dữ liệu cho nó. Bạn có thể chọn cách tạo dữ liệu và lưu lại ở dạng SQlite, hoặc để sẵn dữ liệu trong code, hoặc bạn có thể lưu ra file, hay cao cấp hơn là các bạn có dữ liệu online và các hàm down dữ liệu đó về. Dù thế nào đi nữa thì dữ liệu cũng nên tổ chức ở dạng Json, cho nó phù hợp với bài thực hành hôm nay. Và mình sẽ tạo ra một file Json như vậy rồi đặt vào trong thư mục assets/ ở bước bên dưới.
Trước hết bạn cần tạo thư mục assets/ bằng cách click chuột phải vào thư mục main/ và chọn như hình sau. Khi dialog xuất hiện bạn gõ assets (lưu ý gõ đúng tên nhé).
Sau đó bạn tạo một file để lưu trữ dữ liệu bằng cách click chuột phải vào thư mục assets/ vừa tạo lúc nãy rồi chọn như hình sau. Dialog xuất hiện sau đó bạn gõ phones.json.
Giờ thì bạn nhập vào nội dung Json cho nó.
Full nội dung JSON của file mình copy ra đây.
{ "companies": [ { "name": "LG", "devices": [ { "name": "LG V10" }, { "name": "LG G4 Stylus" }, { "name": "LG G4" }, { "name": "LG Magna" }, { "name": "LG L60" }, { "name": "LG L Fino" }, { "name": "LG L80 2 sim" } ] }, { "name": "Samsung", "devices": [ { "name": "Galaxy Note5" }, { "name": "Galaxy Note4" }, { "name": "Galaxy S7 edge - Injustice" }, { "name": "Galaxy S7 edge" }, { "name": "Galaxy S7" }, { "name": "Galaxy S6 edge+" }, { "name": "Galaxy S6 edge+" } ] }, { "name": "Sony", "devices": [ { "name": "Xperia XZ" }, { "name": "Xperis XA Ultra" }, { "name": "Xperia X" }, { "name": "Xperia Z5 Premium Dual" }, { "name": "Xperia Z5 Compact" }, { "name": "Xperia Z5 Dual" } ] } ] }
Tạo Các Class Wrapper Cho Data
Cái tên Class Wrapper là do mình đặt, nhưng mình biết là cái tên đó đúng, nó chính là các class chứa đựng dữ liệu khi bạn đọc Json từ file lên, các class này sẽ được dùng trong chương trình, và dĩ nhiên sự phân cấp của chúng cũng phải theo phân cấp như trong file, chính vì vậy mà mình mới gọi chúng là class wrapper, cũng có nghĩa là chúng bao lấy dữ liệu.
Chúng ta bắt đầu nói qua và cùng tạo từng class.
Class DeviceModel
Như cấu trúc của Json bên trên kia, chúng ta cùng đi từ trong ra, ta thấy rằng mỗi object trong list devices đều có một biến String tên name. Vậy bạn cũng tạo một class có tên gì cũng được, mình đặt tên là DeviceModel, class này sẽ wrap mỗi object con trong list devices đó.
public class DeviceModel { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
Như đã nói, class này chỉ có một biến name, còn lại là các hàm getter và setter của biến đó.
Class CompanyModel
Tương tự, nhìn vào phần object cao hơn chúng ta thấy có list companies, mỗi item con của biến này đều chứa một biến name kiểu String, và một list devices (list devices này đã được bạn dùng class DeviceModel wrap lại ở trên rồi). Vậy nội dung của class này như sau. Bạn thấy có một biến name, và một list các devices, cùng các hàm getter và setter của các biến này. Mình đặt tên class này là CompanyModel.
public class CompanyModel { private String name; private List<DeviceModel> devices; public String getName() { return name; } public void setName(String name) { this.name = name; } public List<DeviceModel> getDevices() { return devices; } public void setDevices(List<DeviceModel> devices) { this.devices = devices; } }
Class MainContentModel
Ra ngoài gốc của Json thì chúng ta cần có một class chứa list companies. Mình đặt tên class gốc này là MainContentModel.
public class MainContentModel { private List<CompanyModel> companies; public List<CompanyModel> getCompanies() { return companies; } public void setCompanies(List<CompanyModel> companies) { this.companies = companies; } }
Khai Báo Thư Viện Gson
Chúng ta đã dùng Json để lưu trữ nội dung của file, giờ thì chúng ta sẽ sử dụng Gson để tự động chuyển dữ liệu Json về các class wrapper của chúng ta đã tạo ra trên đây. Để dùng được Gson bạn phải vào file build.gradle của app, thêm dòng sau vào dependencies rồi sync lại project.
compile 'com.google.code.gson:gson:2.4'
Xây Dựng AsyncTask Để Đọc Json
Bước trên bạn chỉ mới khai báo thư viện Gson thôi, bây giờ chúng ta mới chính thức đọc nội dung Json và đổ vào MainContentModel. Để đọc nội dung được mượt mà (không bị giật hay chậm ứng dụng) thì mình khuyên bạn nên dùng AsyncTask. Để sử dụng AsyncTask thì mình khai báo một class con của nó ngay trong class MainActivity.java luôn, class con này mình đặt tên là LoadContentAsync, mình khai báo như sau.
class LoadContentAsync extends AsyncTask<Void, Void, MainContentModel> { @Override protected MainContentModel doInBackground(Void... voids) { return null; } @Override protected void onPostExecute(MainContentModel mainContentModel) { super.onPostExecute(mainContentModel); } }
Trong hàm doInBackground() bạn khai báo Gson và MainContentModel trước.
Gson gson = new Gson(); MainContentModel mainContentModel = null;
Tiếp theo, bạn đọc nội dung file phones.json lên thông qua một InputStream, rồi dùng Gson đã khai báo trên đây để đổ nội dung Json vừa đọc vào class MainContentModel.
try { InputStream is = getAssets().open("phones.json"); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); synchronized (this) { mainContentModel = gson.fromJson(reader, MainContentModel.class); } } catch (IOException e) { e.printStackTrace(); }
Đừng quên return trong hàm này.
return mainContentModel;
Hàm onPostExecute() chúng ta xử lý sau.
Thêm ExpandableListView Vào activity_main.xml
Đây là giao diện của ứng dụng, chúng ta sẽ hiển thị ExpandableListView ở toàn màn hình luôn.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <ExpandableListView android:id="@+id/phone_list" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
Tạo Giao Diện Cho Các Item List
Mình lưu ý là ở đây mình dùng từ “các”, vì khác với ListView thông thường chỉ cần một item list, thì ExpandableListView cần tới hai giao diện cho hai item list khác nhau, một cho item cha, và một cho item con khi item cha được expand ra. Bạn hiểu không nào, vậy chúng ta bắt đầu xây dựng hai item đó.
Item List Con
Mình đặt tên item con này là item_device_list.xml, bạn hãy tự tạo một file xml với tên này trong thư mục res/layout/ nhé, nội dung của file xml này như sau.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/device_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_margin="16dp"/> </LinearLayout>
Item List Cha
Item này là item cha của item_device_list trên đây, lần này mình đặt tên nó là item_company_list.xml, nội dung như sau.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/company_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft" android:textStyle="bold" android:layout_margin="16dp"/> </LinearLayout>
Adapter Cho ExpandableListView
Bạn biết mà, ListView nào cũng cần một adapter, vậy bạn hãy tạo một adapter với tên PhoneListAdapter. Nếu như adapter của ListView mà bạn hay xây dựng có thể là con của BaseAdapter, thì với ExpandableListView bạn sẽ phải dùng adapter là con của BaseExpandableListAdapter. Sau đây là source code sau khi bạn tạo ra một adapter, extends base class cho nó, và implement các function cần thiết ban đầu.
public class PhoneListAdapter extends BaseExpandableListAdapter { @Override public int getGroupCount() { return 0; } @Override public int getChildrenCount(int i) { return 0; } @Override public Object getGroup(int i) { return null; } @Override public Object getChild(int i, int i1) { return null; } @Override public long getGroupId(int i) { return 0; } @Override public long getChildId(int i, int i1) { return 0; } @Override public boolean hasStableIds() { return false; } @Override public View getGroupView(int i, boolean b, View view, ViewGroup viewGroup) { return null; } @Override public View getChildView(int i, int i1, boolean b, View view, ViewGroup viewGroup) { return null; } @Override public boolean isChildSelectable(int i, int i1) { return false; } }
Bạn nên tạo một constructor cho adapter, và truyền vào các tham số cần thiết, bao gồm list các CompanyModel mà chúng ta vừa đọc lên ở AsyncTask bên ngoài rồi truyền vào đây.
private Context context; private List<CompanyModel> companyModels; public PhoneListAdapter(Context context, List<CompanyModel> companyModels) { this.context = context; this.companyModels = companyModels; }
Còn đây là các hàm @Override ở bước trên, sau khi đã được thêm nội dung cho các hàm. Nếu bạn đã rành về ListView thì bạn sẽ hiểu code bên dưới đây thôi.
@Override public int getGroupCount() { return companyModels.size(); } @Override public int getChildrenCount(int groupPosition) { return companyModels.get(groupPosition).getDevices().size(); } @Override public Object getGroup(int groupPosition) { return companyModels.get(groupPosition); } @Override public Object getChild(int groupPosition, int childPosition) { return companyModels.get(groupPosition).getDevices().get(childPosition); } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public boolean hasStableIds() { return false; } @Override public View getGroupView(int groupPosition, boolean b, View convertView, ViewGroup viewGroup) { String companyName = ((CompanyModel) getGroup(groupPosition)).getName(); if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.item_company_list, null); } TextView tvName = (TextView) convertView.findViewById(R.id.company_name); tvName.setText(companyName); return convertView; } @Override public View getChildView(int groupPosition, int childPosition, boolean b, View convertView, ViewGroup viewGroup) { final String deviceName = ((DeviceModel) getChild(groupPosition, childPosition)).getName(); if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.item_device_list, null); } TextView tvName = (TextView) convertView.findViewById(R.id.device_name); tvName.setText(deviceName); return convertView; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return false; }
Hoàn Thiện MainActivity.java
Cuối cùng chúng ta sẽ hoàn thiện MainActivity.java, đầu tiên là khai báo một ExpandableListView.
private ExpandableListView lvPhones;
Rồi gán view từ XML qua cho lvPhones này trong hàm onCreate().
lvPhones = (ExpandableListView) findViewById(R.id.phone_list);
Và khởi động AsyncTask ngay sau đó.
new LoadContentAsync().execute();
Cuối cùng, quay lại LoadContentAsync class, ở hàm onPostExecute() của AsyncTask này, bạn khai báo adapter và set nó cho lvPhones, sau khi đã load thành công dữ liệu.
@Override protected void onPostExecute(MainContentModel mainContentModel) { super.onPostExecute(mainContentModel); PhoneListAdapter phoneListAdapter = new PhoneListAdapter(MainActivity.this, mainContentModel.getCompanies()); lvPhones.setAdapter(phoneListAdapter); }
Thực Thi Ứng Dụng
Nào bạn hãy run ứng dụng để xem kết quả nhé.
Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy đánh giá 5 sao nếu thấy thích bài viết, hãy comment bên dưới nếu có thắc mắc, hãy để lại địa chỉ email của bạn để nhận được thông báo mới nhất khi có bài viết mới, và nhớ chia sẻ các bài viết của Yellow Code Books đến nhiều người khác nữa nhé.
Download Source Code Mẫu
Bạn có thể download source code mẫu của bài học hôm nay ở đây.
Reblogged this on gioilaptrinh.
Cho mình hỏi. chỉ cần khai báo là dùng được Gson, không cần phải down thư viện về rồi add vào project hả ad. Cảm ơn ad
Bạn Sùi nên nhớ Gson không có sẵn, nhưng cũng không có nghĩa là mình phải down thư viện đó một cách thủ công.
Bạn chỉ cần làm theo mục “Khai Báo Thư Viện Gson” trên đây, đó là mở file build.gradle của app ra rồi thêm vào dòng sau.
compile ‘com.google.code.gson:gson:2.4’
Khi đó khi bạn build project, thư viện Gson sẽ được down về (mình chưa bị trường hợp này, nhưng một số nguồn nói là bạn phải đảm bảo đang online khi build project nữa, để đảm bảo các thư viện được down online nếu bạn chưa down trước đó).
Cảm ơn ad nhiều nhé. 🙂 Mong ad có thêm nhiều bài hướng dẫn bổ ích 😀
Hi ad,
Mình đã follow theo code của ad nhưng khi mình chạy thì bị undo luôn, dưới đây là log mà mình trace được:
05-08 14:48:52.042 10689-10689/com.example.trungct.expandlistviewexample E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.trungct.expandlistviewexample, PID: 10689
java.lang.NullPointerException: Attempt to invoke interface method ‘int java.util.List.size()’ on a null object reference
at com.example.trungct.expandlistviewexample.Adapter.PhoneListAdapter.getGroupCount(PhoneListAdapter.java:27)
at android.widget.ExpandableListConnector.getCount(ExpandableListConnector.java:397)
at android.widget.ListView.setAdapter(ListView.java:575)
at android.widget.ExpandableListView.setAdapter(ExpandableListView.java:601)
at com.example.trungct.expandlistviewexample.MainActivity$LoadContentAsync.onPostExecute(MainActivity.java:54)
at com.example.trungct.expandlistviewexample.MainActivity$LoadContentAsync.onPostExecute(MainActivity.java:28)
at android.os.AsyncTask.finish(AsyncTask.java:695)
at android.os.AsyncTask.-wrap1(Unknown Source:0)
at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:712)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
mong ad xem giùm mình.
Chào bạn. Theo như mình đoán dựa trên stack trace của bạn thì là do List companyModels bị null rồi nên adapter không lấy được thông số size của list. Sở dĩ object này null vì phương thức đọc json lên của bạn bị lỗi gì rồi. Vậy bạn thử check lại xem bạn đã tạo ra file json hay chưa, các giá trị key trong json có khớp với các biến khai báo trong class chứa nó hay không, cấu trúc của json có hợp lý chưa, và json có để đúng vào thư mục assets không nhé.
cái này có thể áp dụng để làm menu được không ycb, thank