普段 Android アプリの開発を行っている takahirom こと毛受(めんじゅ)です。
さて、Android のIDEとしては先日長きベータ時代に終わりを告げてついに Android Studio が 1.0 となりました。
今まで Eclipse + ADT 環境から移行するのを迷っていた人たちも、ようやく重い腰を上げて Android Studio へのも進むのではないかと思います。
そんな Android Studio では次世代のビルドツールとして注目を集めている Gradle が採用されています。
Gradle の特徴
Gradle で採用されているビルドスクリプトは非常にシンプルな記述をできるのが特徴で
例えば
file("text.txt").text
という記述だけで text.txt の中にある文字列を取得する処理を意味しており、とても直感的で理解しやすくシンプルな仕様のスクリプトです。
Groovy の DSL によって、目的に特化した文法を使うことができるためとても拡張しやすく柔軟性が高く他のビルドシステムと比べても優れている点ではないかと思います。
アプリのテーマの作成を楽にしたい
現在 Homee というホームアプリを担当しています。
Homee には着せ替えテーマがあり、気軽にホームアプリの外観を着せ替えることができます。
例:クリスマステーマ
Homee のテーマは個々にアプリケーションの形で提供されており Playストアに公開されています。
そのため新しくテーマを作成するためには開発環境が必須なので、テーマの壁紙やアイコン、配色などを行うデザインを行う担当デザイナーがAndroid アプリの開発環境を使ってアプリケーションへのパッケージングまで行いながら開発しています。
ただ、その作業が大変でした。
テーマアプリのアプリケーション部分の基本構造は共通仕様ですが、パッケージ名の変更や、画像を差し替えるなど機械的な内容であるにも関わらずアプリケーション開発の知識の下地が必要なることが多く負荷の高い内容ばかりで改善できないかと考えていました。
どうすれば解決できるか
前述のとおり Homee のテーマはアプリケーション部分の構造は同じなので、違いは画像、アプリ名、パッケージ名、文字色などです。
ということは、共通部分の仕様はテンプレート化して、異なる箇所のデータを入力すると Android アプリケーションのプロジェクトを自動的に生成し、そのプロジェクトから apk ファイルを作成させることができれば開発環境に慣れていないデザイナーさんの不可を軽減できるのではないか? と思います。
さらに、そのまま端末までインストールができればテストや動作確認まで楽になりそうです。
そのために Gradle のビルドスクリプトで以下のことを行いました。
- Gradle のタスクを作成する
- Gradle のタスクを簡単に起動できるようにする
- デザイナーでも入力できるように Gradle に GUI でテーマの情報を入力させるダイアログを表示する
- ミスを防止するため、ディレクトリに必要なファイルがあるかどうかを検出する
- 書き換えたアプリ作成のため、ファイルを書き換えながらコピーする
- 簡単に設定ファイルを読み込む
- Androidのパッケージ名を変更する strings colors を追加する
- アプリをビルドして、GradleからAndroid端末で起動まで行う
それぞれの作業で使ったGradleやGroovyのTipsを紹介していきます。
動作環境
Gradle:2.2.1
Android plugin for Gradle:1.0.0
OS:OS X 10.9.4(OS X Mavericks)
1. Gradle のタスクを作成する
Gradleを使うためには Gradle のタスクの作成する必要があるため以下の内容が記述されているスクリプトファイルbuild.gradle
を用意しました。
task createThemeProject << {
//ここにコードを書いていく
}
2. Gradle のタスクを簡単に起動できるようにする
デフォルトタスクの作成
defaultTasks
を指定することで、特に何もしなくても Gradle が自動的にタスクの指定なしでスクリプト内に定義されているタスクを呼び出してくれます。
例えば以下のように実行できます。
$cat build.gradle
task createThemeProject << {
println "Hello, world!"
}
defaultTasks "createThemeProject"
$gradle
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
:createThemeProject
Hello, world!
BUILD SUCCESSFUL
Total time: 1.947 secs
Finder から Gradle タスクを起動する
この Gradle タスクの起動はデザイナーの方にも簡単に起動できるように配慮しました。
Finder から直接起動する必要があります。
Gradle がインストールされていれば gradle wrapper
コマンドを使うと Gradle ラッパーが置かれます。
gradle コマンドがインストールされていなくても gradlew ファイルを呼び出すだけで、タスクを起動できます。
以下のような bash スクリプトを用意して、chmod などで実行権限を与えて、
Finder からダブルクリックでスクリプトを起動すると Gradle タスクが動作するようにしました。
- start (ファイル名)
#!/usr/bin/env bash
cd $(dirname $0)
./gradlew
3. GUI でテーマの情報を入力させるダイアログを表示する
テーマごとの固有の情報の入力もスクリプトを毎回編集したりテキストに記述して設定ファイル化したりするとどうしても運用が煩雑になるのと、デザイナーの方の負担を下げるという目的から外れていると思い、GUI での入力を考えました。
Groovy の SwingBuilder を用いると簡単に GUI で表示させたり、入力することが可能です。
ビルドスクリプトの中で Groovy スクリプトを build.gradle の中に書くこともできます。
このようなダイアログの表示も簡単です!
ここではbuildSrc/src/main/groovy/ThemeDialog.groovy
に以下のようなGroovyスクリプトを用意しました。
ここにGroovyスクリプトを置くことで、ビルド時にGradleがコンパイルしてくれるのでビルドファイルの中で利用できます
import groovy.swing.SwingBuilder
import java.awt.*
class ThemeDialog{
String appName
String directoryName
void show() {
def builder = new SwingBuilder()
builder.edt {
// Dialog settings
dialog(modal: true, // ダイアログがdispose()されるまで処理を止める
title: 'Enter Theme setting', // Dialogのタイトル
alwaysOnTop: true, // 最前面に表示
resizable: false, // リサイズ不可
pack: true, // 全要素を含めて表示
show: true // すぐに表示する
) {
// 縦に並ぶレイアウト
vbox {
// テーマ名ラベル(Android TextViewのようなもの)
label(text: "Please enter theme name:")
// 文字入力フィールド
def nameInput = textField(text: "App Name テスト")
// フォルダ名
label(text: "Please enter folder name:")
// 文字入力フィールド
def directoryNameInput = textField(text: "test")
// OKボタン
button(defaultButton: true, text: 'OK', actionPerformed: {
// ボタンが押された時のイベントハンドラ
appName = nameInput.text
directoryName = directoryNameInput.text
dispose()
})
}
}
}
}
}
ここでは詳細には紹介できませんが、他にもGUIをとても簡潔に記述できるためぜひ調べてみてください。
この SwingBuilder は Mac OS の Android Studio から起動しようとすると CToolkit のクラスがないといった例外を起こしてしまうようでコマンドラインから起動するように対処します。
こんな感じでbuild.gradleから呼び出して起動しています。
task createThemeProject << {
def dialog = new ThemeDialog()
dialog.show()
}
defaultTasks "createThemeProject"
今回のケースでは、テーマアプリケーションに必要なさまざまな入力項目(文字の色や、画像のパスの指定など)で利用しています。
4. 情報の整合性をチェック
次に事故を防ぐためにも、指定されたフォルダに必要なファイルがあるかどうかを検出し問題がないかも確認します。
ここでは必要なファイルがちゃんとあるかどうかをチェックする部分を見ていきます。
- require_files
tab.png
tab_background.png
clock.png
- build.gradle
...
// themeDirは必要なファイル画像があるディレクトリのパス
def themeFileTree = fileTree(themeDir)
// require_imagesから一行ずつ取り出す
file("require_files").eachLine{ requiredFileName ->
// 必要なファイルでフィルタをかける
def filteredFileTree = themeFileTree.filter{file->
file.name == requiredFileName
}
// fileTreeからfileリストに変換
def filteredFiles = filteredFileTree.files
// リストサイズが0以上(存在するかどうか)
assert filteredFiles.size() > 0
}
...
Gradle ではビルドスクリプト内でfileTree()
を使うことで、それぞれのファイルに対して個別の処理を簡単に行うことが出来ます。
例えば今回用意したスクリプトのようにテキストファイルの内容を1行単位で取得したり組み合わせでビルドスクリプトの中でさまざまな環境情報や、ビルドに必要な外部データの取り込みなどを容易に行うことが出来ます。
上記のスクリプトではfileTree()
とfile()
を利用して、必要な画像ファイルがあるかどうかをチェックしています。
assert()
を利用することで、チェックを行うことができ、結果がfalseの場合に分かりやすい出力を出すことができます。
assert filteredFiles.size() > 0
| | |
[] 0 false
この場合はfilteredFiles
が空のリストで、size()
が0を返して、その結果falseになるという形でデバッグに最適です。
5. テーマの固有情報を Android アプリのプロジェクト情報に反映
copy()
で、フォルダにあるファイルをコピーすると同時に Gradle の機能でファイルの内容から特定の文字列を検索して置き換えができます。
def RESOURCE_PATH = "/app/src/main/res/"
def appName = "SweetTheme"
// xmlファイルを書き換えを行いつつ上書き
copy {
// コピー元の設定
from(TEMPLATE_PROJECT_PATH + RESOURCE_PATH){
// コピーするのをxmlのみに限定する
include "**/*.xml"
}
// コピー先の設定
into outputPath + RESOURCE_PATH
filter { line ->
// 一行ごとに置き換えを行う
line.replaceAll "TEMPLATE_APP_NAME", appName
}
}
copy()
のfrom()
でコピー元のディレクトリ into()
でコピー先のディレクトリを設定してコピーできます。
今回はfrom()
にクロージャを渡すことで include()
を用いてxmlのみを指定しています。
また、ここではfilter()
を利用してTEMPLATE_APP_NAME
を SweetTheme という文字列に置き換えしています。
今回では利用していないexpand()
という関数を使用すると SimpleTemplateEngine が利用できるので、それで文字列の置換えを行っていきます。
ただし Android の strings.xml でフォーマット文字列などを利用していると、%1$s
などの文字列がexpand()
で置き換えに利用する文字列と同じ書式になるため利用できませんでした。
と言っても、だいたいの Android アプリケーションで定義されている各種リソースの書き換えなどは Android plugin for Gradle の機能を利用することで解決できます。
それでも解決できないような特殊な事例である values-xx の 多言語環境用の strings.xml の書き換えなどは、このcopyで行いました。
6. 設定ファイルを読み込み
いくつか設定ファイルからデータを読み出す方法がありますが ConfigSlurper というクラスを使う方法を紹介します。
- theme.gradle
theme {
directoryName='test'
themePath='/...'
color='#0000FF'
packageName='com.exmpale.test'
appName='App Name テスト'
}
- build.gradle
def config = new ConfigSlurper().parse(file("theme.gradle").toURL())
println config.theme.color // #0000FF
ConfigSlurper
のparse()
を使うことにより設定を取り出しています。
これにより config.theme.color
で#0000FF
が取り出せます。
7. パッケージ名の変更
Android アプリのパッケージ名を変更する strings colors を追加するため、プロジェクトの中にある build.gradle を変更します。
Android plugin for Gradle にはアプリのパッケージ名を変更したり、リソースを追加するための便利な機能が存在しています。
今回のようなパッケージ名の変更を行うには以下のように記述できます。
- app/build.gradle
...
android {
compileSdkVersion 21
buildToolsVersion "20.0.0"
defaultConfig {
// パッケージ名の変更
applicationId config.theme.packageName
// stringsの変更
resValue "string", "app_name", config.theme.appName
// 色の変更
resValue "color", "text_color", config.theme.color
minSdkVersion 12
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
...
このように applicationId に config から取り出した packageName を適用できます。
さらに resValue を使うことで colors や strings にリソースを追加することもできます。
8. ビルドしたら端末にインストールして起動する
Android plugin for Gradle にはinstallDebug
というタスクが用意されています。
installDebug
はアプリのビルドとインストールを行ってくれるタスクです。
./gradlew installDebug
を実行すると端末にインストールされるのですが、起動してくれませんでした。
なので普通の開発環境では sdk に含まれている adb
コマンドを使って端末やエミュレータにインストールされているアプリを起動させるようにしました。
adb command
adb shell m start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER パッケージ名/Activityのパッケージ名.Activityのクラス名
この adb で指定したパッケージを起動するにはアプリケーションのパッケージパスと Activity のクラス名が必要なので appt コマンドで apk の情報から Activity のパッケージパスを取得します。
aapt dump badging apkファイルのパス
出力例
package: name='com.exmpale.test' versionCode='3' versionName='1.3'
sdkVersion:'14'
...
application-debuggable
launchable-activity: name='com.example.MainActivity' label='Homee HOMEE_App Name テスト theme' icon=''
uses-feature:'android.hardware.touchscreen'
このpackage: name='パッケージ名'
やlaunchable-activity: name='Activityのパッケージ名.Activityのクラス名'
を取得してadb
コマンドに利用すれば起動できそうです。
// タスク定義で、他のタスクに依存させることができ、ここではinstallDebugに依存している
task startDebug(dependsOn: 'installDebug') << {
// Variantというものがあり、これはビルドの種類ごとに存在している
android.applicationVariants.all{variant->
// インストールを伴わないvariantは無視する
if(!variant.install){
return
}
def aaptCommandPath = System.env.ANDROID_HOME + "/build-tools/$project.android.buildToolsRevision/aapt"
def adbCommandPath = System.env.ANDROID_HOME + "/platform-tools/adb"
// ビルドが終わったapkファイルのパスを調べる (ここの記述はPluginのバージョンアップによって変わる可能性が大きいです)
def apkPath = variant.outputs[0].zipAlign.outputFile.path
// aaptコマンドを実行して、apkの中身を探る
def aaptResult = "$aaptCommandPath dump badging $apkPath".execute().text
def packageLine = aaptResult.readLines().find{it.startsWith "package:"}
def packageName
packageLine.eachMatch(".*name='(.*?)'.*"){allText,matchPackageName->
packageName = matchPackageName
}
def launcableActivityLine = aaptResult.readLines().find{it.startsWith "launchable-activity:"}
def activityPackageName
launcableActivityLine.eachMatch(".*name='(.*?)'.*"){allText,matchActivityName->
activityPackageName = matchActivityName
}
// アプリのパッケージ名と起動Activityを指定してアプリの起動
"$adbCommandPath shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER $packageName/$activityPackageName".execute()
}
}
execute()
を行うことで、コマンドを実行できます。
例えば"ls".execute().text
で ls コマンドを実行して、その結果を取得できます。
このスクリプトでは、ANDROID_HOME
の環境変数に Android SDK のパスが設定されていれば、./gradlew startDebug
でアプリをインストールした後に アプリを起動させることが可能です。
まとめ
Gradle や Groovy について Android から触れ始めたのですが、Gradle や Groovy は簡潔に記述できる部分がとても多く、とてもシンプルで分かりやすい記述が採用されており Java の文法も利用できるので エンジニア自身の敷居も低いと思います。
普段の業務などでも利用しやすいものだと思います。
参考書籍
Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築
プログラミングGROOVY
参考サイト
Android Tools Project Site http://tools.android.com/
※ Google、Nexus、Androidは、Google Inc.の登録商標または商標です。
※ Mac、Mac OSおよびOS Xは、米国およびその他の国におけるApple Inc.の登録商標または商標です。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました