Empty Compose Activity から色々なものを作る

2021年09月13日現在、Deprecated でない機能を使って色々なものを作る方法を記載します。

## 各機能の実装方法

### Composable に ViewModel を渡す①

* build.gradle の修正は不要
* ViewModel のインスタンス化は Activity の `onCreate` で行う

```kotlin
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<*>` を作成する

```kotlin
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` アーティファクトをインストールする必要がある

```kotlin
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` は[こちら](https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel-compose/1.0.0-alpha07)から確認できる
* `viewModel` 関数は、ViewModel がなければ作成して返し、あればそれを返す

```kotlin
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 が使われます。

```kotlin
@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` アーティファクトをインストールする必要がある（バージョンは[こちら](https://developer.android.com/jetpack/androidx/releases/navigation?hl=ja)から確認できる）

```kotlin
@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()` でオブザーバブルなインスタンスを取得します。

```kotlin
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` を使うことで前回の状態を保存できる

```kotlin
@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") }
  }
}
```

```kotlin
// 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 では実体が異なるため状態が共有されません。このようなときは、状態を上に持ち上げることで共有できます。

```groovy
// build.gradle に追加するコード
implementation "androidx.navigation:navigation-compose:2.4.0-alpha06"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
```

```kotlin
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` となります。

## 終わりに

この記事は随時更新していこうと思います。