Compose for Desktopでデスクトップアプリを作って画面遷移をする

この記事はKotlin AdeventCalendarの4日目の記事です

qiita.com

Compose for Desktopとは

www.jetbrains.com

端的に説明すると、JetBrainsが提供するKotlin向けのUIフレームワークです。
AndroidのUIフレームワークであるJetpackComposeを利用し、各Platformにネイティブなデスクトップアプリを作ることができます。

簡単なアプリを作ってみる

まず最初にIntellijからプロジェクトを作成します。

これだけで下記のような画面が作成できています

UI周りはJetpackComposeと同様に書くことができますが、今回は画面遷移にフォーカスした話のため割愛します。

画面遷移してみる

本来JetpackComposeであれば navigation-compose を利用することで画面遷移を実装することができるのですが、 navigation-composeAndroidでしか使えないためCompose for Desktopで使うことはできないです。 公式的には特定のライブラリを強制していないですが、参考実装としてDecomposeを使って画面遷移の実装例を上げてくれています。

The Jetpack Compose navigation library (navigation-compose) is an Android-only library, and so can not be used together with Compose for Desktop. Our general attitude is not to “force” people to use a particular first-party library. However there are third-party libraries available. One could consider Decompose as possible solution.

ref) https://github.com/JetBrains/compose-jb/tree/master/tutorials/Navigation

ということでDecomposeを使って画面遷移を実装していきます。

今回はRootでの遷移先として HomeHelloWorld を設定します。

interface RootComponent {
  val childStack: Value<ChildStack<*, Child>>

  // 遷移処理
  fun navigateToHome()
  fun navigateToHelloWorld()

  // 画面
  sealed class Child() {
    object Home : Child()
    object HelloWorld : Child()
  }
}

具体的な実装を書いていきます。

class DefaultRootComponent(
  componentContext: ComponentContext,
) : RootComponent, ComponentContext by componentContext {

  private val navigation = StackNavigation<Configuration>()
  override val childStack = childStack(source = navigation, initialStack = { listOf(Configuration.Home) }, childFactory = ::child)

  private fun child(configuration: Configuration, componentContext: ComponentContext): RootComponent.Child {
    return when (configuration) {
      is Configuration.Home -> RootComponent.Child.Home
      is Configuration.HelloWorld -> RootComponent.Child.HelloWorld
    }
  }

  override fun navigateToHome() {
    navigation.replaceCurrent(Configuration.HomePage)
  }

  override fun navigateToHelloWorld() {
    navigation.replaceCurrent(Configuration.HelloWorldPage)
  }

  // Decomposeに合わせた画面の設定
  sealed class Configuration : Parcelable {
    @Parcelize
    object Home : Configuration()
    @Parcelize
    object HelloWorld: Configuration()
  }
}

ではここから画面遷移の実装を入れていきます

@Composable
fun RootContent(component: RootComponent) {
  val childStack by component.childStack.subscribeAsState()
  val lifecycle = LifecycleRegistry()

  Children(
    stack = childStack,
  ) {
    when (it.instance) {
      is RootComponent.Child.Home -> HomePage(component)
      is RootComponent.Child.HelloWorld -> HelloWorldPage(component)
    }
  }
}

subscribeAsStatechildStack が更新するたびにRootContentがレンダリングされ、現在の子要素が表示されるようになります。 あとは navigateToXXX を画面遷移したいタイミングで実行すれば完成です。

引数を渡したい場合

単純な画面遷移の実装は完了しました。ただ商品一覧から商品詳細に遷移する場合のように、画面遷移では特定の引数を渡して上げたくなるケースが多々あります。 その場合下記のように実装していくことで解決できます。

まず画面遷移に使う navigateToXXX で引数を渡して、Child で該当の引数を受け取れるように修正していきます。

interface RootComponent {
  // etc

  // 画面
  sealed class Child() {
    object Home : Child()
    data class HelloWorld(val text: String) : Child()
  }
}

class DefaultRootComponent(
  componentContext: ComponentContext,
) : RootComponent, ComponentContext by componentContext {

  //etc

  override fun navigateToHelloWorld(text: String) {
    navigation.replaceCurrent(Configuration.HelloWorldPage(text))
  }

  // Decomposeに合わせた画面の設定
  sealed class Configuration : Parcelable {
    @Parcelize
    object Home : Configuration()
    @Parcelize
    data class HelloWorld(val text: String): Configuration()
  }
}

最後に伝播して渡ってきた引数を画面切り替え時に利用します。

@Composable
fun RootContent(component: RootComponent) {
  val childStack by component.childStack.subscribeAsState()
  val lifecycle = LifecycleRegistry()

  Children(
    stack = childStack,
  ) {
    when (val current = it.instance) {
      is RootComponent.Child.Home -> HomePage(component)
      is RootComponent.Child.HelloWorld -> HelloWorldPage(component, current.text)
    }
  }
}

こうすることで実際の画面遷移実行時の引数が画面切り替え時に利用することができます。

まとめ

以上がCompose for Desktopの初歩的な実装とDecomposeを利用した画面遷移の実装でした。 今回はRootのみの画面遷移を例として上げましたが、場合によっては子の画面遷移Componentを作成して画面遷移をネストできるので状況に応じて使っていければと思います。