2
1

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

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

Last updated at Posted at 2021-09-13

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

終わりに

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

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?