Edited at

Android Studioのテンプレートを自作する方法

More than 1 year has passed since last update.


はじめに

この記事はAndroid Studioで使えるテンプレートを自作する方法をそこそこ丁寧に解説します。

テンプレートってのは、↓コレ

スクリーンショット 2018-03-12 10.38.22.png

これを作ると、例えば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

これが全ての起点となるファイルです。これに記載した内容を元に様々な設定項目を追加できます。


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->注釈。テンプレート作成時のダイアログの最上段に出るやつ。

スクリーンショット 2018-03-12 10.59.58.png


  • category->そのまんまでカテゴリー。同じ名前にすれば同じカテゴリに属するようになる

スクリーンショット 2018-03-12 11.01.56.png


  • formfactor->なんのプロジェクトの時に有効かどうか。スマホならMobile、WearableデバイスならWearといった感じ。


  • parameter->ここで指定したパラメータに則り、ダイアログでの項目が表示される



template.xml

    <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で表示。


template.xml

    <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ファイルで参照できるようになる。


globals.xml.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の出力先を指定してる箇所のsrcOutsrcOutKotlinにしてあげればOK


recipe.xml.ftl

このファイルに記載したルールに則り、クラスファイル、フォルダ等々が生成される


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->条件分岐。構文は見ての通り。詳しい構文説明は公式を参照



recipe.xml.ftl

<#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ファイルから抜粋して説明する。


recipe.xml

    <#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->debugCompiledebugImplementationに、testCompiletestImplementationに変化する




mergeファイルの書き方

merge対象のファイルは新規作成と違い、差分部分のみを記載すればよい。今回は多分一番使うであろう、build.gradleとManifest.xmlに対する書き方を説明する。


build.gradle.ftl

<#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.xml

<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など


クラスファイルの書き方


SimpleActivity.kt.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