LoginSignup
3
2

Compose Multiplatformで利用できるようになったNavigationを使ってみた

Last updated at Posted at 2024-05-09

はじめに

Compose Multiplatformは、Jetpack Compose をベースにKMP(Kotlin Multiplatform)プロジェクトでUIを構築するためのフレームワークです。
ここ最近は、目まぐるしいスピードで進化をしており、Resource周りも共通で実装できるようになっています!
そして、alpha版ではありますが、ついに Navigation も利用できるようになりました🎉

実際に Navigation を利用したところ体験が良かったため、本記事で紹介します!

Navigationとは?

Navigationは、アプリ内の様々なコンテンツ間を遷移するためのAndroidでのライブラリです。
このライブラリを利用することで、今まで複雑でした画面遷移の処理を簡潔にしてくれます。
もちろん、Jetpack Compose でも利用でき、 navigation-compose として提供されています。

Compose MultiplatformにおけるNavigationは?

Compose Multiplatformでは、UIのみの共通化とshared/commonMain/App.kt。
そのため、Navigationに関しては、各々のプラットフォームで行う必要があります。

Android, iOSのみのマルチプラットフォームプロジェクトと仮定して、以下に実装例を書きます。

KMP

UIの実装をKMP側で持たせて共通化させます。

shared/commonMain/HomeContent.kt
@Composable
fun HomeContent(
  state: HomeState
) {
  Sample(state = state) // stateを表示するコンポーネント
}
shared/commonMain/SettingContent.kt
@Composable
fun SettingContent(
  modifier: Modifier = Modifier
) {
  LazyColumn(
    modifier = modifier
  ) {
    SettingItem.entries.forEach {
      // SettingItemで定義されている値をもとに項目のコンポーネントを作成する
    }
  }
}

Android

先程記述したように、navigation-compose をプラットフォーム側で実装し、そこに Compose Multiplatform で実装した共通のUI呼び出す形で実装してました。

androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
      BaseTheme {
        AppNavHost()
      }
    }
  }
}
androidApp/AppNavHost.kt
@Composable
fun AppNavHost(
  navController: NavHostController = rememberNavController(),
) {
  NavHost(
    navController = appScaffoldState.navController,
    startDestination = HomeNavGraph.home
  ) {
    homeNavGraph(
      navigateSetting = {
        navController.navigate(SettingNavGraph.setting)
      }
    )
    settingNavGraph(
      navigateBack = {
        navController.navigateUp()
      }
    )
  }
}
androidApp/HomeScreen.kt
object HomeNavGraph {
  const val home = "home"
}

fun NavGraphBuilder.homeNavGraph(
  navigateSetting: () -> Unit
) {
  composable(
    route = HomeNavGraph.home
  ) {
    HomeScreen(
      navigateSetting = navigateSetting
    )
  }
}

@Composable
private fun HomeScreen(
  navigateSetting: () -> Unit
) {
  Scaffold(
    topBar = {
      TopAppBar(
        title = {
          Text("Home")
        },
        actions = {
          IconButton(onClick = navigateSetting) {
            Icon(
              imageVector = Icons.Default.Settings,
              contentDescription = "Settings"
            )
          }
        }
      )
    }
  ) {
    Box(
      modifier = Modifier
        .fillMaxSize()
        .padding(it)
    ) {
      HomeContent(
        state = HomeState.Loaded // 検証目的のため、Stateは仮の値を入れている
      )
    }
  }
}
androidApp/SettingScreen.kt
object SettingNavGraph {
  const val setting = "setting"
}

fun NavGraphBuilder.settingNavGraph(
  navigateBack: () -> Unit
) {
  composable(
    route = SettingNavGraph.setting
  ) {
    SettingScreen(
      navigateBack = navigateBack
    )
  }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingScreen(
  navigateBack: () -> Unit
) {

  Scaffold(
    topBar = {
      TopAppBar(
        title = {
          Text("Setting")
        },
        navigationIcon = {
          IconButton(onClick = navigateBack) {
            Icon(
              imageVector = Icons.AutoMirrored.Default.ArrowBack,
              contentDescription = "Back"
            )
          }
        }
      )
    }
  ) { innerPadding ->
    SettingContent(
      modifier = Modifier.padding(innerPadding)
    )
  }
}

iOS

Compose Multiplatform で作成した Content を UIViewController にして iOS 側で扱えるようにし、NavigationStack を用いて画面遷移を実装してました。

※ SwiftUI で UIViewController を扱うためには、UIViewControllerRepresentable を用いる必要がありますが、実装コードは省きます。

shared/iosMain/HomeContent.ios.kt
fun HomeViewController(
  state: HomeState
) = ComposeUIViewController {
  BaseTheme {
    HomeContent(
      state = state
    )
  }
}
shared/iosMain/SettingContent.ios.kt
fun SettingViewController(
  state: HomeState
) = ComposeUIViewController {
  BaseTheme {
    SettingContent()
  }
}
iosApp/iOSApp.swift
@main
struct iOSApp: App {
	var body: some Scene {
		WindowGroup {
            ContentView()
		}
	}
}
iosApp/ContentView.swift
struct ContentView: View {
    @State var path: [String] = []
    
	var body: some View {
        NavigationStack(path: $path) {
            HomeView(path: $path)
            .navigationDestination(
                for: String.self,
                destination: { appended in
                    switch appended {
                    case "home":
                        HomeView(path: $path)
                    case "setting":
                        SettingView()
                    default:
                        Text("default")
                    }
                }
            )
        }
    }
}

導入されたNavigationを使用してみる

今回導入されました、Navigationは compose-navigation をベースにKMPで使用できるようになっているため、Android側で行っていた実装をKMP側で行うだけで実装できます!

ライブラリの追加

build.gradle.kts
kotlin {
    // ...
    sourceSets {
        // ...
        commonMain.dependencies {
            // ...
            implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha02") // MavenCentralでの公開情報を参考に適切なバージョンを入れる
        }
        // ...
    }
}

KMP

KMP側にNavigationの処理も持たせ、Navigationまで含めたComposable関数を作成します。

shared/commonMain/App.kt
@Composable
fun App() {
  BaseTheme {
    AppNavHost()
  }
}

iOSで呼び出せるように、 AppUIViewController へ変換しておきます。

shared/iosMain/App.kt
fun MainViewController() = ComposeUIViewController { App() }

コードは省きますが、以下も行います。

  • androidApp側で定義していた、AppNavHost.ktをKMP側に持ってくる(本記事では kmp/app に移動させている)
  • androidApp側で定義していた、〇〇Screen.ktの実装をKMP側に持ってくる(本記事では、 kmp/ui/feature に移動させている)

あとは、各プラットフォームで App を呼び出すだけです!

Android

androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
      App()
    }
  }
}

iOS

iosApp/ContentView.swift
struct ComposeAppView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
	var body: some View {
        ComposeAppView().ignoresSafeArea(.all)
    }
}

まとめ

KMPにNavigationの実装を持たせることで、各プラットフォームは1つのComposable関数を呼び出すだけになります!
そのため、ほぼ全て Kotlin だけでAndroid, iOS, Web, Desktopが実装できてしまいます。
これからもKMPは進化していくと思いますので、これからもウォッチしていきたいです!

注意事項

KMPのNavigation機能は、現状alpha版です。
まだ変更される可能性があるため、本番環境での利用は注意が必要です。

参考

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