【JavaScript】【Vue.js】Vuex でも型補完がほしい

前書き

最近お仕事で Vue.js + Vuex をモリモリやっているんですが、せっかく VSCode を使っているのに公式ドキュメントに書いてある方法をそのまま使うと型補完が効かないのでしんどい気持ちになってきました。

commit / dispatchで指定する名前とかペイロードとか、stateから参照するメンバ名とその型とか、さくさく補完して欲しくないですか?IntelliSense に出てこないものは存在しないものと同義じゃないですか?

そういった辛いお気持ちをどうにかするため、色々やってみたメモです。

前提・環境

仕組みの概要

  • コンポーネント内では$vm.storeを参照するのではなく、importでストアのインスタンスを取得する。
  • マッピング用のヘルパー関数(mapStateとか)は使わない。*1
  • ステート用のクラスを作る。
  • ゲッターはステート用のクラスに関数として定義する。
  • commit / dispatchに指定する名前とペイロードを持つクラスを作る。
  • modules.namespacedによる名前空間の管理を行わない。
    • 全部グローバル空間に突っ込むことになるが、commit / dispatchに指定する名前が一意になっていれば問題ない。やり方は後述。
    • ちなみにmodules.namespacedtrueになっていなくても、ステートはstore[${モジュール名}]でアクセスできる。
  • 型情報が失われてしまう箇所はJSDocを書いて情報を補完する。*2

サンプルコード

サンプルとなるコードがないと説明しづらいので適当に例を載せておきます。

また、Githubに今回の動作検証用プロジェクトをあげてあります。今回の論旨に関係ない部分はめちゃめちゃ適当に作ったので、そのまま流用すると痛い目に合うと思います。

ストアの定義

要点は以下の通りです。

  • 検索処理用のモジュール
  • ステートとして検索結果を持つ
  • アクションに検索処理が定義されている
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    search: {
      state: {
        result: []
      },
      mutations: {
        setResult(state, payload) {
          state.result = payload;
        }
      },
      actions: {
        async search({ commit }, payload) {
          const body = JSON.stringify(payload);
          const resp = await fetch("/search", {
            method: "POST",
            headers: {
              Accept: "application/json",
              "Content-Type": "application/json"
            },
            body
          });
          commit("setResult", resp.json());
        }
      }
    }
  }
});

export default store;

コンポーネントの定義

先程のストアがマッピングされている検索用フォーム(src/components/SearchForm.vue)と検索結果(src/components/SearchResult.vue)のコンポーネントです。

<template>
  <div>
    <label> ID: <input type="text" @input="id = $event.target.value" /> </label>

    <button @click.prevent="onSearch">Search</button>
  </div>
</template>

<script>
  export default {
    name: "SearchForm",
    data() {
      return {
        id: ""
      };
    },
    methods: {
      async onSearch() {
        await this.$store.dispatch("search", { id: this.id });
      }
    }
  };
</script>
<template>
  <table v-if="result.length > 0">
    <thead>
      <th>ID</th>
      <th>Name</th>
    </thead>
    <tbody>
      <tr v-for="item in result" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
  export default {
    name: "SearchResult",
    computed: {
      result() {
        return this.$store.state.search.result;
      }
    }
  };
</script>

モジュールを別ファイルに分割する 9dafad2

とりあえずモジュールを分割しましょう。こんな感じのディレクトリ構成にします。

src
└─store
    │  index.js
    └─modules
        └─search
                actions.js
                index.js
                mutations.js
                namespace.js
                state.js

store/modules/search/index.jsでモジュールと名前空間をエクスポートするようにします。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";

const module = {
  state,
  mutations,
  actions
};

export default module;
export { default as nsSearch } from "./namespace";
// store/modules/search/namespace.js
export default "search";

state.js / mutations.js / actions.jsは先程のストア定義からそのまま切り出すだけです。

// store/modules/search/state.js
const state = {
  result: []
};

export default state;
// store/modules/search/mutations.js
const mutations = {
  setResult(state, payload) {
    state.result = payload;
  }
};

export default mutations;
// store/modules/search/actions.js
const actions = {
  async search({ commit }, payload) {
    const body = JSON.stringify(payload);
    const resp = await fetch("/search", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body
    });
    commit("setResult", resp.json());
  }
};

export default actions;

store/index.jsでモジュールをインポートし、Vuex.Storeインスタンスを作るようにします。

// store/index.js
import Vue from "vue";
import Vuex from "vuex";
import searchModule, { nsSearch } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;

これでモジュールが増えてもそれなりに管理しやすくなりました。

ステートのクラス化 8287dd

まずはステートをクラスにしてしまいましょう。何にも考えずにやると、こんな感じでしょうか。

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /** 検索結果 */
    this.result = [];
  }
}

export { SearchState };

export default new SearchState();

クラスの定義(export { SearchState })とシングルトンインスタンスexport default new SearchState())は別々にエクスポートしておきます。理由は後で定義だけ参照したくなるからです。

ステートに定義されたオブジェクトのクラス化

このままだとSearchState#resultの型はany[]となってしまいます。これでは片手落ちなので、ちゃんと検索結果用のクラスも作ってあげます。

// store/modules/search/models.js
/**
 * 検索結果
 */
class SearchResult {
  /**
   * 検索結果
   */
  constructor() {
    /** ID */
    this.id = "";
    /** 名前 */
    this.name = "";
  }
}

export { SearchResult };

state.js側では@typeを指定して型情報を補完してあげます。

import * as Models from "./models";

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /**
     * 検索結果
     * @type {Models.SearchResult[]}
     */
    this.result = [];
  }
}

export { SearchState };

export default new SearchState();

これでSearchModule#result[n]からidnameが補完されるようになりました。

ステートのエクスポート 461a29

とはいえ、今のままではコンポーネント側で参照するときに補完が効きません。意味ないですね。

そこでちゃんと型情報を付与してあげつつ、ストアとは別にステートをエクスポートするようにします。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";

const module = {
  state,
  mutations,
  actions
};

export default module;
// 型定義だけエクスポート
export { SearchState } from "./state";
export { default as nsSearch } from "./namespace";
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
// 型定義 (SearchState) をインポートに追加
import searchModule, { nsSearch, SearchState } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;
// エクスポートする時に @type を指定してあげるとインポート先で型情報が補完される
/** @type {SearchState} */
export const searchState = store.state[nsSearch];

後はコンポーネントsearchStateをインポートしてやれば OK です。

<template>
  <table v-if="result.length > 0">
    <thead>
      <th>ID</th>
      <th>Name</th>
    </thead>
    <tbody>
      <tr v-for="item in result" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
  // searchState をインポート
  import { searchState } from "../store/index.js";

  export default {
    name: "SearchResult",
    computed: {
      result() {
        // $vm.store ではなくインポートしたステートを直接参照
        // return this.$store.state.search.result;
        return searchState.result;
      }
    }
  };
</script>

副次効果として、コンポーネント内のmethodsなどからthis.resultを参照するとちゃんとSearchResult[]として扱われます。最高ですね。

MutationType / ActionTypeの作成 37fa2d

次はcommit / dispatchに指定する名前とペイロードがあやふやすぎる問題をどうにかしていきましょう。

commit / dispatchともに、第一引数に名前を渡す代わりにtypeを持つオブジェクトを渡すことが許されているので、これを活用します。

// 以下はどちらも mutations.setResult に対し { result: [] } を渡す
commit("setResult", { result: [] });
commit({ type: "setResult", result: [] });

// 以下はどちらも actions.search に対し { param: {} } を渡す
dispatch("search", { param: {} });
dispatch({ type: "search", param: {} });

具体的には、typeを参照するとcommit / dispatchで呼び出すべき名前が返ってくるクラスを作成します。

MutationTypeの定義

まずはミューテーションから。

// store/modules/search/mutationTypes.js
import namespace from "./namespace";

/**
 * 検索結果設定
 */
export class SetResult {
  get type() {
    return SetResult.type;
  }

  static get type() {
    return `${namespace}/SetResult`;
  }
}

そしてこのクラスのコンストラクタにペイロードを指定するようにすれば完璧です。

// store/modules/search/mutationTypes.js
import namespace from "./namespace";
import { SearchResult } from "./models";

/**
 * 検索結果設定
 */
export class SetResult {
  /**
   * 検索結果設定
   * @param {SearchResult[]} result 検索結果
   */
  constructor(result) {
    /**
     * 検索結果
     * @type {SearchResult[]}
     */
    this.result = result;
  }

  get type() {
    return SetResult.type;
  }

  static get type() {
    return `${namespace}/SetResult`;
  }
}

