月: 2021年9月

Kotlin1.5で追加されるvalue class

引用:https://kotlinlang.org/docs/whatsnew15.html#inline-classes

引用:https://kotlinlang.org/docs/inline-classes.html

今まで

今まではinline classでしたが、kotlin1.5からはvalue classに変更になります。inline classはdeprecatedになります。

使い方

@JvmInline
value class User(val id: String)

上記のように使います。但し条件があって

  • 単一のプロパティしか持てない
  • valのみしか持てない
  • 参照の比較ができない(===で比較できない)
  • Javaから直接使えない(オールKotlinプロジェクトではない場合は注意)

data classと何が違う?

これだけだとdata classと何が違うのかわからないのですが、value classを使うとdata classよりパフォーマンスが良いです。よって単一のプロパティのみの場合はvalue classを使う、それ以外はdata classを使えば良いと思います。

typealiasで同じことできるけど?

typealias UserId = Int

fun changeUserId(userId: UserId) {
    // ...
    println(userId)
}

fun main() {
    val userId: UserId = 0
    changeUserId(userId)
    
    val intUserId: Int = 1
    changeUserId(intUserId)
}

https://pl.kotl.in/HYoV8l5k1

fun changeUserId()はUserId型の引数を期待していますが、Int型を渡しても動いてしまいます。これは実装を強制しているのに別の型も渡せてしまうためバグの温床になります。UserId型で渡すようにしましょう。

//typealias UserId = Int
@JvmInline
value class UserId(val userId: Int)

fun changeUserId(userId: UserId) {
    // ...
    println(userId)
}

fun main() {
    val userId: UserId = UserId(0)
    changeUserId(userId)
    
    val intUserId: Int = 1
    changeUserId(intUserId)	// compile時error
}

https://pl.kotl.in/Z6bhnLjMa

このようにコンパイル時に型が合わないため、エラーとなります。アプリ実行前に間違った引数を渡していることがわかるのでtypealiasよりvalue classを使うようにしましょう。


ViewModel内でUiModel(UiState)を使おう(その2)[UiModel編]

はじめに

前回でsealed classを使ってViewModelをすっきり書けるようになりました。今回はさらにすっきり書けるようにUiModelについて説明します。

対象

  • ViewModelを利用している方
  • MediatorLiveDataを利用している方
  • kotlinが読める方

ViewModel、MediatorLiveDataについてはドキュメントがネット上に多くありますので、ここでは説明しません。


LiveDataが複数必要になった

前回はうまくsealed classを使って1つのLiveDataに複数の意味を持たせて、まとめることができました。しかしこれ以上にLiveDataが必要になった場合はどうしましょう?1つの画面内でAPIから取得したデータをRecyclerViewを表示し、現在の状態をTextViewに表示し、Toolbarメニューの表示内容を切り替えることもあるでしょう。そうすると複数のLiveDataがどうしても必要になります。そのような場合にUiModelを使ってみましょう。

UiModelとは?

簡単に言うと「複数のLiveDataをMediatorLiveDataで合体させて、まとめて観測する仕組み」です。DroidKaigi2020のConferenceAppに使われているので、参考にすると良いです。UiModelを使うときはこの拡張関数をプロジェクト内に置きましょう。
UiModelについてはAndroidアプリエンジニア共通認識ではない為、複数人で開発しているプロジェクトに導入する際は注意しましょう。使いこなすのがちょっと難しいので開発スピードの鈍化やバグを生んでしまう可能性もあります。またMediatorLiveDataを使いこなしている人には不要かもしれません。

追加のLiveDataを用意する

前回の状態からさらに画面内にRecyclerViewにListを表示する必要があるとします

class MyViewModel : ViewModel() {
    private val _textState = MutableLiveData<TextState>(TextState.Loading)
    val textState: LiveData<TextState> = _textState
    private val _listItems = MutableLiveData<List<String>>(listOf())
    val listItems: LiveData<List<String>> = _listItems
    …
}

UiModelを用意する

MyViewModel内のLiveDataをまとめるMyUiModelクラスを定義します。

data class MyUiModel(
    val textState: TextState = TextState.Loading,
    val listItems: List<String> = listOf()
) {
    companion object {
        operator fun invoke(
            current: MyUiModel,
            textState: TextState,
            listItems: List<String>
        ) = MyUiModel(
            textState = textState,
            listItems = listItems
        )
    }
}  

ViewModelにUiModelを宣言する

class MyViewModel : ViewModel() {
    // UiModelを利用する時はMutableLiveDataの初期化時に初期データを入れてください
    private val _textState = MutableLiveData<TextState>(TextState.Loading)
    // UiModelを利用する時はLiveData不要
    //val textState: LiveData<TextState> = _textState

    // UiModelを利用する時はMutableLiveDataの宣言時に初期データを入れてください
    private val _listItems = MutableLiveData<List<String>>(listOf())
    // UiModelを利用する時はLiveData不要
    //val listItems: LiveData<List<String>> = _listItems

    val uiModel: LiveData<MyUiModel> by lazy {
        combine(
                MyUiModel(),
                _textState,
                _listItems
        ) { current, textState, listItems ->
            MyUiModel(
                    current = current,
                    textState = textState,
                    listItems = listItems
            )
        }
    }
    …
}

