はじめに
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側で持たせて共通化させます。
@Composable
fun HomeContent(
state: HomeState
) {
Sample(state = state) // stateを表示するコンポーネント
}
@Composable
fun SettingContent(
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
SettingItem.entries.forEach {
// SettingItemで定義されている値をもとに項目のコンポーネントを作成する
}
}
}
Android
先程記述したように、navigation-compose
をプラットフォーム側で実装し、そこに Compose Multiplatform
で実装した共通のUI呼び出す形で実装してました。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
BaseTheme {
AppNavHost()
}
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController = rememberNavController(),
) {
NavHost(
navController = appScaffoldState.navController,
startDestination = HomeNavGraph.home
) {
homeNavGraph(
navigateSetting = {
navController.navigate(SettingNavGraph.setting)
}
)
settingNavGraph(
navigateBack = {
navController.navigateUp()
}
)
}
}
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は仮の値を入れている
)
}
}
}
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
を用いる必要がありますが、実装コードは省きます。
fun HomeViewController(
state: HomeState
) = ComposeUIViewController {
BaseTheme {
HomeContent(
state = state
)
}
}
fun SettingViewController(
state: HomeState
) = ComposeUIViewController {
BaseTheme {
SettingContent()
}
}
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
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側で行うだけで実装できます!
ライブラリの追加
kotlin {
// ...
sourceSets {
// ...
commonMain.dependencies {
// ...
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha02") // MavenCentralでの公開情報を参考に適切なバージョンを入れる
}
// ...
}
}
KMP
KMP側にNavigationの処理も持たせ、Navigationまで含めたComposable関数を作成します。
@Composable
fun App() {
BaseTheme {
AppNavHost()
}
}
iOSで呼び出せるように、 App
を UIViewController
へ変換しておきます。
fun MainViewController() = ComposeUIViewController { App() }
コードは省きますが、以下も行います。
- androidApp側で定義していた、AppNavHost.ktをKMP側に持ってくる(本記事では
kmp/app
に移動させている) - androidApp側で定義していた、〇〇Screen.ktの実装をKMP側に持ってくる(本記事では、
kmp/ui/feature
に移動させている)
あとは、各プラットフォームで App
を呼び出すだけです!
Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
App()
}
}
}
iOS
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版です。
まだ変更される可能性があるため、本番環境での利用は注意が必要です。