do blog <- post

about this blog

渋谷で働くお父ちゃんプログラマの技術メモ

recent public projects

Status updating…

found on

Android で Either を使おう

- - posted in Android, Either, Optional, RxAndroid, RxJava

先日 Android で Optional を使おう というエントリを書きました。

Optional<T>ってのは 値があるかないか を表現するって言いましたけど、あるメソッドがOptionalで包んだ値Tを返して来るってことは、言い換えるとOptionalある処理が成功したかまたは失敗した ことを表していると言えます。

しかしこれだと失敗したことは分かるけどその理由までは分からないですね。こういうときHaskellではEitherというデータ型を利用します。

1
2
Prelude> :i Either
data Either a b = Left a | Right b

Either失敗理由と成功データ の両方を表現するデータ型です。
Leftが失敗、Rightが成功に対応します。rightという単語の “右” と “正しい” の両方の意味が掛かった洒落ですね。これ、Androidにもあると便利なんですよ。

Javaでは伝統的に例外ケースはその名の通り “例外” で表現してきましたが、これは本質的に副作用です。それに微妙にRxJavaと相性が悪いです。

RxJavaでは複数のObservableをチェインさせてプロミス的な使い方をすることがままありますが、この間に起きた例外はSubscriberonErrorに来ます。
しかし 例外が発生したわけではないけど成功じゃないパターン って時々ありますよね。
んー例えば何でも良いんだけど、

  1. HTTP 200だけど{'availability':false}みたいなJSONでエラーを伝えてくるAPI
  2. 必要なパラメータが足りてない、形式が不正
  3. 2みたいなケースをfilterオペレータで間引くんじゃなくてエラーは伝播させたい

などなど。少なくとも自分はこういうときの銀の弾丸をまだ見つけられていません。

こういう時独自の例外をthrowしてonErrorで捕まえるのはあんまりしっくりきてません。
例外を単純な場合分けに使うのはいかにも筋が良くないし、そもそもObservableonCompleteonErrorでそのライフサイクルを終えるので、復旧困難なケース以外でここに放り込むのは何か違う気がしてます。

これはEitherを使うしかないでしょう!

Either を作る

JavaにはEitherなんてイカしたものは勿論ないので作るしかありませんが、「Java Either」とかでググると Is there an equivalent of Scala’s Either in Java 8? なんてのがすぐに見つかります。

この例はJava8でしか動かせないのでAndroid用に書き換えたのが以下です。
一部うまく推論してくれないところがありましたが使わなそうなので無理に移植せず削除しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package functional.data;

import com.eccyan.optional.Optional;

import rx.functions.Action1;
import rx.functions.Func1;

/**
 * borrowed from http://stackoverflow.com/questions/26162407/is-there-an-equivalent-of-scalas-either-in-java-8
 */
public final class Either<L, R> {
    public static <L, R> Either<L, R> left(L value) {
        return new Either<>(Optional.of(value), Optional.empty());
    }

    public static <L, R> Either<L, R> right(R value) {
        return new Either<>(Optional.empty(), Optional.of(value));
    }

    private final Optional<L> left;
    private final Optional<R> right;

    private Either(Optional<L> l, Optional<R> r) {
        left = l;
        right = r;
    }

    public <T> Either<T, R> mapLeft(Func1<? super L, ? extends T> lFunc) {
        return new Either<>(left.map(lFunc), right);
    }

    public <T> Either<L, T> mapRight(Func1<? super R, ? extends T> rFunc) {
        return new Either<>(left, right.map(rFunc));
    }

    public void apply(Action1<? super L> lFunc, Action1<? super R> rFunc) {
        left.ifPresent(lFunc);
        right.ifPresent(rFunc);
    }
}

早速自分のアプリに組み込んでみました。使い方はこんな感じです。
HaskellやScalaだとLeftRightでパターンマッチできるのですが、Javaだとそれも無理なので Either#apply(leftラムダ式, rightラムダ式) みたいな感じでお茶を濁していますね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bind(R.id.weatherInput)
EditText editTextCity;

@OnClick(R.id.submit)
void onClickSubmit(Button button) {
    WeatherApiCreator.create(CurrentWeatherService.class).getByCityName(editTextCity.getText().toString())
            .map(currentWeatherResponse -> {
                Either<Throwable, String> either;
                if (currentWeatherResponse.getCode() != 200) {
                    either = Either.left(new RuntimeException("error code: " + currentWeatherResponse.getCode()));
                } else {
                    either = Either.right("Humidity: " + currentWeatherResponse.getMain().getHumidity());
                }
                return either;
            })
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                    either -> either.apply(
                            left -> Toast.makeText(this, left.getMessage(), Toast.LENGTH_SHORT).show(),
                            right -> Toast.makeText(this, right, Toast.LENGTH_SHORT).show()
                    ),
                    error -> Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show()
            );
}

もうひとつ大事なことがあります。 Eitherは写像を作る※ ことができます。
以下の例をみてください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.map(currentWeatherResponse -> {
    Either<Throwable, String> either;
    if (currentWeatherResponse.getCode() != 200) {
        either = Either.left(new RuntimeException("error code: " + currentWeatherResponse.getCode()));
    } else {
        either = Either.right("Humidity: " + currentWeatherResponse.getMain().getHumidity());
    }
    return either;
})
.map(either -> either.mapRight(String::toUpperCase))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
        either -> either.apply(
                left -> Toast.makeText(this, left.getMessage(), Toast.LENGTH_SHORT).show(),
                right -> Toast.makeText(this, right, Toast.LENGTH_SHORT).show()
        ),
        error -> Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show()

この例だと、処理が成功していれば結果をすべて大文字に変換しています。例があまりにもしょうもなくて申し訳ないです!

重要なのは、途中で処理が成功していようが失敗していようが気にせず mapRight でRightを写すことができている点です。
処理が途中で失敗しても、Rightは空っぽなので変換処理は単に空振りして、LeftはLeftのまま伝播するという寸法です。

言いたいこと伝わりますかね? 利用者が isRight とか isLeft とか判定してるようじゃ意味が無い のです。

Either 右翼?

以下、余談です。

上で僕は Eitherは写像を作ることができる と言いましたが、この例だとそれが嘘であることが識者にはバレバレだと思います。端的に言うと上のようなEitherモナドじゃありません。
Either自体はmapで写すことができておらず mapRight, mapLeft なんていう方法でそれぞれを操作しています。

こういうのは、LeftとRightを対等に扱ったEitherとか言われるみたいです。Scalaの標準ライブラリのEitherや上のJavaのコードなんかがその例です。

対して、HaskellやScalazのEitherは Right-Biased Eitehr とか呼ばれています。右寄りの、右派のっていう意味です。EitherをそもそもRightのコンテナとして利用しようという考え方です。
このようなEitherはRight値をひとつ包むモナドのように動作します。また、LeftとRightを結合するといかなる場合もLeftになります。

Androidで利用できるRight-BiasedなEitherは夏休みの自由研究にでもしようかと思います。それでは。

参考リンク

GCPのCentOSでtcp_tw_recycleが有効になっていてハマった

- - posted in CentOS, GCE, GCP

Google Cloud Platform(以下GCP)を評価中であるが、Google Compute Engine(以下GCE)でCentOS 6をイメージとして選択してインスタンスを起動してSocket.IO環境を構築したところ、AndroidとGCE間で 時々 原因不明のSSLハンドシェイク失敗に悩まされ非常にハマった。

