普段Javaで書いている部分をScalaで書けるようになった。
Javaだと、文字列や複雑なデータ構造を処理したり、関数プログラミングがしづらい。lambda無いし。そういう部分だけでもScalaでやりたい。

https://github.com/pfn/android-sdk-pluginというフレームワークを使って、既存のAndroidアプリのプロジェクトでScalaとJavaを混ぜこぜに書けるようにする方法を説明する。

全体の簡単な手順

  1. homebrewでscalaとsbtをインストールする
  2. Androidのプロジェクトを普通に作る or 既存のAndroidプロジェクトを用意する
  3. sbtで使う設定ファイル3つを配置する
  4. sbt初回起動時に必要なライブラリがインストールされて、Androidプロジェクトとsbtプロジェクトが共存した状態になる
  5. sbtでandroid:packageすると、1つのapkになる

という感じ。


参考にしたサイト

この2つしか読んでいない。
もし俺の記事でよくわからない箇所があったら、下手にググって余計なの読む前に下の2つをちゃんと読んだ方がいい。
今回使っている「android-sdk-plugin」と似たような位置づけのフレームワークとして、「android-plugin」と「scaloid」がある。
前者は少し古い物でこれを参考にandroid-sdk-pluginが作られた。
後者はandroid-sdk-pluginの上にさらに乗せるライブラリで、XMLでのレイアウトをscalaだけで書くようになっていたりしていて、既存のAndroidプロジェクト内でScalaも使うという用途にはちょっと向いていないと思った。

このように似たライブラリがあるので、安易に「scala android」とかでググると関係ない情報を読むことになる。公式ドキュメント重要。


試しに作ったもの

ここに置いた
https://github.com/shokai/ScalaAndroidTestApp
「つらい」が「っらぃ」のようになるだけのアプリ。
Scalaっぽい(Javaでは書きにくい)文字列やMapの処理と、Javaで書かれているAndroid SDKのGUI機能両方を使ってみたかったのでやってみた。
元ネタはScalaをちょっと勉強したに書いた研究室の練習問題。

MainActivityのコードを見ればわかるが、scalaだと辞書作って1文字ずつチェックして置換するような処理はJavaよりも書きやすい。


Androidアプリの新規作成

Android開発環境をインストールしなおしたに書いた手法でHomebrewでAndroid SDKをインストールした。
Eclipseは入れていない。emacsとterminalだけで開発する。

brew install android-sdk


まずプロジェクトのビルドターゲットを確認してから
android list
id: 3 or "android-18"
Name: Android 4.3
Type: Platform
API level: 18
Revision: 2

新規プロジェクトを作る。とりあえずビルド、端末にインストールまでする
android create project --target "android-18" --name ScalaTestApp --path `pwd` --activity MainActivity --package org.shokai.scalatestapp
android update project --path `pwd`
ant debug
adb install -r bin/ScalaTestApp-debug.apk


Scalaが使えるようにする

antの代わりにsbtを使う。

brew update
brew install scala sbt
scala 2.10.2 と sbt 0.13.0 がインストールされた。
sbt(simple-build-tool)は名前に反してシンプルではなく、node.jsのnpmに近い多機能な物で、
  • main()があるファイルを適当に探して、実行してくれる
  • scalaのライブラリのパッケージ管理、バージョン固定
  • 実行するscalaインタプリタのバージョン指定
  • ScalaとJavaを共存させ、.classなどの位置を気にせずに混ぜこぜで実行できる
  • Javaの場合は.classなどの中間ファイルも適当な場所に生成、管理してくれる
  • コマンド名の頭に「~ 」(チルダスペース)を付けると、ソースファイルの変更を検知してコマンドを再実行してくれる
などの機能がある。
いくつかはscala android-sdk-pluginがやってくれている機能なのかもしれない。
まだsbt使い込んでいないから完全に把握しきれていない。

sbt自体がなかなか面白いプロダクトなので、Getting Started — sbt Documentationを読んだ方がいい。