これでnew SetResult(result)という形でインスタンスを作れば{type: "search/SetResult", result: result}というオブジェクトを得られるようになりました。

また、@paramを記述することでペイロードとして渡すべき型の情報も明示されるようになったため、より安全にcommitを呼び出すことが可能になります。

さらにtypenamespaceを含めて返すようにすることで、実質namespacedtrueにしたのと同じ効果を得ることができます。

さらにさらに、今まで作ったクラスを組み合わせることでmutations内の関数でも型補完を効かせることができるようになります。

// store/modules/search/mutations.js
import { SearchState } from "./state";
import * as types from "./mutationTypes";

const mutations = {
  /**
   * 検索結果設定
   * @param {SearchState} state
   * @param {types.SetResult} payload
   */
  [types.SetResult.type](state, { result }) {
    state.result = result;
  }
};

export default mutations;

ActionTypeの定義

アクションも同じようなものを作れば OK です。

// store/modules/search/actionTypes.js
import namespace from "./namespace";

/**
 * 検索処理
 */
export class Search {
  /**
   * 検索処理
   * @param {string} id ID
   */
  constructor(id) {
    /**
     * ID
     * @type {string}
     */
    this.id;
  }

  get type() {
    return Search.type;
  }

  static get type() {
    return `${namespace}/Search`;
  }
}
// store/modules/search/actions.js
import { ActionContext } from "vuex";
import * as types from "./actionTypes";
import * as mutationTypes from "./mutationTypes";

const actions = {
  /**
   * 検索処理
   * @param {ActionContext} context
   * @param {types.Search} payload
   */
  async [types.Search.type]({ commit }, { id }) {
    const body = JSON.stringify({
      id
    });
    const resp = await fetch("/search", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body
    });
    // action 内で commit を呼ぶ時も当然 MutationType を使える
    commit(new mutationTypes.SetResult(resp.json()));
  }
};

export default actions;

MutationType / ActionTypeのエクスポート

後はこれらをエクスポートしてあげればコンポーネントからも自由に使うことができます。

今はモジュールも一つ、MutationTypeActionTypeも一つずつしか定義されていませんが、これらがどんどん増えていくとインポートが大変なので、store/modules/search/index.jsでは* as SearchMutation / SearchActionとして一度インポートしておき、そのままエクスポートしておくと良いです。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
// MutationType
import * as SearchMutation from "./mutationTypes";
import actions from "./actions";
// ActionType
import * as SearchAction from "./actionTypes";

const module = {
  state,
  mutations,
  actions
};

export default module;
export { SearchState } from "./state";
export { default as nsSearch } from "./namespace";
// MutationType / ActionType をエクスポート
export { SearchMutation, SearchAction };
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
import searchModule, { nsSearch, SearchState } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;
/** @type {SearchState} */
export const searchState = store.state[nsSearch];
// MutationType / ActionType をエクスポート
export { SearchMutation, SearchAction } from "./modules/search/index";

コンポーネントからは以下のように呼び出します。

<template>
  <div>
    <label> ID: <input type="text" @input="id = $event.target.value" /> </label>

    <button @click.prevent="onSearch">Search</button>
  </div>
</template>

<script>
  // SearchAction をインポート
  import store, { SearchAction } from "../store/index.js";
  export default {
    name: "SearchForm",
    data() {
      return {
        id: ""
      };
    },
    methods: {
      async onSearch() {
        // dispatch の引数を SearchAction.Search のインスタンスに変更
        // await this.$store.dispatch("search", { id: this.id });
        await store.dispatch(new SearchAction.Search(this.id));
      }
    }
  };
</script>

ゲッターの扱いについて 279266

後 Vuex の機能として残っているのはゲッターぐらいなんですが、実はこれ、stateのクラスに関数として定義してしまっても特に問題ありません。

というか、store.getters経由だとどう頑張っても型補完は無理です。*3

大人しくstate内に定義しましょう。

// store/modules/search/state.js
import * as Models from "./models";

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /**
     * 検索結果
     * @type {Models.SearchResult[]}
     */
    this.result = [];
  }

  // これをゲッターの代わりにする
  get sortedByIdResult() {
    return this.result.sort((a, b) => {
      const idA = a.id.toUpperCase();
      const idB = b.id.toUpperCase();
      if (idA < idB) {
        return -1;
      }
      if (idA > idB) {
        return 1;
      }

      return 0;
    });
  }
}

export { SearchState };

export default new SearchState();

まとめ

と言うわけで、ここまでやると Vuex で型補完をバリバリ活用することができます。

そこそこ面倒な感じがするかもしれませんが、これぐらいやっておかないと大規模なアプリを作るときにかなりしんどいです。かなりしんどいです。今。ナウ。

*1:この辺を使おうとすると型補完は不可能。

*2:TypeScript を使うなら不要。

*3:store.getters["search/foo"] で返ってくる型をどこでどう定義しろと。

【PowerShell】PowerShellの$LASTEXITCODEとcmdの%ERRORLEVEL%が一致しないケース

前書き

約二年ぶりの更新です。元々メモ書きのようなブログですが、ここ二年ほどメモするほどの内容がありませんでした。

Spring Boot とか、Flutter とか、vue.js とか、Azure のアレコレとか、AWS のアレコレとか、Firebase のアレコレとか、色々やってたんですけどね。

上記のうち、Flutter の話はいつかしますが、今日は PowerShell の話です。

$LASTEXITCODEとは

PowerShell には自動変数と呼ばれる、一種の予約語のような変数があります。

$LASTEXITCODEはその中の一つであり、

最後に実行された Windows ベースのプログラムの終了コードが格納されます。

と書いてある通り、他プロセスの実行結果が格納されます。

.batでバリバリとバッチ処理を作ったことがある人なら「あ、%ERRORLEVEL%みたいなもんね。」と思うかもしれません。合ってます。合っていますが、完全に%ERRORLEVEL%と互換性があるわけではなく、それが原因でめちゃめちゃにハマりました。

$LASTEXITCODE%ERRORLEVEL%が一致しなくなるバッチ

こんな.batファイルを作ります。サブルーチンの方でexit /b 1を返し、メイン処理は特に何も返さない、それだけの処理です。

@echo off
call :hoge
exit /b
:hoge
exit /b 1

作った.batファイルをコマンドプロンプトで呼び出し、%ERRORLEVEL%を調べます。普通に1が返ってきます。

> test.bat

> echo %ERRORLEVEL%
1

PowerShell で同じように呼び出し、$LASTEXITCODEを調べます。

> .\test.bat

> $LASTEXITCODE
0

$LASTEXITCODE%ERRORLEVEL%を見ているわけではないので、メイン処理の返り値を取得してしまいます。よって、0が表示されます。

ワークアラウンド

自分で作った.batならメイン処理でexit /b %ERRORLEVEL%とすればいいんですが、諸々の事情*1.batの方を直せない場合、PowerShell 側で何とかする必要があります。

考え方は簡単です。明示的にcmd.exe /Cでバッチを叩いた後、exit %ERRORLEVEL%すればいいだけです。

&でコマンドを連続実行してやればワンライナーで楽勝です。遅延環境変数を展開できるよう/V:ONの指定を忘れずに。

> cmd /V:ON /C "test.bat & exit !ERRORLEVEL!"

> $LASTEXITCODE
1

まとめ

うーん。めんどくさい。

*1:他チームが作ったバッチ処理なので勝手に直せない、とか…。

【Git】他人のプロジェクトをsubmoduleで追加し、Pull Requestを自分でマージする方法

前書き

色々なライブラリを調べていると、「おっ、この PR いいじゃーん」と思ってもメンテナが全然やる気を出さないせいで一向にmasterに取り込まれないことが多々あり、mastersubmoduleで引っ張ってきた後、PR をマージするってことをたまにやるんですが、たまにしかやらないからか方法を毎回忘れるのでメモしておきます。

サブモジュールの追加

git submodule addすれば OK

git submodule add ${cloneしたいリポジトリのURL} ${新規に作るディレクトリ名}

PR のマージ

マージ用のブランチ作成

サブモジュールのディレクトリで適当なブランチを作ってそっちに切り替えておきましょう。*1

git checkout -b work

PR のマージ

以下のコマンドでpullしてきましょう。

git pull https://github.com/${PRを送ってきた人のID}/${プロジェクト名} ${PRを送ってきた人のブランチ名}

まとめ

特に何も言うことはないです。

*1:うっかり pull しちゃってもこれで安心ですね。

【JavaScript】明日から使えるRiot.js

前書き

相変わらず Web エンジニアみたいなお仕事をしているんですが、最近訳あって IE 8 からの呪縛から解き放たれました。