※MutableLiveDataを上記のように初期化するにはimplementation "androidx.lifecycle:lifecycle-extensions:$version"が必要です


FragmentでUiModelを観測する

class MyFragment : Fragment() {
    override fun onViewCreated((view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.uiModel.observe(viewLifecycleOwner) { uiModel ->
            // uiModel.textState表示処理
            textView.text = when (uiModel.textState) {
                is TextState.Loading -> "loading"
                is TextState.Loaded -> textState.text
            }
            // uiModel.listItems表示処理
            recyclerViewAdapter.notifyDataSetChanged()
            listItemsCount.text = uiModel.listItems.size.toString()
        }
    }
    …
}

ViewModel側でpostValueする

ViewModel側でデータの更新があった場合は_textState.postValue()、_listItems.postValue()を実行する度にUiModelを観測しているFragmentが発火します。
例えば初期状態から_textState.postValue(TextState.Loaded(“loaded”))を実行すると_textStateは初期状態のTextState.LoadingからTextState.Loadedに値が変化したので、_textState = TextState.Loaded(“loaded”)及び_listItems = listOf()がFragmentに返されます。
さらにその状態から_listItems.postValue(listOf(“first”, “second”))を実行すると_listItemsは初期状態のlistOf()からlistOf(“first”,”second”)に値が変化したので、textState = TextState.Loaded(“loaded”)及び_listItems = listOf(“first”,”second”)がFragmentに返されます。


まとめ

今まではデータの数が増える度にLiveDataとそれに対応するMutableLiveDataを用意していましたが、UiModelを使うことでViewModel内ではLiveDataのみ宣言しUiModelで合体、Fragment側ではLiveDataの数だけ観測するコードも不要になり、一度のviewModel.uiModel.observe(viewLifecycleOwner){}で済むようになりました。


ViewModel内でUiModel(UiState)を使おう(その1)[sealed class編]

はじめに

ViewModelは現在多くのプロジェクトで利用されているかと思いますが、LiveDataが多くなってくると管理するのが大変になってきます。最終的にはUiModel(UiState)を使ってLiveDataの数を少なくし、ViewModelに関するコードをすっきりさせるのが目的です。今回は最初の段階としてsealed classを使ってViewModelをきれいに書く方法です。

対象

  • kotlinが読める方
  • ViewModelを利用している方(MVVMのViewModelというよりはAndroidのViewModelライブラリ)


LiveDataの型をどうしていますか?

例えば画面に表示する文字列をViewModelで管理する場合、下記のようにLiveDataを用意します。

class MyViewModel : ViewModel() {
    private val _text = MutableLiveData<String>("") // 初期化方法は導入しているライブラリによっても違うのでお好きな方法で
    val text: LiveData<String> = _text
    …
}

開発中や開発初期の場合はこれでも良いですが、あとから「文字列はサーバから通信して取得したいな」とか「文字列を使用したら空文字にしたりデフォルト値にしたい」とかいろんな要望があとから出てくることもあると思います。今回は通信中を表すisLoadingを下記のように実装します。

class MyViewModel : ViewModel() {
    private val _text = MutableLiveData<String>("")
    val text: LiveData<String> = _test
    private val _isLoading = MutableLiveData<Boolean>(false)
    val isLoading: LiveData<Boolean> = _isLoading
    …
}


LiveDataに状態を持たせる

まだLiveDataは2つなので、そんなに問題にはなりませんが、GUI上で表示したいデータや扱いたいデータが多くなってくるとLiveDataの数がどんどん増えてしまいます。このような場合は新しくsealed class、object、data classを利用してすっきり書けるようになります。下記のようなクラスを用意します。

sealed class TextState {
    data class Loaded(val text: String) : TextState()
    object Loading : TextState()
}


ViewModelをすっきりさせる

TextStateを作ったことにより、MyViewModelは下記のように書き直すことができます。

class MyViewModel : ViewModel() {
    private val _textState = MutableLiveData<TextState>(TextState.Loading)
    val textState: LiveData<TextState> = _textState
    …
}

このように実装することでLiveDataの数が一つ減って文字列と状態を1つのLiveDataで管理できるようになったので、すっきりしました。あとは通信中の場合_textState.postValue(TextState.Loading)、通信完了時は_textState.postValue(TextState.Loaded(“complete”))とするとLiveDataを観測する側は通信中、通信完了及び取得した文字列を扱えるようになりました。

class MyFragment : Fragment() {
    override fun onViewCreated((view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        myViewModel.textState.observe(viewLifecycleOwner) { textState ->
            textView.text = when (textState) {
                is TextState.Loading -> "loading"
                is TextState.Loaded -> textState.text
                // sealed classなのでelseを書く必要がありません
            }
        }
        /*
        これが不要になりました。
        myViewModel.isLoading.observe(viewLifecycleOwner) {
        }
        */
    }
    …
}

まとめ

LiveData内の変数の型をこのように変更するとViewModel内のコードが短くなり、それを観測する側のFragmentやActivityもLiveDataの数だけobserveする必要がなくなったので、見通しが良くなったのではないでしょうか?もし通信に失敗した場合の状態も追加したいのであればTextStateにobject LoadError : TextState()とかを追加すればコードの変更量を少なく、エラー表示にも対応でき、変更に強いViewModelを作ることが可能です。