8
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 5 years have passed since last update.

Android #2Advent Calendar 2018

Day 14

NonNullとして扱えるAndroid Architecture ComponentsのLiveDataを作る

Last updated at Posted at 2018-12-13

Android Architecture ComponentsLiveData、便利ですよね。
しかしながら(特にKotlinから)使う場合にいつも気になるのが、valueをNonNullとして扱えない点です。
運用上Null非許容として扱っているのにLiveDataをかませるとアンラップする必要があり、フォースアンラップしてもいいのですが精神衛生上良くありません。
なのでLiveDataをちょこっと拡張してNonNullに限定したクラスを作ってみました。

1. NonNullなObserverを作る

まずそもそものLiveDataのObserverはJavaでプラットフォーム型で定義されており、これをどうにかしないとNonNullとしては扱えません。
以下がオリジナルのObserverコードです

Observer.java
/**
 * A simple callback that can receive from {@link LiveData}.
 *
 * @param <T> The type of the parameter
 *
 * @see LiveData LiveData - for a usage description.
 */
public interface Observer<T> {
    /**
     * Called when the data is changed.
     * @param t  The new data
     */
    void onChanged(T t);
}

なのでNonNullなアノテーションをつけたNonNullObserverを作成します。

NonNullObserver.java
public interface NonNullObserver<T> {
    void onChanged(@NonNull T t);
}

ちなみにJavaで書いているのはあえてで、SAM変換を出来るようにして既存のObserverと使い勝手をあわせるためです。
よりKotlinっぽく書きたい場合は独立したインターフェースとして定義せず、observer: ((T) -> Unit)とかのブロック構文を使うといいと思います

2. lateinitなLiveDataクラスを作る

次に作るのがNonNull…ではなくlateinit版LiveDataです。(LateInit版が不要であればNonNull版とマージしてしまってもよいかと思います)
Kotlinのlateinit varと同じように初期値を入れる前提でNonNullとして扱います。
また初期値を入れる前に取得しようとするとUninitializedPropertyAccessException()を発生させるように記述しています

LateInitLiveData.kt
open class LateInitLiveData<T> : LiveData<T>() {

    private val observers = mutableMapOf<NonNullObserver<T>, Observer<T>>()

