listviewのセルの高さが動的（可変）な場合のページングの方法

ページング

無数のやり方を試みました。
iOSでもandroidでも大苦戦しました。
どうにもこうにも不具合が取れずコードも汚くて目を覆いたくなるようなものばかり量産してしまいました

何度も泣かされてきたので暫定で一番いいやり方を共有したいと思います。
今のところこれが万能かつシンプルです。

~~失敗例についても書き連ねたいところですが、ちょっとめんどくさいのでやめます~~


成功例のざっくりとした方針としては

**1. adapterからposition（表示位置）を取得しコールバックをする準備をする
2. activity(またはfragment)で表示位置のコールバックを受け取る準備をする
3. 一定のpositionで新しいセル情報を取得してadapterに追加しページングする**

これだけです。
何でこれだけのことに四苦八苦していたのだという話なのですが、知ってれば簡単、知らなければ大変でした。
ちなみにlistViewでなくともrecyclerViewなど他のものでも代替可能です。

今回はメッセージ一覧をページングして表示することとします。

それではもう少し細かく見ていきましょう。

#1. adapterからposition（表示位置）を取得しコールバックをする準備をする

説明する前にアダプタークラスのコードを貼ります。

```java:MessageAdapter.java

public class MessageAdapter extends Adapter {
    // ***リスナーをメンバにおきます
    private Listener listener;

    //(コンストラクタ省略)

    @Override
    public View getView(final int position, View contentView, final ViewGroup parent) {
        final ViewHolder holder;

        // ＊＊＊リスナーを渡されたときにpositionをコールバックします
        if (listener != null){
            listener.sendPos(position);
        }
        
        // listView特有のセルの再利用のコードです。今はスルーしていいです。
        if (contentView == null) {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            contentView = inflater.inflate(R.layout.message_item, parent, false);
            holder = new ViewHolder(contentView);
            contentView.setTag(holder);
        } else {
            holder = (ViewHolder) contentView.getTag();
        }
    }
    // ＊＊＊オーバーライドするためのメソッドを用意します(コールバックのために必要)
    public interface Listener{
        void sendPos(int pos);
    }

    // ＊＊＊activityやfragmentをリスナーとしてセットするときに使います
    public void setListener(PosListener listener) {
        this.listener = listener;
    }

    // viewHolderクラスです。ここにセルのViewを詰め込んときます
    //（今回はメッセージのテキストが表示されるTextViewのみ）
    static class ViewHolder {
        public TextView messageText;

        public ViewHolder(View view) {
            this.messageText = view.findViewById(R.id.text_view);
        }
    }
}
```

getViewの引数のpositionが表示位置を表しています。
こいつをactivityやfragmentにコールバックします。
コールバックに必要な部分は「***」から始まるコメントを付けています。

#2. activity(またはfragment)で表示位置のコールバックを受け取る準備をする

上記だけではまだコールバックはできていません。
今度はコールバックを受ける側での実装が必要です。

```java:MessageFragment.java
// implements MessageAdapter.Lisgenerを忘れずに
public class MessageFragment extends Fragment implements MessageAdapter.Listener {

    private ListView messageList;
    private MessageAdapter messageAdapter;
    private int page = 1;

    @Override
    public void onCreate(Bundle saved){
        super.onCreate(saved);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_message, null);
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState){
        super.onActivityCreated(savedInstanceState);

        // messageAdapterを設定
        messageAdapter = new MessageAdapter(getActivity());
        // リスナー（コールバックの受け手）として自分（MessageFragment）をセットする
        messageAdapter.setListener(this);

        // listViewを設定
        messageList = this.getActivity().findViewById(R.id.messageList);
        // listViewにadapterをセット（setAdapterは一度のみにすること *1）
        if (getActivity()!=null) {
            messageList.setAdapter(messageAdapter);
        }
        // APIを呼び出しメッセージを取得
        new GetMessageInfo().execute();
    }
}
```

あとはsendPosをoverrideするだけでコールバックを受け取ることができます。
また、GetMessageInfo()はAPIを叩くクラスです。
これらについては次節で。

#3. 一定のpositionで新しいセル情報を取得してadapterに追加しページングする


