読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】【Kotlin】DataBindingとListView(和歌山トイレマップで遊んでみる 10)

Android Kotlin

はじめに

久々復活。最後の記事からふと思い立ってコードをKotlinに置き換えたりしていたのですが、しばらく放置したままでした。
7月にKansai.ktも開催されるということで、そろそろちゃんとやろうではないか、ということで色々トライしております。

今回は、「このアプリについて」画面をDataBindingとListViewを使って書きなおしてみた時のあれこれを書き留めていきます。

JavaからKotlinに置き換えたところは、基本的にはConverte機能を使っただけですので、特に引っかかったりした部分についてのみ取り上げていく予定です。

DataBindingを使う

KotlinでAndroidアプリを書く壁になっていたと思われるのがこのDataBinding。

前回と同じように書いても、クラス名 + Binding という名前のクラスが生成されず、 Javaで専用のクラスを作成して、Kotlinからはそれを呼び出して利用していたようです。

現在はというと、以下のように書くだけでJavaで書く場合と同じように使うことができました。

app / src / build.gradle

    dataBinding {
        enabled = true
    }
}
~省略~
dependencies {
~省略~
    kapt 'com.android.databinding:compiler:1.0-rc5'
~省略~
}
kapt {
    generateStubs = true
}

生成されたクラス

今回は後述するListViewの表示アイテムに使用しました(フラグに合わせて要素の表示・非表示をしたかったため)。

まず対象のLayoutファイルから。

about_app_listitem.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android" >
    < data >
        < import type="android.view.View"/ >
        < variable
            name="aboutappitemclass"
            type="jp.searchwakayamatoilet.AboutListItem"/ >
    < /data >
< LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
    < TextView
        android:id="@+id/about_list_area_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/aboutapp_visible_item_margin"
        android:text="@{aboutappitemclass.areaTitle}"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textStyle="bold"
        android:visibility="@{aboutappitemclass.hasAreaTitle? View.VISIBLE: View.GONE}"/ >
    < TextView
        android:id="@+id/about_list_item_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/aboutapp_visible_item_margin"
        android:text="@{aboutappitemclass.itemTitle}"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="@android:color/black"
        android:textStyle="bold"
        android:visibility="@{aboutappitemclass.hasItemTitle? View.VISIBLE: View.GONE}" / >
    < TextView
        android:background="#E5F3FF"
        android:id="@+id/about_list_item_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="0dp"
        android:padding="10dp"
        android:text="@{aboutappitemclass.description}" / >
    < TextView
        android:autoLink="web"
        android:background="#E5F3FF"
        android:id="@+id/about_list_item_link"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="0dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="10dp"
        android:padding="10dp"
        android:text="@{aboutappitemclass.link}"/ >
< /LinearLayout >
< /layout >

で、ListViewのAdapterにてDataBindingクラスを生成します。

AboutAppDataAdapter.kt

import android.content.Context
import android.databinding.DataBindingUtil
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import jp.searchwakayamatoilet.databinding.AboutAppListitemBinding
import java.util.ArrayList

class AboutAppDataAdapter(context: Context, newListItemList: ArrayList) : BaseAdapter() {
    lateinit private var layoutInflater :LayoutInflater
    lateinit private var aboutDataList: ArrayList
    init{
        layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        aboutDataList = newListItemList
    }
    override fun getCount(): Int{
        return aboutDataList.count()
    }
    override fun getItem(position: Int): AboutAppListItem {
        return aboutDataList.get(position)
    }
    override fun getItemId(position: Int): Long{
        return position.toLong()
    }
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
        val binding: AboutAppListitemBinding? = DataBindingUtil.inflate(layoutInflater, R.layout.about_app_listitem, parent, false)

        // ※エラーは無視する.
        binding?.aboutappitemclass = aboutDataList.get(position)
        return binding?.root
    }
}

エラー