で、前々から使ってみたかったんだけど機会がなかったRiot.jsを使うことができるようになったので、ここ一ヶ月ほどもりもり書いていました。

ある程度の知見を得ることができたので、適当にメモしていきます。

Riot.js の目的

割とReactと比較されることが多い…と言うか、自分でReact の目指すところは悪くないんだけどあの文法はいくらなんでもクソすぎと言っている*1んですが、最終目標はDOM 操作における jQuery みたいな立ち位置を Web Components で目指すところっぽいです。Polymerの方が実質的なライバルでしょう。*2

また、ちょっとしたデータバインディングもできる*3ためか、Vue.jsからイチャモンをつけられているんですが、そもそもお互いが目指すゴールが違うので比較すること自体がなんだか妙な話です。*4

なお、あくまでライブラリであって、フレームワークではないです。「これ一本で Web アプリをバリバリつくるんじゃ~い!」と思うのであれば、AngularJSでも使ったほうがいいと思います。

つい最近Shadow DOM v1がとうとう Chrome に実装されたことでちょっとしたニュースになりましたが、あれもまだまだ気軽に使えるとは言いがたい*5ので、こう言うライブラリでもっと楽できればいいよね、みたいな気楽なスタンスです。

基本的な使い方

できる限り簡単に Web Components を実装したい!が最終目的なので、公式のガイドを 30 分~ 1 時間ぐらいかけて読めばほとんど全機能を使うことができます。

が、流石に実例がないとわかりづらいので、Bootstrap のサンプルを黙々と Riot.js に書き換えていく作業をやってみましょう。

Bootstrap も「CSS によるコンポーネント化を目指す」みたいなところがあるので、Web Components の概念と非常に相性がよくサンプルとして最適ですし、わかりやすく実用性があります。*6

また、読むだけだとやっぱりわかりづらいので、適当なリポジトリを作っておきました。Express で動くので、Node.js が入ってればすぐ遊べます。

元のソース

面倒なので<body>以下のところを一部引っこ抜いてきました。

<body>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <div class="navbar-header">
        <button
          type="button"
          class="navbar-toggle collapsed"
          data-toggle="collapse"
          data-target="#navbar"
          aria-expanded="false"
          aria-controls="navbar"
        >
          <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Project name</a>
      </div>
      <div id="navbar" class="navbar-collapse collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>
    </div>
  </nav>

  <div class="container">
    <div class="jumbotron">
      <h1>Navbar example</h1>
      <p>
        This example is a quick exercise to illustrate how the default, static
        and fixed to top navbar work. It includes the responsive CSS and HTML,
        so it also adapts to your viewport and device.
      </p>
      <p>
        To see the difference between static and fixed top navbars, just scroll.
      </p>
      <p>
        <a
          class="btn btn-lg btn-primary"
          href="../../components/#navbar"
          role="button"
          >View navbar docs &raquo;</a
        >
      </p>
    </div>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
</body>

どっから手を付けて行こうかな、って感じですが、簡単そうな<div class="jumbotron">からいきますか。

カスタムタグと<yield/> [diff]

まずは適当な.tagファイルを作ります。jumbotron なんだから、jumbotron.tagとかでいいんじゃないですかね。(適当)

作り方はとても簡単です。

  1. ルート要素として新しく作りたいタグ(今回は<jumbotron>)を指定する。
  2. ルート要素の配下に実際に表示される内容を書く
<jumbotron>
  <div class="jumbotron">
    <yield/>
  </div>
</jumbotron>

これで終わりです。<yield/>を指定することで、呼び出し元のカスタムタグの中に記述された HTML をそのまま読み込んでくれます。

次は実際にこのカスタムタグを先ほどの html で読み込んでみましょう。これもとても簡単です。

  1. riot+compiler.min.jsを読み込む
  2. script type="riot/tag"を指定し、.tagを読み込む
  3. riot.mountでマウントする
<div class="container">
  <jumbotron>
    <h1>Navbar example</h1>
    <p>
      This example is a quick exercise to illustrate how the default, static and
      fixed to top navbar work. It includes the responsive CSS and HTML, so it
      also adapts to your viewport and device.
    </p>
    <p>
      To see the difference between static and fixed top navbars, just scroll.
    </p>
    <p>
      <a
        class="btn btn-lg btn-primary"
        href="../../components/#navbar"
        role="button"
        >View navbar docs &raquo;</a
      >
    </p>
  </jumbotron>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
<script
  type="text/javascript"
  src="./javascripts/riot+compiler.min.js"
></script>
<script type="riot/tag" src="./components/jumbotron.tag"></script>
<script>
  riot.mount("jumbotron");
</script>

当然これだけだと何の旨味もありません。次は<nav>に書かれたグローバルナビゲーションもコンポーネント化してみましょう。

まずは何も考えず html をコピペして.tagを作ってしまいます。名前はglobal-nav.tagとかでいいでしょう。

次はカスタムタグのマウントです。マウントする要素が増えてきたらriot.mount('*')で一括指定してしまいましょう。

<body>
  <global-nav></global-nav>

  <div class="container">
    <jumbotron>
      <h1>Navbar example</h1>
      <p>
        This example is a quick exercise to illustrate how the default, static
        and fixed to top navbar work. It includes the responsive CSS and HTML,
        so it also adapts to your viewport and device.
      </p>
      <p>
        To see the difference between static and fixed top navbars, just scroll.
      </p>
      <p>
        <a
          class="btn btn-lg btn-primary"
          href="../../components/#navbar"
          role="button"
          >View navbar docs &raquo;</a
        >
      </p>
    </jumbotron>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
  <script
    type="text/javascript"
    src="./javascripts/riot+compiler.min.js"
  ></script>
  <script type="riot/tag" src="./components/jumbotron.tag"></script>
  <script type="riot/tag" src="./components/global-nav.tag"></script>
  <script>
    riot.mount("*");
  </script>
</body>

随分すっきりしましたね。すっきりしすぎてむしろちょっと不安です。

ですが、これで<global-nav>を指定するだけでどんなページでもグローバルナビゲーションを呼び出すことが可能になりました。

ついでに<nav>の中に書かれていた色んな要素もコンポーネント化してしまいましょう。一つの.tagファイルに複数のカスタムタグを定義できるので、黙々と分解してしまいます。

<global-nav>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <nav-header></nav-header>
      <navbar></navbar>
    </div>
  </nav>
</global-nav>

<nav-header>
  <div class="navbar-header">
    <button
      type="button"
      class="navbar-toggle collapsed"
      data-toggle="collapse"
      data-target="#navbar"
      aria-expanded="false"
      aria-controls="navbar"
    >
      <span class="sr-only">Toggle navigation</span>
      <span class="icon-bar"></span> <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </button>
    <a class="navbar-brand" href="#">Project name</a>
  </div>
</nav-header>

<navbar>
  <div id="navbar" class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
      <li class="active"><a href="#">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </div>
</navbar>

カスタムタグの中にロジックを持たせる [diff]

困ったことに、このまま使っても<navbar>.activeが一切変わらず使い物になりません。

普通ならページごとに.activeを適用する場所だけを変えればいいんですが、せっかくコンポーネント化したのにそれはダサすぎです。自動でやってもらえるようにしましょう。