Androidプロジェクト内にファイルを3つ置いてsbt起動すればscalaが使えるようになる。
それぞれのファイル内でAndroid SDKやsbtのバージョンを指定しているので注意。

build.sbt (1行ずつ空けなければならないらしい)
import android.Keys._

android.Plugin.androidBuild

name := "ScalaTestApp"

platformTarget in Android := "android-18"

run <<= run in Android

install <<= install in Android

project/build.properties
sbt.version=0.13.0

project/plugin.sbt
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.0.6")


sbt起動
sbt
sbtのコンソールが起動し、自動的に依存ファイルがインストールされてscalaが使えるようになる。

とりあえずJavaのままだけどビルドする。sbtのコンソール内で
compile
が通ればok。


JavaをScalaに書きなおす

MainActivity.javaをMainActivity.scalaにリネームし、書き直す。
package org.shokai.scalatestapp;

import android.app.Activity;
import android.os.Bundle;

class MainActivity extends Activity{
override def onCreate(savedInstanceState:Bundle){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}

sbtコンソールでビルドする。
android:package-debug
同じ名前の.javaと.scalaが混在しているとビルドが通らないが、別名なら混在していていい。


adbで実機にインストールする
adb install -r bin/ScalaTestApp-debug.apk
scalaでアプリが書けましたね。



大文字ひらがなを小文字ひらがなに変換するアプリを作る

レイアウトのXMLとScalaで書きなおしたMainActivityを編集する。


res/layout/main.xml にボタンとEditTextを追加
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText
android:id="@+id/editTextSource"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textShortMessage" />
<Button
android:id="@+id/btnRun"
android:text="小文字に変換"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
<TextView
android:id="@+id/textViewResult"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="result here" />
</LinearLayout>


src/org/shokai/scalatestapp/MainActivity.scala
package org.shokai.scalatestapp;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.EditText;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;

class MainActivity extends Activity{

var editTextSource:EditText = _
var textViewResult:TextView = _
var btnRun:Button = _

override def onCreate(savedInstanceState:Bundle){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

editTextSource = findViewById(R.id.editTextSource).asInstanceOf[EditText]
textViewResult = findViewById(R.id.textViewResult).asInstanceOf[TextView]
btnRun = findViewById(R.id.btnRun).asInstanceOf[Button]

btnRun.setOnClickListener( new OnClickListener(){
override def onClick(v:View){
val source = editTextSource.getText().toString()
trace(s"source: $source")
val result = kana_downcase(source)
trace(s"result: $result")
textViewResult.setText(result)
}
})
}

def kana_downcase(str:String):String = {
val chars = scala.collection.immutable.Map[String,String](
"あ" -> "ぁ",
"い" -> "ぃ",
"う" -> "ぅ",
"え" -> "ぇ",
"お" -> "ぉ",
"つ" -> "っ",
"よ" -> "ょ",
"わ" -> "ゎ")

return str.split("").
map(c =>
if(chars.contains(c)) chars(c) else c
).mkString
}

def trace(message:String){
Log.v("ScalaTestApp", message)
}
}
scalaなので、変換表を気軽にMapで作って1つずつチェックして置換して結合とか書けるし、
いちいちtoString()しないで s"$変数名" で変数展開して文字列に埋め込んだりできて便利だった。

ビルドするとこうなる


.gitignore

Android SDKは中間ファイルをたくさん生成するので、Gitにコミットする時にディレクトリそのままaddするとdiffが酷い状態になる。
さらにscala android-sdk-pluginも中間ファイルを生成する。
必要ないファイルはignoreしておくべき。

.gitignore
.DS_Store
*.log
*~
*#*
*.class
.classpath
.project
bin
build.xml
local.properties
proguard.cfg
.settings
target
project/target
tmp
local.propertiesなどもcommitしないようにしているので、リポジトリをcloneしなおして来た直後は
android update project --path `pwd`
を実行して、それぞれの環境に合わせたAndroid SDKのパスを生成しなおすようにしている。
こういうビルド作法はREADMEに書いておくといい