Androidアプリで検索機能実装にExpandableLayoutを使ったらとても使いやすくなった

[bowyer-app](https://github.com/bowyer-app)です。
今回新しいアプリ[弓道のアプリ 採点簿 for チーム](https://play.google.com/store/apps/details?id=com.bowyer.app.android.kyudoscoreteam&hl=ja)の中で、スコアや選手の検索を実装しました。
僕のアプリは、タイトル、人数、日付などいろんな条件で検索する必要がありました。
より操作を簡単にするために極力画面遷移を挟まず、その場で検索したかったので何かいい方法は無いかなと探してたら、
[AAkira](https://github.com/AAkira)さんの作った[ExpandableLayout](https://github.com/AAkira/ExpandableLayout)がちょうど良かったので入れてみました。

# UI
|通常時|詳細検索時|
|---|---|
|![device-2016-05-15-102732.png](https://qiita-image-store.s3.amazonaws.com/0/92349/d0208cd4-e850-352b-5628-61a02d4db809.png) |![device-2016-05-15-101844.png](https://qiita-image-store.s3.amazonaws.com/0/92349/ce3de573-2369-7c66-8ad8-3c0522a81ce5.png)|

僕のアプリから一部コードの切り出してこちらに公開してますので、よかったら御覧ください
[https://github.com/bowyer-app/PlayerManage](https://github.com/bowyer-app/PlayerManage)

必要なときにカスタムな検索機能が出てくるのでとても使いやすくなりました。
気づいたらどの画面でも[ExpandableLayout](https://github.com/AAkira/ExpandableLayout)を使っていました。

# DB
DBには[square](https://github.com/square)さんの[sqlbrite](https://github.com/square/sqlbrite)を使いました。
細かいところはsql書きたいけど、それ以外のところを書くのはめんどくさかったので、sqliteをラップしてくれているこちらを選定しました。

## 今回工夫した点
Daoの実装を工夫してみました。
[PlayerDao.java](https://github.com/bowyer-app/PlayerManage/blob/develop/app/src/main/java/com/bowyer/app/playermanage/database/dao/PlayerDao.java)

sqlbriteのサンプルでは、検索条件を書くときにSQLを定義し

```java:ItemsFragment.java
private static final String LIST_QUERY = "SELECT * FROM "
      + TodoItem.TABLE
      + " WHERE "
      + TodoItem.LIST_ID
      + " = ? ORDER BY "
      + TodoItem.COMPLETE
      + " ASC";

@Inject BriteDatabase db;
```
上記で定義したsqlを元にdbに対してqueryを実行しています。

```java:ItemFragment.java
String listId = String.valueOf(getListId());
subscriptions.add(db.createQuery(TodoItem.TABLE, LIST_QUERY, listId)
        .mapToList(TodoItem.MAPPER)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(adapter));
```

`BriteDatabase`の`createQuery`を見てみると

```java:BriteDatabase.java
public QueryObservable createQuery(@NonNull final String table, @NonNull String sql,
      @NonNull String... args) {
```
引数に`args`を渡しているので、こことsqlの`= ? `の部分がズレると検索条件があわなくなり、引数足りないよってエラー出たりします。
僕が作った[PlayerDao.java](https://github.com/bowyer-app/PlayerManage/blob/develop/app/src/main/java/com/bowyer/app/playermanage/database/dao/PlayerDao.java)では、`sql`と`args`を関連付けた`PlayerQuery`というオブジェクトを生成できるようにしました。

```java:PlayerDao.java
public static final class SQLBuilder {
    private String sql = "SELECT * FROM " + Player.TABLE + " WHERE ";
    private final List<String> values = new ArrayList<>();

    private String getAndSql() {
      if (!values.isEmpty()) {
        sql = sql + " and ";
      }
      return sql;
    }

    private String getOrSql() {
      if (!values.isEmpty()) {
        sql = sql + " or ";
      }
      return sql;
    }

    public SQLBuilder name(String name) {
      if (TextUtils.isEmpty(name)) {
        return this;
      }
      //全部マッチさせるため
      sql = getAndSql() + Player.LAST_NAME + " like ? ";
      values.add("%" + name + "%");
      sql = getOrSql() + Player.FIRST_NAME + " like ? ";
      values.add("%" + name + "%");
      sql = getOrSql() + Player.LAST_NAME_PHONETIC + " like ? ";
      values.add("%" + name + "%");
      sql = getOrSql() + Player.FIRST_NAME_PHONETIC + " like ? ";
      values.add("%" + name + "%");
      return this;
    }

    public SQLBuilder sex(Sex sex) {
      if (sex == Sex.ALL) {
        return this;
      }
      sql = getAndSql() + Player.SEX + " = ? ";
      values.add(String.valueOf(sex.getSex()));
      return this;
    }

    public SQLBuilder rank(Rank rank) {
      if (rank == Rank.ALL) {
        return this;
      }
      sql = getAndSql() + Player.RANK + " = ? ";
      values.add(rank.getRank());
      return this;
    }

    public SQLBuilder id(long playerId) {
      sql = getAndSql() + Player.ID + " = ? ";
      values.add(String.valueOf(playerId));
      return this;
    }

    public PlayerQuery build() {
      if (values.isEmpty()) {
        sql = sql.replace("WHERE", "");
      }
      return new PlayerQuery(sql, values.toArray(new String[values.size()]));
    }
  }

public static class PlayerQuery {
    public final String sql;
    public final String[] values;

    public PlayerQuery(String sql, String[] values) {
      this.sql = sql + " ORDER BY " + Player.ID + " DESC, " + Player.LAST_NAME + " ASC";
      this.values = values;
    }
  }
```

`Builder`パターンを使って`sql`のカラムと`args`の値を関連付けて組み立てているので、
足りないとかズレるということが発生しません。

使い方はこちら、[MainActivity.java#L182-L192](https://github.com/bowyer-app/PlayerManage/blob/develop/app/src/main/java/com/bowyer/app/playermanage/ui/activity/MainActivity.java#L182-L192)

検索条件が変わる度に`PlayerDao.PlayerQuery`を生成して`PlayerDao`に食わせるだけ。

```java:MainActivity.java
private void doSearch() {
    String name = mSearchQueryText.getText().toString();
    Sex sex = getCheckedSex(mSexGroup.getCheckedRadioButtonId());
    Rank rank = Rank.of(mRankText.getText().toString());
    PlayerDao.PlayerQuery playerQuery =
        new PlayerDao.SQLBuilder().name(name).sex(sex).rank(rank).build();
    mSubscription = (PlayerDao.getPlayerByQuery(mDb, playerQuery)
        .mapToList(Player.MAPPER)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(mAdapter));
  }
```

sql書くのすごくめんどくさいし、条件が複雑になっていくと検索句のカラム名と値がズレてよくわからなくなるので、こういった工夫が役に立つのではと思います。

[https://github.com/bowyer-app/PlayerManage](https://github.com/bowyer-app/PlayerManage)
こちらにソースコードを公開してあるのでよかったら見てください。
もっとこうしたほうが良いよってのがあればPR頂けると嬉しいです。