【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使い方メモとハマりどころメモ

前書き

最近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アノテーションを使います。

key/value形式に自動で変換して欲しい場合は@FormUrlEncoded@Filedを使います。

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

@BodyはそのまんまBody部に値をぶちこみます。ただ、Converter(後述)を指定することで独自の型でもいい感じに変換してくれます。*3

また、@FormUrlEncodedは強制的にUTF-8エンコードするので、他の文字コードでPOSTしなきゃいけないような場合も@Bodyを使わざるを得ないのかもしれません。(未検証)

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

また、@FormUrlEncodedなデータをPOSTするときにデフォルト値を設定したいことが結構あるんですが、現在開発中なので、@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;
}
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です。*4

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

ヘッダの指定

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も色々さわりたい*6場合は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本体とは別モジュール*7なので、依存関係に追記が必要なことを忘れないでおきましょう。

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

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

レスポンスのCall Adapter

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

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

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

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

ライブラリ ラップする型 モジュール
RxJava ObservableSingle com.squareup.retrofit2:adapter-rxjava
Guava ListenableFuture com.squareup.retrofit2:adapter-guava
Java 8 CompleteableFuture com.squareup.retrofit2:adapter-java8

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

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

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

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

レスポンスの自動型変換

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

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

ライブラリ モジュール
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*11 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を指定していても使用できます*12

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

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

まとめ

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

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

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

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

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

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

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

*7:なんでやねん。

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

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

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

*11:Wikiに載っている内容は誤っているので注意

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

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

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

前書き

最近(と言っても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;
   }
}
<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 firstName = new ObservableField();
   private final ObservableField lastName = new ObservableField();

   public User(String firstName, String lastName) {
       this.firstName.set(firstName);
       this.lastName.set(lastName);
   }
   public ObservableField getFirstName() {
       return this.firstName;
   }
   public ObservableField 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} />
   </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}"

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でラップする

前書き

前回の記事でプロンプトをごにょごにょしていたんですが、プロンプトだけで使える特殊文字だとか、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エスケープシーケンスの方は何かに応用できるかもしれません。(エラーメッセージに色を付ける、とか…。)

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

参考

【NYAGOS】Nyagosのプロンプトにgitの現在のbranch名を表示する

前書き

「ちょっと作業するかー→コミットするかー→ここmasterだ!あびゃー!」って言うことがよくあります。

病院に行くお金と時間がないので、Nyagosのプロンプトに現在のbranch名を表示するようにしました。

しくみ

gitで現在のbranch名を取得するにはgit rev-parse--abbrev-refオプションを渡すのが一番簡単です。

nyagos.evalに上記コマンドを渡せばOKです。当然gitで管理されていないフォルダだと怒られるので、エラー出力は捨てます。具体的にはこんな感じ。

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

if (git_branch_name ~= '') then
  -- TODO: プロンプトにgit_branch_nameを表示する
end

実例

デフォルトの.nyagosでやるとしたら、こんな感じになります。

nyagos.env.prompt='$L'.. nyagos.getenv('COMPUTERNAME') .. ':$P$G'
share.org_prompter=nyagos.prompt

nyagos.prompt = function(this)
    local prompt_message = this
    -- プロンプトにgit_branch_nameを表示する
    local git_branch_name = nyagos.eval('git rev-parse --abbrev-ref HEAD 2>nul')
    if (git_branch_name ~= '') then
        -- もちろんgit_branch_nameに適当な色をつけたりしてもOK
        prompt_message = prompt_message .. ' [' .. git_branch_name .. ']'
    end
    
    -- ここで改行をくっつける
    prompt_massage = prompt_message .. '$_$$$s'
    
    return share.org_prompter('$e[36;40;1m'..prompt_message..'$e[37;1m')
end

まとめ

同じ要領で色んなバージョン管理システムのアレができるんですが、git以外は特に使ってないのでこれで十分です。

毎回gitコマンドを叩くので若干重くなったりするのが難点です。

参考

【Atom】入れておきたいパッケージメモ2015年版

前書き

以前、【Atom】やっておきたい設定+入れておきたいパッケージメモと言う記事を書いたんですが、思った以上に閲覧、リンクされ、未だに読まれています。

それは別にいいんですが、今となっては流石に内容が古すぎて申し訳ないので、最新のおすすめパッケージを書いておきます。前回の記事と全く同じものもありますが、それだけ使えるってことです。

