ViewModel+LiveData+Databindingを使ってみる


キーボードが表示されているときだけViewを非表示にするというサンプルです。

ソースコード

設定

build.gradle
//略
    dataBinding {
        enabled = true
    }
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$rootProject.kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
    implementation 'androidx.core:core-ktx:1.1.0-alpha05'

    //lifecycle
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha02'
}

dataBindingを有効にして
androidx.lifecycle:lifecycle-extensionsをインポートしています。
androidxを使わない場合は
android.arch.lifecycle:extensionsをインポートします。

AndroidManifest.xml
    <activity
            android:name="com.ringored.view.StartActivity"
            android:windowSoftInputMode="adjustResize"
    >

windowSoftInputMode="adjustResize"にすることでキーボードが表示される度にキーボードの領域を除いた画面サイズで再描画します。

ViewModel

MainViewModel.kt
class MainViewModel(application: Application) : AndroidViewModel(application) {
    val isKeyboardShowing: MutableLiveData<Boolean> by lazy {
        MutableLiveData<Boolean>()
    }
}

AndroidViewModelを継承した自作のViewModelに
isKeyboardShowingというBooleanのMutableLiveDataを作成します。
あとでこのisKeyboardShowingをレイアウトからobserve(観察)します

View

MainActivity.kt
class StartActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
            showLogin()
        }
    }
    fun showLogin(){
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragment_container, MainFragment(), TAG_OF_FRAGMENT)
            .commit()
    }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/fragment_container"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:orientation="vertical">
</FrameLayout>

ActivityはFragmentをTransactionしているだけ
Activityに描画する場合は以下のFragmentをActivityに置き換えます。

MainFragment.kt

const val TAG_OF_FRAGMENT = "MainFragment"

class MainFragment : Fragment() {
    private lateinit var binding: FragmentMainBinding
    private var parent: ViewGroup? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)
        binding.lifecycleOwner = this
        binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        setKeyboardStatusObserver()
        return binding.root
    }

    //keyboardの状態を監視してliveDataに通知
    private fun setKeyboardStatusObserver() {
        parent = activity!!.fragment_container as ViewGroup
        parent?.viewTreeObserver?.addOnGlobalLayoutListener {
            parent?.let {
                //rootViewの高さを比較して200px以上あればimeが表示されているとみなす
                binding.viewModel?.isKeyboardShowing?.postValue(it.rootView.height - it.height > 200)
            }
        }
    }
}

bindingの型はxmlの名前をキャメルケースにして後ろにBindingをつけたものがビルド時に自動生成されます。
fragment_main-> FragmentMainBinding
うまくインポートできない場合はBuild->Rebuild Projectしてください。

binding.lifecycleOwner = this

lifecycleOwnerを自分のフラグメントに設定します。
これしないと動きません。

binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

ViewModelProvidersからViewModelを取得してbindingにセットします。

parent?.viewTreeObserver?.addOnGlobalLayoutListener {
            parent?.let {
                //rootViewの高さを比較して200px以上あればimeが表示されているとみなす
                binding.viewModel?.isKeyboardShowing?.postValue(it.rootView.height - it.height > 200)
            }
        }

viewが再描画される度にviewModelのisKeyboardShowingを更新します。

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

    <data>
        <import type="android.view.View"/>
        <variable name="viewModel"
                type="jp.co.synchrolife.vm.start.login.LoginViewModel" />
    </data>    
    <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:visibility="@{viewModel.isKeyboardShowing ? View.GONE: View.VISIBLE}"
                app:srcCompat="@drawable/fruit_apple"/>
        <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="100dp"
                android:hint="パスワード"
                style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
            <com.google.android.material.textfield.TextInputEditText
                    android:singleLine="true"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"/>
        </com.google.android.material.textfield.TextInputLayout>
    </LinearLayout>
</layout>

全体をlayoutタグで囲み
View.GONE,VISIBLEを使用するためにandroid.view.Viewをインポートします
MainViewModelをobserveするためvariableにパッケージを追加します。

android:visibility="@{viewModel.isKeyboardShowing ? View.GONE: View.VISIBLE}" 

これでLiveData(isKeyboardShowing)が更新される度、GONE VISIBLEを切り替えてくるようになりました。