結論から言うと、当該インスタンスでは net.ipv4.tcp_tw_recycle が有効になっていた。これを無効にすることで問題は解決した。

1
2
3
4
5
6
% sysctl net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 1

% sudo sysctl -w net.ipv4.tcp_tw_recycle=0
% sysctl net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_recycle = 0

設定を永続化させたかったら /etc/sysctl.conf に書けば良い。

ちなみに net.ipv4.tcp_tw_recycle はTIME_WAIT状態のソケットを効率的に再利用するためのLinuxカーネル特有の仕組みとのこと。
これが有効の場合、TCPパケットのタイムスタンプ情報を見て同一IPから新しいパケットが届くと古いソケットを開放するらしい。
ただ、同一NAT下の複数端末が同時に接続しに来た場合に各ノードを区別できずに単純に古いものをドロップするような挙動をすることがあるようで、今回の僕のケースはそれに該当するようだ。

(※この辺り門外漢なので記述が正確でなかったらすみません。)

参考までに、AWSのAmazon Linuxの今日時点での最新であるAmazon Linux AMI 2015.03で確認したところ、 net.ipv4.tcp_tw_recycle = 0 となっており無効であるようだ。
個人的にはこれに合わせておくと無難だろうと判断した。

なお、GCEのどのイメージでも同様かどうかは一切確認してない。CentOS 6なんていう古いのを選んだのも、Amazon Linuxからの移行コストが一番少なそうだからで、普通に新規構築するならCentOSみたいな保守的で新陳代謝の遅いディストロは特に選択するメリットも無い気がする。

ただ、この問題そのものは原因究明に非常〜〜〜に苦労した(WireSharkやらtcpdumpやら使いまくった。途中マジで死のうかと思った)ので、同じような人が居るかも知れないのでブログにまとめておく。

以下参考リンク

Android で Optional を使おう

- - posted in Android, Optional, RxAndroid, RxJava

Shibuya.apk #2 というイベントで登壇させていただきました。資料はここにあります。
テーマは「RxJavaとOptionalで関数型Androidしよう」ですが昨今の巷での関数型言語の流行りに便乗した釣りタイトルです。誠に申し訳ございません。
で、そのテーマの前半部分である Optional<T> についてここで詳しくまとめておきたいと思います。

Optionalとは何か

Optional とは あるかもしれないしないかもしれない ものを表現する型です。
Haskell でいう Maybe a = Nothing | Just a です。Scalaでいう Option[T] = Some(T) | None です。
Optional<String> ってのがあると、文字列があるかもしれないしないかもしれないって意味になります。

これの何が嬉しいかっつーと if (str != null && !TextUtils.isEmpty(str)) みたいなつまらないボイラープレートを書かなくて済むんですよ。
あと、これが一番大事なので最初に書くけど、 Optionalの中身があるかどうか気にせずOptionalをmapで写したりできる んです。これは死ぬほど大事なので後でコード例とともに示します。
あとはまあ、 Optional<T> って書いとくと ああこれは中身がnullになりうるんだな ってことが仕様としてハッキリします。これはドキュメントに書くより、@Nullable 修飾するより強力です。

AndroidはJava8使えない

Java8からは Optional<T> が使えます。Java8ではラムダ式だとか関数クラス型だとかStream APIとかが追加されてますが、個人的にはOptionalが最も大切です。
で、御存知の通りAndroidは未だにJava6互換でいくつかJava7のAPIが使えるにとどまっており、非常にレガシーな書き方を余儀なくされます。とても悲しい。

ところが、私の敬愛するsys1yagiさんという方の RxJavaのObservableでOptionalを代行する という神のようなブログエントリでAndroidでもOptionalが使えることが示されました。

当該エントリをそのままコピペしてもほとんど申し分ないOptionalが扱えるのですが、一部RxJavaのObservableがむき出しになっており、RxJava内でOptionalを取り回したいときにちょっと見た目がわかりづらくなるので完全にOptionalという名前でラップしたライブラリを自作しようとしたところ、なんと既にありました。eccyan/RxJava-Optional です。作者さま本当にありがとうございます。

AndroidでOptional早速使おう

gradleで入れられます。

1
2
compile 'io.reactivex:rxandroid:0.25.0'
compile 'com.eccyan:rxjava-optional:1.1.2'

バージョンに注意してください。公式のREADMEだと1.1.0になってるのですが、このバージョンだとAPIレベル19以降でしか使えないObjects.requireNonNullがそのまま使われており、古いAndroidで動きません。
1.1.2では自作のObjectsクラスがバンドルされており古いAndroidでもちゃんと動きます。

インストールできたら早速使いまくりましょう。

なお、以下の例ではラムダ式が出てきまくりますが、これはRetrolambdaというライブラリとAndroid Studioのプラグインを組み合わせて使ってます。
インストール方法がちょっとややこしいので別エントリにまとめ次第追記します。

Optionalでくるむ

まずOptionalを使うポイントですが、前述のとおりあるかもしれないしないかもしれないところで使います。
具体的に言うと、関数の最後で Map#get だとか String#indexOf の結果を戻してるような箇所はOptionalで包む格好の場所だと思います。

値をOptinalで包むメソッドは3つ用意されてます。

  • Optional.of(T)
  • Optional.ofNullable(T)
  • Optional.empty()

Optional.of(obj) はobjがnullだった場合にぬるぽを投げます。対して、Optional.ofNullable(obj) はnullかも知れないobjも安心して包めます。
だからと言って ofNullabeを毎回使いましょう みたいなルールにしちゃダメですよ。
nullじゃないことが明らかな場合、またはnullであっちゃならない場所ではofを使うべきです。なぜなら本来nullであってはならない場所ならその場でぬるぽで落ちるべきであるからです。

nullでないのが明らかならTでいいじゃんという声もありそうですが、戻り値としてはOptional<T>を返したいというのとその場でobjがnullでないことが明らかというのはまた別の話です。

話がそれましたが、Optional.empty()Optional.ofNullable(null) と同じことです。

Optionalから値を取り出す

Optionalから値を取り出します。ホントはmapとかが一番重要なんですが、後に譲ります。

  • Optional#get()
  • Optional#orElse(T)
  • Optional#orElseCall(() -> {T})
  • Optional#orElseThrow(() -> {Throwable})

Optional#get() は中身がnullのときはNoSuchElementExceptionが投げられます。これはぬるぽを踏むのと一緒であんま意味がないので僕は使ったことないです。

Optional#orElse(T) は中身がnullのときに引数で与えた値が取り出せます。mapと組み合わせて死ぬほど使うので覚えておいてください。

Optional#orElseCall(() -> {T})orElseに似てますが、中身がnullのときに初めてラムダ式が評価されてその結果が取り出せます。引数が遅延評価されるのでより関数型っぽいですね。初期化処理がコスト高い場合なんかはこっちを使うと良いかもしれません。

Optional#orElseThrow(() -> {Throwable}) は中身がnullのときはラムダ式で作られた例外が投げられます。
Optionalを何回もmapfilterして最終的に中身がなかったら例外を上げるっていうケース、時々あるような気がするので何回か使った記憶がありますが、それでもorElseを使う頻度よりはるかに少ないはずです。
もしorElseThrowをどこででも使ってるとしたらそれはなんかOptionalを誤解してる気がします。普通にTを返すメソッドをthrows HogeExceptionしてください。

Optionalをモナドとして使うよ

