読者です 読者をやめる 読者になる 読者になる

パンダのメモ帳

技術系のネタをゆるゆると

Scala + sbt-android + IntelliJ で快適Androidアプリ開発

Scala Android sbt IntelliJ

まえがき

このエントリは Android Advent Calendar 2013 ならびに Scala Advent Calendar 2013 の6日目(12/6)の記事になります。

僕が Scala に触れ始めたのは2012年の11月頃なのでかれこれちょうど1年前ぐらいです。 以前から Android アプリを開発していた身としては当然 Android アプリも Scala で書けたらなぁ、と考えるわけですが当時「Scala Android」とかでググる現状 #Scala で #Android をやるのは思っている以上に罠が多いという話 みたいまとめがあったりして「初心者お断り」感がすごかったのを覚えています。

その年の Android Advent Calendar 2012 でこんな記事を書いてる人がいたり、 今年に入ってこんな記事を見かけたりして、 改めて「あれ、意外と簡単にできるんじゃね?」と思って手を出したら結局やっぱり色々と罠に遭ったのでその辺のノウハウをまとめてみました。

本記事の内容

  • Android Support Library, revision 19 を使用して API Level 9 をサポートしつつ Fragment や ActionBar を使用したアプリを Scala で書くための環境構築
  • Scala + sbt-android + IntelliJ によるアプリ開発の簡単な説明と待ち受ける罠

開発環境

開発環境の概要は次の通り。

開発環境を構築する

Android SDK

これがないと始まらない。

公式サイト からそれぞれのプラットフォーム向けのアーカイブ(ADT Bundle じゃない方)をダウンロードして適当な場所に展開してください。 自分の場合(OS X)は /opt/local/android-sdk-macosx に設置しています。

設置後、.bashrc なり .zshrc なりを編集して設置パスを環境変数 ANDROID_HOME として export します。ついでにコマンドにもパスを通しておきます。

export ANDROID_HOME=/opt/local/android-sdk-macosx
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools

この段階では各バージョンのSDKが未導入の状態なのでターミナルから android update sdk -u を実行して一式ダウンロードしてください(結構時間が掛かります)。

Maven 3.1.x

次の Maven Android SDK Deployer を利用するために必要になります。 OS X の場合は brew install maven でOK。他の環境のことはよく知らないので「Maven インストール」とかでググってください。 3.0.x だとうまく動かないので 3.1.x を使用する必要があります。

Maven Android SDK Deployer

Android Support Library など Maven Central Repository にないパッケージをローカルの Maven Repository から解決できるようにしてくれるユーティリティです。

github から最新のソースコード一式を git clone して mvn install するだけでそんなに時間も掛からずに終わるはずです。

$ git clone git@github.com:mosabua/maven-android-sdk-deployer.git
$ cd maven-android-sdk-deployer
$ mvn install

sbt

Android のビルドと言えば最近はもっぱら Gradle なわけですが今回は Scala の定番ビルドツール sbt を使用します。 大きな理由としては Scala で書いた Android アプリケーションをビルドするためのプラグイン sbt-android がすでに存在するためです。

OS X の場合はこれも brew install sbt でOKです。 その他の環境の場合、公式サイトで各環境向けのパッケージが配布されているのでそちらを使えば多分OKです。

giter8

giter8 は Scala 製のプロジェクト生成ツールです。使わなくても困りはしないですがあった方が圧倒的に便利。 これも OS X なら brew install giter8 でインストールできます。その他の環境の場合は Conscript を使ってインストールするのがオススメらしいんですが、公式READMEでも紹介されているこちらの記事(日本語)が参考になるんじゃないかと思います。

[IntelliJ IDEA]

Android 開発者にとっては Android Studio のベースとなっていることでも注目されている最近流行のIDE(総合開発環境)です。 Eclipse?いえ、知らない子ですね……。

自分の場合は元々 Scala での開発用に使っていたのもあって IntelliJ IDEA + Android Plugin + Scala Plugin で開発しています。 どちらのプラグインも無料版の Community Edition でサポートされているので今回は無料版で十分です。今回はエディタ兼デバッガとして使用します。

未導入の場合はこのページからダウンロードしてインストールしてください。 Android Support/Android Designer プラグインは標準でインストールされているので Scala Plugin を追加するだけですぐに使えます。

ところで、この記事を書いている最中に IntelliJ IDEA 13 がリリースされましたが今回は IntelliJ IDEA 12 ベースで話を進めます。IntelliJ IDEA 13 では sbt が標準でサポートされるなど気になる新機能も多いので早く試したいです。

プロジェクトを作成する