KotlinでDataBindingを使用すると、生成されるクラス(今回は「AboutAppListitemBinding」)で「cannot access class 'jp.searchwakayamatoilet.AboutAppListItem'. Check your module classpath for missing or conflicting dependencies」というエラーが表示されます。
またそれに関連してか、生成したクラスで「binding.root」としてRootViewを取得しようとすると、「Unresolved reference」とエラーが表示されます。

前者の方はIssue報告が上がっているようで、実はエラーが出たままでもビルド、実行が可能です。

https://youtrack.jetbrains.com/issue/KT-12402#u=1463619483291

なのでかなり気にはなりますが、この部分でのエラーは無視します。

ListView

これまでアプリについての紹介やクレジットを表示する画面では、一つの項目ごとにTextViewを作成して表示していました。
しかし流石にこれでは変更がしづらい…ということで、ListViewで置き換えることにしました。

RecyclerViewにしなかったのは、このアプリがAndroid 4.1以上を対象としているためです。

Item、Adapterを作成する

RecyclerViewと同じく、ListViewに表示するItem、Adapterを作成します。

Layoutはabout_app_listitem.xmlを参照してください。

AboutAppListItem.kt

import android.databinding.BaseObservable
import android.databinding.Bindable

class AboutAppListItem(newAreaTitle: String, newHasAreaTitle: Boolean
                       , newItemTitle: String, newHasItemTitle: Boolean, newDescription: String, newLink: String): BaseObservable(){
    private var areaTitle = ""
    private var hasAreaTitle = false
    private var itemTitle = ""
    private var hasItemTitle = false
    private var description = ""
    private var link = ""

    init{
        areaTitle = newAreaTitle
        hasAreaTitle = newHasAreaTitle
        itemTitle = newItemTitle
        hasItemTitle = newHasItemTitle
        description = newDescription
        link = newLink
    }
    @Bindable fun getAreaTitle(): String{
        return areaTitle
    }
    @Bindable fun getHasAreaTitle(): Boolean{
        return hasAreaTitle
    }
    @Bindable fun getItemTitle(): String{
        return itemTitle
    }
    @Bindable fun getHasItemTitle(): Boolean{
        return hasItemTitle
    }
    @Bindable fun getDescription(): String{
        return description
    }
    @Bindable fun getLink(): String{
        return link
    }
}

AdapterはAboutAppDataAdapter.ktを参照してください。

で、Activityからデータを突っ込みます。

activity_aboutapp.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="jp.searchwakayamatoilet.AboutAppActivity" >
    < data >
        < variable
            name="aboutappclass"
            type="jp.searchwakayamatoilet.AboutAppActivity" / >
    < /data >
    < RelativeLayout
        android:id="@+id/about_activity"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        < ListView
            android:id="@+id/about_listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="70dp"
            android:divider="@null"/ >
        < include layout="@layout/layout_toolbar" / >
    < /RelativeLayout >

  • ListViewのdeviderをNullにすることで、Item同士の枠線を非表示にできます。

AboutAppActivity.kt

import android.databinding.DataBindingUtil
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.MenuItem
import android.widget.ListView
import jp.searchwakayamatoilet.databinding.ActivityAboutappBinding
import java.util.ArrayList