<navbar>
  <div id="navbar" class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
      <li each="{ nav in navs }" class="{ active: isActive(nav.link) }">
        <a href="{ nav.link }">{ nav.title }</a>
      </li>
    </ul>
  </div>

  <script>
    this.navs = [
      {
        link: "index.html",
        title: "Home"
      },
      {
        link: "about.html",
        title: "About"
      },
      {
        link: "contact.html",
        title: "Contact"
      }
    ];

    this.isActive = function(path) {
      return path == location.pathname.replace(/^\//, "");
    };
  </script>
</navbar>

Riot.js ではカスタムタグ自身が持つ JavaScript のオブジェクトを自由にバインディング可能です。*7

また、ifeachを使うことで要素の表示/非表示やループを簡単に定義することができます。

カスタムタグ内にのみ適用されるスタイルを定義する [diff]

ある日、不意に「グローバルナビゲーションの.activeの背景色だけ変えたいな…」と思う日が来るかもしれません。*8ちゃんとコンポーネント化してあれば簡単にできます。カスタムタグ内で<style scoped>を定義しましょう。*9

<global-nav>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <nav-header></nav-header>
      <div id="navbar" class="navbar-collapse collapse">
        <navbar></navbar>
        <ul class="nav navbar-nav navbar-right">
          <li class="active"><a href="./">scope test</a></li>
        </ul>
      </div>
    </div>
  </nav>
</global-nav>

<navbar>
  <ul class="nav navbar-nav">
    <li each="{ nav in navs }" class="{ active: isActive(nav.link) }">
      <a href="{ nav.link }">{ nav.title }</a>
    </li>
  </ul>

  <style scoped>
    /* bootstrapの指定が複雑すぎるので!importantで強制的に上書き */
    .active > a {
      background-color: #bbdefb !important;
    }
  </style>

  <script>
    this.navs = [
      {
        link: "index.html",
        title: "Home"
      },
      {
        link: "about.html",
        title: "About"
      },
      {
        link: "contact.html",
        title: "Contact"
      }
    ];

    this.isActive = function(path) {
      return path == location.pathname.replace(/^\//, "");
    };
  </script>
</navbar>

ついでに本当にカスタムタグ内だけで完結しているかすぐわかるようscope testなるものも<global-nav>に追加しておきました。scopedを削除するとそっちも変な青色になるのがわかると思います。

また、「ある特定のルールだけは scoped にして、他は全体に適用できるようにしたい」と思ってしまった人のために、:scope擬似クラスなんてものも用意されています。

正直デフォルトで全部scopedにしておいてほしいような気もします*10が、まぁ妙な事故を起こさないよう適切に指定してあげてください。

ちょっと応用的な使い方

なんとなく使い方はわかってきたと思うので、もう少し応用的な使い方も説明していきましょう。

次はこっちのサンプルを Riot.js で書きなおしていきます。

呼び出し元からのパラメータを設定 [diff]

まずはブログの内容部分をコンポーネント化してみますか。

元々はこんな感じでした。

<div class="blog-post">
  <h2 class="blog-post-title">Sample blog post</h2>
  <p class="blog-post-meta">January 1, 2014 by <a href="#">Mark</a></p>

  <!-- 本文が書かれているだけなので省略 -->
</div>

カスタムタグにぶち込みます。ついでに CSS も持ってきてしまいましょう。ここ以外で使わなそうだし。

<blog-post>
  <div class="blog-post">
    <yield/>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }

  </style>
</blog-post>

</blog-post>

せっかくコンポーネント化したんだから、タイトルや投稿日、投稿者なんかはできれば外部からパラメータとして欲しいですね。当然できます。

カスタムタグはこんな感じに定義します。

<blog-post>
  <div class="blog-post">
    <h2 class="blog-post-title">{ opts.title }</h2>
    <p class="blog-post-meta">{ opts.meta } by <a href="{ opts.link }">{ opts.author }</a></p>
    <yield/>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }

  </style>
</blog-post>

呼び出し元はこんな感じです。

<blog-post
  title="Sample blog post"
  meta="January 1, 2014"
  author="Mark"
  link="#"
>
  <!-- 本文なので省略 -->
</blog-post>

見てもらえればわかる通り、HTML の属性として指定したものはカスタムタグ内でoptsから取得することができます。どこでoptsを使ってどこで<yield/>を使うかは設計者の腕の見せどころです。

また、optsに渡す情報はタグのマウント時に動的に指定することもできます。*11

<script>
  riot.mount("blog-post", {
    title: "Sample blog post",
    meta: "January 1, 2014",
    author: "Mark",
    link: "#"
  });
</script>

サーバからの情報と連携 [diff]

「ブログの内容は全部 JSON に書かれていて、それを読み込んで表示する」みたいなパターンをやってみましょう。

まずはデータの準備です。どこかの何かの API 叩くと、こんな感じの JSON 配列が来るとします。

[
  {
    "title": "Sample blog post",
    "meta": "January 1, 2014",
    "author": "Mark",
    "link": "#",
    "body": "<p>This blog post shows a few different types of content that's supported and styled with Bootstrap. Basic typography, images, and code are all supported.</p>\n<hr>\n<p>Cum sociis natoque penatibus et magnis <a href=\"#\">dis parturient montes</a>, nascetur ridiculus mus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Sed posuere consectetur est at lobortis. Cras mattis consectetur purus sit amet fermentum.</p>\n<blockquote>\n<p>Curabitur blandit tempus porttitor. <strong>Nullam quis risus eget urna mollis</strong> ornare vel eu leo. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>\n</blockquote>\n<p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p>\n<h2>Heading</h2>\n<p>Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>\n<h3>Sub-heading</h3>\n<p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</p>\n<pre><code>Example code block</code></pre>\n<p>Aenean lacinia bibendum nulla sed consectetur. Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>\n<h3>Sub-heading</h3>\n<p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean lacinia bibendum nulla sed consectetur. Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>\n<ul>\n<li>Praesent commodo cursus magna, vel scelerisque nisl consectetur et.</li>\n<li>Donec id elit non mi porta gravida at eget metus.</li>\n<li>Nulla vitae elit libero, a pharetra augue.</li>\n</ul>\n<p>Donec ullamcorper nulla non metus auctor fringilla. Nulla vitae elit libero, a pharetra augue.</p>\n<ol>\n<li>Vestibulum id ligula porta felis euismod semper.</li>\n<li>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</li>\n<li>Maecenas sed diam eget risus varius blandit sit amet non magna.</li>\n</ol>\n<p>Cras mattis consectetur purus sit amet fermentum. Sed posuere consectetur est at lobortis.</p>"
  }
  // 以下、同じようなものなので省略
]

この配列をeachでぐるぐる回し、先ほどの<blog-post>に表示させたいですね。こんな感じに。

<blog-main>
  <div class="col-sm-8 blog-main">
    <blog-post
      each="{ post in posts }"
      title="{ post.title }"
      meta="{ post.meta }"
      author="{ post.author }"
      link="{ post.link }"
      body="{ post.body }"
    >
    </blog-post>

    <nav>
      <ul class="pager">
        <li><a href="#">Previous</a></li>
        <li><a href="#">Next</a></li>
      </ul>
    </nav>
  </div>

  <style scoped>
    .blog-main {
      font-size: 18px;
      line-height: 1.5;
    }
  </style>

  <script>
    var self = this;
    this.posts = [];
    // API作るの面倒なのでJSON読みこむだけ
    $.get("/javascripts/blog.json").done(function(data) {
      self.posts = data;
      self.update();
    });
  </script>
</blog-main>

<blog-post>
  <div class="blog-post" name="body">
    <h2 class="blog-post-title">{ opts.title }</h2>
    <p class="blog-post-meta">
      { opts.meta } by <a href="{ opts.link }">{ opts.author }</a>
    </p>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }
  </style>

  <script>
    // opts.bodyをそのまま表示するとエスケープされてしまうので、何らかの要素のinnerHTMLに追加しないといけない。
    // カスタムタグ内のnameが付いているDOMはthisからアクセスできるので、適当にdivにbodyなんてnameを指定している。
    $(this.body).append($(opts.body));
  </script>
</blog-post>

ここでのキモは JSONAjax で取得した後のself.update()です。確かに Riot.js はバインディング機構を持ってはいますが、厳密に Observable なわけではありません。カスタムタグ外のコンテクスト*12で何らかの値を更新した場合はちゃんと通知してあげる必要があります。*13

まとめ

本当はもっと落とし穴的な要素も紹介したかったんですが、あまりにも長くなってしまった*14ので一旦切ります。

続きを書けるのはいつになるんでしょうね。1 ヶ月後とかですかね。

参考

*1:私が Riot.js を気に入ったのはこれをはっきり言い切ったところが一番大きいです。

*2:今 Polymer ってどうなってるんでしょうね?React 以降全然話を聞かなくなってしまいましたが。

*3:と言うか、「コンポーネント」を謳っている以上、何らかの形で動的に表示するデータを差し替えられなきゃ使えたもんじゃない。

*4:実際、Riot.js 側の公式ガイドでは Vue.js の話は全くしていない。

*5:かなり頑張ってるとは思いますが…。

*6:私が最近やった仕事もひたすら「Bootstrap のせいでネストが深くなるところを Riot.js で書き直す」でした。

*7:script タグは省略可能ですが、個人的には唐突感があってあまり好きじゃないのでちゃんと書いてます。

*8:来ないと思う。

*9:なお、style scoped = Scoped CSS 自体は Riot.js 独自の機能ではなく、ちゃんとした HTML の仕様です。(実装が進んでいるとは言っていない) この記事で詳しく解説されているので読んでおきましょう。

*10:全体に適用するなら普通に.css ファイル書けばいいし…。

*11:ちなみに、マウント時に動的指定しつつ属性値で渡している場合は属性値で渡した値が優先される。

*12:めちゃめちゃ簡単に言うと、this がカスタムタグ以外を指している状態

*13:公式にObserverがあるように見えますが、これはどちらかと言うとカスタムタグ同士の Pub/Sub を実現するためのものです。

