ジェットブレインズはより高速で使いやすいAndroidの開発スタイルとしてAnkoを発表しました。KotlinはAndroidの画面をデザインするために、DSL(ドメイン固有言語)としてAnkoライブラリーを提供しています。簡単な例を示します。
imageViewとButtonからなる簡単なAndroidのUIを次に示します。
Ankoのコードは次のようになります。
verticalLayout{
imageView(R.drawable.anko_logo).
lparams(width= matchParent) {
padding = dip(20)
margin = dip(15)
}
button("Tap to Like") {
onClick { toast("Thanks for the love!") }
}
}
このコードでは、イメージとボタンのコンテナとして機能する縦方向リニアレイアウトを定義しました。 レイアウトのビュー配置はlparams()を使って定義されています。また、ボタンをクリックしたときの動作も、UI定義の中でKotlinのインライン関数を使って定義されています。
■Ankoを使う利点
- ソースコードにUIのレイアウトを埋め込めるので型安全
- XMLを使っていないので、XMLをパースするCPU時間を使う必要がなく効率的
- UIをプログラム化したあと、Anko DSLのフラグメントを関数化すればコードの再利用ができる
- 明らかにコードがより簡潔で、読みやすく、理解しやすくなる
この記事では、Anko LayoutとKotlinを使って、タスク管理アプリ(ToDo アプリ)を作ります。
タスク管理アプリはGitHubのレポジトリにあります。
Android StudioにAnkoライブラリーを加える
AndroidプロジェクトにKotlinを加える方法は『Streamline Android Java Code with Kotlin(KotlinでAndroidのJavaコードを簡略化する)』を読んでください。プロジェクトをコンパイルできるようにするためには、Kotlinと一緒にAnkoの依存オブジェクトをapp/build.gradleに加える必要があります。
compile 'org.jetbrains.anko:anko-sdk15:0.8.3'
//sdk19, 21 and 23 are also available
この依存オブジェクトは、アプリケーションがターゲットにしているminSdkVersionに基づいて追加します。上の例ではminSdkVersion15以上19未満をターゲットとしている場合です。ほかに必要なAnkoライブラリーは、AnkoのGitHubレポジトリでチェックできます。
次のライブラリーも使います。
compile 'org.jetbrains.anko:anko-design:0.8.3'
compile 'org.jetbrains.anko:anko-appcompat-v7:0.8.3'
アクティビティにAnko Layoutを呼ぶ
XMLのレイアウトはもう書きませんので、XML Viewsを呼んだり、findViewById()メソッドを使ったりする必要はありません。Anko UIクラスがMainUIだとすると、アクティビティのコンテンツを次のように設定します。
var ui = MainUI() //MainUI class replaces the XML layout
ui.setContentView(this) //this refers to the Activity class
新しいKotlinファイルMainActivity.ktを作り、下のコードを追加します。
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.jetbrains.anko.*;
import java.util.*
class MainActivity : AppCompatActivity() {
val task_list = ArrayList<String>() //list consisting of tasks
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
val arrayList = savedInstanceState.get("ToDoList")
task_list.addAll(arrayList as List<String>)
}
var adapter=TodoAdapter(task_list) //define adapter
var ui = MainUI(adapter) //define Anko UI Layout to be used
ui.setContentView(this) //Set Anko UI to this Activity
}
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putStringArrayList("ToDoList", task_list)
super.onSaveInstanceState(outState)
}
}
task_listは、タスク管理アプリではリストビューのTodoAdapterを格納するArrayListです。MainUI(adapter)はAnko UIファイルで、TodoAdapterクラスのアダプターを引数とします。次に、TodoAdapterクラスを作ります。
リストビューのためにアダプターを作る
TodoAdapterクラスは、ArrayList<String>型のメンバーフィールドlistを持ち、BaseAdapterを拡張します。従って、次の4つのメンバー関数をオーバーライドする必要があります。
public int getCount()
public Object getItem(int i)
public long getItemId(int i)
public View getView(int i, View view, ViewGroup viewGroup)
getView()メソッドでは、Ankoを使ってリスト項目のレイアウトをデザインします。
override fun getView(i : Int, v : View?, parent : ViewGroup?) : View {
return with(parent!!.context) {
//taskNum will serve as the S.No. of the list starting from 1
var taskNum: Int = i +1
//Layout for a list view item
linearLayout {
lparams(width = matchParent, height = wrapContent)
padding = dip(10)
orientation = HORIZONTAL
//Task Number
textView {
id = R.id.taskNum
text=""+taskNum
textSize = 16f
typeface = Typeface.MONOSPACE
padding =dip(5)
}
//Task Name
textView {
id = R.id.taskName
text=list.get(i)
textSize = 16f
typeface = DEFAULT_BOLD
padding =dip(5)
}
}
}
}
- この関数は、リスト項目を含んだビューを返す。ビューはhorizontalListViewレイアウト。Kotlinのwith文を使って実現でき、1つのオブジェクトインスタンスに一度に何種類ものメソッドを呼べる
- リスト項目は、タスク番号とタスク名を表示するために、それぞれ2つのtextviewを含む
- linearLayout、textViewは拡張関数。拡張機能で新しい機能を持つクラスを作れる
- text、textSize、typefaceはandroid.widget.TextViewクラスに、参照および代入メソッドが定義されている。paddingはAnkoに定義された拡張プロパティ
次に、リストを操作する関数を定義しなければなりません。TodoAdapterクラスのadd(String)とdelete(Int)関数です。add(String)は加えるタスク名を引数として取ります。下に示すように、項目の位置をdelete(Int)の引数とします。
//function to add an item to the list
fun add(text: String) {
list.add(list.size, text)
notifyDataSetChanged() //refreshes the underlying dataset
}
//function to delete an item from list
fun delete(i:Int) {
list.removeAt(i)
notifyDataSetChanged() //refreshes the underlying dataset
}
リストがデザインできたので、リストに項目を加えたり削除したりできるようにします。これでアダプタークラスのコードは完成です。
import android.graphics.Typeface
import android.graphics.Typeface.DEFAULT_BOLD
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.LinearLayout.HORIZONTAL
import org.jetbrains.anko.*
import java.util.*
class TodoAdapter(val list: ArrayList<String> = ArrayList<String>()) : BaseAdapter() {
override fun getView(i : Int, v : View?, parent : ViewGroup?) : View {
return with(parent!!.context) {
//taskNum will serve as the S.No. of the list starting from 1
var taskNum: Int = i +1
//Layout for a list view item
linearLayout {
id = R.id.listItemContainer
lparams(width = matchParent, height = wrapContent)
padding = dip(10)
orientation = HORIZONTAL
textView {
id = R.id.taskNum
text=""+taskNum
textSize = 16f
typeface = Typeface.MONOSPACE
padding =dip(5)
}
textView {
id = R.id.taskName
text=list.get(i)
textSize = 16f
typeface = DEFAULT_BOLD
padding =dip(5)
}
}
}
}
override fun getItem(position : Int) : String {
return list[position]
}
override fun getCount() : Int {
return list.size
}
override fun getItemId(position : Int) : Long {
//can be used to return the item's ID column of table
return 0L
}
//function to add an item to the list
fun add(text: String) {
list.add(list.size, text)
notifyDataSetChanged()
}
//function to delete an item from list
fun delete(i:Int) {
list.removeAt(i)
notifyDataSetChanged()
}
}
クラスファイルでAnko DSLを使うには、org.jetbrains.anko.*をインポートしなければならないことに注意してください。
To-Do画面をデザインする
Ankoでは、KotlinのクラスごとにアクティビティのUIを持てます。したがって、各画面はKotlinクラスのUIとアクティビティのペアと考えられます。UIクラスは、org.jetbrains.ankoに定義されたAnkoComponent<T>インターフェイスの機能を拡張して開発します。このインターフェイスとともに、ジェットブレインズはDSL layout preview機能を無償で提供しています。 Android Studioで Anko DSL layout previewがどのように見えるかを次に示します。
対応するAnko Previewのプラグインはここからダウンロードできます。記事を書いている時点では、Android Studio 2.2用のAnko DSL Previewは未完成であることに留意してください。
タスク管理アプリに話を戻しますが、これから、すべてのタスクのリストを対象としたMainUIクラスをデザインします。MainUIクラスはAnkoComponent<T>インターフェイスを拡張します、TはUIのオーナー、すなわちコンテンツがこのUIとなるアクティビティとなります。例の場合、オーナーはすでに上で定義したMainActivityです。次に、初期化するときにTodAadapterオブジェクトをクラスに渡す必要があります。なぜなら、このアダプターがリストを埋めるために使われるからです。MainUクラスの宣言は次のようになります。
class MainUI(val todoAdapter : TodoAdapter) : AnkoComponent<MainActivity>
次にcreateView()関数をオーバーライドして、AnkoContextオブジェクトを引数としてView型を返すようにします。
override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {}
createView()関数で定義するUIは、オーナーのアクティビティに返されます。例の場合、オーナーのアクティビティはMainActivityです。これから、createView()メソッドのコーディングをします。
Step1 ホーム画面のデザイン
最初はホーム画面のタスクのリストは空です。従って、textViewはユーザーにその日のToDoリストを作るように求めます。
return relativeLayout {
//declaring the ListView
var todoList : ListView? =null
//textView displayed when there is no task
val hintListView = textView("What's your Todo List for today?") {
textSize = 20f
}.lparams {
centerInParent()
}
}
centerInParent()はビューのレイアウトが上下左右で相対的に中心に来るように定義するヘルパーメソッドです。作成しているのはタスク管理アプリなので、もっとも重要なのはタスクを表示するリストです。listViewを定義します。
//listView
verticalLayout {
todoList=listView {
//assign adapter
adapter = todoAdapter
}
}.lparams {
margin = dip(5)
}
todoAdapterはクラス宣言で定義したMainUIクラスのメンバー変数です。todoAdapterクラスのオブジェクトで、リストを埋めるtodoAdapterの値でlistViewのadapterを初期化します。
Material design原則に従って、ユーザーがタスクを追加するのを手助けするためにホーム画面の右下部にfloatingActionButtonを設けました。Ankoでは、floatingActionButtonを次のようにプログラムします。
floatingActionButton {
imageResource = android.R.drawable.ic_input_add
}.lparams {
//setting button to bottom right of the screen
margin = dip(10)
alignParentBottom()
alignParentEnd()
alignParentRight()
gravity = Gravity.BOTTOM or Gravity.END
}
Step2 AddTaskアラートダイアログを表示する
Ankoには、ビューにonClickListenerをセットする簡単な方法があります。onClick()メソッドを加えることで、floatingActionButtonにonClickListenerを追加できます。floatingActionButtonをクリックするとダイアログボックスが表示されるようにします。ダイアログボックスは、ユーザーにタスクを入力してリストに加えるように促します。
floatingActionButton {
imageResource = android.R.drawable.ic_input_add
onClick {
val adapter = todoList?.adapter as TodoAdapter
alert {
customView {
verticalLayout {
//Dialog Title
toolbar {
id = R.id.dialog_toolbar
lparams(width = matchParent, height = wrapContent)
backgroundColor = ContextCompat.getColor(ctx, R.color.colorAccent)
title = "What's your next milestone?"
setTitleTextColor(ContextCompat.getColor(ctx, android.R.color.white))
}
val task = editText {
hint = "To do task "
padding = dip(20)
}
positiveButton("Add") {
if(task.text.toString().isEmpty()) {
toast("Oops!! Your task says nothing!")
}
else {
adapter.add(task.text.toString())
showHideHintListView(todoList!!)
}
}
}
}
}.show()
}
}.lparams {
//setting button to bottom right of the screen
margin = dip(10)
alignParentBottom()
alignParentEnd()
alignParentRight()
gravity = Gravity.BOTTOM or Gravity.END
}
- alert{}はAnkoダイアログボックスを生成するインライン関数。デフォルトではAnkoダイアログボックスにテキストメッセージをセットし、postiveButtonとnegativeButtonを配置する
- verticalLayoutは向きが垂直なlinearLayout
- toolbarを使ってダイアログにタイトルを追加。カスタマイズの一例として、ダイアログのビューに色を付けるには、backgroundColor=ContextCompat.getColor(ctx,R.color.colorAccentのようにする。
ctxはorg.jetbrains.ankoパッケージのAlertDialogBuilderクラスで定義されたContextを指し、Androidにどのコンテキストを指しているか知らせるために引数として渡す - postiveButton()はユーザーがダイアログを入力したときの動作を定義するAnkoのヘルパーメソッド。ここではタスクが空でないことをチェックして、TodoAdapterクラスで定義したaddメソッドを使って、リストアダプターにタスクを追加する
- showHideHintListView(todoList!!)は、リストを表示するのにスペースを空けるためホーム画面に出てくるテキストビューhintListViewを隠すように定義するメソッド。リストビューが空であれば、hintListViewを表示し、そうでなければ隠す
//function to show or hide above textView fun showHideHintListView(listView: ListView) { if (getTotalListItems(listView)>0) { hintListView.visibility = View.GONE } else { hintListView.visibility = View.VISIBLE } }
ここで、getTotalListItems(listView)は、引数listViewの中の項目の数を返すMainUIクラスのメンバーメソッドです。これは標準のKotlin関数です。
//function to get total number of items in list
fun getTotalListItems(list: ListView?) = list?.adapter?.count ?: 0
最後にfloatingActionButtonをクリックすると、次のダイアログが表示されます。
タスクを追加すると、リストが見られます。
Step3 タスクを削除する
リストから項目を削除するdelete(Int)をTodoAdapterクラスに定義したのを思い出してください。これから、このメソッドを呼ぶUIをデザインします。Androidのデザインパターンに従って、タスクを長押しするとオプションを表示するようにします。リスト項目をonLongClickしたときの動作を定義します。listViewの定義に戻って、次を加えます。
onItemLongClick { adapterView, view, i, l ->
val options = listOf("Delete")
selector("Task Options", options) { j ->
var task=adapter.getItem(i)
todoAdapter?.delete(i)
//check if list is empty then show hint
showHideHintListView(this@listView)
longToast("Task ${task} has been deleted")
}
true
}
- todoAdapterはTodoAdapterクラスのオブジェクト。adapterでdeleteメソッドを呼ぶと、変更されている可能性を示すエラーが出る。そこでtodoAdapterでdeleteメソッドを呼ぶ必要がある。別の方法としてTodoAdapterにadapterを型変換する方法がある。Kotlinでは次のようにする。
(adapter as TodoAdapter)?.delete(i)
iはクリックされているアイテムの場所
- selectorはAnko dialogの1種で、クリック可能な項目を指定できるようにする。ここではオプション「デリート(消去)」だけを使う。ほかのオプションを選べるようにもできる。下に例を示す
verticalLayout { todoList=listView { adapter = todoAdapter onItemLongClick { adapterView, view, i, l -> val options = listOf("Completed","In Progress","Not Started","Delete") selector("Task Options", options) { j -> if (j == 3) { var task=adapter.getItem(i) todoAdapter?.delete(i) showHideHintListView(this@listView) longToast("Task ${task} has been deleted") }else{ longToast("Task ${adapter.getItem(i).toString()} has been marked as \"${options[j]}\"") } } true } } }.lparams { margin = dip(5) }
データベースの更新、ユーザーへの通知、あるいはそのほかのコードをトースト通知の代わりに実行して、タスク管理アプリの機能強化もできます。selecterダイアログが画面でどのように見えるか次に示します。
MainUIクラスのコードの完成版は次のようになります。
import android.support.v4.content.ContextCompat
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.ListView
import org.jetbrains.anko.*
import org.jetbrains.anko.appcompat.v7.toolbar
import org.jetbrains.anko.design.floatingActionButton
class MainUI(val todoAdapter: TodoAdapter) : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
return relativeLayout {
var todoList : ListView? =null
//textView displayed when there is no task
val hintListView = textView("What's your Todo List for today?") {
textSize = 20f
}.lparams {
centerInParent()
}
//function to show or hide above textView
fun showHideHintListView(listView: ListView) {
if (getTotalListItems(listView)>0) {
hintListView.visibility = View.GONE
} else {
hintListView.visibility = View.VISIBLE
}
}
//layout to display ListView
verticalLayout {
todoList=listView {
adapter = todoAdapter
onItemLongClick { adapterView, view, i, l ->
val options = listOf("Completed","In Progress","Not Started","Delete")
selector("Task Options", options) { j ->
if (j == 3) {
var task=adapter.getItem(i)
todoAdapter?.delete(i)
showHideHintListView(this@listView)
longToast("Task ${task} has been deleted")
}else{
longToast("Task ${adapter.getItem(i).toString()} has been marked as \"${options[j]}\"")
}
}
true
}
}
}.lparams {
margin = dip(5)
}
//Add task FloatingActionButton at bottom right
floatingActionButton {
imageResource = android.R.drawable.ic_input_add
onClick {
val adapter = todoList?.adapter as TodoAdapter
alert {
customView {
verticalLayout {
toolbar {
id = R.id.dialog_toolbar
lparams(width = matchParent, height = wrapContent)
backgroundColor = ContextCompat.getColor(ctx, R.color.colorAccent)
title = "What's your next milestone?"
setTitleTextColor(ContextCompat.getColor(ctx, android.R.color.white))
}
val task = editText {
hint = "To do task "
padding = dip(20)
}
positiveButton("Add") {
if(task.text.toString().isEmpty()) {
toast("Oops!! Your task says nothing!")
}
else {
adapter.add(task.text.toString())
showHideHintListView(todoList!!)
}
}
}
}
}.show()
}
}.lparams {
//setting button to bottom right of the screen
margin = dip(10)
alignParentBottom()
alignParentEnd()
alignParentRight()
gravity = Gravity.BOTTOM or Gravity.END
}
}.apply {
layoutParams = FrameLayout.LayoutParams(matchParent, matchParent)
.apply {
leftMargin = dip(5)
rightMargin = dip(5)
}
}
}
//function to get total number of items in list
fun getTotalListItems(list: ListView?) = list?.adapter?.count ?: 0
}
最後に
このタスク管理アプリを作るのにXMLのレイアウトリソースはまったく使っていませんが、同じようなスタイルのアプリをデザインできました。Ankoはデータを提示したり、ユーザーインタラクションに反応したり、データベースに接続したり、さらには、アプリのアクティビティやフラグメントの重荷から解放したりしてくれます。また、UIとアクティビティクラスを独立させることで、アプリをMVP(Model-View-Presenter)アーキテクチャーに近づけます。Ankoのさらに優れた特徴はこちらを参照してください。
コンパイルに時間がかかったり、アプリのサイズが大きくなったりする欠点がありますが、コードの再利用、メンテナンス、テストに大きな効果があります。説明してきたようにKotlin-AnkoはAndroidのアプリを作るのに便利なツールです。
(原文:Building a UI with Kotlin and Anko)
[翻訳:関 宏也/編集:Livit]