下記はAPI呼び出しでメッセージを取ってくるクラスですが、伝えたいことは、onPostExecute()にてbeansを作るべきということだけです。（情報取得後に呼ばれるメソッド）
そうしなければnullが入ります。
 これ以外に特筆すべき点はないので、onPostExecute以降だけ読んでいただければ大丈夫です。


```java:MessageFragment.java
// implements MessageAdapter.Lisgenerを忘れずに
public class MessageFragment extends Fragment implements MessageAdapter.Listener {

    private ListView messageList;
    private MessageAdapter messageAdapter;
    private int page = 1;

    @Override
    public void onCreate(Bundle saved){
        super.onCreate(saved);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_message, null);
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState){
        super.onActivityCreated(savedInstanceState);

        // messageAdapterを設定
        messageAdapter = new MessageAdapter(getActivity());
        // リスナー（コールバックの受け手）として自分（MessageFragment）をセットする
        messageAdapter.setListener(this);

        // listViewを設定
        messageList = this.getActivity().findViewById(R.id.messageList);
        // listViewにadapterをセット（setAdapterは一度のみにすること *1）
        if (getActivity()!=null) {
            messageList.setAdapter(messageAdapter);
        }
        // APIを呼び出しメッセージを取得
        new GetMessageInfo().execute();
    }


    // 下記はAPI呼び出しでメッセージを取ってくるクラスですが、伝えたいことは
    // onPostExecute()にてbeansを作るべきということだけです。（情報取得後に呼ばれるメソッド）
    // そうしなければnullが入ります。
    // これ以外に特筆すべき点はないので、onPostExecuteだけ読んでいただければ大丈夫です。

    private class GetMessageInfo extends AsyncTask<Void,Void,String> {

        String json;

        @Override
        protected void onPreExecute() {
            // メッセージを取得するuserのIDとメッセージのpageをAPIに送る
            json = HOGE_GOMAIN.MESSAGE_API + "?id=" + userId "&page=" + page;
        }

        // doInBackgroundはスルーしてオッケー
        @Override
        protected String doInBackground(Void... voids) {
            try {
                URL url = new URL(json);
                HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
                InputStream inputStream = httpURLConnection.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

                StringBuilder stringBuilder = new StringBuilder();

                while ((json = bufferedReader.readLine())!=null){
                    stringBuilder.append(json);
                }

                bufferedReader.close();
                inputStream.close();
                httpURLConnection.disconnect();
                return stringBuilder.toString().trim();

            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(String result){
            // ***サーバーからメッセージ情報を取ってくる処理が終わってからアダプターに値をセットする
            setMessageBeans(result);
        }
    }



    private void createMessageAdapter(String result) {
        messageBeans.clear();

        messageAdapter.addAll(createMessageBeans(result));

        // adapterにBeansが追加されたことを通知(setAdapterを使わないこと）
        messageAdapter.notifyDataSetChanged();
    }

    private ArrayList<MessageBean> createMessageBeans(String result) {
        try {
            ArrayList<MessageBean> messageBeans = new ArrayList<MessageBean>();
            JSONObject messageData = new JSONObject(result);

            int state = messageData.getInt("status");
            if (state == 100) {
                JSONArray json = messageData.getJSONArray("result");
                for (int i = 0; i < json.length(); i++) {
                    JSONObject jsonobject = json.getJSONObject(i);
                    messageText = jsonobject.getString("message_text");

                    MessageBean messageBean = new MessageBean();
                    messageBean.setMessageText(messageText);
                    messageBeans.add(messageBean);
                }
            }

        } catch (JSONException e) {
            e.printStackTrace();
        }
        return messageBeans;
    }


    // adapterからのコールバックを受け取る
    @Override
    public void sendPos(int pos){
        // 最後のセルを表示したら次のページを読み込む
        if (messageAdapter.getCount() - 2 < pos){
            page++;
            new GetMessageInfo().execute();
        }
    }
}
```


taskは別ファイルに分けてコールバックした方がスッキリキレイですし実際はそう実装していますが、コールバックまみれでは混乱すると思うのでぶん投げときました。
重要な部分はコメントを付けてるのでほぼ読み流してオッケーです。



こんな感じで、安定したページングを実現できました。
なお、いらない部分を端折ったので端折ったのでなるべく過不足なく書いたつもりですが、端折り過ぎて不足していたり無駄なコードが残っている可能性がありますが、位置情報をコールバックする手法について参考にしていただければ幸いです。