*14:サンプルがデカすぎるのが悪い。

【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ

2018/11/29 追記

  • UTF-8 以外の文字コードで POST / PUT を行う方法を追記しました。
  • RxJava 2.x 用の CallAdapter を追記しました。

前書き

最近Retrofitを使うことがあったんですが、イントロダクションをちょろっと読んだぐらいだと「え、そーなの?」と思うような事象に何度も見舞われたので、メモしておきます。

API の設定

HTTP メソッドやパス、クエリやパラメータなどを適当に作ったインターフェースとアノテーションで表現します。

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

ベースとなる URL はRetrofit.Builder#baseUrlで渡すので、相対パスを書いておけば OK です。

アノテーションだけでどうにでもなってしまうので、色んなサービスの API を叩きたい時に共通の型がなくて却って使いにくいこともたまーにありますが、その辺は自分で共通の型を作ればいいだけの話です。

HTTP Methods

HTTP のメソッドに対応するアノテーションは以下のものが用意されています。必要十分って感じですね。

PATCHってDELETE以上に見たことないけど、意識の高い API 設計者は使うんでしょうか。

独自のメソッドを用意している意識が高すぎる API に対してはHTTP#methodに渡しましょう。

あと、微妙にハマりポイントなんですが、HEADを指定したメソッドの返り値の型は必ずCall<Void>にする必要があります。*1

普通にヘッダだけ欲しいんだけどどうしたらええの?と言う質問が Issue にあるんですが、ガン無視されています。私はもう面倒なので GET メソッド投げるようにしてしまいました。(怒)

ルーティング

@GET("users/{user}/repos"){user}部分をメソッドの引数で渡すことができます。

対応する引数にPathアノテーションをつけてあげれば OK です。

マルチバイト文字とか記号とかをうにゃうにゃしたい時はPath#encodedtrueを渡してあげましょう。

クエリ

Queryアノテーションを指定してあげればよしなにやってくれます。

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

optional なパラメータが多い時はQueryMapアノテーションを使うと便利です。

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);

@Query@QueryMapencodedtrueを指定することで URL エンコードできるので、特に理由がないならやっておいた方が色々安全だと思います。

また、うっかりミスでこういうことをやってしまうことがあるんですが、これは怒られます。

// クエリ部分にPathで値を渡すのは無理
// 渡せるようにしても良い気がするけどね。
@GET("group/{id}/users?sort={sort}")
Call<List<User>> groupList(@Path("id") int groupId, @Path("sort") String sort);

クエリ応用編

@Queryを指定する引数を配列 / 可変長引数にすることで、同じクエリを複数指定することができます。

@GET("/list")
Call<ResponseBody> list(@Query("category") String... category);

例えば、service.list("hoge", "fuga")と呼び出したら、/list?category=hoge&category=&fugaになります。*2

また、特定のクエリを固定値で指定しておく場合はこのように書けば OK です。うまいこと後ろにクエリを足してくれます。

@GET("group/{id}/users?sort=desc")
Call<List<User>> groupList(@Path("id") int groupId, @Query("filter") String filter);

クエリ更に応用編(外法)

GETだけでなくどのメソッドでもいいんですが、Urlアノテーションを引数として渡すことで API のパス自体を動的に変更できます。

public interface GitHubService {
  @GET
  Call<List<Repo>> listRepos(@Url String url);
}

極端な話、この@Urlにパスもクエリもぶち込んでしまえば動いてくれます。型安全とは何だったのか。

POST のパラメータ

多分、PUTでも同じだと思います。

パラメータとして渡すにはBodyField(もしくはFieldMapアノテーションを使います。

application/x-www-form-urlencoded形式での POST

application/x-www-form-urlencoded (key=value) 形式に自動で変換して欲しい場合は@FormUrlEncoded@Filedを使います。

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

デフォルト値を設定する方法は現在開発中なので、@FieldMapと適当なデフォルト値を持つMap<String, String>を作るメソッドを併用するぐらいしか対処法がありません。

@FormUrlEncoded
@POST("user/edit")
// Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
Call<User> updateUser(@FieldMap Map<String, String> names);
public static Map<String, String> createFullName(String first) {
    final Map<String, String> param = new HashMap<>();
    param.put("first_name", first);
    param.put("last_name", "hogehoge");
    return param;
}

それ以外の形式での POST

@Bodyを使うとそのまんま Body 部に値をぶちこみます。application/jsonなどを渡したい場合はこちらを使いましょう。

ちなみにConverter(後述)を指定することで独自の型でもいい感じに変換してくれます。*3

@POST("users/new")
Call<User> createUser(@Body User user);

UTF-8 以外でエンコードしたapplication/x-www-form-urlencoded

@FormUrlEncodedはパラメータを強制的に UTF-8エンコードしてしまいます。この文字コードを変えることはできません。*4

どうしても他の文字コードで POST する必要がある場合も@Bodyを使う必要があります。

Converterを自作してもいいですが、スカラー値を扱う Converterが公式で用意されているので、自分でapplication/x-www-form-urlencoded形式のStringを作ってしまうのが早いです。*5

// API定義
@POST
//@FormUrlEncoded
//Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
Call<User> updateUser(@Body String body);
// 呼び出し方
String firstName = "山田";
String lastName = "太郎";

Function<String, String> encode = (s) -> {
  try {
    // java.net.URLEncoderでUTF-8以外の文字コードに変換
    return URLEncoder.encode(s, "Shift-JIS");
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException(e);
  }
};

// 自分でapplication/x-www-form-urlencoded形式のStringを作る
String body = String.format("%s=%s&%s=%s",
    encode.apply("first_name"),
    encode.apply(firstName),
    encode.apply("last_name"),
    encode.apply(lastName));

// StringをRequestBodyとして投げる
Call<User> user = service.updateUser(body);

Multipart なデータを POST

ちゃんとMultipartと言うアノテーションがあります。

渡す値はPart(もしくはPartMapアノテーションで指定します。

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

渡せる型はデフォルトだとokhttp3.MultipartBody.Partokhttp3.RequestBodyで固定されているようです。もちろん対応するConverterがあれば独自の型でも OK です。*6

なお、@Multipart@FormUrlEncodedは併用できません。Content-Typeが矛盾するからです。*7

ヘッダの指定

Headersアノテーションを指定しておくことで、事前にヘッダを作っておくことができます。

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

また、Headerアノテーションを引数に指定することで、動的にヘッダ部を変更することができます。

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

ただ、API 共通で指定したいヘッダはokhttp3.Interceptorを作ったほうがいいです。(後述)

実際の通信部分の設定

Retrofit.Builder#clientで指定するokhttp3.OkHttpClientが実際に HTTP 通信を行うクライアントになります。

どんな設定ができるかはokhttp3.OkHttpClient.Builderのメソッドを見れば大体わかると思います。また、Wiki の Recipesもかなり参考になると思います。

Proxy や Timeout、Cookie / キャッシュの管理もこれでやることになるので、Retrofit を使う場合は必ず一読しておいたほうがいいです。

ちなみにOkHttp3 ではOkHttpClientのコネクションプールを各インスタンス毎に持っているのでシングルトンで持っておくことが推奨されています。Interceptorタイムアウト値、リトライ回数なんかを API 別に設定したい場合はokhttp3.OkHttpClient#newBuilderを使いましょう

okhttp3.Interceptorの設定

下手なコードを読むよりWikiを読んだほうがわかりやすいです。

okhttp3.Interceptorを実装するにあたって覚えておくことは以下の 3 つだけです。

okhttp3.Responseも色々さわりたい*8場合はproceedで取得した内容をごにょごにょしましょう。

単純に共通のヘッダを指定したいだけならこんな感じになります。複数のヘッダを指定したい場合はokhttp3.Headersを使ったりしましょう。

public class CommonInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        return chain.proceed(
            chain.request().newBuilder()
                .header("User-Agent", "OkHttp Example")
                .build()
        );
    }
}

Cookie の設定

okhttp3.CookieJarを適当に実装してあげましょう。

一ミリも頭を使わない実装をするとこんな感じになります。ただ、あまりにも Cookie の仕様をガン無視しているのでおすすめできません。

public class NoHeadCookieJar implements CookieJar {
    private final List<Cookie> saveCookies = new ArrayList<>();

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        for(Cookie c : cookies) {
           if(!saveCookies.contains(c)) saveCookies.add(c);
        }
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        return saveCookies;
    }
}

きちんと Cookie を管理するのであればJavaNetCookieJarを使用するのが安全です。

JavaNetCookieJarCookieCookieHandler(=CookieManager)で管理するので、他の HTTP クライアントや、Android であればWebViewCookie を共有しやすくもなります。