Atom自体の更新によって必要性が薄くなったパッケージ

前回の記事には載せていたものの、Atom自体のアップデートによってわざわざパッケージを入れなくても良くなったものがいくつかあります。

Japanese Wrap

Atom1.2でCJK(Chinese Japanese Korean)の禁則処理が公式対応となったため、必要なくなりました。

むしろ導入するとバグが発生するとの情報もあるので、今まで入れてた人はアンインストールしておきましょう。

Save Session

デフォルトで前回開いていたプロジェクトやファイルを表示できるようになったので、不要になりました。

このパッケージのREADMEにもProject Status: Deprecatedと記載されています。

Autocomplete Plus

必要なくなったのではなく、公式のパッケージとなったため、明示的にインストールする必要がなくなりました。(Atomをインストールすると最初からバンドルされている)

入れないと辛すぎるパッケージ

Auto Encoding

読み込んだファイルの文字コードを推測し、自動でAtomの設定を変更してくれます。

「えーマジShift-JIS!?」「キモーイ」「UTF-8以外の文字コードが許されるのは小学生までだよねー」「キャハハハハ」なプロジェクトで大活躍します。

Color Picker

カラーコードをAtom上でプレビューしながらゴニョゴニョできます。後述するPigmentsと組み合わせて使うと非常に便利です。

Docblockr

ブロックコメントを自動生成してくれます。

かなり便利…と言うか、これなしでブロックコメントを書こうとすると死ぬほど辛いです。

Pigments

カラーコードに応じた色を表示してくれたり、CSS内のカラーコードをパレットとして保存しておいてくれたり、HEXからRGBに変換してくれたりと、これでもかってほど便利です。

LESS / SCSSなどの変数や関数にも対応してます。無敵かよ。

Project Manager

頻繁に使うプロジェクトを登録することが出来ます。

保存方法は「プロジェクトを開く」→[Packages]→[Project Manager]→[Save Project]

呼び出すときはCtrl + Shift + Alt + P

Symbols Tree View

画面右側にシンボル(変数とか関数とか)のツリービューを出してくれます。

めちゃめちゃに長いソースを読まなきゃいけない時に頼りになります。

入れておくとそこそこ便利なパッケージ

Command Toolbar

「たまに使うんだけどキーバインドを覚えるほどではない」みたいなものを登録しておくと捗ります。

私は主にGit Plusと組み合わせて「キーバインドもgitの引数もパッと思い出せないけどたまに使う」みたいなものを登録しています。

Git Plus

元々AtomはGitに中途半端に対応しているんですが、中途半端すぎていらいらしてくるので、精神衛生上これを入れておくのが無難だと思われます。

ただ、ぶっちゃけた話、ほとんどの場合はSource Treeを使った方が早いし便利です。

Highlight Selected

文字列を選択すると同ファイル内の同じ文字列をハイライトしてくれます。

ソースをじっくりと追う時に非常にありがたい存在です。

Live Archive

Atomでの編集履歴を保存しておき、簡単に比較することが可能です。

ぶっちゃけそんなに使わないんですが、本当にたまに役立ってくれます。

Minimapとそのプラグイン

コードのミニマップをちょろっと出してくれます。

これ自体は便利ともどうでもいいともなんとも言えないんですが、プラグインが色々と便利です。

Tree View Finder

画面左側のTreeViewを拡張し、ファイルサイズや更新日付を表示してくれます。

ソート機能もつきます。個人的にはむしろそっちがメインです。

わりかしどうでもいい or 趣味的なもの

File Icons

アイコンがちょっとオシャレになります。デフォルトよりはファイルが一目で見分けられるので意外と便利です。

Japanese Menu

Atomのメニューバーや設定画面などを日本語化してくれます。

そんなに難しい英語は使われていないのでいらないっちゃいらないんですが、プロジェクト全員で使いたい!って時に、導入のハードルを下げる効果ぐらいはあるかもしれません。むしろgitの方がハードル高そう。

また、当然ですが、個別パッケージの設定画面なんかは一切翻訳されません。

テーマ

色々入れてみて気に入ったものを使えばいいと思います。私はatom-dark-ui-slimrailscast-themeを使っています。

まとめ

他にもいくつかパッケージを入れてるんですが、特定の言語用のパッケージ(atom-typescriptとか)なので、説明は割愛します。