KotlinとDataBindingでAndroidアプリを開発していて躓いたことをつらつらと書く

プログラミング 8月 11, 2016

0. まえがき

最近ブログの改良も飽きてきたので←、Androidアプリでも作ってみるかと思い、Android Studio 2を導入しました。


Android Studioとても良いですね。かなり昔Eclipseで開発していた時は、重いしわけわからんしで、ホントに簡単なアプリを作って挫折したのですが、Android StudioはUIもJetBrains製品と同じで、2.0からHotSwapとか色々入ってて良い感じです。


ちょろっと調べてみると、ちょっと前から話題になっていたKotlinで開発できること、Data Bindingが使えるようになっているようです。折角なので、新しい環境でfindByIdからおさらばしようと環境を作っていたのですが、意外とハマりまくったので、メモ書きとして残しておきます。



1. 2つのbuild.gradleの書き分け

KotlinやData Bindingの利用には、Gladleの設定を追加しないといけないのですが、まずここでかなり時間を食いました。


プロジェクトの構成上、build.gradleファイルは、rootディレクトリ(プロジェクトの名前のディレクトリ)と、root/appディレクトリの二箇所に存在します。


そして、色んなサイトにおいてここを明記せずに解説されていて、どっちのファイルに設定を書くかわかりませんでした。


そして僕が最終的に(?)行き着いたファイルが以下のようになりました。

buildscript {
    ext.kotlin_version = '1.0.3'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.2' 
        classpath "com.android.databinding:dataBinder:1.0-rc4"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
apply plugin: 'com.android.application'
apply plugin: 'com.android.databinding'
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "net.maytry.www.smartwiki"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
//  上記で apply plugin: 'com.android.databinding' を設定していたらここはいらないっぽい
//  dataBinding {
//      enabled = true
//  }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    kapt 'com.android.databinding:compiler:1.0-rc5' // これはここで書く
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'com.android.support:support-v4:23.1.1'
}
kapt {
    generateStubs = true
}
repositories {
    mavenCentral()
}

特に、

apply plugin: 'com.android.databinding'
dataBinding {enabled = true}

を同時に書いていて落ちたり、

kapt 'com.android.databinding:compiler:1.0-rc5'

の書く場所を間違えたりが多かったです。



2. @BindingAdapterがcompanion objectで使えない

xmlのアトリビュートにバインディングしたい時に、Javaでは@BindingAdapterを付けたstaticセッター等を用意するのが一般的なようです。


しかし、Kotlinではstaticは無く、代替としてcompanion objectを用います。ところが、上記の@BindingAdapterはこれでは動きません。以下のように書きます。

object CustomSetter {
    @JvmStatic
    @BindingAdapter("hogeList")
    fun setHogeList(listView: ListView, hogeList: List<String>) {
        val adapter: HogeAdapter = HogeAdapter(listView.context, R.layout.hoge_list_item, hogeList)
        listView.adapter = adapter
        listView.onItemClickListener = OnClickButton()
    }
}

@JvmStaticがポイントです。



3. FragmentのDataBindingがわからん

今回のDataBindingの使用で、FragmentにももちろんBindingできるようになったのですが、その使い方で悩みました。


例えばFragment自身がBindingする場合

class MyFragment : Fragment() {
    ...
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        val binding: FragmentMyBinding = FragmentMyBinding.bind(view)
        binding.hoge = hoge
    }
}

のように書けます。そして、もしこのFragmentが他のXMLから呼ばれた時などは、そのままバインディングされた値が表示されると思います。


固定値の表示などはこれで事足りますが、では呼び出し元のActivityから値を渡したい時はどうするのでしょうか。


以下に例を紹介します。

class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {

    private val mHogeList: MutableList<String> = mutableListOf()
    private val hogeList: List<String> = mHogeList

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        val fragment = HogeListFragment.newInstance(hogeList)
        supportFragmentManager.beginTransaction().add(R.id.content_main, fragment).commit()
...

    var genreList: List<Genre>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (arguments != null) {
            hogeList = arguments.getSerializable(ARG_PARAM1) as List<String>
        }
    }

    companion object {
        private val ARG_PARAM1 = "param1"

        fun newInstance(genreList: List<Genre>): GenreListFragment {
            val fragment = HogeListFragment()
            val args = Bundle()
            args.putSerializable(ARG_PARAM1, hogeList as Serializable)
            fragment.arguments = args
            return fragment
        }
    }
    ...
}

ここでは、FragmentのnewInstanceを介して、Activity側でインスタンスを作成しています。その時の引数に、Fragmentに持たせたい値をセットすることで、Fragment側はonCreateの中でセットすることができます。



4. ListViewのonItemClickListenerが動かない

これも結構ハマりました。ListViewの子要素にButton等のonClickListenerが設定できるものが入ると、ListViewのクリックイベントがブロックされるようです。


解決方法は、ListViewの親レイアウトに以下を設定すること(他にもあるが一例)。
android:descendantFocusability="blocksDescendants"


これでボタンのクリックイベントと共存できます。



5. Fragmentのイベントリスナーバインディング

クリックイベント等のリスナーももちろんバインディングできます。Activityはandroid:onClick=@{hoge}で上手く行くのですが、Fragmentの場合はapp:onClickListener=@{fuga}じゃないと上手くいきませんでした(自分だけ?)



9. あとがき

やはりWebと違ってアプリ系はデバッグが難しいですね。開発言語もKotlinで、まだまだナレッジが溜まっていない感じもあるので、Kotlinユーザの皆さんのお力をお借りして頑張っていきたいですね。



参考資料

slont

金融ベンチャーでWebエンジニア。美と酒とTechで生きてる。Vue.jsが至高。Elixir好き。個人事業とWebアプリ案件もやってます。 アプリ→https://app.cullet.me Android→https://play.google.com/store/apps/details?id=net.maytry.cullet.android

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.