ちなみにokhttp本体とは別モジュール*9なので、依存関係に追記が必要なことを忘れないでおきましょう。

dependencies {
    compile "com.squareup.okhttp3:okhttp-urlconnection:3.2.0"
}

今回は詳しく説明しませんが、同じくokhttp-urlconnection内にあるJavaNetAuthenticatorも中々便利そうなので、Retrofit or okhttp でプロキシ認証が必要になったら参考にしたり流用したりしましょう。

レスポンスの Call Adapter

Retrofit 2.x は、デフォルトのままだとレスポンスを必ずCall<T>でラップするようになっています。*10

Call<T>には同期的に実行するexecuteと非同期的に実行するenqueueの 2 つのメソッドが用意されています。

enqueueで渡すCallback<T>は確かに必要最低限の機能が用意されていますが、Promise パターンが使えないので、複数の API を組み合わせて実行しようとすると JavaScript もびっくりなコールバック地獄になります。

それは面倒だよね、ってことで、もっと便利なものにラップできるよう、Call Adapterが用意されています。

ライブラリ ラップする型 モジュール
RxJava 1.x Observable / Single com.squareup.retrofit2:adapter-rxjava
RxJava 2.x Observable / Flowable / Single / Maybe / Completable com.squareup.retrofit2:adapter-rxjava2
Guava ListenableFuture com.squareup.retrofit2:adapter-guava
Java 8 CompleteableFuture com.squareup.retrofit2:adapter-java8

実際にどの Call Adapter を使うかはRetrofit.Builder#addCallAdapterFactoryで指定します。

RxJava 1.x の Adapter を指定するならこんな感じです。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com")
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

なお、RxJavaCallAdapterFactoryにはRxJavaCallAdapterFactory#createWithScheduler(Scheduler)と言うメソッドも用意されています。ここで指定しておけば生成されるObservableに対して自動でsubscribeOnしてくれるので、上手く活用しましょう。*11

リクエスト・レスポンスの自動型変換

HTTP 通信によって取得した内容(Entity)は事前にConverterを指定しておくことで自動で型変換を行うことができます。*12

公式に用意されているものは以下の通りです。

ライブラリ モジュール
Gson com.squareup.retrofit2:converter-gson
Jackson com.squareup.retrofit2:converter-jackson
Moshi com.squareup.retrofit2:converter-moshi
Protobuf com.squareup.retrofit2:converter-protobuf
Wire com.squareup.retrofit2:converter-wire
Simple XML com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String) com.squareup.retrofit2:converter-scalars

使用する Converter はRetrofit.Builder#addConverterFactoryで指定することができます。Call Adapter と一緒ですね。

Gson の Converter を指定する場合はこんな感じになります。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

HTTP 通信結果の取得

ステータスコードやヘッダといった情報をコールバックで取得したい場合は、返り値の型をResponse<T>でラップしましょう。(ex. Call<Response<String>>

ちなみにResponse<T>は Converter に一切依存しないので、どんな Converter を指定していても使用できます*13

また、戻り値の型をokhttp3.ResponseBodyにすることで、生の Entity や Body を取得することもできます。(ex. Call<ResponseBody>

こちらもConverter に一切依存しないので、「欲しい Converter が用意されてないけど自力で実装するのダルい」「型変換のところでエラーが出るから値を確認しつつデバッグしたい」などの場合に使いましょう。*14

まとめ

またなんか無駄に濃密な記事になってしまいました。

*1:それ以外の型を指定すると「HEAD method must use Void as response type」と怒られる

*2:@QueryMap で同じようなことができるかどうかは未検証。Key が同じになっちゃうから難しいような…?

*3:Retrofit 1.x では勝手に JSON 形式にしていたようだが、今はちゃんと Converter を指定してやる必要がある。

*4:現在の application/x-www-form-urlencoded の仕様的に仕方ないんですが…。

*5:Converter を自作しても結局同じようなことをする必要がある。

*6:Introductionで紹介されている Converter にはそれ用のものがないので、余程のことがないなら MultipartBody.Part か RequestBody でいい気がする。

*7:と、そんな回答がされている。まぁ、サーバ側の実装が悪いとしか言いようがないっちゃないよね。

*8:本来返されるレスポンスの内容をここで書き換えたりするのは行儀が悪い(と言うか、Retrofit においてそれは Converter のお仕事である)ので、各情報の取得だけにとどめておいた方が無難。

*9:なんでやねん。

*10:1.x では違ったらしいけど割愛。

*11:特に Android では「呼び出し元で subscribeOn し忘れた」のようなしょうもない事故を防止するためになるべくこっちを使うべき。

*12:レスポンスだけでなく、リクエストで使う引数の型も Converter で変換できる。

*13:もちろん一つも Converter を指定していなくても使用可能。

*14:Converter.Factoryの実装はどうしても「どんな型が指定されても大丈夫!」って感じにせざるを得ないので、しょうもない Converter を大量生産するよりはコールバック内でそれぞれ型変換した方がまだマシな気がする。(個人の感想です。)

【Android】Android の Data Binding でできること(基本編)

2018/12/01 追記

  • シンタックスハイライトを適用しました。
  • こんな場末の技術ブログを見るぐらいなら公式のドキュメントをちゃんと読んだほうがいいと思います。
    • 記事公開当時は大した情報が記載されていませんでしたが、今はかなり充実しています。

前書き

最近(と言っても 1 ~ 2 ヶ月前の話ですが)、訳あって携帯を機種変更し、Android 5.1.1 の端末を手に入れました。

いい機会なので開発環境も一新し、Eclipse + ADT とか言うファッキンな環境を捨てて Android Studio + Gradle + Kotlin に切り替えました。

IntelliJ の Lombok プラグイン@ExtensionMethodに対応していれば Kotlin は恐らく触りもしなかったんですが、Scala をちょっと触っていたこともあってか、中々快適に開発できています。普段の業務もこれで書きたいぐらいです。

Kotlin の話はいずれするかもしれないし、しないかもしれませんが、今回は Android Studio 1.3 からサポートされ始めたData Bindingの話をします。

Data Binding とは?

レイアウト用の XML/res/layout/内のやつ)にデータを定義することで、そのデータのプロパティに直接アクセスできるようになりました。

以下のコードを見ればなんとなくわかると思います。(コードはドキュメントのサンプルそのままです。)

public class User {
   private final String firstName;
   private final String lastName;

   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       return this.lastName;
   }
}
<!--?xml version="1.0" encoding="utf-8"?-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

なんか別の言語で GUI のアプリを作るときも似たようなやつを必死に書いた気がするんですが、きっと気のせいでしょう。と言うか、XAML よりもシンプル(当社比)でいい感じです。Java だとわざわざ getter を作らなきゃいけないからそこで帳消しですよ。

実際にデータをセットする時はこんな感じに呼び出します。

main_activity.xmlであれば、MainActivityBindingと言うクラスが自動で生成されます。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
       User user = new User("Test", "User");
       binding.setUser(user);
    }
}

プラグインの導入

Android Plugin for Gradle の1.5.0-alpha1からプラグインが統合されたため、基本的には module レベルのbuild.gradledataBinding { enabled = true }を追加するだけで OK です。

android {
    // ...
    dataBinding {
        enabled = true
    }
}

何らかの事情で古いものを使っている人は別途プラグインを入れる必要があります。まずは root のbuild.gradle内のdependenciesに DataBinder を追加します。

最新版がどれかわからない、って人はここにあるので適当に見繕ってきてください。

buildscript {
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        // ...
        classpath "com.android.databinding:dataBinder:1.0-rc4"
    }
}

次に module レベルでのbuild.gradleに設定を入れていきます。と言っても、apply plugin: 'com.android.databinding'を突っ込むだけです。

apply plugin: 'com.android.databinding'

android {
    // ...

データオブジェクト

プロパティの定義

Java のフィールドをプロパティとして呼び出すためには、以下の二条件のうちどちらか一つを満たしていれば OK です。

  • public なフィールドである
  • getter が定義されている

具体的にはこんな感じですね。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}
public class User {
   private final String firstName;
   private final String lastName;

   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       return this.lastName;
   }
}

データ変更の検知

上記の例だとデータを変更してもViewの表示は変わりません。

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
   // firstNameの値を変更してもTextViewの表示は変わらない
   user.setFirstName("Hoge")
}

データが変わったら自動でViewの表示も変わって欲しい人もいると思います。そんな人のためにいくつかの手段が用意されています。

まずはObservableFieldsを使用する例です。

public class User {
   private final ObservableField

<string> firstName = new ObservableField<string>();
   private final ObservableField<string> lastName = new ObservableField<string>();