開発環境が構築できたら giter8 を使ってプロジェクトを作成します。 今回の目的である「Android Support Library を使ったアプリ」のためのテンプレートを GitHub で公開しているので今回はそれを使用します。パッケージ名は「com.example.helloworld」プロジェクト名は「Hello World」とします。デフォルト値で問題ない場合はなにも入力せずにエンターキーを押せばOKです。

$ g8 shogogg/scala-android-app.g8
(中略・初回起動時は必要なライブラリ等をダウンロードするのでちょっと時間が掛かる)

Template for Android apps in Scala 

package [com.example.android.app]: com.example.helloworld
name [My Android Project]: Hello World
keyalias [alias]: <リリース時の署名に使用するキーストアのエイリアス>
scalaVersion [2.10.3]: <ビルドに使用する Scala のバージョン>
versionCode [0]: <アプリのバージョン(管理用・整数)>
version [0.1.0]: <アプリのバージョン(表示用・文字列)>
targetSdkVersion [19]: <アプリの targetSdkVersion>
mainActivity [MainActivity]: <アプリのメインアクティビティ名>
minSdkVersion [9]: <アプリの minSdkVersion>

Template applied in ./hello-world

デフォルトでは targetSdkVersion を最新の 19 [Android 4.4 KitKat], minSdkVersion を 9 [Android 2.3.x Gingerbread] としています。

ディレクトリ構成など

sbt-android ディレクトリ構成はこんなカンジになります。

$ cd /path/to/hello-world
$ tree .
.
├── project
│   ├── build.properties
│   ├── build.scala
│   └── plugins.sbt
└── src
    └── main
        ├── AndroidManifest.xml
        ├── res
        │   ├── layout
        │   │   └── main.xml
        │   └── values
        │       ├── strings.xml
        │       └── styles.xml
        └── scala
            └── com
                └── example
                    └── helloworld
                        ├── MainActivity.scala
                        └── support
                            ├── ActivitySupport.scala
                            ├── AndroidFuture.scala
                            ├── AndroidPromise.scala
                            ├── AsyncSupport.scala
                            ├── ScalaAsyncTask.scala
                            ├── TypedViewContainer.scala
                            └── package.scala

11 directories, 15 files

Eclipse + ADT などで作成するアプリのディレクトリ構成なんかとは随分違います。

src ディレクトリ以下にあるのがソースコードとリソース関連のファイルになります。 ちなみに src/main/java ディレクトリを作成すれば Javaソースコードを混在させることもできますし, Assets を使用したい場合は src/main/assets ディレクトリを作成すればOKです。 src/main/scala/com/example/helloworld/support 以下にあるのは補助用のクラスです。後で解説します。

project ディレクトリ以下にあるのがビルド関連の設定・定義ファイルです。以下、それぞれについて簡単に解説します。

project/build.properties

project/build.properties では使用する sbt のバージョンを指定しています。

sbt.version=0.12.4

project/plugins.sbt

project/plugins.sbt では sbt で使用するプラグインを指定しています。

resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)

addSbtPlugin("org.scala-sbt" % "sbt-android" % "0.7")

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")

1行目はリポジトリの追加、3,5行目はそれぞれ sbt-android プラグインと sbt-idea プラグインをプロジェクトに追加しています。 sbt インターフェース上で plugins.sbt ファイルでは各コマンドの間に空行が必要になるので編集する場合は注意してください。

project/build.scala

project/build.scala は各種ビルド情報を定義するファイルです。 通常構成の Android プロジェクトでは AndroidManifest.xml に記述していたような内容の一部もこちらに記述します。 ちょっと長いのでここには載せませんが少しお節介な程度にコメントしておいたので参考にしてください。

src/main/AndroidManifest.xml

通常構成の Android プロジェクトではルートディレクトリ直下にある AndroidManifest.xml ですが sbt-android なプロジェクトでは src/main 以下に配置されています。 一部の内容を project/build.scala に記述する以外は通常のプロジェクトと同様です。

ビルドしてみる

g8 コマンドで生成されたプロジェクトディレクトリに移動後, sbt コマンドを実行して sbt インターフェースを起動します。 プロジェクトを git で管理する場合はこのタイミングで first commit するのがいいと思います。

# ディレクトリに移動
$ cd ./hello-world

# git リポジトリを作成して初回コミット
$ git init
$ git add --all
$ git commit -m 'first commit'

# sbt を起動
$ sbt
[info] Loading project definition from /path/to/hello-world/project
[info] Set current project to Hello World (in build file:/path/to/hello-world/)
> 

