2
8

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 1 year has passed since last update.

【AndroidStudio】StateFlowとViewModelのサンプル【Kotlin】

Last updated at Posted at 2022-01-16

はじめに

これからの時代はLiveDataでModelViewだ!という記事を見て勉強してたのに、最近はLiveDataも古くてStateFlowってやつが最新らしいです。

StateFlowの記事はたくさんありましたが、LiveDataすら知らない自分には難しいものが多かったので初心者向けとしてStateFlowとViewModelが動くサンプルを作りました。

作ったもの

スイッチに連動するテキストと、2つの入力をまとめて表示するテキストを作りました。
具体的な処理はすべてViewModelで書いています。
Videotogif.gif

知識

ViewModelって何?

今までActivityクラスで書いていたUI周りの処理をViewModelで書くことで、UIと変数の値を常に一致させたりActivityクラスに書く処理を少なくするのが目的みたいです。

DataBindingの双方向と単方向

変数の変更をUIに反映するけどUI側からの変更は変数に反映しないのが単方向、UI側の変更を変数に反映するのが双方向です。

flowって何?

その前にコルーチンって何?

別スレッドを使ったりして非同期に処理を行う仕組みらしいです。flowもコルーチンの1つです。

コルーチンは"どのスレッドで動かすか"や"どのライフサイクル(スコープ)に合わせるか"などの情報を含みます。
UI処理はMainスレッドでやらないといけないとか、正しいスコープを選ばないとメモリリークするとかあるらしいです。

flowってなに?

発信側と受信側があって、発信側がemit()をしたときに受信側が存在していれば処理が流れていく仕組みらしい。
上流の処理をおこなうとそこから流れるように自動で下流も処理を行っているイメージ?

StateFlowってなに?

状態を表すflow、初期値が存在したりflowと少し違う。
ViewModelで使えるのはこれ。

実装の解説

gradle

コルーチンとdata bindingを使うにはgradleファイルに追記する必要があります。

build.gradle(Module:.app)

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions' //kotlinの補完機能を使うのに必要
    id 'kotlin-kapt' //Data Bindingに必要
}

android {
  ()
    buildFeatures {
        dataBinding true
    }

}

dependencies {

    ()
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"   //コルーチンに必要
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' //コルーチンに必要 
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' //viewModelScopeに必要
}

ViewModel

ViewModelの変数はxmlやActivityで設定することでUIと連動するようになります。
ここではViewModel側の処理の流れを説明します。

以下はスイッチに連動するテキストのコードです。

MyViewModel.kt
    val switch:MutableStateFlow<Boolean> = MutableStateFlow(true) //双方向bindingの変数

    //switchのON OFFに合わせてテキストを変更する
    val onOff:StateFlow<String> = switch.map {
        if(it){"ON"}else{"OFF"}
    }.stateIn(viewModelScope, SharingStarted.Eagerly,"...")

MutableStateFlowは値の変更ができるStateFlowで、UIの変更を変数に反映する双方向的なdata bindingをおこなうにはこれを使います。これをxmlと結びつけることでUIの変更に合わせて変数も変化するようになります。

MutableStateFlowであるswitchは、値が変更されたときに処理を流します。それをonOffが受け取ることでswitchに連動した処理を行うことができるようになります。

switchの変更をonOffが受け取り、onOffの値を変化させています。onOffのmap関数内でのitはswitchから流れてきたBooleanが入ります。ちなみにKotlinのif文は式なのでif(it){"ON"}else{"OFF"}は三項演算子 it ? "ON" : "OFF" と等価です。

mapの返り値はflowなのでstateInでStateFlowに変換します。引数viewModelScopeはstateflowの生存期間を表します。
viewModelScopeは(おそらく)ViewModelが破棄されるタイミングで同時に破棄されます。
SharingStarted.Eagerlyはよくわかりません....
"..."はStateFlowの初期値になりますswitchから変更が1回も流れてきてないときはこの初期値が呼ばれます。

xmlの設定

xmlも普段使っているものとは変わってきます。
activity_main.xmlの1行目で右クリック → Show Context Actions → Combart to data binding layoutをクリックしてください。

