表示するアイテムを使い回して節約--AndroidのListViewの営み

2015年12月21日


多数のデータを、スクロールさせながらリスト表示できるListViewは、Androidのアプリを作る上では欠かせない機能ですが、スクロールにより新しいデータを表示するときは、スクロールで画面から消えた表示内容を使い回すなど、効率向上のため、内部ではいろいろな工夫が行われている様子です。

このような仕組みを理解できると、面白いです。

リストビューの例

アダプタをつけてListViewにデータを関連づける

ListViewは、たくさんのデータをリスト形式で表示します。

表示したいデータは、例えば配列などで用意できます。

ListViewでは、直接データの配列を与えて表示することは、基本的にできません。

表示するデータは、「アダプタ」と呼ばれる別のクラスを介して、リストビューに届きます。

アダプタでは、リストビューから要求のあったデータに対して、表示するためのビューを作ってリストビューに返します。

つまり、アダプタではそれぞれのデータを、表示したい形に変換して、リストビューに送っているわけです。それぞれのデータを、どのように表示するかは、アダプタを工夫して作れば、自由にレイアウトすることができます。

[リストビュー]⇔[アダプタ]⇔[データ]

リストビューを使う上で一番手っ取り早いのが、ArrayAdapterを使うことです。ArrayAdapterを使えば、配列の形のデータを、リストビューに渡すことができます。

ArrayAdapterは、各データをテキストビューの形で表示するためのプログラムを内蔵しているので、テキストビューに表示するだけであれば、文字列の配列をセットすれば、すぐにアダプタが完成します。

表示する各項目がテキストビュー1個だけではなく、もう少し凝った表示となる場合は、アダプタの派生クラスを作るなどして、自分で作ったレイアウトに、データを表示させることもできます。

例えばArrayAdapterの派生クラスを作って、ビューにデータを流し込むメソッドをオーバーライドすることで、具体的にデータを流し込む方法を指定できます。

表示の工夫あれこれ

ListViewがアダプタに要求するのは今すぐ表示するデータだけ

リストビューは、リストに含まれる全てのデータを一度に要求するわけではありません。

さし当たって画面に表示される要素だけを、アダプターに要求します。

例えば、表示するデータが10件ある場合でも、画面に一度に4件しか表示できないときは、リストビューは、初期段階では4件分のデータにしかアクセスしません。

従って、データが100件あっても1000件あっても、ほとんど待たされずにデータが表示されます。

画面をスクロールさせて、別の要素が表示されるときは、表示されそうになった瞬間に、アダプタに対して新たに表示されるデータを要求します。

画面から消えた表示項目は使い回される

リストビューに表示されるデータは、1件ずつがView(ビュー)の形で表示されます。Viewクラスだけでなく、TextViewでも、複数の要素が入ったレイアウトでも、Viewの仲間なら置くことができるので、柔軟な表示ができます。

この、表示のためのビューを作っているのがアダプタですが、表示するデータを表示するビューのインスタンスを作る処理には、かなりの労力を要します。

そこで、リストビューでは、スクロールして画面から消えたビューのインスタンスを、スクロールにより新しく表示されるデータを表示するためのインスタンスに使い回すようになっています。

つまり、こういうことです。

こんなわけで、表示項目のビューのインスタンスは、5つあれば十分ということになります。

ソースコードでインフレートの方法を指定する例

例えば、以下はチェックボックスとテキストビューを横に並べたレイアウトと、そのレイアウトにInteger型のデータを流し込む場合のソースの一例です。