    @MainThread
    @Deprecated(
        message = "use observe for NonNull.",
        replaceWith = ReplaceWith("observe(owner, NonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, observer)
    }

    @MainThread
    @Deprecated(
        message = "use observeForever for NonNull.",
        replaceWith = ReplaceWith("observeForever(nonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun observeForever(observer: Observer<in T>) {
        super.observeForever(observer)
    }

    @MainThread
    @Deprecated(
        message = "use observe for NonNull.",
        replaceWith = ReplaceWith("removeObserver(nonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun removeObserver(observer: Observer<in T>) {
        super.removeObserver(observer)
    }

    @MainThread
    open fun observe(owner: LifecycleOwner, nonNullObserver: NonNullObserver<T>) {
        val observer = Observer<T> { nonNullObserver.onChanged(it!!) }
        observers[nonNullObserver] = observer
        super.observe(owner, observer)
    }

    @MainThread
    open fun observeForever(nonNullObserver: NonNullObserver<T>) {
        val observer = Observer<T> { nonNullObserver.onChanged(it!!) }
        observers[nonNullObserver] = observer
        super.observeForever(observer)
    }

    @MainThread
    open fun removeObserver(nonNullObserver: NonNullObserver<T>) {
        val observer = observers[nonNullObserver]
        if (observer != null) {
            observers.remove(nonNullObserver)
            super.removeObserver(observer)
        }
    }

    override fun getValue(): T {
        return super.getValue() ?: throw UninitializedPropertyAccessException()
    }

    @MainThread
    public override fun setValue(value: T) {
        super.setValue(value)
    }

    public override fun postValue(value: T) {
        super.postValue(value)
    }

}

使用するObserverをNonNullObserverにするにあたり、新たにNonNullObserver用のメソッドを生やしています。
既存のObserverメソッドは紛らわしいので@Deprecated扱いにして見えなくしています。このへんはお好みです。
また、既存のObserverとNonNullObserverを紐付けてHashMapでメンバー変数として保持しているのはremoveObserverする際に実際のObserverを取り出すためです。

3. NonNullなLiveDataを作る

LateInit版を作ってしまえばNonNull版を作るのは簡単で、コンストラクタでの値代入を必須にするだけです

open class NonNullLiveData<T>(initialValue: T) : LateInitLiveData<T>() {

    init {
        value = initialValue
    }

}

使い方

NonNullObserver, LateInitLiveData, NonNullLiveDataを使うことでKotlinのNonNull構文やlateinitに近い形でLiveDataを扱うことが出来ます

MainViewModel.kt
class MainViewModel(application: Application) : AndroidViewModel(application) {

    val title = LateInitLiveData<String>() // or NonNullLiveData<String>("")

    init {
        title.value = "Main!" // Can't insert null
    }

}

MainActivity.kt
class MainActivity : AppCompatActivity() {

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

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

        viewModel.label.observe(this, NonNullObserver {
            title = it // it is NonNull!
        })
    }
}

通常のMutableLiveDataだと常にNonNullかどうかを意識する必要があり、nullセーフに扱うことが出来ませんでしたがNonNullLiveDataを用意してあげることでよりLiveData内ではnullを完全に排除する事ができ、安全にLiveDataが使えるようになります。
Kotlin言語を使うのであればなるべくNonNullに寄せたほうが事故が少ないので、個人的にはNonNullLiveDataを作ってからはそっちを使っており、MutableLiveDataはやむを得ずnull許可の値を受け付けるときだけ使うと行った使い方をしています。

おまけ(NonNullLiveEvent)

以前「一度だけ通知するAndroid Architecture ComponentsのLiveDataを作る」というタイトルでLiveEventという自作クラスを紹介したのですが、こちらのクラスも似たようなクラスを作ることでNonNullとして扱うことも出来ます。

NonNullLiveEvent.kt
open class NonNullLiveEvent<T> : LiveData<T>() {

    private val observers = mutableMapOf<NonNullObserver<T>, Observer<T>>()
    private val dispatchedTagSet = mutableSetOf<String>()

    @MainThread
    @Deprecated(
        message = "use observe for NonNull.",
        replaceWith = ReplaceWith("observe(owner, \"\", nonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        observe(owner, "", NonNullObserver { observer.onChanged(it) })
    }

    @MainThread
    @Deprecated(
        message = "use observeForever for NonNull.",
        replaceWith = ReplaceWith("observeForever(\"\", nonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun observeForever(observer: Observer<in T>) {
        super.observeForever(observer)
    }

    @MainThread
    @Deprecated(
        message = "use observe for NonNull.",
        replaceWith = ReplaceWith("removeObserver(nonNullObserver)"),
        level = DeprecationLevel.HIDDEN
    )
    override fun removeObserver(observer: Observer<in T>) {
        super.removeObserver(observer)
    }

    @MainThread
    @Deprecated(
        message = "should not use getValue() in LiveEvent.",
        level = DeprecationLevel.HIDDEN
    )
    override fun getValue(): T? {
        return super.getValue()
    }

    @MainThread
    open fun observe(owner: LifecycleOwner, tag: String, nonNullObserver: NonNullObserver<T>) {
        val observer = Observer<T> {
            val internalTag = owner::class.java.name + "#" + tag
            if (!dispatchedTagSet.contains(internalTag)) {
                dispatchedTagSet.add(internalTag)
                nonNullObserver.onChanged(it!!)
            }
        }
        observers[nonNullObserver] = observer
        super.observe(owner, observer)
    }

    @MainThread
    open fun observeForever(tag: String, nonNullObserver: NonNullObserver<T>) {
        val observer = Observer<T> {
            if (!dispatchedTagSet.contains(tag)) {
                dispatchedTagSet.add(tag)
                nonNullObserver.onChanged(it!!)
            }
        }
        observers[nonNullObserver] = observer
        super.observeForever(observer)
    }

    @MainThread
    open fun removeObserver(nonNullObserver: NonNullObserver<T>) {
        val observer = observers[nonNullObserver]
        if (observer != null) {
            observers.remove(nonNullObserver)
            super.removeObserver(observer)
        }
    }

    @MainThread
    open fun call(t: T) {
        dispatchedTagSet.clear()
        value = t
    }

}

参考

NonNull LiveData with Kotlin extension - ProAndroidDev

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