FirebaseのRealtime Databaseでクリアタイムランキングを作成する(Androidアプリ)

前回の続きです。
https://qiita.com/KIRIN3qiita/items/4573d26187b8c9e53fc6

今回はRealtime Databaseの実践編としてゲームクリアタイムのランキングを作成します。
![qiita.png](https://qiita-image-store.s3.amazonaws.com/0/252283/b0a1a76d-cef3-04e6-5df1-2dd25bf2c9f5.png)

私が昔作成したお釣り支払いゲームのクリアタイムランキング画像です。

自作のしょぼいゲームアプリでも、クリアタイムランキングがあると少しは形になります。（インストール数100ぐらいですが・・・）
至高の支払い(https://play.google.com/store/apps/details?id=jp.kirin3.changegame&hl=ja)
これを応用すればスコアランキングなども作れます。

##ルール作成
$ Variablesを利用して、ユーザーごとのデータを保存するルールを作成します。

[$ Variablesについて](https://firebase.google.com/docs/database/security/securing-data)
\---------------------------
$ 接頭辞を持つキャプチャ変数を宣言することにより、読み取りや書き込みのパスの一部をキャプチャできます。これはワイルドカードとして機能し、ルール宣言の内部で使用するために該当のキーの値を保存します
\---------------------------

ユーザーごとの名前、クリア時間、クリア日時、ユーザーIDを保存するルールを作成しました。

```json:ルール
{
  /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
  "rules": {
     "info": {
      ".read": true,
      ".write": true,
      "$user_id": {
        "name": {
          ".validate": "newData.isString() &&
                          newData.val().length < 9"
        },
        "time": {
          ".validate": "newData.isNumber()"
        },
        "date": {
          ".validate": "newData.isString()"
        },
        "user_id": {
          ".validate": "newData.isString()"
        }
      }
    }
  }
}
```
"$user_id"の項目の中に"user_id"があるのは気持ちが悪いですが、同じ階層のデータを引き抜くイメージなのであると便利です。

##データ登録
まずデータ保存、取得に利用するクラスを作成。

```java:User 
public static class User {
    public int rankingNo;
    public String name;
    public Double time;
    public String date;
    public String userId;

    // 空のコンストラクタの宣言が必須
    public User() {
    }

    public User(String _name, Double _time,String _date,     String _userId) {
        name = _name;
        time = _time;
        date = _date;
        userId = _userId;
    }
    public void setRankingNo( int _rankingNo ){
        rankingNo = _rankingNo;
    }

    public Integer getRankingNo(){
        return rankingNo;
    }
    public String getName(){
        return name;
    }
    public Double getTime(){
        return time;
    }
    public String getDate(){
        return date;
    }
    public String getUserId(){
        return userId;
    }
}
```

```java:データ登録
// とりあえずUserIdはUUIDで作成。（その後、毎回変わらないようにプリファランスに保存するなどが必要）
String sUserId = UUID.randomUUID().toString();

User user = new User( "UMEHARA",5.3,"2019-10-21 09:00:27" ,sUserId);

// インスタンスの取得
FirebaseDatabase database = FirebaseDatabase.getInstance();

// ファイルパスを指定してリファレンスを取得
DatabaseReference ref = database.getReference("info");

// データを登録
ref.child(sUserId).setValue(user);
```

##データ取得
取得前に一度だけ終端データを登録しておきます。（なぜ必要かは後で説明）

```java:終端データ登録
public void SaveTerminal(){
    String userId = TARMINAL;
    String userName = "ターミナル";
    Double time = 99999.9;
    String date = "1999-01-01 00:00:00";

    FirebaseDatabase database = FirebaseDatabase.getInstance();
    DatabaseReference ref = database.getReference("info");

    User user = new User( userName,time,date,userId );
    ref.child(userId).setValue(user);
}
```

```java:データ取得
// ユーザーデータ配列
public static ArrayList<User> sUsers;
// ランキングの最大取得数
final int OUTPUT_RANKING_NUM = 100;

FirebaseDatabase database = FirebaseDatabase.getInstance();
// ファイルパスを指定してリファレンスを取得
final DatabaseReference refUser = database.getReference("info");
// クリア時間でソート、最大取得数を設定
refUser.orderByChild("time").limitToFirst(OUTPUT_RANKING_NUM + 1).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
        User user = dataSnapshot.getValue(User.class);

        // ランキング順位もオブジェクトに保存
        sRankNo++;
        user.setRankingNo(sRankNo);

        // 終端データを受け取ったら、アダプターに渡し、リストビューに表示
        if( dataSnapshot.getKey().equals(TARMINAL) || sRankNo == OUTPUT_RANKING_NUM + 1 ){
            UserAdapter adapter = new UserAdapter(getApplicationContext(), 0, sUsers);
            sListView.setAdapter(adapter);
        }
        // 配列に保存
        else {
            sUsers.add(user);
        }
    }
    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String s) {
    }
    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
    }
    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String s) {
    }
    @Override
    public void onCancelled(DatabaseError databaseError) {
    }
});
```

取得したデータを配列に入れて、アダプターに渡し、ListViewで表示します。
データはクリア時間でソートされ、最大100件の取得に設定しています。
データの取得完了が判断できないので終端データ（必ず最後に取得するデータ）を登録してデータの終わりを判断しています。ルール文で"$user_id"の項目の中に"user_id"があるなど、もっとよい書き方があるかもしれませんが、一応これでも動くということでご参考にして頂けたらと思います。
Realtime Databaseは癖がすごくて使いこなすのは大変ですね。