3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android 初心者向けAdvent Calendar 2019

Day 18

ViewModel + LiveData + Databindingでボタンの活性制御

Last updated at Posted at 2019-12-17

今回の仕様

メールアドレス = 入力されている
パスワード = 6文字以上

この2つが満たされているときだけログインボタンが押せるような、よくあるログイン画面を作りたいと思います。

こんな画面を想定
qiita_ad2019.gif

環境

Android Studio 3.5.2
その他はbuild.gradle参照してください。

必要な知識

  • Android Studioを使える
  • kotlinを書ける
  • activityとfragmentを使ったことがある

リポジトリ

iwahara/button_enabled_sample: for Advent Calendar 2019

はじめに

今回はAndroidXを使うので、プロジェクトを作る際にそれを有効化します。
Fragment+ViewModelを選ぶとはじめから画面表示まで揃ってるのでやりやすいです。
プロジェクトの設定でAndroidXを使う設定を忘れずに。
configure_project.png

app/build.gradle

DataBindingを使うので有効化します。
kotlin-kaptプラグインとdataBindingの有効化を設定します。

//追加
apply plugin: 'kotlin-kapt'

android {

    //追加
    dataBinding {
        enabled = true
    }
}

また、依存ライブラリは以下の通りです。


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
    implementation 'com.google.android.material:material:1.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

MainViewModelクラス

MainFragmentのViewModelです。今回の肝となるクラスです。
表示に必要なロジックを書くクラスになります。
詳しい解説はあとにして、まずはクラス全体のコードです。

全体
package com.sample.buttonenabled.ui.main

import androidx.lifecycle.*

class MainViewModel : ViewModel() {

    val mailAddress = MutableLiveData<String>("")
    val password = MutableLiveData<String>("")
    val isSaveMailAddress = MutableLiveData<Boolean>(false)

    private val _isButtonLoginEnabled = MediatorLiveData<Boolean>()

    val isButtonLoginEnabled: LiveData<Boolean> = _isButtonLoginEnabled

    init {

        val observer = Observer<String> {
            val mail = this.mailAddress.value ?: ""
            val password = this.password.value ?: ""
            this._isButtonLoginEnabled.value = mail.isNotEmpty() && password.trim().length >= 6
        }
        _isButtonLoginEnabled.addSource(mailAddress, observer)
        _isButtonLoginEnabled.addSource(password, observer)
    }
}

解説

Observableな変数を定義

ここで各画面変数をLiveDataとして宣言します。
LiveDataを使うことで、画面にて入力があった際に自動的にこれらの変数に値が格納されるようになります(別途layoutでの設定が必要になりますが)。

基本的にはMutableLiveDataですが、最後のisButtonLoginEnabledだけMediatorLiveDataなので注意!


    val mailAddress = MutableLiveData<String>("")
    val password = MutableLiveData<String>("")
    val isSaveMailAddress = MutableLiveData<Boolean>(false)

    val isButtonLoginEnabled = MediatorLiveData<Boolean>()

initでMediatorLiveDataの設定を行う

MediatorLiveDataは複数のLiveDataをもとに値を更新したいときに使います。
今回であれば、メールアドレスとパスワードをもとに、ログインボタンの活性制御をしたい感じですね。
そのためには、まずandroidx.lifecycle.Observerを使って動作を定義し、isButtonLoginEnabledに監視したい値とともに設定する必要があります。
なお、Observerの型パラメータは監視されるLiveDataの型パラメータと一致する必要があります。
今回は両方ともStringだったので同じObserverを使いまわしていますが、もし違う場合はObserverを別で定義する必要があります。

本題からは外れますが、MediatorLiveDataをpublicにはせず、LiveDataとして露出しています。
これは、ViewModel内のデータでのみ更新可能にするためです。

init {

        val observer = Observer<String> {
            val mail = this.mailAddress.value ?: ""
            val password = this.password.value ?: ""
            this._isButtonLoginEnabled.value = mail.isNotEmpty() && password.trim().length >= 6
        }
        _isButtonLoginEnabled.addSource(mailAddress, observer)
        _isButtonLoginEnabled.addSource(password, observer)
    }

layout/main_fragment.xml