   public User(String firstName, String lastName) {
       this.firstName.set(firstName);
       this.lastName.set(lastName);
   }
   public ObservableField<string> getFirstName() {
       return this.firstName;
   }
   public ObservableField<string> getLastName() {
       return this.lastName;
   }

}
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
   // firstNameの値を変更するとTextViewの表示も変わる
   user.getFirstName.set("Hoge");
}

うーん、このダサさ。ちなみにプリミティブ型のためにObservableBooleanとかObservableIntなんてのがそれぞれも用意されてます。一秒でも早く Java が絶滅してくれないかなと思ったことでしょう。

他にもObservable なMapListの実装方法なんかもありますが、「XML実体参照を書かないといけないの、流石にダサすぎでは?」ぐらいしか言うことないので、必要なら読んどいてください。

もうちょっとスマートな方法としてBaseObservableを継承する方法があります。

public class User extends BaseObservable {
   private final String firstName;
   private final String lastName;

   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }

   @Bindable // 変更を通知したいプロパティには@Bindbleをつける
   public String getFirstName() {
       return this.firstName;
   }

   @Bindable
   public String getLastName() {
       return this.lastName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
       // @Bindableがついているプロパティは
       // BR.プロパティ名と言うフィールドが自動で生成される
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
   // firstNameの値を変更するとTextViewの表示も変わる
   user.setFirstName("Hoge");
}

implementsじゃなくてextendsなのがキツいところですね。

本当はBaseObservableではなくObservableを実装すればいいみたいなんですが、当然通知の仕組みは自力で実装する必要があります。馬鹿にしていらっしゃるのかな?(敬語)

でもまぁ、ソースを読む限り大した実装ではないので、必要なら自力で実装しちゃってもいいかもです。

また、どうしても setter に追記が必要なので、lombok を使っている人からすると二倍しんどいです。勘弁して欲しいですね。

イベントのバインド

android:onClickのように何らかの Listener を設定できる場所にはイベントをバインドすることができます。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }

   public void showFullName(View v) {
       Toast.makeText(v.getContext(), this.firstName + " " + this.lastName, Toast.LENGTH_SHORT).show();
   }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
       <Button android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="show fullname"
           android:onclick=@{user.showFullName} />
   </LinearLayout>
</layout>

また、本来なら定義されていないはずのandroid:onLongClickと言う attr にイベントをバインドすることもできます。理由は後述。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
       <Button android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="show fullname"
           android:onLongClick=@{user.showFullName} /> <!-- android:onLongClick -->
   </LinearLayout>
</layout>

@BindingMethod@BindingAdapter

独自のイベントを設定するには@BindingMethod@BindingAdapterと言う黒魔術アノテーションを駆使する必要があります。

と言うか、先ほどの例で挙げたandroid:onclickandroid:onLongClick@BindingMethodでやってるだけです。この 2 つのアノテーションは既存のViewだろうがなんだろうが attr を拡張することができます。

公式ドキュメントを読むと@BindingMethodsetter をリネームしたい時@BindingAdapter独自の setter を定義したい時に使うといいよと紹介されています。

つまり、目的のViewが既に対応するようなメソッドを持っている時は@BindingMethodを、完全に自分で定義(拡張)したい時は@BindingAdapterを使ってね、ぐらいのイメージでしょうか。

@BindingMethodの使用例

せっかくだから先ほどの例がどのように定義されているのか実際のソースを見てみましょう。

と言っても関係あるとこだけしか抜き出しませんが。長いしね。

@BindingMethods({
        @BindingMethod(type = View.class, attribute = "android:onClick", method = "setOnClickListener"),
        @BindingMethod(type = View.class, attribute = "android:onLongClick", method = "setOnLongClickListener"),
})
public class ViewBindingAdapter {
  ...

上記の記述により、Viewを継承しているクラスのandroid:onClickに何らかのメソッドがバインドされた場合、View.setOnClickListenerへ処理を移譲することができます。中々クールですね。

ちなみに@BindingMethods@Target({ElementType.TYPE})で定義されていますが、多分、本当はどこでもよかったんでしょうね…。

@BindingAdapterの使用例

例えば、ImageViewに対してGlideをつかって画像をセットしたい時とかあると思います。

(公式ドキュメントはなぜかPicassoを例にしていますが、あえて競合他社のライブラリを例にした理由は謎です。)

面倒なので Model は超単純にしましょう。

public class Model {
    public final String imageUrl;
    public Model(String imageUrl) {
        this.imageUrl = imageUrl;
    }
}

こんな形でバインドできたらありがたいです。名前空間androidじゃなくてres-autoなので注意しましょう。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="m" type="com.example.Model"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <ImageView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:imageUrl="@{m.imageUrl}" />
   </LinearLayout>
</layout>

わざわざImageViewを継承して独自のattrを作らずともBindingAdapterを使うことで簡単に実現できます。

public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl"})
    public static void loadImage(ImageView v, String imageUrl) {
        Glide.with(v.getContext()).load(imageUrl).into(v);
    }
}

「画像を読み込めたらそれを表示してほしいけど、失敗したらdrawableの画像を表示したい」なんて要件も当然あると思います。

そんな時は@BindingAdapterに複数の引数を渡せば OK です。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="m" type="com.example.Model"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <ImageView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:imageUrl="@{m.imageUrl}"
           app:error="@{@drawable/error}" />
   </LinearLayout>
</layout>
public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl", "bind:error"})
    public static void loadImage(ImageView v, String imageUrl, Drawable error) {
        Glide.with(v.getContext()).load(imageUrl).error(error).into(v);
    }
}

ただし、上記の場合だと必ずapp:imageUrlapp:errorの両者を指定する必要があります。

app:errorを指定しないこともあるかも…という場合は、@BindingAdapterを指定するメソッドを 2 つ用意するしかありません。(ダサい)

public final class ImageViewExtensions {
    @BindingAdapter({"bind:imageUrl"})
    public static void loadImage(ImageView v, String imageUrl) {
        Glide.with(v.getContext()).load(imageUrl).into(v);
    }
    @BindingAdapter({"bind:imageUrl", "bind:error"})
    public static void loadImage(ImageView v, String imageUrl, Drawable error) {
        Glide.with(v.getContext()).load(imageUrl).error(error).into(v);
    }
}

レイアウト XML の細かい文法

前述した内容だけでもある程度使えますが、レイアウト XML でもっと色んなことができるようになってるのでガンガン活用しましょう。

演算子

ちょっとした計算や bool 値の判定なんかもレイアウト XML 内でできるようになりました。

具体的に何ができるかは公式ドキュメントを読んでください。5 分もあれば読めますよ。

以下、基本的なコード例です。公式のやつそのまんまですが。

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? android.view.View.GONE : android.view.View.VISIBLE}"
android:transitionName='@{"image_" + id}'

View#VISIBLEView#GONEの切り替えなんかは相当楽になりますね。

また、(pure な)Java にはまだないNull 合体演算子がサポートされています。

android:text="@{user.displayName ?? user.lastName}"
<!-- android:text="@{user.displayName != null ? user.displayName : user.lastName}" と同義 -->

Import

<data>内に Import 文を書くことが可能です。

で?と思うかもしれませんが、Android SDKフレームワーク内のあれこれを使うときとかに便利です。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <import type="android.view.View"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
   </LinearLayout>
</layout>

package は違うけど同名のクラスを使う場合はaliasを設定できます。

<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>

バインディングクラスのパッケージ / 名称変更

バインディングクラスってなんじゃい、って話ですが、例えばmain_activity.xmlであれば、MainActivityBindingと言うクラスが自動で生成されます。このMainActivityBindingバインディングクラスです。

もし root パッケージがcom.example.my.appだとしたら、MainActivityBindingcom.example.my.app.databindingに配置されます。

何らかの理由があってこのデフォルトで生成されるクラスの名前や、生成されるパッケージを変更したい場合はdata要素の attribute としてclassを指定してあげれば OK です。

クラス名を変えるだけならこんな感じです。

<data class="MainActivityViewHolder">
    ...
</data>

パッケージも変更したいのであれば、フルパスで指定してあげましょう。

<data class="com.example.MainActivityViewHolder">
    ...
</data>

root パッケージ直下に置きたい場合は先頭に.をつけてあげれば OK です。

<data class=".MainActivityViewHolder">
    ...
</data>

Include 先へのバインディング

Include したいレイアウトファイルにも要素をバインドできます。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

当然と言えば当然ですが、Include 先のレイアウトファイルでもちゃんとバインディングの設定をしておく必要があります。

上記の例で言えば、name.xmlcontact.xml内でuserと言う variable がなければ叱られます。

