2021年09月13日現在、Deprecated でない機能を使って色々なものを作る方法を記載します。
各機能の実装方法
Composable に ViewModel を渡す①
- build.gradle の修正は不要
- ViewModel のインスタンス化は Activity の
onCreate
で行う
class MyViewModel : ViewModel() {
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
setContent { Foo(viewModel) }
}
}
@Composable
fun Foo(viewModel: MyViewModel) {
Text("hello, world")
}
作られる ViewModel は MainActivity のライフサイクルと紐付けられるため、MainActivity と ViewModel の生存期間は同じです。
Composable 内で ViewModel の変更を検知する
- build.gradle の修正は不要
- ViewModel 内で
State<*>
を作成する
class MyViewModel : ViewModel() {
val count = mutableStateOf(0)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
setContent { Foo(viewModel) }
}
}
@Composable
fun Foo(viewModel: MyViewModel) {
Row {
Text(viewModel.count.value.toString())
Button(onClick = { viewModel.count.value += 1 }) {
Text("click me!")
}
}
}
このように、State<*>
が ViewModel に存在していても正常に動作します。
Composable 内で ViewModel にある LiveData の変更を検知する
-
androidx.compose.runtime:runtime-livedata:$composeVersion
アーティファクトをインストールする必要がある
class MyViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
fun onChange(value: Int) {
_count.value = value
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
setContent { Foo(viewModel) }
}
}
@Composable
fun Foo(viewModel: MyViewModel) {
val count = viewModel.count.observeAsState(0)
Row {
Text(count.value.toString())
Button(onClick = { viewModel.onChange(count.value + 1) }) {
Text("click me!")
}
}
}
ひとつ前の mutableStateOf
を使う方法に比べてコード量が増えています。mutableStateOf
で要件を満たせるのであればそちらを使いたいところです。しかし、mutableStateOf
を ViewModel 内で使うコードが公式サイトで見当たらないため、もしかするとバッドプラクティスかもしれません。
Composable に ViewModel を渡す②
-
viewModel
関数を使用するためにandroidx.lifecycle:lifecycle-viewmodel-compose:$latestVersion
アーティファクトをインストールする必要がある。$latestVersion
はこちらから確認できる -
viewModel
関数は、ViewModel がなければ作成して返し、あればそれを返す
class MyViewModel : ViewModel() {
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { Foo() }
}
}
@Composable
fun Foo(vm: MyViewModel = viewModel()) {
}
作成される ViewModel は、その Composable を利用するアクティビティが終了するまで同じものが使われます。そして、Composable が異なっていてもアクティビティが同じであればひとつの ViewModel が使われます。
@Composable
fun Foo(vm: MyViewModel = viewModel()) {
Button(onClick = { vm.count.value += 1 }) { Text("click me!") }
}
@Composable
fun Bar(vm: MyViewModel = viewModel()) {
Text(vm.count.value.toString())
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
Foo()
Bar()
}
}
}
}
上記コードの Foo
Composable のボタンをタップすると、Bar
Composable のテキストが更新されます。
単方向データフローのバケツリレー方式は冗長になりがち、React の Context 方式は依存関係がわかりづらいという欠点がありますが、viewModel
関数は両方の欠点をうまく取り除いています。
Navigation を追加する
-
androidx.navigation:navigation-compose:$latestVersion
アーティファクトをインストールする必要がある(バージョンはこちらから確認できる)
@Composable
fun Hoge() {
val navController = rememberNavController()
Column {
Button(onClick = { navController.navigate("bar") }) {
Text("click me!")
}
NavHost(navController, startDestination = "foo") {
composable("foo") { Text("foo") }
composable("bar") { Text("bar") }
}
}
}
ボタンを押すと、画面表示が "foo" から "bar" に切り替わります。
Destination が切り替わったときに Composable に何らかの変化を起こしたいときは、navController.currentBackStackEntryAsState()
でオブザーバブルなインスタンスを取得します。
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
上記コードは、Destination が切り替わったときに再評価されます。それにより、currentRoute
が常に今の Destination を表すようになります。
navBackStackEntry
が Nullable である理由は、Destination が存在しないケースがあるからです。NavHost
関数の startDestination
引数により最初の Destination が設定されるため、それまでは Destination が存在しない状態になります。
Navigation で、一覧画面から詳細画面に遷移して一覧画面に戻ったときに、元の状態を保持しておく
-
androidx.navigation:navigation-compose:$latestVersion
アーティファクトをインストールする必要がある -
rememberSaveable
を使うことで前回の状態を保存できる
@Composable
fun Hoge() {
val navController = rememberNavController()
Column {
NavHost(navController, startDestination = "foo") {
composable("foo") { Foo { navController.navigate("bar") } }
composable("bar") { Bar { navController.popBackStack() } }
}
}
}
@Composable
fun Foo(toBar: () -> Unit) {
var done by rememberSaveable {
mutableStateOf(false)
}
LaunchedEffect(Unit) {
delay(1000)
done = true
}
Column {
Button(onClick = toBar, enabled = done) { Text("show detail") }
Text(if (done) "Done!" else "Loading...")
}
}
@Composable
fun Bar(back: () -> Unit) {
Column {
Button(onClick = back) { Text("back") }
}
}
// 1
composable("bar") { Bar { navController.popBackStack() } }
// 2
composable("bar") { Bar { navController.navigate("foo") } }
1 を 2 に書き換えたときは前回の状態がクリアされます。これは、ナビゲーションコントローラのバックキューに存在する前回の状態と .navigate("foo")
により作成される Composable は実体が別々だからです。
Navigation で .navigate()
を使って 画面 A -> 画面 B -> 画面 A と遷移したときに、前回の 画面 A の状態が残るようにする
たとえ rememberSaveable
を使ったとしても、最初の 画面 A と最後の 画面 A では実体が異なるため状態が共有されません。このようなときは、状態を上に持ち上げることで共有できます。
// build.gradle に追加するコード
implementation "androidx.navigation:navigation-compose:2.4.0-alpha06"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Hoge()
}
}
}
@Composable
fun Hoge() {
var done by remember { mutableStateOf(false) }
val navController = rememberNavController()
NavHost(navController, startDestination = "foo") {
composable("foo") {
Foo(
done = done,
onDone = { done = true },
toBar = { navController.navigate("bar") },
)
}
composable("bar") { Bar { navController.navigate("foo") } }
}
}
@Composable
fun Foo(done: Boolean, onDone: () -> Unit, toBar: () -> Unit) {
LaunchedEffect(done) {
if (!done) {
delay(3000)
onDone()
}
}
Column {
Button(onClick = toBar) { Text("to bar") }
Text(if (done) "Done!" else "Loading...")
}
}
@Composable
fun Bar(toFoo: () -> Unit) {
Button(onClick = toFoo) { Text("to foo") }
}
Foo
の持っていた done
という状態を上に移動しました。こうすることで、Navigation で画面を切り替えたときであっても状態を保つことができます。
LaunchedEffect
に渡した suspend 関数の実行は .navigate("bar")
のときにキャンセルされるため、再度 Foo
を表示した時点から3秒後に done = true
となります。
終わりに
この記事は随時更新していこうと思います。