[Ionic x Vue] リストを無限スクロールさせる

はじめに

Ionicはクロスプラットフォームのモバイルアプリ向けフレームワークです。主にiOS/Android向けに、ワンソースコードでモバイルアプリを開発することができます。

今回は無限スクロールの使い方を解説します。

事前準備

こちらのブログ を通して作成したプロジェクトをベースに進めます。

無限スクロールとは

モバイルアプリでリスト表示を実装する際、差分読み込みを行うことで無限にスクロールできるような振る舞いを持たせることがよくあります。具体的には以下の順番で処理します。

  1. まず20件読み込む(1画面に収まる程度)
  2. ユーザーがリストを一番下までスクロールする
  3. 一番下までスクロールしたことをハンドリングする
  4. 追加で10件読み込む(その際には一番下にローディングを表示する)
  5. 2-4 を繰り返す
  6. 全て読み込み終わったら完了

このような処理をiOSやAndroidで実現したい場合は、頑張って実装するかライブラリを使うかの2択になります。

Ionicでは標準で用意されているので便利です。

次のように動作します。

無限スクロールを実装する

それでは実装しましょう。サンプルの Home.vue に無限スクロールするリストを表示します。

まずサンプルのデータとして以下を Home.vue と同じ階層に用意します。長いので省略していますが、以降の処理の関係上、サンプルでは10の倍数の個数になるように用意してください。

export default [
  {
    "name": "Aline Grover",
    "created": "November 28, 2012"
  },
  {
    "name": "Nevada Anders",
    "created": "January 18, 2014"
  },
  {
    "name": "Nicholas Morissette",
    "created": "November 11, 2014"
  },
  {
    "name": "Rusty Umland",
    "created": "January 8, 2019"
  }, ...
];

次に無限スクロールの実装です。まずはソースコード全体を眺めつつ、ポイントを解説します。

<template>
  <ion-page class="ion-page" id="content">
    <ion-header>
      <ion-toolbar>
        <ion-title>Hello World</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content fullscreen class="ion-padding">
      <ion-list v-if="users">
        <ion-item v-for="(user, index) of users" :key="index">
          <ion-avatar slot="start">
            <img :src="'https://www.gravatar.com/avatar/' + index + '?d=monsterid&f=y'">
          </ion-avatar>
          <ion-label>
            <h2>{{ user.name }}</h2>
            <p>Created {{ user.created }}</p>
          </ion-label>
        </ion-item>
      </ion-list>
      <ion-infinite-scroll ref="infinite" threshold="100px" position="bottom">
        <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
        </ion-infinite-scroll-content>
      </ion-infinite-scroll>
    </ion-content>
  </ion-page>
</template>

<script>
/* eslint-disable */

import data from './data';

export default {
  name: "home",
  data() {
    return {
      users: [],
    };
  },
  mounted() {
    this.appendItems(20);
    this.$refs.infinite.addEventListener('ionInfinite', this.loadData);
  },
  methods: {
    async loadData() {
      if (this.users.length < data.length) {
        console.log('Loading data...');
        await this.wait(500);
        this.$refs.infinite.complete();
        this.appendItems(10);
        console.log('Done');
      } else {
        console.log('No More Data');
        this.$refs.infinite.disabled = true;
      }
    },
    appendItems(number) {
      console.log('length is', this.users.length);
      const originalLength = this.users.length;
      for (let i = 0; i < number; i++) {
        const user = data[originalLength + i];
        this.users.push(user);
      }
    },
    wait(time) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve();
        }, time);
      });
    }
  }
}
</script>

リスト表示

まず基本のリスト表示ですが、Vueのリストレンダリングを使って実装します。

<template>
  <ion-list v-if="users">
    <ion-item v-for="(user, index) of users" :key="index">
      【アイテムの中身】
    </ion-item>
  </ion-list>
</template>

この users というデータは、スクリプト側にある data() で返却しています。

export default {
  name: "home",
  data() {
    return {
      users: [],
    };
  }
}

users に要素を追加すると、リストのアイテムも増えます。

無限スクロール

無限スクロールを制御しているのは次のコードです。

<ion-infinite-scroll ref="infinite" threshold="100px" position="bottom">
  <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
  </ion-infinite-scroll-content>
</ion-infinite-scroll>

こちらは主に一番下までスクロールしたことのハンドリング、およびローディングの表示などを担当しています。

threshold 属性は一番下までスクロールしたことのハンドリングの閾値です。100px を指定すると、残り100pxまでスクロールが到達すると次の読み込みを行うためのイベントを走らせることができます。この数が多ければ多いほどユーザーに気づかれない形で読み込める確率が上がりますが、同時に先読みしすぎる問題も出てくるので、100pxなどが丁度良いと思います。

初期表示と追加読み込み

極めて重要なポイントは 追加読み込みのイベントハンドラは、初期読み込みが終わってから設定した方が良い という点です。

IonicInfiniteScrollでは初期読み込みが完了している・していないに関わらず、一番下までスクロールしたことをハンドリングするため、初期読み込みが終わっていない状態でハンドリングしてしまうと初期表示で追加読み込みが必要、といった状態になってしまいます。

そのため以下では、初期読み込みが終わってからイベントハンドラを設定するようにしています。

mounted() {
  // 初期読み込み
  this.appendItems(20);
  // 追加読み込みイベントをハンドリング
  this.$refs.infinite.addEventListener('ionInfinite', this.loadData);
}

また、追加の読み込みが一度完了するごとに complete を呼ぶことで、重複した追加読み込みなどは起きません。

this.$refs.infinite.complete();

最後まで読み込み終わったら、非活性化することで二度と追加読み込みは走らないようにできます。

this.$refs.infinite.disabled = true;

これで以下のような動作になりました。

リスト表示の必需品!

サーバーからデータを取得して表示するような機能はよくある機能の1つだと思います。ぜひ参考にしてみてください。