はじめに
この記事はAndroid Studioで使えるテンプレートを自作する方法をそこそこ丁寧に解説します。
テンプレートってのは、↓コレ
これを作ると、例えばAcitivityのクラスを作ると同時に、関連するFragmentやxmlも同時に作成する、といったことができる。
因みに、私自身すべてを理解しているわけではないので、解らない部分はスルーしたりします。。
また、今回の内容はココにある程度書いています。ただ、最新のフォーマットでは無いので(最新は5だが記事は4)、全て鵜呑みにすると(多分)ハマるので注意です。
更新履歴
2018/4/13 globals.xml.ftlにリソースの出力先制御について追記
既存のテンプレートを見てみよう
まずは既存のテンプレートがどうなっているかを知ることで理解が深まるかと思います。
場所ですが、Macの場合
/Applications/Android Studio.app/Contents/plugins/android/lib/templates/activities
以下にあります。
Windowsの場合、
$ANDROID_STUDIO_FOLDER$\plugins\android\lib\templates\activities\
になります。
自作したテンプレートは上記のactivitiesフォルダいかに配置すればOK。
また、自作したテンプレートはAndroid Studioのバージョンアップに伴い削除されるので注意が必要。バージョンアップしたら再配置する必要がある。
前説はこのくらいで、今回はこの中のBaseActivity
を例に見ていきます。
テンプレートのフォルダ構成
ほぼ全てのテンプレートで同じ構成になってます。なので、自作するときも真似ればOK
BaseActivity
+ globals.xml.ftl
+ recipe_fragmentxml.ftl
+ recipe.xml.ftl
+ template_basic_activity_fragment.png
+ template_basic_activity.png
+ template.xml
+ root
+ res
| + layout
| + activity_fragment_container.xml.ftl
| + fragment_simple.xml.ftl
+ src
+ app_package
+ SimpleActivity.java.ftl
+ SimpleActivity.kt.ftl
+ SimpleActivityFragment.java.ftl
+ SimpleActivityFragment.kt.ftl
それぞれの役割について見ていきましょう
template.xml
これが全ての起点となるファイルです。これに記載した内容を元に様々な設定項目を追加できます。
<?xml version="1.0"?>
<template
format="5"
revision="6"
name="Basic Activity"
minApi="9"
minBuildApi="14"
description="Creates a new basic activity with an app bar.">
<category value="Activity"/>
<formfactor value="Mobile"/>
- format->(多分)諸々の記載ルールのフォーマット?5を指定しておけば良さそう
- revision->このテンプレートのリビジョン。任意で。
- name->そのまんま。ここで書いた名前がテンプレート一覧で表示される
- minApi/minBuildApi/->そのまんまなので説明割愛
- description->注釈。テンプレート作成時のダイアログの最上段に出るやつ。
- category->そのまんまでカテゴリー。同じ名前にすれば同じカテゴリに属するようになる
-
formfactor->なんのプロジェクトの時に有効かどうか。スマホなら
Mobile
、WearableデバイスならWear
といった感じ。 -
parameter->ここで指定したパラメータに則り、ダイアログでの項目が表示される
<parameter
id="activityClass"
name="Activity Name"
type="string"
constraints="class|unique|nonempty"
suggest="${layoutToActivity(layoutName)}"
default="MainActivity"
help="The name of the activity class to create"/>
<!-- 中略 -->
<parameter
id="menuName"
name="Menu Resource Name"
type="string"
constraints="layout|unique|nonempty"
suggest="menu_${classToResource(activityClass)}"
visibility="isNewProject!false"
default="menu_main"
help="The name of the resource file to create for the menu items"/>
- id->そのまんま。一意に定まること。このIdをのちのftlファイルで参照する
- name->そのまんま。項目名になる
- type->入力方式。確認しているもので
string
,enum
,boolean
がある。stringならテキスト入力フォーム、booleanならチェックボックスが表示、といった具合にいい感じに表示される - constraints->制約条件。上記の例だと、classの属性であること、ユニークであること、空文字ではないこと、となる。上記にないものの制約として
activity
,layout
,exists
,package
,empty
といったものがある。 - suggest->自動で入力されるフォーム。メソッド名からも推測できる通り、layout名を変えると自動でActivity名に変換してくれる。
- default->デフォルト値。これが初期値として入力される
- help->ヘルプ。補助説明的なやつ
- visibility->そのまんまで項目を表示するかどうかを表す。trueで表示。
<parameter
id="useFragment"
name="Use a Fragment"
type="boolean"
default="false"
help="If true, the content will be a fragment"/>
<!-- 中略 -->
<thumbs>
<!-- default thumbnail is required -->
<thumb>template_basic_activity.png</thumb>
<!-- attributes act as selectors based on chosen parameters -->
<thumb useFragment="true">template_basic_activity_fragment.png</thumb>
</thumbs>
<globals file="globals.xml.ftl"/>
<execute file="recipe.xml.ftl"/>
- thumbs->サムネイル。要素として
<thumb>
を持つ。2つあった場合は後方優先?かもしれない。また、属性として使われているuseFragment
は上記の通り、parameter
のIdとなっている。 - globals->globals.xml.ftlを読み込む。ここに記載した内容は全recipe.xml.ftlファイル系で参照できるようになる。イメージとしては共通メソッドや共通の変数を追加するイメージ、
- execute->指定したレシピファイルを元に、クラスファイルやxmlファイルを作成する。
変数やメソッドの書き方
ところどころに出てきた$から始まる記載方法について
基本文法
${メソッド名(引数)}
となっている。例えば上記のsuggest="menu_${classToResource(activityClass)}"
を例に見る。classToResource
メソッドに対して、引数としてactivityClass
を渡している。この結果が例えばmogemoge
となっていた場合、最終的なsuggestの内容はmenu_mogemoge
となる。
ではこのメソッドがどこで定義されているのか?という話になるが、多分、非公開なメソッドになっている。なので既存のテンプレートで使われているメソッド名から推測し使うか、有志の人?がまとめてくれてたりするのでそちらを参照するとよい。
globals.xml.ftl
ここで定義したglobal変数は、その名の通り全てのftlファイルで参照できるようになる。
<?xml version="1.0"?>
<globals>
<global id="requireTheme" type="boolean" value="true" />
<#include "../common/common_globals.xml.ftl" />
<global id="simpleLayoutName" value="<#if appCompatActivity>${contentLayoutName}<#else>${layoutName}</#if>" />
<global id="appBarLayoutName" value="${layoutName}" />
<global id="fragmentClass" value="${activityClass}Fragment" />
</globals>
- global->idとvalueがセット。基本はstringとして扱われるがtypeでboolean指定も可能?みたい
- include->そのまんまでファイルをincludeして実行する(ただ、この場合は各種設定を読み込むことを意味する)
リソースの出力先をkotlinにしたいぞ
以下の定義を追加してあげよう
<global id="srcOutKotlin" value="app/src/main/kotlin/${slashedPackageName(packageName)}" />
そうすると出力先にsrcOutKotlin
が使えるようになる。あとは次章のrecipe.xml.ftlの出力先を指定してる箇所のsrcOut
をsrcOutKotlin
にしてあげればOK
recipe.xml.ftl
このファイルに記載したルールに則り、クラスファイル、フォルダ等々が生成される
<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
<@kt.addAllKotlinDependencies />
<#include "../common/recipe_manifest.xml.ftl" />
<#if useFragment>
<#include "recipe_fragment.xml.ftl" />
<#else>
<#include "../common/recipe_simple.xml.ftl" />
</#if>
-
import->includeと似ているが、includeはファイル内の処理を問答無用で全て実行するのに対し、importはあくまで参照追加になる、と思う。なので
@kt.addAllKotlinDependencies
といった形で呼び出すことで、初めてkotlin_macros.ftl
内のaddAllKotlinDependencies
が実行される -
if else->条件分岐。構文は見ての通り。詳しい構文説明は公式を参照
<#if generateKotlin>
<instantiate from="root/src/app_package/SimpleActivity.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<#else>
<instantiate from="root/src/app_package/SimpleActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</#if>
- instantiate->いわゆるファイル生成処理をする。厳密に言えばfromで指定したファイルをtoで指定した場所に出力する。また、
escapeXmlAttribute
は引数で指定したディレクトリまでのパスを変換してくれる。 - open->テンプレート生成処理が終わった後に開く処理を追加できる。
これ以外で頻度高めな構文もあるので、別のrecipe.xml.ftlファイルから抜粋して説明する。
<#if createKotlinDir>
<mkdir at="${escapeXmlAttribute(projectOut)}/src/androidTest/kotlin"/>
<mkdir at="${escapeXmlAttribute(projectOut)}/src/main/kotlin"/>
<mkdir at="${escapeXmlAttribute(projectOut)}/src/test/kotlin"/>
</#if>
<mkdir at="${escapeXmlAttribute(srcOutKotlin)}/data"/>
<mkdir at="${escapeXmlAttribute(srcOutKotlin)}/data/datasource"/>
<!-- 中略 -->
<merge from="root/build.gradle.ftl"
to="${escapeXmlAttribute(projectOut)}/build.gradle" />
<merge from="root/top_build.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/build.gradle" />
<dependency mavenUrl="com.google.dagger:dagger:2.+" />
<dependency mavenUrl="com.google.dagger:dagger-android:2.+" />
<dependency mavenUrl="com.google.dagger:dagger-android-support:2.+" />
<dependency mavenUrl="org.mockito:mockito-core:+" gradleConfiguration="testCompile" />
<dependency mavenUrl="com.squareup.leakcanary:leakcanary-android:+" gradleConfiguration="debugCompile"/>
- mkdir->そのまんまでフォルダ作成処理
- merge->既存プロジェクトにあるファイルに対してマージする。toに書かれているファイルに対して、fromの内容をマージする。
- dependency->app/build.gradleにある、
dependencies
に指定した要素を追加する。上記のmerge処理で事足りそうだが、構文が壊れるのでdependencyで追加しないといけない。なぜ壊れるのかはワカラナイ。。。逆に、いわゆるkapt
,apt
指定の記載はmergeでやる必要がある。 - gradleConfiguration->
debugCompile
でdebugImplementation
に、testCompile
でtestImplementation
に変化する
mergeファイルの書き方
merge対象のファイルは新規作成と違い、差分部分のみを記載すればよい。今回は多分一番使うであろう、build.gradleとManifest.xmlに対する書き方を説明する。
<#if includeRealm>
apply plugin : 'realm-android'
</#if>
apply plugin : 'kotlin-kapt'
apply plugin : 'kotlin-android-extensions'
android {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
androidTest.java.srcDirs += 'src/androidTest/kotlin'
main.kotlin.srcDirs += 'src/main/kotlin'
test.kotlin.srcDirs += 'src/test/kotlin'
androidTest.kotlin.srcDirs += 'src/androidTest/kotlin' }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
<#compress>
kapt 'com.google.dagger:dagger-compiler:2.+'
kapt 'com.google.dagger:dagger-android-processor:2.+'
</#compress>
}
全体的な記載方法として、各ルートタグから書く必要がある。具体的には、sourceSets
という項目をandroid
というタグに追加したい場合は、上記のようにandroid
タグから書けばOK。
- compress->このタグ内で書いた処理は空行削除になる。dependencies内で直接記載する場合、compressの記載がないと最終出力時に謎の改行がはいったりする。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
<activity android:name="${packageName}.${activityName}"/>
</application>
</manifest>
上記のbuild.gradleと同じように、ルートタグから記載してやればOK。
root以下のファイル群
root以下には各種テンプレートが置かれている構成になっている。拡張子はftl。これはFreeMakerと言われる言語、らしい。
フォルダ構成に明確なルールは無いが、慣習として以下のようになっている
- root/resフォルダ->リソース系のフォルダ。レイアウトなら
root/res/layout/
、drawableならroot/res/drawable/
といったフォルダ構成 - root/srcフォルダ->クラスファイル系のフォルダ。基本は
root/src/app_package/
以下に配置する。 - root直下->上記のいずれにも属さないファイルたちを配置する。よくあるのはbuild.gradle.ftl,Manifest.xml.ftlなど
クラスファイルの書き方
package ${escapeKotlinIdentifiers(packageName)}
import android.os.Bundle
<#if hasAppBar>
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
<#else>
import ${superClassFqcn}
</#if>
<#if isNewProject>
import android.view.Menu
import android.view.MenuItem
</#if>
<#if applicationPackage??>
import ${applicationPackage}.R
</#if>
import kotlinx.android.synthetic.main.${layoutName}.*
<#if includeCppSupport!false>
<#if useFragment!false>
import kotlinx.android.synthetic.main.${fragmentLayoutName}.*
<#else>
import kotlinx.android.synthetic.main.${simpleLayoutName}.*
</#if>
</#if>
class ${activityClass} : ${superClass}() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.${layoutName})
<#if hasAppBar>
setSupportActionBar(toolbar)
fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
</#if>
<#if parentActivityClass?has_content>
${kotlinActionBar}?.setDisplayHomeAsUpEnabled(true)
</#if>
<#include "../../../../common/jni_code_usage.kt.ftl">
}
<#if isNewProject>
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.${menuName}, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when(item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
</#if>
<#include "../../../../common/jni_code_snippet.kt.ftl">
}
基本的にはクラスファイルをそのまま書けば良い。
ただ、文法自体はFreeMakerのものが使えるのでif文で条件分岐させたり、${}
で変数を参照したりできる。多少読みにくいが、一度理想系のクラスファイルを記載し、その後変数化したかったり、条件分岐させたいところを適宜置き換えていけば良いと思う。
ちなみに<#if applicationPackage??>
の??
はいわゆるexistチェック。変数が存在するならtrueを返す
なお、includeで指定すると、指定先のコードが挿入されるイメージ。
デバッグ方法
といってもTry&Errorをするしか無いのが現状、、だと思う。
作成したテンプレートをactivities
フォルダ以下に配置しAndroidStudioを再起動。その後インポートされたテンプレートを実行してみて、おかしければAndroid Studioがエラーを教えてくれるのでその情報を頼りに修正していく感じ。
さいごに
テンプレート作成は少々時間がかかるが、一度作成してしまえば様々なプロジェクトで応用が利くので一度作ってみてはいかがだろうか。
個人的に効果的なのはArchitectureを統一したい、といったケースには非常に刺さる気がしてる。
Kotlin+CleanArchitectureのテンプレートが以下にあるので、参考にしてみてはどうだろうか
https://github.com/kiuchikeisuke/Android-Studio-CleanArchitecture-template-forKotlin