また、<merge>要素直下の include に対してはバインドできないそうなので気をつけましょうね。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <!-- merge直下のincludeなのでこれはダメ -->
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

暗黙の型変換

これまた黒魔術っぽいんですが…。本来なら適切でない型がバインドされても、@BindingConversionが指定されているメソッドに移譲して型変換することができます。

ちょっと何言ってるかわかんないですよね。例えばこんな感じです。

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

View.setBackgroundDrawableしか受け付けないんですが、colorのリソース ID(int)を渡したいとします。

当然このままでは怒られるので、ColorDrawableに変換する static メソッドを作り、@BindingConversionを指定します。

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

これでandroid:backgroundintが渡されたとしてもColorDrawableに変換してくれます。

何も知らないと非常に混乱しそうなので、用量、用法を守ってお使いください…。と言うか、@BindingAdapterと違って適用する attr を制限できないっぽいのでめちゃめちゃ危険です。使わない方がいいかもしれません。

ちなみに、Drawableintのどちらかが返されるみたいなパターンは普通にダメです。

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

とは言え、static メソッドは普通に呼べるのでそっちで回避できます。

<View
   android:background="@{isError ? @drawable/error : ColorExtensions.convertColorToDrawable(@color/white)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

…あれ?じゃあほんとにこれいらなくね?

まとめ

久しぶりに濃厚な記事になってしまいました。

基本編と銘打ってはいますが、応用編を書く予定とネタは今のところありません。

自分でも色々いじってみたテスト用プロジェクトをGitHubにおいておきました。それぞれ、こんな感じです。

  1. Data Binding の導入
  2. とりあえず適当にバインディングしてみる
  3. Observable なプロパティとイベントのバインディング
  4. BaseObservableを継承したパターン
  5. @MethodAdapterで遊んでみる

参考

ぶっちゃけ公式ドキュメントが一番わかりやすかったです。

【NYAGOS】プロンプトで使える特殊文字と ANSI エスケープシーケンスを Lua でラップする

2018/12/01 追記

前書き

前回の記事でプロンプトをごにょごにょしていたんですが、プロンプトだけで使える特殊文字だとか、ANSI エスケープシーケンスだとかをいちいち文字列で書くのが面倒になりました。

そもそもぱぱっと書けませんし読めませんよそんなもん。ゆとり教育では学びませんでした。

しかし、現代では教育だけでなくマシンの CPU やメモリもゆとりが出てきています。可読性を高めるため、全部 Lua でラップしてしまいましょう。

プロンプトだけで使える特殊文字

色々探していたらMSDNに一覧がありました。ここには転記しません。かわりに今回使うコードを載せます。

share.prompt = {
  eq='$q',     -- equal '='
  dollar='$$', -- dollar '$'
  time='$t',   -- current time
  date='$d',   -- current date
  path='$p',   -- current path (with drive letter)
  ver='$v',    -- windows version
  drive='$n',  -- current drive letter
  gt='$g',     -- greater than '>'
  lt='$l',     -- less than '<'
  pipe='$b',   -- pipe '|'
  crlf='$_',   -- line break
  esc='$e',    -- escape
  bs='$h',     -- backspace
  amp='$a',    -- ampersand '&'
  l_par='$c',  -- left parenthesis '('
  r_par='$f',  -- right parenthesis ')'
  space='$s',  -- space
}

ちなみに$の後の文字は大文字・小文字を区別しないそうです。

また、$h(backspace)は$t(time)とか$d(date)とかでとってきた文字列を頑張って削る時に使うそうです。全くもってゆとりがありません。

ANSI エスケープシーケンス

ANSI エスケープシーケンス自体についてはこの記事が非常にわかりやすかったです。歴史的経緯や古き良き C 言語のサンプルもちょこっと書いてあるので、暇な人は読んでみましょう。

細かい一覧は英語版 Wikipediaを読むのが一番いいんですが、何をどれだけ読もうとも、Nyagos で使えるのはansicolor書いてあるやつだけです。なので、ここだけラップします。

ANSI エスケープシーケンスのラップ

ANSI エスケープシーケンスはプロンプト以外でも普通に使えるので、先ほど作ったshare.promptテーブルにぶちこむのは流石に乱暴です。

別途share.escape_sequenceと言うテーブルを作ります。

share.escape_sequence = {
  -- attribute
  attr = {
    off='0',
    bold='1',
    bold_off='21',
    underline='4',
    underline_off='24',
    blink='5',
    blink_off='25'
  },

  -- foreground
  fg = {
    black='30',
    red='31',
    green='32',
    yellow='33',
    blue='34',
    magenta='35',
    cyan='36',
    white='37',
    default='39',
    -- prefix 'l' is 'Light'
    l_gray='90',
    l_red='91',
    l_green='92',
    l_yellow='93',
    l_blue='94',
    l_magenta='95',
    l_cyan='96',
    l_white='97'
  },

  -- background
  bg = {
    black='40',
    red='41',
    green='42',
    yellow='43',
    blue='44',
    magenta='45',
    cyan='46',
    white='47',
    default='49',
    -- prefix 'l' is 'Light'
    l_gray='100',
    l_red='101',
    l_green='102',
    l_yellow='103',
    l_blue='104',
    l_magenta='105',
    l_cyan='106',
    l_white='107'
  },
}

ANSI エスケープシーケンスの文法

厳密にはANSI エスケープシーケンスにおける色付けの文法ですが…。

基本的には\x1b[(該当するコード)mと言う文字列を作成します。例えば、赤字にしたいなら\x1b[31mですね。

「ここからここまでの文字列に適応する」と言った指定はできないため、適用させた後は再度 ANSI エスケープシーケンスを作って元に戻す必要があります。(そのためにdefaultとかoffと言った要素がある。)ちなみにこれを忘れるとその後の標準出力がずっと真っ赤になったりします。怖いですね。

また、複数の要素を一気に指定したい場合は\x1b[(該当するコード);(該当するコード);...mと記述することができます。例えば、文字色は赤字、背景色は灰色、ついでに下線も引いてやろう、って場合は、\x1b[31;100;4mとなります。指定するコードの順番に制限はないです。(つまり、\x1b[4;100;31mでも同じ効果を得られる。)

ANSI エスケープシーケンスを作成する関数

上記の内容を踏まえ、share.escape_sequenceに各コードのテーブルを渡すと ANSI エスケープシーケンスを作ってくれる関数を作ります。

share.escape_sequence = {
  -- ...省略...

  create_sequence = function(...)
    local attrs = {...}
    local joined_attrs = ''

    for n, v in pairs(attrs) do
      local val = tostring(v)
      if val ~= nil then
        joined_attrs = joined_attrs ~= ''
          and joined_attrs .. ';' .. val
          or val
      end
    end

    return string.format('\x1b[%sm', joined_attrs)
  end
}

呼び出しはこんなイメージです。何をやっているのか大分わかりやすくなったと思いませんか?

local esc = share.escape_sequence
-- colorに'\x1b[2;31;49m'が返ってくる
local color = esc.create_sequence(esc.attr.bold, esc.fg.red, esc.bg.default)

実践

実践って言うか、単に私の.nyagosの中身から抜粋しただけですが…。

share.org_prompter=nyagos.prompt
nyagos.prompt = function(this)
    local prompt = share.prompt
    local esc = share.escape_sequence

    -- add dir name (with drive letter)
    local c_dir = esc.create_sequence(esc.attr.bold, esc.fg.red)

    local prompt_message = string.format('%s[%s]', c_dir, prompt.path)

    -- check git branch
    local git_branch_name = nyagos.eval('git rev-parse --abbrev-ref HEAD 2>nul')

    if (git_branch_name ~= '') then
        -- add git branch name
        local c_branch = esc.create_sequence(esc.fg.l_yellow)
        prompt_message = string.format(prompt_message .. ' %s[%s]', c_branch, git_branch_name)
    end

    -- add line break
    prompt_message = prompt_message .. prompt.crlf

    -- add dollar
    local c_dollar = esc.create_sequence(esc.fg.red)

    prompt_message = prompt_message .. c_dollar .. prompt.dollar

    -- add input command color
    local c_input_command = esc.create_sequence(esc.fg.white)

    prompt_message = string.format(prompt_message .. ' %s', c_input_command)

    return share.org_prompter(prompt_message)
end

元々が冗長な仕組みなので仕方ないんですが、どうしても長くなってしまいますね。

まとめ

プロンプトの方はともかく、ANSI エスケープシーケンスの方は何かに応用できるかもしれません。(エラーメッセージに色を付ける、とか…。)

現状は全くのノーアイデアです。何か思いついたらまた作りましょう。

参考