スクリーンショット 2022-01-17 001013.png

するとタグが出現するのでそこにMyViewModelを登録しましょう

activity_main.xml
    <data>
        <variable
            name="viewModel"
            type="com.example.myapplication.MyViewModel" />
    </data>

ここでnameはxml内で使う変数名なので(おそらく)何でも良いですが、typeはMyViewModelを作った場所を入力してください。
自分のMyViewModelの場所は以下のようになっています。

スクリーンショット 2022-01-17 002649.png

そうしたらあとはViewModelとbindしたい値に"@{viewModel.(変数名)}"を入れていきます。
双方行data bindingの場合は"@={viewModel.(変数名)}"の形になります。

Activity

ActivityでxmlとViewModelをくっつけます。
詳しいことはよくわからないのでおまじないだと思って書いています。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel:MyViewModel=ViewModelProvider(this).get(MyViewModel::class.java)
        val _binding: ActivityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main)
        _binding.viewModel=viewModel
        _binding.lifecycleOwner=this
    }

コード全文

build.gradle(Module:.app)
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions' //kotlinの補完機能を使うのに必要
    id 'kotlin-kapt' //Data Bindingに必要
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.myapplication"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        dataBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"   //コルーチンに必要
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' //コルーチンに必要
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' //viewModelScopeに必要
}
activity_main.xml
<?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.example.myapplication.MyViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Switch
            android:id="@+id/switch1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dp"
            android:checked="@={viewModel.switch}"
            android:minHeight="48dp"
            android:text="Switch"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/onOff" />

        <TextView
            android:id="@+id/onOff"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100dp"
            android:text="@{viewModel.onOff}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/combinerText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100dp"
            android:text="@{viewModel.combineText}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/switch1" />

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/combinerText">

            <EditText
                android:id="@+id/leftText"
                android:layout_width="133dp"
                android:layout_height="52dp"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:ems="10"
                android:inputType="textPersonName"
                android:text="@={viewModel.leftText}" />

            <EditText
                android:id="@+id/rightText"
                android:layout_width="133dp"
                android:layout_height="52dp"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:ems="10"
                android:inputType="textPersonName"
                android:text="@={viewModel.rightText}" />
        </LinearLayout>

        <Button
            android:id="@+id/resetButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="RESET"
            android:onClick="@{()->viewModel.reset()}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/linearLayout" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MyViewModel.kt
package com.example.myapplication//ここは自分の方のものを使ってください

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class MyViewModel:ViewModel() {

    val switch:MutableStateFlow<Boolean> = MutableStateFlow(true) //双方向bindingの変数

    //switchのON OFFに合わせてテキストを変更する
    val onOff:StateFlow<String> = switch.map {
        if(it){"ON"}else{"OFF"}
    }.stateIn(viewModelScope, SharingStarted.Eagerly,"...")


    val leftText:MutableStateFlow<String> = MutableStateFlow("")
    val rightText:MutableStateFlow<String> = MutableStateFlow("")

    //leftTextとrightTextのどちらかが変更されたらcombineTextを更新
    val combineText:StateFlow<String> = channelFlow<String> {
        launch {
            leftText.collect{
                send(it)
            }
        }
        launch {
            rightText.collect(){
                send(it)
            }
        }
    }.map{
        "${leftText.value}${rightText.value}"
    }.stateIn(viewModelScope, SharingStarted.Eagerly,"")

    //leftTextとrightTextをリセットする。valueが変更されるとcombineTextにflowが流れてcombineTextも自動的に変更される
    fun reset(){
        leftText.value=""
        rightText.value=""
    }



}


MainActivity.kt
package com.example.myapplication//ここは自分の方のものを使ってください

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel:MyViewModel= ViewModelProvider(this).get(MyViewModel::class.java)
        val _binding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
        _binding.viewModel=viewModel
        _binding.lifecycleOwner=this
    }
}

最後に

正直この書き方でメモリリークをするかどうか分からないのでちゃんとやりたい方はStateFlowのメモリリークについて調べたほうが良いです。

2
8
1

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
2
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?