class AboutAppActivity : AppCompatActivity() {
    lateinit private var binding: ActivityAboutappBinding
    private var homeId = -1
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_about)
~省略~

        binding.aboutListview.scrollBarStyle = ListView.SCROLLBARS_OUTSIDE_OVERLAY;

        // ListViewにアイテムを追加する.
        val aboutDataList: ArrayList = ArrayList()
        // 第二引数: 大項目(このアプリについてorCredit)のタイトルがある場合はTrue.
        // 第四引数: 小項目(Creditの各項目名)のタイトルがある場合はTrue.
        aboutDataList.add(getItem(getString(R.string.about_title)
                , true
                , ""
                , false
                , getString(R.string.about_description)
                , getString(R.string.about_project_url)))

        aboutDataList.add(getItem(getString(R.string.about_credits_title)
                , true
                , getString(R.string.about_credits_title_toiletmap)
                , true
                , getString(R.string.about_credits_toiletmap)
                , getString(R.string.about_credits_toiletmap_url)))

        aboutDataList.add(getItem(""
                , false
                , getString(R.string.about_credits_title_kotlin)
                , true
                , getString(R.string.about_credits_kotlin)
                , getString(R.string.about_credits_kotlin_url)))

        aboutDataList.add(getItem(""
                , false
                , getString(R.string.about_credits_title_rxjava)
                , true
                , getString(R.string.about_credits_rxjava)
                , getString(R.string.about_credits_rxjava_url)))

        aboutDataList.add(getItem(""
                , false
                , getString(R.string.about_credits_title_rxandroid)
                , true
                , getString(R.string.about_credits_rxandroid)
                , getString(R.string.about_credits_rxandroid_url)))

        aboutDataList.add(getItem(""
                , false
                , getString(R.string.about_credits_title_lightweightstreamapi)
                , true
                , getString(R.string.about_credits_lightweightstreamapi)
                , getString(R.string.about_credits_lightweightstreamapi_url)))

        // 作成したデータをAdapterにセットする.
        binding.aboutListview.adapter = AboutAppDataAdapter(this, aboutDataList)
    }
~省略~
    private fun getItem(areaTitle: String, hasAreaTitle: Boolean, itemTitle: String, hasItemTitle: Boolean
                        , description: String, link: String): AboutAppListItem {
        val newItem = AboutAppListItem(areaTitle, hasAreaTitle, itemTitle, hasItemTitle
                , description, link)

        return newItem
    }
}

ListViewのItemクラスをArrayとしてデータを作成し、それを格納したAdapterをListViewにセットします。

ListViewのgetView

ListViewにAdapterが追加され、表示されるときにAboutAppDataAdapter.ktの「getView」が呼ばれます。

この時2つハマったことがありました。

1. 第二引数のViewをNull許容にしていなかった

最初はgetViewの「getView(position: Int, convertView: View?, parent: ViewGroup)」の2つ目の引数をNull許容にしておらず、Activityを切り替えてListViewが表示されるときにIllegalStateExceptionが発生しました。

2. データの更新を引数のViewがNullの時にしか行っていなかった

最初は以下のように、第二引数のViewがNullの場合のみDataBindingクラスを生成し、値をセットする、という処理を行っていました。

AboutAppDataAdapter.kt(失敗)

~省略~
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
        if(convertView
        val binding: AboutAppListitemBinding? = DataBindingUtil.inflate(layoutInflater, R.layout.about_app_listitem, parent, false)

        // ※エラーは無視する.
        binding?.aboutappitemclass = aboutDataList.get(position)
        return binding?.root
    }
}

エラーは発生せず表示はされるのですが、ListViewがデフォルトの位置にある場合に表示されないItem(下の方に表示されていて、スライドすると見える)が、何故かセットしているArrayの最初のものに置き換わってしまう、という問題が発生しました。

ググッてみたところ、getViewが呼ばれた時は常に値を更新する必要がある、とのことでした。

で、Javaの場合は引数のViewに値を入れて、それを戻り値として返すことができるのですが、Kotlinではvalとして扱われていて値の代入ができません。

ということで、ViewがNullかどうかにかかわらず生成したDataBindingクラスのrootを返すことにしました。

…毎回DataBindingクラスを生成するのはどうよ?と思ったのですが、初期化時に一度だけ生成しようとすると、大きな空白が表示されたり、最後のアイテム以外が表示されなかったりと面白いことになってしまったため、断念しました。

AboutAppDataAdapter.kt

~省略~
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
        val binding: AboutAppListitemBinding? = DataBindingUtil.inflate(layoutInflater, R.layout.about_app_listitem, parent, false)

        // ※エラーは無視する.
        binding?.aboutappitemclass = aboutDataList.get(position)
        return binding?.root
    }
}

参考

DataBinding

ListView