Cách Xây Dựng Một ListView Có Thể Expand/Collapse Trong Android

Posted by

Đượ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ụ.

Expand/Collapse ListView Mẫu

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 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.

Expand/Collapse ListView Kết Quả

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….

Tạo mới project có ListView

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 mới project có ListView

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é).

Tạo thư mục chứa dữ liệu cho ListView

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.

Tạo file chứa dữ liệu cho ListView

Giờ thì bạn nhập vào nội dung Json cho nó.

Nội dung file chứa dữ liệu cho ListView

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 gettersetter 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 gettersetter 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 GsonMainContentModel 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.

Advertisements
Rating: 5.0/5. From 1 vote.
Please wait...

5 comments

  1. 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

    1. 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 đó).

      1. Cảm ơn ad nhiều nhé. 🙂 Mong ad có thêm nhiều bài hướng dẫn bổ ích 😀

Gửi phản hồi