以下は、表示するアイテムのレイアウトです。(list_item_1.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <CheckBox
        android:id="@+id/litem_cbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"/>
    <TextView
        android:id="@+id/litem_label1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:textSize="30sp" />
</LinearLayout>

ちなみに、チェックボックスをつけているのは、これからの説明のためで、機能上意味があるわけではありません。

上記のレイアウトに、表示するアイテムのデータを流し込むためのアダプターのソース(MyArrayAdapter.java)を、以下に示します。

package jp.ne.sakura.sugi.test208;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.TextView;
import java.util.List;

public class MyArrayAdapter extends ArrayAdapter<Integer> {
    private LayoutInflater mInflater;
    public MyArrayAdapter(Context context, int resource, List<Integer> objects) {
        super(context, resource, objects);
        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int n = getItem(position);
        if(convertView == null){
            convertView = mInflater.inflate(R.layout.list_item_1, null);
            Log.d("myarrayadapter", "インフレート:"+position);
        }else{
            Log.d("myarrayadapter", "使い回し:"+position);
        }
        CheckBox chk = (CheckBox)convertView.findViewById(R.id.litem_cbox);
        chk.setText(""+n);
        TextView label = (TextView)convertView.findViewById(R.id.litem_label1);
        String s = label.getText().toString();
        label.setText(s+n+" ");
        return convertView;
    }
}

getView()は、positionで指定された位置に表示するデータを、ビューの形で返すメソッドです。

getView()で、表示したいレイアウトでインスタンスを作り、データを流し込んで返せば、それがリストビューに表示されます。

getView()の引数のうち、convertViewがnullでない場合は、convertViewに、使い回したいビューのインスタンスが入っています。つまり、画面から消えた表示項目のビューが入っています。convertViewがnullの場合は、使い回せるビューがないので、新規にレイアウトにあったビューを作る必要があります。

ソースコードでは、以下の工夫を行い、使い回しているかどうかが分かるようにしました。

同時に5件程度表示できる状態で、画面をスクロールさせてみたところ、以下のようにログが出ました。最初の6件は新規にインスタンスをインフレートして作っていますが、残りは使い回しになっていたことが、はっきりと分かります。

12-21 11:12:15.395 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:0
12-21 11:12:15.407 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:1
12-21 11:12:15.412 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:2
12-21 11:12:15.422 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:3
12-21 11:12:15.427 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:4
12-21 11:12:16.417 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: インフレート:5
12-21 11:12:17.947 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: 使い回し:6
12-21 11:12:20.114 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: 使い回し:7
12-21 11:12:21.314 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: 使い回し:8
12-21 11:12:22.314 26179-26179/jp.ne.sakura.sugi.test208 D/myarrayadapter: 使い回し:9

画面の表示を見ると、「6」以降のデータは、「0」「1」「2」「3」を表示していたインスタンスを使い回して表示されていることが分かります。

ちなみに、チェックボックスにチェックを入れてスクロールすると、チェックされたボックスが上側に消えると上側から出てきます。逆も同じです。

例えば、"0"にチェックを入れて、スクロールさせると、使い回された"6"には、最初からチェックが入っています。

0にチェックを入れた様子

6にチェックがついて出てくる。

チェックボックスがチェックされているかの状態は、アダプタのgetView()では操作していません。従って、使い回されたビューでは、前回表示されていたときの状態が、そのまま引き継がれています。

なお、使い回されたビューのインスタンスには、前回表示したときのデータが残っていることが保証されているわけではないです。偶然前回の表示が残っていたと理解するべきでしょう。

面白い工夫だけどアプリ開発上若干の負担あり

このように、Androidでは、なるべくレイアウトのインフレートを行わなくてもいいようにするために、リスト表示される各ビューを使い回して、処理時間やメモリーを節約していることが分かりました。

このような仕組みがある背景には、スマートフォンやタブレットは、パソコンと比べればメモリの量が限られていることがあります。最近ではそうでもなくなってきたようですが。

いろいろと工夫して効率のよいプログラムを作ることは大切ですが、アプリを書く上では若干の負担があります。

使い回しができるインスタンスがあれば、そのまま使い、なければ新しく作る、というロジックをアプリ開発者が記述する必要があるので、開発の際、細かいところに気を取られて、アプリの内容に集中しにくくなるかもしれません。

処理効率のよさと、開発の簡便さを両立させることは、簡単なことではありません。リストビューのこのような仕組みは、両者のバランスを取ってうまれた結果なのかもしれません。


杉原俊雄のホームページAndroidアプリ開発メモ(もくじ)

(c) 2015 Toshio Sugihara. All rights reserved.