冒頭で Optionalは中身があるかどうか気にせずOptionalのままmapで写したりできる って書きました。これめっちゃ大事なことなんですよ。

たとえば、Optional#isPresent() っていう、中身の値があるかどうか調べるメソッドがあるんですが、ちょっと下のコード見てください。

1
2
3
if (opt.isPresent()) {
   String val = opt.get();
}

これ何の意味があるんでしょうか。これ今までの退屈なnullチェックと同じですよね。Optional使った意味全くないですよね。なのでこれは最悪です。こんなの書かないようにしてください。

Optionalは下みたいなクールな方法で扱います。

  • Optional#ifPresent(値を消費するラムダ式)
  • Optional#map(TをUに写すラムダ式)
  • Optional#flatMap(TをOptional<U>に写すラムダ式)
  • Optional#filter(Tを新しいTに写すときの条件を示すラムダ式)
Optional#ifPresent

ifPresentはOptionalの中身があったときだけ引数のラムダ式を実行してくれます。

1
2
Optional<String> strOpt = Optional.of("V8!!!");
strOpt.ifPresent(s -> Log.i(TAG, s));

これはもう見ての通りです。注意点は、ifPresent値を返す式ではない ということです。そういう場合は後述のmapを使います。

Optional#map

mapは元のOptionalに包まれた<T>の値を、新しい<U>に写します。要するに変換です。

1
2
3
Optional<String> strOpt = Optional.of("123");
strOpt.map(s -> Integer.valueOf(s)) // strOpt.map(Integer::valueOf) とも書けるよ!
      .ifPresent(i -> Log.i(TAG, "i: " + i));

上記は分かりやすさのためにほとんど意味のないコードですが、何かエンティティ(例えばユーザ情報)をmapしてユーザ名だけの圏を得たりするのは非常によくする操作なんじゃないでしょうか。

maporElseをつなぐと例外処理など使わずとも上から下までOptionalとその写像だけを連ねた結果をエレガントに扱うことができますよ。

1
2
3
int result = Optional.ofNullable(strNullable)
                     .map(Integer::valueOf)
                     .orElse(123);

この例はしょうもなさすぎてエレガントとは言い難いですがね!けどアイディアは伝わるでしょう。mapはいくら連ねてもいいんですよ。

Optional#flatMap

flatMapはOptionalで包まれた値を次に渡すときに使います。ちょっと分かりづらいという人も居ますが、簡単ですよ。
mapTからUを写すのに使う、つまりmapの返り値は値型そのものなんですが、既にOptional<U>を返す関数とかがあるときにそれをそのまま使っちゃうとOptional<Optional<U>>が写されちゃうんで、それが適切でないときにflatMapでぺしゃんこにします。flatten(=平らにする)して(map=写す)だけです。

1
2
3
Optional<Token> tokenOpt = getToken();
tokenOpt.flatMap(token -> someApiCall(token, args))
        .ifPresent(apiResult -> processApiResult(apapiResult)); // this::processApiResult とも書けry

こんな感じ。
この例でいうとsomeApiCallOptional<ApiResult>を返すので、そのままだとifPresentOptional<Optional<ApiResult>>が渡ってしまうのでflatMapでぺったんこにしています。ぺったんこの意図するところ、伝わりますかね。

Optional#filter

最後にfilterです。これは例を見てもらうと簡単です。

1
2
3
Optional.of(123)
        .filter(i -> i > 100)
        .orElse(100);

filterに渡すラムダ式は値を受け取って真偽値を返すようなものを渡します。真になったものだけ生き残るというわけです。

まとめ

この通り、Java8のOptionalとほぼ遜色のないコードを書けることが分かると思います。正しく使う限りデメリットが見当たらないのでぜひ使うことをおすすめしたいです。

本エントリでは触れませんが、この代用版OptionalとRxJava/RxAndroidは組み合わせて使うとめっちゃ強力です。(元々同じものなんですけどね)
機会があったらその辺も書こうかなと思います。では。

参考リンク

プログラマとして32歳を生きる

- - posted in Life

32歳になった。32という数字には大きな意味がある。16進数で20、2進数で100000といずれもキリが良い。プログラマとしてもひとつの区切りだと思っている。

32歳のプログラマはもう初心者では居られない

自分は現在で4社目で、転職の過程でそれはもう色んな会社に落とされて悔しい思いをいっぱいした。また、会社員としてはここ数年は採用担当技術者として色んな方と向き合う機会が多かった。つまり、企業として何歳ぐらいの人間がどのくらいの実力や職責を求められるか割りとよく分かっている自覚がある。

32歳のプログラマは、以下のものが求められる。

  • 少なくとも1つ、突出した技能が必要。少なくともその技能では数名のチームのテックリードになれる。
  • 数名のチームをまとめあげる力。自分も戦士のひとりとしてコードを書きつつチームとしての結果を出せる。
  • 望むらくは複数のスキルを持ち合わせている。
    • Andriod + iOS
    • クライアントサイド + サーバサイド
    • サーバサイド + インフラ
    • 複数言語、複数プログラミングパラダイム…等々

面接で ○○やりたい みたいな甘っちょろいことを言っても何のプラスにもならない。
「Dockerやりたい」と心の中で思ったならッ!その時スデにdocker runは終わっているんだッ!

我がお師さんたちの年齢

これは非常に個人的な事情なのだが、自分のプログラマとしてのすべてを作り上げてくださったお師匠たちがちょうどこのぐらいの年齢だった。
僕はプログラマとしては大変遅咲きで、技術者として本当に一人前にしてもらったのは27歳の時に運良く拾ってもらった会社のおかげだ。この会社の先輩方が仕込んでくださらなければ、僕は今頃その辺のパチンコ屋の裏で野垂れ死んでいたと思う。

当時、なぜ自分があのような技術力で名前を轟かせる会社に採ってもらえたのか分からなかった。今でも分からない。ただひとつ言えることは、自分は幸運だったということだけだ。

採用の難しさ、そしてバトンをつないで行くということ

自分が採用する側になって始めて採用がどれほど難しい行為かを思い知ることになった。
どれだけ全力を注いでも、良い人材をそれと見抜けず取りこぼしてしまうし、そうでもない人を10年にひとりの逸材と思い込んでしまう。僕などはまさに後者でたまたま引っかかったに過ぎないのだ。

ただしここからが重要なのだが、人はたったひとつのチャンスを貰って生まれ変わることが出来るということだ。僕など、まともに戦力になったのは28〜9の頃だったのでは無いかと思うが、毎日「自分は最下層の人間なのだ。何の恥をかくことがあろうか。なんでも訊いてしまえ。」という思いで分からないことをひとつひとつ潰して行った結果、前々職の最後〜前職では小さいチームの開発リーダーを任せていただけるまでに成長できた。

この感謝を、我慢強く鍛えてくださった師匠たちにお返しするのかというと、それはきっと違うのだなと思う。僕も同じように、いま何者でもない若者たちにバトンを渡していくのだなと。

40歳、50歳までプログラマで居るために

32ぐらいで年寄りぶるなとお叱りを受けそうだが、それでも20代の頃のように本を読んだら絶対に忘れないということが出来なくなった。記憶力も体力も低下を感じる。おまけに家庭を持ち、好きな技術をとことんまで掘り下げたり、好きなだけ勉強会に行ったりすることが無理になってしまった。

ただきっと、諦めたら試合終了なんだろうなと思う。いつか来るその日までは、常に情報を収集し、本を買い、新しい技術を習得し続けたいと思う。プログラマは葛西臨海水族園のマグロと同じだ。停まったら死だ。