ViewModelをDataBindingして使うための設定が必要となります。
こちらも詳しい解説はあとにして、まずはコード全体です。

全体
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewmodel"
            type="com.sample.buttonenabled.ui.main.MainViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.login.LoginFragment">


        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <EditText
                android:id="@+id/edit_email"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="メールアドレス"
                android:text="@={viewmodel.mailAddress}"
                android:inputType="textEmailAddress"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/layout_mail_address">

            <EditText
                android:id="@+id/edit_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="パスワード"
                android:inputType="textPassword"
                android:text="@={viewmodel.password}"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>

        <androidx.appcompat.widget.AppCompatCheckBox
            android:id="@+id/check_save_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="メールアドレスを保存する"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/layout_password"
            android:checked="@={viewmodel.saveMailAddress}"
            />

        <Button
            android:id="@+id/button_login"
            android:enabled="@{viewmodel.isButtonLoginEnabled}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="ログイン"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/check_save_mail_address" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

解説

画面で使うViewModelの定義

まずは、ルート要素をlayoutタグにし、その中でdataConstraintLayoutを定義するようにします。
dataの子要素としてvariableを定義し、先程作ったMainViewModelを指定します。
nameは何でも良いのですが、ここではわかりやすくviewmodelとしています。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewmodel"
            type="com.sample.buttonenabled.ui.main.MainViewModel" />
    </data>

画面項目にViewModelの該当するLiveDataをBindする

android:text="@={viewmodel.mailAddress}"でviewModelのMailAdressをbindしています。
これで、メールアドレスの入力欄に何か入力されたら自動でViewModelにも設定されるようになります。
パスワードも同様に設定します。


        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_mail_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <EditText
                android:id="@+id/edit_email"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="メールアドレス"
                android:text="@={viewmodel.mailAddress}"
                android:inputType="textEmailAddress"
                android:maxLines="1" />

        </com.google.android.material.textfield.TextInputLayout>

Buttonの活性制御にViewModelのisButtonLoginEnabledをBindする

android:enabled="@{viewmodel.isButtonLoginEnabled}"をenabledにbindすることで、活性制御を自動で行うようにします。


        <Button
            android:id="@+id/button_login"
            android:enabled="@{viewmodel.isButtonLoginEnabled}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="ログイン"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/check_save_mail_address" />

MainFragmentクラス

ViewModelを作って保持し、ログインボタンを押したときの処理を書きます。
ロジックはViewModelに持っているので、ほぼ表示とイベント設定に関することのみ書きます。

詳しい解説はあとにして、まずはクラス全体のコードです。

全体
package com.sample.buttonenabled.ui.main

import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import com.sample.buttonenabled.R
import com.sample.buttonenabled.databinding.MainFragmentBinding
import kotlinx.android.synthetic.main.main_fragment.*

class MainFragment : Fragment() {
    private lateinit var binding: MainFragmentBinding

    companion object {
        fun newInstance() = MainFragment()
    }

    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this).get(MainViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewmodel = this.viewModel
        return binding.root
    }

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


        this.button_login.setOnClickListener {
            val email = this.viewModel.mailAddress.value
            val pass = this.viewModel.password.value
            Toast.makeText(requireContext(), "${email} : ${pass}", Toast.LENGTH_SHORT).show()
        }
    }

}

解説

ViewModelの取得

ViewModelは普通にインスタンス化するのではなく、ViewModelProvidersから取得するようにします。


    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this).get(MainViewModel::class.java)
    }

なお、2.2.0からはViewModelProviders.ofはdeprecatedになります。
推奨される方法は以下の記事を参考にしてください。

Android Jetpack(AndroidXライブラリ)の最近の更新 - Qiita

DataBindingの設定

MainFragmentBinding は事前にリビルドしないと出てこないので、一度リビルドしましょう。
inflaterの代わりにDataBindingUtil.inflateを使います。
その際にlifecycleOnwerとviewmodelを設定します。こうすると、ViewModelのライフサイクルが適切に処理されるようになります。

    private lateinit var binding: MainFragmentBinding


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewmodel = this.viewModel
        return binding.root
    }

実際に動かす

ここまで来たら動くようになっているはずです。
ここまでボイラープレートをなくすことが出来るとは、便利な世の中になりましたね。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?