9月 232013
普段Javaで書いている部分をScalaで書けるようになった。
Javaだと、文字列や複雑なデータ構造を処理したり、関数プログラミングがしづらい。lambda無いし。そういう部分だけでもScalaでやりたい。
https://github.com/pfn/android-sdk-pluginというフレームワークを使って、既存のAndroidアプリのプロジェクトでScalaとJavaを混ぜこぜに書けるようにする方法を説明する。
全体の簡単な手順
- homebrewでscalaとsbtをインストールする
- Androidのプロジェクトを普通に作る or 既存のAndroidプロジェクトを用意する
- sbtで使う設定ファイル3つを配置する
- sbt初回起動時に必要なライブラリがインストールされて、Androidプロジェクトとsbtプロジェクトが共存した状態になる
- 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 updatescala 2.10.2 と sbt 0.13.0 がインストールされた。
brew install scala sbt
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起動
sbtsbtのコンソールが起動し、自動的に依存ファイルがインストールされて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.apkscalaでアプリが書けましたね。
大文字ひらがなを小文字ひらがなに変換するアプリを作る
レイアウトの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;scalaなので、変換表を気軽にMapで作って1つずつチェックして置換して結合とか書けるし、
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)
}
}
いちいちtoString()しないで s"$変数名" で変数展開して文字列に埋め込んだりできて便利だった。
ビルドするとこうなる
.gitignore
Android SDKは中間ファイルをたくさん生成するので、Gitにコミットする時にディレクトリそのままaddするとdiffが酷い状態になる。さらにscala android-sdk-pluginも中間ファイルを生成する。
必要ないファイルはignoreしておくべき。
.gitignore
.DS_Storelocal.propertiesなどもcommitしないようにしているので、リポジトリをcloneしなおして来た直後は
*.log
*~
*#*
*.class
.classpath
.project
bin
build.xml
local.properties
proguard.cfg
.settings
target
project/target
tmp
android update project --path `pwd`を実行して、それぞれの環境に合わせたAndroid SDKのパスを生成しなおすようにしている。
こういうビルド作法はREADMEに書いておくといい