この sbt インターフェースでは様々なタスク(コマンド)を実行することができます。主なタスクは次の通り。

 compile: 各種ソースコードをコンパイルし .class ファイルを生成します。
     apk: JAR, DEX, APKファイルを生成します。未コンパイルの場合は自動でコンパイルも実行します。
 install: 生成されたAPKファイルを実機に転送します。APKが生成されていない場合は自動で生成します。
   start: 生成されたAPKファイルを転送し、実機上でアプリを起動します。APKが生成されていない場合は自動で生成します。
  reload: ビルド定義ファイルの再読込を行います。ビルド定義を修正したりした場合に使用します。
 console: REPLを起動します。
    exit: sbt インターフェースを終了します。

雛形のプロジェクトはそのままビルドできる状態なので start コマンドでコンパイル → APK生成 → 実機に転送 → 実行までを行ってみます。

> start
(中略)
[info] Dexing /path/to/hello-world/target/classes-hello-world-compile-0.1.0.dex
[info] Packaging /path/to/hello-world/target/hello-world-compile-0.1.0.apk
[info] Installing hello-world-compile-0.1.0.apk
[success] Total time: 30 s, completed 2013/12/06 18:07:50
> 

実機上でこんな画面が表示されれば成功です。

ちなみに sbt の各コマンド(タスク)の前にチルダ(~)を付けると、ファイルの変更を監視して変更をトリガーに自動でタスクを実行してくれます。 例えば

> ~compile

としておくことで自動でコンパイルが走ります。便利。

IntelliJ IDEA でプロジェクトを開く

gen-idea タスクを実行することで IntelliJ IDEA のプロジェクトに必要なメタファイルを生成することができます。 この辺はもしかしたら最新の IntelliJ IDEA 13 ではいらなくなるかも。

> gen-idea
[info] Creating IDEA module for project 'Hello World' ...
[info] Running compile:managed-sources ...
[info] Extracting library compatibility-v7-appcompat-19.apklib
[info] Generated /path/to/hello-world/target/scala-2.10/src_managed/main/AndroidManifest.xml
[info] Generated 0 source files from 1 ApkLibs
[info] Running AAPT for package com.example.helloworld
[info] Wrote /path/to/hello-world/target/scala-2.10/src_managed/main/scala/com/example/helloworld/TR.scala
[info] Running AAPT for package android.support.v7.appcompat
[info] Resolving android.support#compatibility-v7-appcompat;19 ...
[info] Excluding folder target
[info] Created /path/to/hello-world/.idea/IdeaProject.iml
[info] Created /path/to/hello-world/.idea
[info] Excluding folder /path/to/hello-world/target/scala-2.10/aarlib_managed
[info] Excluding folder /path/to/hello-world/target/scala-2.10/cache
[info] Excluding folder /path/to/hello-world/target/scala-2.10/classes
[info] Excluding folder /path/to/hello-world/target/scala-2.10/apklib_managed/res
[info] Excluding folder /path/to/hello-world/target/resolution-cache
[info] Excluding folder /path/to/hello-world/target/streams
[info] Created /path/to/hello-world/.idea_modules/Hello World.iml
[info] Created /path/to/hello-world/.idea_modules/Hello World-build.iml
>

gen-idea タスクが無事に完了したら IntelliJ IDEA でプロジェクトを開いてみます。 Open Project から hello-world ディレクトリを選択すればOKです。

デバッガを使う

IntelliJ からデバッガを起動してみます。 まずメニューから RunEdit Configurations... を選択します。

左上の をクリックして Android Application を選択。

各項目の設定は次の通り。

  • Name には適当に名前を入力
  • Module は今回作成したプロジェクト(Hello World)を選択
  • Launche default Activity にチェックを入れる
  • Deploy Application のチェックを外す
  • Target Device は好みで
  • Before launchMake を選択して《−》をクリックして削除する
  • OK ボタンをクリックして完了

これだけで使えれば完璧なんですが、あともう1カ所設定が必要です。左のプロジェクトツリーのルートを右クリックして Open Module Settings をクリック。

ModulesAndroidStructure タブに Manifest file という項目があるのでこれを /path/to/hello-world/target/scala-2.10/src_managed/main/AndroidManifest.xml に変更します。

これで準備は完了です。 RunDebug 'Hello World' を選択してデバッガを起動します。 こんな画面が表示され、端末上でアプリが起動すれば成功です。

もちろんブレークポイントを設定することもできます。

以上で環境設定は完了です。これでアプリ開発をスタートできますね!

待ち受ける罠

実は、ここまで至るまでにいくつかの罠にハマりました。

ライブラリプロジェクトのリソースが参照できない(未解決)

Scala + sbt-android はマルチプロジェクトに対応しています。 もちろんライブラリプロジェクトを作成し、それを利用することもできるのですがメインのプロジェクトからライブラリプロジェクトのリソースを参照することができません(おそらくバグ)。