31歳の振り返りと32歳の目標

たしか以下を目標にしてたと思う

  • iOSアプリ書く
  • Dockerやる
  • SICPやる
  • ScalaかClojureやる

結果は…

  • iOSはしょうもないタスク管理アプリ書いた
    • リリースはしていないので全然ダメ
    • ただiOSもAndroidと本質的な部分は別になんにも変わらないことが分かって安心した
    • Obj-Cの苦手意識は全く無くなった。むしろ名前付き引数とかブロックを使った関数オブジェクトリテラル等、Androidで使えるJava7よりマシとすら感じる
  • Dockerは相当色々調べた。ブログにももっともっと書きたいことがある。
    • 開発環境ではかなり使うようになったけどまだプロダクション投入してないのでダメ
    • プロダクションで使うイメージがまだ湧いてない…
  • SICPやる
    • 1章で(転職により)離脱…情けない
  • ScalaかClojureやる
    • コップ本読み進めてます。まだ何もプロダクトは書いてない…

ということでひどい有様ですね。28点ぐらいですね。

あまり目標を大きくしてもアレなので、とりあえず↑を全部プロダクション投入することを引き続き目標にしようかなと思います。

まとめ

生涯プログラマで居たいです。iOS, Android, サーバサイドは流行りのやつをちょろちょろ追いかけてたらあと数年は飯食えますかね…
同年代のプログラマと生存戦略を語りたいです。ご連絡お待ちしております。

DroidKaigiで登壇してきました

- - posted in Android, DroidKaigi

DroidKaigi 2015で登壇する幸運にあずかりました。

DroidKaigiはAndroidデベロッパによるAndroidデベロッパのためのカンファレンスとして企画・開催されました。いかなる企業や団体、利権とも無縁の純粋なるAndroidハッカーのためのイベントです。
自分としてもDroidKaigiの趣旨に大きく賛同するものであり、企画してくださった @mhidaka 氏ならびに運営のみなさんには本当に感謝しています。

何とか発表したかったので Call For Papers に5つぐらい応募したところ、情熱が届いたのか 絶対落ちないアプリの作り方 を採択していただきました。名だたる大企業の有名デベロッパひしめく中、無名な自分の論文を採用してくださったことは本当に感激でした。

DroidKaigiは募集開始5分で先着枠200人が埋まり、抽選枠200人も455人の応募があるという大盛況ぶりで、当日も欠席者がほとんどなく会場の椅子という椅子がびっしり埋まるという賑わいでした。Android開発者もこういった「開発のみ」にフィーチャーしたカンファレンスを熱望していたことが見て取れます。

こうした中で発表するのはホビットの心臓を持つ自分としては生きた心地がしませんでしたが、非常に良い経験になりました。発表資料の難易度、資料の作り込み、発表の時間配分等々含めて反省点は多く残りましたが後悔はしていません。2016, 2017年と毎年開催されて欲しいですし、毎年刺激的な論文を応募し続けようと思います。

自分で言うと思いあがりも甚だしいですが、ここ半年ぐらい、だいたいAndroidでやりたいことはやり尽くした感があってiOS開発に手を出したりしていたのですが、DroidKaigiで改めてAndroidそのものに対する愛が深まりました。Androidは単なるハッカーのおもちゃから大きく飛躍し、今まさに円熟期を迎えています。Androidの最初期には乗り遅れたものの、Android1.5から成長を見届けてきたひとりとして、これからもAndroidと一緒に成長していこうと思いました。

それから、2015年にAndroid開発を始める人向けに最新の開発エッセンスを詰め込んだ記事なり勉強会なりを初めて行きたいなと考えています。まずは身内から始めていきますが、社外にもそういった需要がありそうなら公開イベントとしてやっていこうと思っているのでTwitterなりで気軽に話しかけていただければと思います。

とりあえずMacでDocker入門する

- - posted in Docker

とりあえずMacでDocker入門したのでメモ。
後述するブリッジインタフェースの件とかは意外に情報がなくて苦労した。

Dockerとは

いま流行りのコンテナ型仮想化技術のひとつ。
これについては長くなるので別のエントリに書いておく。
とりあえずAWSのEC2とかとはちょっと違う仮想化技術だということが伝わればよい。

事前準備

下準備

深く考えずに下をコピペ

1
2
3
4
5
6
$ brew update
$ brew tap homebrew/binary
$ brew install docker
$ brew install boot2docker
$ boot2docker init
$ boot2docker up

boot2docker up したら docker server に接続するための情報が出力されるので .zshrc に書いとく。

1
2
3
4
5
6
7
8
$ cat<<'EOS'>~/.zshrc
> # boot2docker
> export DOCKER_HOST=tcp://192.168.59.103:2376
> export DOCKER_CERT_PATH=/Users/shiroyama/.boot2docker/certs/boot2docker-vm
> export DOCKER_TLS_VERIFY=1
EOS

$ source ~/.zshrc

こんなのが出たらOK。まだコンテナ追加してないのでエラーが出ないことが確認できれば充分。

1
2
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

余談だけど boot2docker は何者かというと、MacでDockerを使うための橋渡し。DockerはLinuxカーネルの機能を使うのでMacにはネイティブ対応していない。

実はboot2dockerはDockerが使えるLinuxインスタンスのVMインスタンス+周辺スクリプト群にすぎない。仮想化技術を使うために仮想化技術を使うのだ。まあMacでちょっと試す分には大変ありがたい。

boot2dockerにブリッジインタフェースを追加

このあと実際にDockerコンテナを作っていくんだけど、その前に重要なステップがあって、以下の手順でboot2dockerにブリッジインタフェースを追加しておく。これをしておかないとDockerコンテナとMacとで通信ができないので不便なことこの上ない。

  1. boot2docker down
  2. VirtualBoxを起動しboot2docker-vmを選択
  3. 設定からネットワークを選んでアダプター3を有効化しブリッジアダプターを選択する。
  4. boot2docker up

boot2docker bridge

boot2docker上のコンテナに別ホストからアクセスする とかが参考になるよ。

Dockerコンテナ is 何

Dockerコンテナは雑に言うとVMインスタンスに相当するもの。
ただ、DockerコンテナをXenとかKVMみたいな、いわゆる普通のLinuxのVMインスタンスだと思って使うと大変な目に遭う(つくりも思想もまったく別物)ので、これも詳しくは別エントリに譲りたい。

Dockerfile

DockerコンテナはDockerfileという設定ファイルにしたがってセットアップされて起動する。

Dockerfileの中に、

  • ベースとなるイメージはUbuntuで…
  • Nginxインストールして…
  • 80番でLISTENして…
  • 起動!

みたいなことを順番に書いていくというわけ。

Dockerfile書いてみる

なんか適当に test みたいなディレクトリ掘ってそこにDockerfileというファイル名で作成する。

1
2
3
4
5
6
FROM ubuntu:latest

RUN apt-get update && apt-get install -y nginx

EXPOSE 80
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]

なんとなく意味分かるんじゃなかろうか。

Dockerfileはそれだけで1エントリ書くつもりなのでそこで詳しく書く。

Dockerコンテナを起動

1
2
3
4
5
6
7
$ docker build -t firstdocker/nginx .

$ docker images
REPOSITORY           TAG                  IMAGE ID            CREATED             VIRTUAL SIZE
firstdocker/nginx    latest               d9fcf4f72bcd        5 seconds ago       227 MB

