StateFlowとDatabindingを試してみる
先日、Androud StudioのCanaryリリースでStateFlowがDataBindingに対応したので試してみました。
今回確認したのは以下のみになります
- StateFlowを使ってDataBindingが使えることを確認
- MutableStateFlowを使って双方向DataBindingができることを確認
実際のソース
今回はViewModelを以下のように定義しました。
class MainViewModel : ViewModel() { val editTextFirst = MutableStateFlow("") val editTextSecond = MutableStateFlow("") private val first = editTextFirst.map { it.toFloatOrNull() } private val second = editTextSecond.map { it.toFloatOrNull() } private val result = first.combine(second) { a, b -> a ?: return@combine null b ?: return@combine null a * b } private val _text = MutableStateFlow("") val text = _text.asStateFlow() init { viewModelScope.launch { result.collect { _text.value = it.toString() } } } }
(result: Flow
あとは、LiveDataを使うときと同じようにレイアウトファイルに設定してあげます。
<TextView android:id="@+id/message" android:layout_width="0dp" android:layout_height="wrap_content" android:text="@{viewModel.text}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/edit_text_first"/> <EditText android:id="@+id/edit_text_first" android:layout_width="0dp" android:layout_height="wrap_content" android:inputType="numberDecimal" android:text="@={viewModel.editTextFirst}" app:layout_constraintTop_toBottomOf="@id/message" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/edit_text_second"/> <EditText android:id="@+id/edit_text_second" android:layout_width="0dp" android:layout_height="wrap_content" android:inputType="numberDecimal" android:text="@={viewModel.editTextSecond}" app:layout_constraintTop_toBottomOf="@id/message" app:layout_constraintStart_toEndOf="@id/edit_text_first" app:layout_constraintEnd_toEndOf="parent"/>
これによって、LiveDataと同じようにDataBindingできることが確認できました。
AndroidのNavigationについて調べてみた。
きっかけ
特定の状況下で、画面が遷移するような実装をしていた。そのときに、以下のようなエラーになったので、それについて調べてみた。
navigation destination com.github.ymatoi.navigationsample:id/action_firstFragment_to_secondFragment is unknown to this NavController
状況と解決策
このような状況を navigation で定義している。
自動的に画面を遷移させるために、FirstFragmentのonViewCreatedに対して、以下のようなコードを書いている。 すると、起動時には問題なく、画面遷移をして、SecondFragmentに移動するが、戻るボタンを押すとクラッシュしてしまう
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) findNavController().navigate(R.id.action_firstFragment_to_secondFragment) }
これを解決するには、navigationの遷移部分を以下のようにしてあげればよい。こうすることで、戻るボタンを押しても、瞬時にsecondFragmentに遷移するようになる
findNavController().navigate(R.id.secondFragment)
調べ中
確証は持てていないが
FirstFragment の onViewCreated で、findNavController().currentDestination
の値を出力すると、戻るボタンを押したあとでは、SecondFragmentのidを出力するので、
R.id.action_firstFragment_to_secondFragment
が動くのはcurrentDestinationがFirstFragmentのidのときのみだと推測している。
R.id.secondFragment
のように直接Fragmentの指定をしてあげる場合は、currentDestinationに依存せずに、遷移できるようです。
上の説、あまり自身がないので、くわしいかたどなたかぁ。。
あと、そうすると、currentDestinationはいつ変わるのかって話題もきになってる。
追記
たとえば、SecondFragmentでfindNavController().popBackStack()
で戻ると、クラッシュしないみたいです。
そのときは、findNavController().currentDestination()
が期待値なので、BackButtonを押したときのみクラッシュするみたいですね。
AndroidのNavigationのnavArgsが便利そう。
Android + kotlin
最近、仕事で使っている Navigation Architecture Componentのバージョンを 1.0.0-alpha07から一気に2.0.0まで上げました。
そのときに、リリースノートを見ていたときに by navArgs()
というものを知ったのでメモ
これによって、例えば以下のように書いていたものが
val args = HogeFragmentArgs.fromBundle(arguments!!)
のようにfromBundleを噛ませて書いていたのが、Fragmentのクラスのメンバ変数として
private val args: HogeFragment by navArgs()
のようにかけることを知りました。
これを使うためには1.0.0-alpha10
以上の-ktx
のnavigation architecture componentが必要になります。
これによってメンバ変数として定義し、初期化を使用時に遅らせることができます。
従来でもメンバ変数として定義し、初期化を遅らせることはlateinitを使えばできたのですが、それがより簡単にできるようになりました。
蛇足
さてさて、navArgs()とはなんじゃろなとなったので、Android Studioで実装に飛ぶと
- FragmentNavArgsLazy.kt
@MainThread inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) { arguments ?: throw IllegalStateException("Fragment $this has null arguments") }
のような実装がされています。ここはkotlinの拡張関数を使って、NavArgsLazyを呼び出しているので、更に実装を見てみると
- NavArgsLazy.kt
class NavArgsLazy<Args : NavArgs>( private val navArgsClass: KClass<Args>, private val argumentProducer: () -> Bundle ) : Lazy<Args> { private var cached: Args? = null override val value: Args get() { var args = cached if (args == null) { val arguments = argumentProducer() val method: Method = methodMap[navArgsClass] ?: navArgsClass.java.getMethod("fromBundle", *methodSignature).also { method -> // Save a reference to the method methodMap[navArgsClass] = method } @Suppress("UNCHECKED_CAST") args = method.invoke(null, arguments) as Args cached = args } return args } override fun isInitialized() = cached != null }
のような記述が見えることになります。
このクラスが評価されるとget()
が呼び出され、キャッシュを持っている場合はキャッシュを返し、
初回呼び出し時には、argumentProducer
を使ってFragmentのargumentsが渡されているので、argumentsを型変数Argsに変換して返されることになるようです。
#
- リリースノート定期的に見よう。
- これ調べてたら
by
が何やってるのか気になったので動作を調べてみたい - バージョンを上げたら
arguments
がNullableを返すようになっていてfromBundle
がNonNullを受け取るので変更が面倒だった。。 - 上のソースコードのリンクのURLを貼りたいがどこかよくわかっておらず。。(汗)
気づいたら転職して一年が経過しそう
履歴
2015/04/01 From: 機械系大学院 To: O1社
2018/04/01 From: O1社 To: 無職
2018/05/01 From: 無職 To: O2 社
大学院
ロボットやってた
O1社
インフラ設計や構築や開発とたまにバックエンドやフロントの開発やってた
無職
就職活動とHorizon Zero Dawn、龍が如く 極
O2社
最後に
だいぶ久しぶりにはてなにログインしたら、ブログだけ作って放置していることを見つけた。 過去の自分が調べたこと、知ったこと等、細かいことはすぐ忘れてしまうので雑多に記録に残していきたい。 未来の自分が振り返るときに役に立つことを願って。
このブログタイトルをどういう気持ちでつけたのかを忘れてしまった (元ネタはクロスチャンネルだとは覚えている。
未来の自分は他人なのでコメントを残すべきところだ