Scala で Android アプリを作ろうと思い立ったとき Android Support Library v7 appcompat を使うことは必須条件でした。 しかし Android Support Library v7 appcompat は ActionBar に対応するために通常の手順ではライブラリプロジェクトとして参照し、そのリソースを使う必要があります。

今回は Maven Android SDK Deployer を使ってローカルリポジトリを構築し、依存関係に jar と apklib を含めることで対応しました。

Scala の Future は使えない(使いづらい)

Scala 勢はご存じかと思いますが Scala には非同期処理を簡単に扱える scala.concurrent.Future というクラスがあります。 下記の様に future { ... } で括った部分が別スレッドで実行され、その結果を使って別の計算を行う、というコードを下記の様に簡単に書くことができます。

import scala.concurrent._
import ExecutionContext.Implicits.global

val x: Future[String] = future {
  "Hello, %s".format("World")
}
x onSuccess { msg =>
  println(msg)
}

ところがこれを Android で使おうと思ったときに一点問題があります。 それは Future を使った処理は onSuccessonFailure, onComplete で実行される処理も別スレッドで行われる=UIスレッドでない、という問題です。

Android ではUIに関する処理はメインスレッドであるUIスレッドで行う必要がありますが Future を使うとUIスレッドに処理が帰ってくることはありません。 おとなしく AsyncTask を使いましょう。

2013-12-10 追記: AsyncTaskActivity#runOnUiThread を使った ExecutionContext を独自実装し、 onSuccess の引数に明示的に指定すれば Future を使って非同期に処理をしつつ onSuccess を UIスレッド上で実行できるみたいです(参考: @OE_uia さんの Gist)。

ところが AsyncTask が素直には使えない

Future が使いづらいことがわかりガッカリしたのも束の間, AsyncTask を使うのにも罠が待ち受けていました。 まず最初は次のように素直に AsyncTask を継承したクラスを作成しました。

import android.os.AsyncTask

class SomeAsyncTask extends AsyncTask[Void, Void, String] {
  def doInBackground(params: Void*): String = "Hello, %s".format("World")
  override def onPostExecute(result: String): Unit = { /* Something to do */ }
}

val task = new SomeAsyncTask
task.execute()

これをビルドし、実行すると AbstractMethodError が発生し doInBackground メソッドがない と怒られます。 定義してるのにも関わらず。ビルドが通っているのにも関わらず、です。

原因は Scala と Javaジェネリクス(型パラメータ)を使った可変長引数のビルド結果が異なる ことです。 詳しいことはこちらのページが詳しいです。

解決方法として、次のようなクラスを定義してこちらを継承することで問題なく AsyncTask を使うことができます。

import android.os.AsyncTask

abstract class ScalaAsyncTask[Params,Progress,Result] extends AsyncTask[Params, Progress, Result] {

  // doInBackground では Scala で新たに定義した asyncAction メソッドに処理を委譲する
  final def doInBackground(params: Params*): Result = asyncAction(params: _*)

  // onProgressUpdate でも同様に Scala で新たに定義した onProgress に処理を委譲する
  override final def onProgressUpdate(progress: Progress*) = onProgress(progress: _*)

  // Scala で新たなメソッドを定義する
  // doInBackground/onProgressUpdate はオーバーライドせず、こちらをオーバーライドして使用する
  def asyncAction(params: Params*): Result
  def onProgress(progress: Progress*): Unit = {}

}

具体的には可変長引数の部分を型パラメータのままオーバーライドし、実際に使用するサブクラスで定義・オーバーライドするのは Scala で新たに定義したメソッドにすることでビルド結果の差をなくしています。

今回用意した g8 テンプレート には上記の ScalaAsyncTask を改良したクラスが含まれています。 また、以下のようにして単純な非同期処理であれば前述の Future 風に(とかいうと Scala ガチ勢からマサカリ飛んできそうですが……)簡単に書けるようにしてあります。

import android.widget.Toast
import com.example.helloworld.support._

val x = async {
  "Hello, %s".format("World")
}
x onSuccess { text =>
  Toast.makeText(context, text, Toast. LENGTH_SHORT).show()
}

最後に

現在, Android アプリ「電話帳R」を Scala で書き直しています。 最初に書いたとおり少し前まで Scala で Android アプリを作る=ネタ扱いでしたが、個人的な感触では意外に実用的(になった)なんじゃないかなーと思います。 なにより Scala は書いていて楽しいです。

2014年はもっと Scala が流行りますように! あと、この世から Gingerbread 搭載端末が駆逐されますように!!

参考URL一覧