$ docker run -d -p 80:80 firstdocker/nginx
docker build

まず、1行目で docker build してDockerfileからDockerイメージを作る。DockerイメージはDockerコンテナのもとになるものである。

-t オプションで後から参照しやすいようにタグをつけている。レポジトリ名/バージョンが慣習っぽいけど僕はサービス名/コンテナの役割みたいな感じで付けてる。まあ公式サイト見てください。

最後の . でカレントディレクトリにあるDockerfileをビルドする。

エラーなくビルドできたら docker images で確認。

docker run

最後に docker run -d -p 80:80 firstdocker/nginx でDockerコンテナを起動している。

-d オプションでDockerコンテナをバックグラウンドで起動。

-p オプションで dockerサーバのポート:dockerコンテナのポート のようにして、ポートのバインディングをする。今回のケースだと、NginxがインストールされたDockerコンテナの80番ポートをDockerサーバ、つまりboot2dockerの80番ポートに割り当てるというわけだ。

最後の firstdocker/nginx で今から起動するDockerコンテナの元になるイメージを指定している。もちろん最前こしらえたものを指定している。

docker run-d オプションの代わりに

1
$ docker run -i -t -p 80:80 firstdocker/nginx bash

こんな風にして、Dockerコンテナを立ち上げつつコンテナにBASHをログインシェルとしてログインするというようなこともできる。ただしこの場合はDockerfileに書いた CMD ["/usr/sbin/nginx", "-g", "daemon off;"] が実行されないのでNginxが起動しない。

この辺りはDockerfileだけを深く掘り下げたエントリで詳しく書くのでいまはあまり気にしないで欲しい。

確認してみよう!

boot2docker ssh して ifconfig してみよう。
上の方でboot2dockerのネットワークアダプター3をブリッジにしたので eth2 の情報をメモしておく。

1
2
3
4
5
6
7
8
9
10
11
$ ifconfig
...
eth2      Link encap:Ethernet  HWaddr 08:00:27:D0:9B:9F
          inet addr:192.168.11.111  Bcast:192.168.11.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fed0:9b9f/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:76 errors:0 dropped:0 overruns:0 frame:0
          TX packets:13 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:42936 (41.9 KiB)  TX bytes:2559 (2.4 KiB)
          Interrupt:17 Base address:0xd060

僕の環境では 192.168.11.111 がboot2dockerの(Macから到達可能な)IPアドレスだ。メモしておく。

Macで任意のブラウザを立ち上げて、192.168.11.111 にアクセスしてNginxのデフォルト画面が表示されたらOKだ!

dockerコンテナに接続

docker ps で起動中のコンテナを確認してみる。

1
2
3
$ docker ps
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS              PORTS                NAMES
703409c1d96c        firstdocker/nginx:latest   "/usr/sbin/nginx -g    46 minutes ago      Up 7 seconds        0.0.0.0:80->80/tcp   trusting_cori

先ほどのコンテナが起動している。

停まっているコンテナは docker start [NAME] で起動できる。他にも start/stop/restart/kill など色々あるが、だいたい名前をみて想像がつくと思う。詳しくは --help してみると分かる。

で、起動しているインスタンスには以下のようにして接続する。

1
$ docker exec -it trusting_cori bash

docker exec -it まではとりあえずおまじないでいいです。

trusting_cori は対象のコンテナ名を指定している。docker ps の結果を参照のこと。

最後の bash でBASHをログインシェルにしてログインすることを意味している。

実行したら普通のLinuxのようにコンテナ内をウロウロできたはずだ。

余談だが、docker exec はログインのためのものではなく、コンテナの任意のコマンドを実行できる命令である。つまり

1
2
3
4
5
$ docker exec -it trusting_cori cat /var/log/nginx/access.log

192.168.11.108 - - [21/Mar/2015:15:22:54 +0000] "GET /favicon.ico HTTP/1.1" 404 208 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"
192.168.11.108 - - [21/Mar/2015:16:09:20 +0000] "GET / HTTP/1.1" 200 396 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"
192.168.11.108 - - [21/Mar/2015:16:09:20 +0000] "GET /favicon.ico HTTP/1.1" 404 208 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"

こんな風に外からログをみたりするのも簡単にできる。

まとめ

ここまでで、多分コピペするだけなら15分ぐらいでNginxをDockerコンテナ上に立てられたはずだ。なんて便利で簡単なんだと思った方もいるかも知れないが、甘い

入門記事はこのエントリと同じぐらいの内容をサラッと書いてあることが多いと思うが、Dockerの本当の素晴らしさと苦しみはこの先にある。僕が本当に書きたかったエントリはそれである。そのエントリを書きたいがために、わざわざQiitaでも見りゃなんぼでも載っているような内容を書いたわけだ。

次のエントリは「Dockerは何であって何でないか」というようなエントリになるはずだ。

そのエントリでNginxの起動がなんで CMD ["/usr/sbin/nginx", "-g", "daemon off;"] みたいなけったいな方法なのか、Dockerコンテナにログインして ps aux | grep nginx とかしてみたら、なんでNginxのプロセスIDが1なのか、そういうことを書いていきたいと思う。

参考書籍

Docker入門 Immutable Infrastructureを実現する

少し情報が古くなってる部分があるけどこのエントリよりは200倍ぐらい網羅的に色んな事が解説されてるので600円の投資対効果はかなり高いと思う。何も知識のない人はひと通り読むといいかも。

VimでMarkdownのリンク書くときに便利なプラギン

- - posted in Markdown, Vim

このブログは Octopress 使ってるのでMarkdownで書いてるんだけどMarkdownのリンク書くのドイメンじゃないですか。そういうひとは vimtaku/vim-operator-mdurl 使うといいですよ。

とりあえず NeoBundle 'vimtaku/vim-operator-mdurl' して入れてください。

使い方

初期状態で LM がマップされてます。バッティングしてる人はうまいこと変えてください。

1) Lの方

http://mana.bo/ みたいなテキストがあったらここで LiW とかすると [http://mana.bo/](http://mana.bo/) になります。mattn/vim-textobj-url とか入れてる人は Liu の方が自然ですね。

2) Mの方

http://mana.bo/ みたいなURLがヤンクされた状態で 株式会社マナボ みたいなテキストの上で MiW とかすると [株式会社マナボ](http://mana.bo/ "株式会社マナボ") になります。最高クールじゃないすか。

Vim は世界で最も優れたテキストエディタなんでみなさん積極的に使いましょう。それでは。

参考リンク:Vim-operator-mdurl という Vim Plugin 書いた

Lisper必携 paredit.vim

- - posted in Lisp, SICP, Scheme, Vim

paredit.vim という Vim プラグインを今日はじめて知ったが凄すぎる!
あまりに人生を変えられる出会いだったのでブログに書かざるを得なかった。

paredit.vim is 何

Vimmer が Lisp のS式を書くのが著しく快適になるプラグイン

1
(define (sum a b c) (+ a b c))

例えばこの関数の仮引数 b のところで D して行末までバッサリ削除しようとすると…

1
(define (sum a))

なんと括弧の対応を残した上で削除してくれる。奇跡か!

他にも、

  • 改行するといい感じにインデントしてくれる
  • 括弧を書くと閉じ括弧を自動挿入してくれる

などS式を書きやすいようになっている。

まだこれだけしか調べてないが、これだけで200倍ぐらい書きやすくなった!

