もうXMLを使わずにAndroidのUIが作れる!「Anko」って知ってる?

2017/05/16

Ankul Jain

74

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Androidが登場したときから、UIを作るにはXMLを使うのが普通でした。理屈ではJavaを使ってUIをプログラムできますが、あまり実用的ではありません。最近、ジェットブレインズがJava仮想マシンをターゲットとした近代的な言語Kotlinを開発しましたが、AndroidのUIプログラミングに役立ちそうです。

ジェットブレインズはより高速で使いやすいAndroidの開発スタイルとしてAnkoを発表しました。KotlinはAndroidの画面をデザインするために、DSL(ドメイン固有言語)としてAnkoライブラリーを提供しています。簡単な例を示します。

imageViewButtonからなる簡単な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を含む
  • linearLayouttextViewは拡張関数。拡張機能で新しい機能を持つクラスを作れる
  • texttextSizetypefaceandroid.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がどのように見えるかを次に示します。

DSL Layout Preview

出典: blog.jetbrains.com

対応する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 ホーム画面のデザイン
To-do Home

最初はホーム画面のタスクのリストは空です。従って、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の値でlistViewadapterを初期化します。

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()メソッドを加えることで、floatingActionButtononClickListenerを追加できます。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ダイアログボックスにテキストメッセージをセットし、postiveButtonnegativeButtonを配置する
  • verticalLayoutは向きが垂直なlinearLayout
  • toolbarを使ってダイアログにタイトルを追加。カスタマイズの一例として、ダイアログのビューに色を付けるには、backgroundColor=ContextCompat.getColor(ctx,R.color.colorAccentのようにする。
    ctxorg.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をクリックすると、次のダイアログが表示されます。

Add Task

タスクを追加すると、リストが見られます。

Task List

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
}
  • todoAdapterTodoAdapterクラスのオブジェクト。adapterdeleteメソッドを呼ぶと、変更されている可能性を示すエラーが出る。そこでtodoAdapterdeleteメソッドを呼ぶ必要がある。別の方法としてTodoAdapteradapterを型変換する方法がある。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ダイアログが画面でどのように見えるか次に示します。

Task Options

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

Copyright © 2017, Ankul Jain All Rights Reserved.

Ankul Jain

Ankul Jain

インド出身のフリーランスのテクニカルライター兼開発者です。新しい機会と技術を開拓するのが大好きで、現在はAndroidやほかのモバイルプラットホームに注目しています。scheduLAWyerの共同設立者で、アプリの開発もしています。

Loading...