今回の仕様
メールアドレス = 入力されている
パスワード = 6文字以上
この2つが満たされているときだけログインボタンが押せるような、よくあるログイン画面を作りたいと思います。
環境
Android Studio 3.5.2
その他はbuild.gradle参照してください。
必要な知識
- Android Studioを使える
- kotlinを書ける
- activityとfragmentを使ったことがある
リポジトリ
iwahara/button_enabled_sample: for Advent Calendar 2019
はじめに
今回はAndroidXを使うので、プロジェクトを作る際にそれを有効化します。
Fragment+ViewModelを選ぶとはじめから画面表示まで揃ってるのでやりやすいです。
プロジェクトの設定でAndroidXを使う設定を忘れずに。
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
タグにし、その中でdata
とConstraintLayout
を定義するようにします。
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
}
実際に動かす
ここまで来たら動くようになっているはずです。
ここまでボイラープレートをなくすことが出来るとは、便利な世の中になりましたね。