使い方

1
2
3
4
5
6
7
8
NeoBundle 'vim-scripts/paredit.vim'

:NeoBundleInstall

:help paredit

" 必要に応じて
:set filetype=scheme

Scheme と Clojure は対応していることを確認済み!

以上、SICP 勉強会のお供にどうぞ。

RubyMotion for Android でカスタム Appliation クラスを利用する

- - posted in Android, RubyMotion

RubyMotion のバージョンが 3.1 になり、Android 版でカスタム Application クラスが利用できるようになった。

実を言うとこれまでは独自定義の Application クラスを利用することが出来なかったのだが、その旨 RubyMotion のサポートで質問したところ、作者の Laurent さんが一瞬で対応してくれたのだ。
なんてCOOLなんだ!みんなも RubyMotion を買おう!

では早速、独自 Application クラスの使い方を簡単に紹介しておく。RubyMotion for Android の入門に関しては前回書いた拙エントリを参考にして欲しい。

1) RubyMotion を更新

以下のコマンドで更新。--pre を忘れないようにしよう

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
% sudo motion update --pre

Password:
Connecting to the server...
Downloading software update...
######################################################################## 100.0%
Installing software update...
RubyMotion pre-release update installed in /Library/RubyMotionPre

= RubyMotion 3.1 =

  * Added the RUBYMOTION_VERSION and RUBYMOTION_ENV constants, which have the
    same values as RubyMotion for iOS / OS X.
  * Added support for the Android R class. Thanks to Mark Villacampa for the
    patch.
  * Added the `app.application_class' setting, which can be used to specify
    the name of a custom Android::App::Application subclass that should be used
    by the application. By default, the variable has a nil value, which means
    a default class will be used.
  * Improved app versioning. `app.api_version' will now map to the
    `minSdkVersion' manifest attribute. Added `app.target_api_version' which
    will map to `targetSdkVersion'. Both settings will have the latest API
    version as the default value, except for 20 (L). We recommend that you
    leave `app.target_api_version' intact and only modify `app.api_version'.
  * Improved the REPL so that "self" now always points to the current app
    activity. (Only works for API 14 or above.)
  * Added support for `&foo' constructs (which should dispatch #to_proc).
  * Added Symbol#to_proc.
  * Fixed a bug where `app.vendor_project' would generate a .bridgesupport
    file containing illegal XML characters (such as '<' or '>') inside
    attributes. Thanks to Mark Villacampa for the patch.
  * Fixed a bug where the dispatch of an overloaded Java method without
    arguments would fail. The runtime will now return the first method of the
    list (since we can't match any argument).
  * Fixed the `rake {emulator,device}' tasks to terminate the application in
    case the user leaves the REPL session.
  * Fixed a bug when dispatching certain methods using reflection (REPL) that
    return java.lang.Object.
  * Fixed a bug in the compiler where a crash would happen when trying to
    override a Java method accepting a Java array as argument.
  * Fixed a bug in the dispatcher when yielding certain runtime-generated
    blocks (ex. enumerators) where the return value would be destroyed.

2) カスタム Application クラスを準備

Android 用のテンプレートを生成し、

1
2
% motion create --template=android Hello
% cd ./Hello

Android::App::Application クラスを継承した独自 Application クラスを定義する。

試しに onCreate(), onTerminate() でログを出すようにしてみた。
※ onTerminate() は実機では呼ばれないので注意。理解の便宜上書いてみた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% vim app/my_application.rb

class MyApplication < Android::App::Application
  def onCreate()
    super
    puts "Application#onCreate()"
  end

  # will never be called on a production Android device
  def onTerminate()
    super
    puts "Application#onTTerminate()"
  end
end

3) Rakefile を修正

app.application_class に独自定義の Application クラスを指定

1
2
3
4
5
6
7
8
9
10
11
% vim Rakefile

# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotionPre/lib")
require 'motion/project/template/android'

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'Hello'
  app.application_class = 'MyApplication'
end

最初どんなプロパティか分からなくて戸惑ったけど /Library/RubyMotionPre/lib/motion/project/template/android.rb みたらすぐ分かった。

4) 確認

確認してみよう!

1
2
3
4
5
6
7
8
9
10
11
12
13
% rake device

    Create ./build/Development-19/classes.dex
    Create ./build/Development-19/Hello.apk
      Sign ./build/Development-19/Hello.apk
     Align ./build/Development-19/Hello.apk
   Install ./build/Development-19/Hello.apk
4066 KB/s (638813 bytes in 0.153s)
     Start com.yourcompany.hello/.MainActivity
--------- beginning of /dev/log/main
--------- beginning of /dev/log/system

I/com/yourcompany/hello(11394): Application#onCreate()

一番下の行に注目!Application の開始時に一度だけ出ている!

前述のとおり onTerminate() は実機では呼ばれないが仕様なので気にしないでください。

まとめ

独自 Application クラスを利用できるようになったことで実戦的なアプリ開発の可能性がグッと広がったように思う。
ライブラリの初期化とか、静的 Context 取得メソッドとか、アプリケーションライフサイクルで管理する Observer とか、実際の Android 開発では利用シーンの枚挙にいとまがない。

いよいよ次回は少し実戦的なアプリを実装して GitHub に上げてその記事を書こうと思う。それでは楽しい RubyMotion ライフを!

RubyMotion for Android を使ってみる

- - posted in Android, Ruby, RubyKaigi, RubyMotion

9/16日、RubyMotion が Android に対応し public Beta として公開された。

9/18〜20まで開催された RubyKaigi 2014 でも RubyMotion for Android に関するセッション があり、かなり感銘を受けたのでブログにまとめることにする。

RubyMotion とは何か

すごく簡単に言うと iOS や OSX, そして Android のネイティブアプリを Ruby で書けるというツール群である。

http://www.rubymotion.com/

Titanium や PhoneGap とは明確に異なり、各ランタイムが直接解釈できるネイティブコードを生成するのが特徴。従ってパフォーマンス的なオーバーヘッドがほとんどなく ※1 、利用できる OS の API にも基本的に制限がない。

何より Ruby で書けるのである。なんて素晴らしいんだ。

利用方法

公式サイトの英語を読むのがめんどくさい人のために2014年9月時点でのライセンス形態等について箇条書きしておく。

  • 有料。199.99ドル
    • 1年間のバグフィクス・改良などのアップデートを受けられる
    • 1年間のサポートのためのプライベートチケットを切ることができる
    • 自分の理解では1度買えば1年経つとアップデートは受けられないがそのまま使い続けられる
  • 30日間はトライアル期間として気に入らなければいつでもやめることができる
  • 今は学割的なものはない
  • 1年経つと99.99ドルで(アップデート等の)ライセンスを延長できる
  • 言い忘れたけど OSX の動く Mac が必要なのでそれ以外の環境の方は残念

これを高いと見るか安いと見るかは価値観次第だと思うが、トライアル期間があるので少しでも興味がある人は一度試してみると良いと思う。(最初にカードを登録する必要あり。30日以内のキャンセルは “返金扱い” なのかそれとも “30日後に初めて課金される” のかは知らない。興味もない)

使ってみる

公式サイト から早速購入してみよう。RubyKaigi 期間中はなんと15%オフ(毎年恒例らしい)なのだが、ブログにまとめるのをモタモタしているうちに閉会してしまった…

buy rubymotion

購入し終わると登録したメールアドレス宛にライセンスキーとインストーラのダウンロードリンクが届く。 ダウンロードリンクは忘れがちなのでメールは保存しておこう。

ダブルクリックして指示に従っていると何の問題もなくインストールは完了する。

Xcode や AndroidStudio と違い、専用の(エディタを含んだ GUI の)IDE 環境がインストールされる訳ではない。RubyMotion は CUI のビルドツール群という印象に近い。

ツール群は

1
% /Library/RubyMotion/

以下にインストールされ、最も使われる(というかこれしか使ったことがない)motion コマンドは

1
% /usr/bin/motion

にリンクされる。普通にしているとここにはパスが通っているはずなのでコンソールで motion とタイプするだけですぐに利用できるはずだ。

Hello from RubyMotion for Android

それでは RubyMotion for Android を使って Hello World をしてみよう。
公式の Getting Started をなぞるだけ で実に簡単に動かすことができるが、例によって英語の説明を読みたくない人のために箇条書きにする。

1. プレビュー版をインストール

今日時点で Android 版は public Beta という扱いなので、標準のコンポーネントには含まれていない。以下のコマンドでインストールする。

1
% sudo motion update --pre

ファイルは以下のパスにインストールされるが普通に使っている限り意識することはない。

1
% /Library/RubyMotionPre

2. JDK をインストール

JDK 6 をインストールする必要がある。

7でも動くけど6を推奨

みたいなこと書いてあるけど、7だと動かない場合があった ※2 ので言うとおりにすることを強くおすすめする。

OpenJDK か Oracle の JDK か、みたいなことは書いてないが、公式サイトからは Apple の Java for OSX 2014-001 というのにリンクが張ってあったので素直にこれを入れると良いだろう。

Mac に複数の JDK が入っている場合はやっかいで、僕の知る限り Java には rbenv 的なランタイムのバージョン管理とサンドボックス化を提供してくれる仕組みはない(最近の言語でこういった仕組みがない言語の方がむしろ珍しいよね…まったくトホホだ)ので、自分で JAVA_HOME を変えてやる必要がある。

1
% ls -l /System/Library/Frameworks/JavaVM.framework/Versions

とかしてみて、1.6 がある且つ複数のバージョンが入っているようなら .zshrc とかに

1
2
3
4
5
6
7
8
9
10
11
% cat<<'EOS'>>~/.zshrc
export JAVA_HOME=`/System/Library/Frameworks/JavaVM.framework/Versions/A/Commands/java_home -v "1.6"`
PATH=${JAVA_HOME}/bin:${PATH}
EOS

% source ~/.zshrc

% java -version
java version "1.6.0_65"
Java(TM) SE Runtime Environment (build 1.6.0_65-b14-466.1-11M4716)
Java HotSpot(TM) 64-Bit Server VM (build 20.65-b04-466.1, mixed mode)

のようにして java 1.6 が使われていることを確認しておこう。この Qiita を参考にすると良い。

3. Androiid SDK をインストール

公式サイトの例に従い、まずは RubyMotion for Android の作業ディレクトリを作成する。

1
% mkdir ~/android-rubymotion

次に公式サイトでは Eclipse ADT をインストールしてその sdk をコピーするように書いてあるが、今更 Eclipse を使っている人も居ないと思うので Android Studio の sdk をシンボリックリンクで利用すると良い。SDK さえ入れられれば方法は問わない。 ※3

1
2
3
4
% cd ~/android-rubymotion
% ln -s /Applications/Android\ Studio.app/sdk sdk
% ls sdk/
add-ons        build-tools    docs           extras         platform-tools platforms      samples        sources        system-images  temp           tools

4. Androiid NDK をインストール

ネイティブコンパイルのために Mac OS X 64-bit NDK for 32-bit target を入れる必要がある。

間違えやすいが、必ず Platform (32-bit target) かつ Mac OS X 64-bit の NDK でなくてはならない。さもなくば動かない旨明記してある。

ダウンロードしたら展開して sdk と同じディレクトリにコピーする。

1
2
3
4
5
6
7
% tar jxvf android-ndk32-r10b-darwin-x86_64.tar.bz2
% cp -r android-ndk-r10 ~/android-rubymotion/ndk
% cd ~/android-rubymotion
% ls ndk/
GNUmakefile               build                     find-win-host.cmd         ndk-depends               ndk-gdb-py.cmd            ndk-which                 remove-windows-symlink.sh tests
README.TXT                docs                      ndk-build                 ndk-gdb                   ndk-gdb.py                platforms                 samples                   toolchains
RELEASE.TXT               documentation.html        ndk-build.cmd             ndk-gdb-py                ndk-stack                 prebuilt                  sources

最後に SDK と NDK を環境変数に登録する。

1
2
3
4
5
6
7
8
9
10
% cat<<'EOS'>>~/.zshrc
export RUBYMOTION_ANDROID_SDK=~/android-rubymotion/sdk
export RUBYMOTION_ANDROID_NDK=~/android-rubymotion/ndk
EOS

% source ~/.zshrc

% env | grep RUBYMOTION_ANDROID
RUBYMOTION_ANDROID_SDK=/Users/shiroyama/android-rubymotion/sdk
RUBYMOTION_ANDROID_NDK=/Users/shiroyama/android-rubymotion/ndk

こんな感じになっていればOK。

5. 必要なバージョンの SDK パッケージを入手

自分の持っている Android のバージョン (API Level) に合わせてビルドするため、SDK Manager からパッケージを入手する。自分の持っている Android の API レベルは このへん を参考にすれば良いだろう。
例えば Android 4.4 KitKat の端末を持っている場合は API Level 19 ということになる。

確認し終わったら、

1
2
% cd ~/android-rubymotion
% ./sdk/tools/android

とすると下図のようなウィンドウが表示される。

SDK Manager

対応する API レベルのパッケージをとりあえず全部入れておけば間違いない。 ※4

6. 端末を開発モードへ

既に Android 開発をしたことのある人はここは読み飛ばしてください。
初めての人は このへん を参考にしながら開発者モードを表示して USB デバッグを有効にしてください。

いよいよ準備完了!

7. Hello World

任意の場所で以下のコマンドを実行すると、RubyMotion for Android 用のテンプレートプロジェクトが生成される。

1
2
% motion create --template=android Hello
% cd Hello

Android 端末が USB で接続され、開発用に認識されていることを確認してから ※5 いよいよ実行してみよう。

1
% rake device

コンソールに以下のようなログが表示され、Android 端末にタイトルが Hello とだけ書かれた真っ黒な画面が表示されたら成功である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
% rake device

   Compile ./app/main_activity.rb
    Create ./build/Development-19/lib/armeabi/libpayload.so
    Create ./build/Development-19/lib/armeabi/gdbserver
    Create ./build/Development-19/lib/armeabi/gdb.setup
    Create ./build/Development-19/AndroidManifest.xml
    Create ./build/Development-19/classes/com/yourcompany/hello/MainActivity.class
    Create ./build/Development-19/classes.dex
    Create ./build/Development-19/Hello.apk
      Sign ./build/Development-19/Hello.apk
     Align ./build/Development-19/Hello.apk
   Install ./build/Development-19/Hello.apk
6102 KB/s (633859 bytes in 0.101s)
     Start com.yourcompany.hello/.MainActivity
--------- beginning of /dev/log/main
--------- beginning of /dev/log/system

Hello World 01

もし以下の様なエラーが表示されたら、期待した API バージョンに対してビルドされていない。

1
2
3
% rake device

    ERROR! It looks like your version of the NDK does not support API level L. Switch to a lower API level or install a more recent NDK.

先ほどの Hello ディレクトリ以下の Rakefile を任意のエディタで以下のように編集しよう。
10行目がミソだ。

1
2
3
4
5
6
7
8
9
10
11
% vim Rakefile

# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotionPre/lib")
require 'motion/project/template/android'

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'Hello'
  app.api_version = '19'
end

もう一度 rake device してうまくいくか試す。

以上で Hello World は終了である。ここまではそれほど難しくないはずだ。

はじめてのコントローラ

いよいよ自分でコードを書いていく。

1
% app/main_activity.rb

を任意のエディタで開き、以下のように編集してみよう。

1
2
3
4
5
6
7
8
9
10
% vim app/main_activity.rb

class MainActivity < Android::App::Activity
  def onCreate(savedInstanceState)
    super
    text_view = Android::Widget::TextView.new(self)
    text_view.text = 'Hello RubyMotion!'
    self.contentView = text_view
  end
end

保存して rake device すると以下のような画面が表示されるはずだ。

Hello World 02

コードの内容は簡単で、

  1. 6行目で TextView のインスタンスを生成し
  2. 7行目で文字列を設定し
  3. 8行目でそれをこの Activity にセットしている

というわけである。なんだか Android 1.5 の頃によく見た入門サイトのようなコードだ。

後で少し補足するが、RubyMotion for Android はこれ以上でも以下でもない。
一旦次に読み進めて欲しい。

素晴らしき REPL

RubyMotion には素晴らしい REPL ※6 が備わっている。
先ほどのコードを次のように変更してみよう。

1
2
3
4
5
6
7
8
9
10
11
12
% vim app/main_activity.rb

class MainActivity < Android::App::Activity
  def onCreate(savedInstanceState)
    super
    text_view = Android::Widget::TextView.new(self)
    text_view.text = 'Hello RubyMotion!'
    text_view.textSize = 10
    text_view.setId 12345
    self.contentView = text_view
  end
end
  1. 8行目でテキストサイズを指定し
  2. 9行目で View に対して一意な ID を割り振っている

再び実行するが、今度はコンソールで

  1. text = self.findViewById 12345
  2. text.setTextSize 100

のように入力してみよう。

1
2
3
4
5
6
7
% rake device

>> text = self.findViewById 12345
=> #<android.widget.TextView:0x50600025>
>> text.setTextSize 100
=> #<android.widget.TextView:0x50800025>
>>

Hello World 03

なんと、対話的に TextView オブジェクトを取り出し、あまつさえ値をセットしなおして即座に端末に反映されてしまった!

SUPER COOOOOOOOOOL!!!

一旦これだけのために RubyMotion を使ってみて欲しいぐらいだ。マジで。

更に踏み込んでいく

実際のところ、これだけでは実用的なアプリではない。

  1. 現実問題、レイアウトをコードで指定することは実際の Android 開発現場ではほとんどありえない
  2. 外部ライブラリどうやって使うの?
  3. リソースどこで指定するの?

などなど問題は山積みである。

幸い、RubyKaigi で開発者の方と直接お話しをする機会があり、世の中に数多存在するネイティブ Java で書かれたライブラリたちは RubyMotion で使えないのか質問したところ「使える」という回答をもらっており、やり方もざっくりお聞きしてある。

その辺り、どんどん踏み込んで行きたいのだが、いい加減エントリも長くなってきたので別エントリを立てて解説したいと思う。

RubyMotion for Android はまだドキュメントも満足に揃っていないので、この辺りを自分で調べるのは少々骨が折れるのだが、幸い GitHub に サンプルプロジェクト を上げてくれているので、興味のある人は是非 clone して実行してみて欲しい。

RubyMotion for Android は世界を救うか

というわけで、本エントリはまとめに入るが、RubyMotion はネイティブ開発の銀の弾丸になるだろうか?

答えはもちろんノーだ。

先ほどのコントローラのコードを思い出して欲しい。
あのコードは、Ruby を生まれてこの方みたことなくても Android 開発経験があれば読める。
逆に言えば、Rubyist でも Android 未経験であればあのコードは(全く理解不能ということはないだろうが)良くわからないはずだ。

RubyMotion はそれさえあれば Android/iOS のことを何も知らなくてもアプリを開発できるものでは全くない。

ライフライクル、イベントハンドリング、非同期処理などなど、OS のフレームワークをもろに意識したコードを書く必要がある。結局はネイティブ開発の知識なしには絶対に成り立たない。

しかしながら、それがどうした?とも言いたい。

  • OS と密接に関わる部分以外は柔軟で記述性の高い Ruby で書ける
  • そのような部分は Android/iOS で共通のライブラリとして実装可能
  • Ruby で書くと楽しい!

僕はこれらの現実を踏まえた上で、それでも RubyMotion が痛く気に入った。同じように感じる人が RubyMotion を触ってみるきっかけになれば幸いである。

以下、補足。

RubyMotion 補足

ライセンスが有効なうちは以下のコマンドで定期的に RubyMotion をアップデートしよう。開発はかなり活発なようだ。

1
% sudo motion update

バージョンは以下で確認することができる。

1
2
% motion --version
2.24

バージョンは一日に一回サーバと通信してチェックしているとのことで、ライセンス失効が近付くとコマンドラインで教えてくれる。必要に応じて更新すると良いだろう。

その他、サポートが必要な場合は以下のコマンドでブラウザのサポートページへ移動できる。ライセンスが有効な間は自分でチケットを作成して対応してもらうことも可能なはずだ。

1
% motion support

それでは良い RubyMotion ライフを。


注釈

※1 Android の場合、ランタイムは Dalvik でも ART でもちゃんと動く。詳しくはよく分かってないのだが、Dalvik 環境下では Ruby のコードから DEX を、ART 環境下では LLVM が解釈できるコードをそれぞれ直接吐き出しているのだろうか?詳しい方教えてください。

(9/24 追記)

ART ランタイムについて勘違いしていたが、ART はインストール時に dex を端末内でコンパイルするものなので RubyMotion がどうこうとはあまり関係ないかも、と教えていただいた。ありがとうございます!

ちなみに 公式ブログ を読んでいたら、RubyMotion for Android では Ruby のコードを LLVM ベースの静的コンパイラで直接 ARM のマシン語に変換し、ランタイムは JNI 越しにそれらのコードにアクセスして実行されるのだと解説してあった。素晴らしい。

(追記ここまで)

※2 JDK 7 だと署名時のコマンドライン引数が変わっていて普通に adb install しようとしても入らない端末(or version?) がある。このへん 参照。

※3 良くわからない人は このへん とか見ればいいかも知れません。

※4 良くわからない人は API 10〜19 ぐらいまで全部入れておけば良いです。時間とストレージの無駄ではありますが分からずに悩むよりはマシです。

※5 以下のコマンドを実行して何も表示されないと、開発端末として認識されていない。再度説明を読み直して USB デバッグが有効になっているか確認し、それでもダメならケーブルが「充電専用」とかでないことを確認してみよう。意外と良くあるミス。

1
2
3
% ./sdk/platform-tools/adb devices
List of devices attached
0301044a08e4ae97    device

※6 Read Eval Print Loop のこと。その名のごとく、書いたコードを対話的に逐次評価して表示してくれるツール。 個人的に REPL を提供しない言語はクソ言語認定している。