Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Third Edition · Android 15, iOS 18, Desktop · Kotlin 2.1.20 · Android Studio Meerkat

C. Appendix C: Sharing Your Compose UI Across Multiple Platforms
Written by Carlos Mota

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Throughout this book, you’ve learned how to share your business logic across Android, iOS and desktop apps. What if you could go a step further and also share your Compose UI?

That’s right — along with Kotlin Multiplatform, you now have Compose Multiplatform, which allows you to share your Compose UI with Android, iOS and desktop apps.

Note: This appendix uses learn, the project you built in chapters 11 through 14.

Setting Up an iOS App to Use Compose Multiplatform

To follow along with the code examples throughout this appendix, download the project and open 17-appendix-c-sharing-your-compose-ui-across-multiple-platforms/projects/starter with Android Studio.

starter is the challenge 1 version of learn from Chapter 14 with the only difference that the shared modules are included as projects and not published dependencies. It contains the base of the project that you’ll build here, and final gives you something to compare your code with when you’re done.

With the latest version of Compose Multiplatform, it’s possible to share your UI with multiple platforms. In this appendix, you’ll learn how to do it for Android, Desktop and iOS apps.

Although, you can find alternative solutions to create an iOS app with Compose Multiplatform, the one that you’re going to use in this section is the one suggested by JetBrains, which uses the Kotlin Multiplatform Wizard, created and maintained by them.

Start by opening the wizard and define:

  • Project Name: learn
  • Project ID: com.kodeco.learn

Confirm that you have the Android, iOS (with the share UI setting on), and Desktop targets selected, then click DOWNLOAD.

Extract the content from the .zip file. Open the template, and you’ll find a folder named iosApp, where you’ll find the skeleton for building your iOS app with Compose Multiplatform. Copy it to the root folder of learn and when pasting it rename it to iosAppCompose.

Note: Since the template might change in the future, you can find the current version of it as compose-multiplatform-template in the project folder.

Your project structure should now be similar to this one:

Fig. B.1 — Project view hierarchy
Fig. B.1 — Project view hierarchy

Open the iosApp.xcodeproj file located on iosAppCompose with Xcode. Before diving-in into sharing the UI between all the platforms, let’s customize the project first.

Open ContentView.swift. Here is the entry point for the (Compose) screen to be loaded. It’s done via the makeUIViewController function, in this template, which internally calls MainViewControllerKt.MainViewController(). You’ll create this implementation later in the chapter. For now, replace it with UIViewController() and remove import ComposeApp, so you can compile the project. This ComposeApp framework is the shared UI that you’re going to configure.

Open iosApp (the root folder) and go to BuildPhases. Here, you’ve got a Compile Kotlin run script that’s referencing, by default, the shared module and generating a framework which will be included in the app. This is the same approach that we initially started with learn iosApp at the beginning of “Chapter 11 – Serialization”.

Since, you haven’t created the shared UI module, comment the embedAndSignAppleFrameworkForXcode task for now:

#./gradlew :composeApp:embedAndSignAppleFrameworkForXcode

Compile the project. You should see an empty screen similar to this one:

Fig. B.2 — iOS App
Fig. B.2 — iOS App

Depending on the current version of Java that you have set as your JAVA_HOME you might see an error similar to the following:

‘compileJava’ task (current target is 17) and ‘compileKotlin’ task (current target is 18) jvm target compatibility should be set to the same Java version.

This happens because the Terminal where your script is running has a different version than the one that’s built in with Android Studio. You can change your JAVA_HOME to reflect the same directory, or you can just add the following before any instruction in the Compile Kotlin run script:

Make sure the path in the command above matches your version of Android Studion. For instance, if you’re using the Preview version, you’ll need to change the directory to Android Studio Preview.app.

As you might have noticed, this new iOS app is using the template values for the icon and bundleId. Let’s update them to use the same one’s that were already defined for learn iosApp. Open the Config.xcconfig file located inside the Configuration folder and replace the existing content with:

If you now go to iosApp and click on the General section and look for the Bundle Identifier setting, which is under Identity, you’ll see that both the app name and bundle ID were updated to learn and com.kodeco.learn respectively.

Finally, open Assets and remove the existing AppIcon. Open Finder and navigate to iosApp/iosApp/Assets.xcassets and copy the existing AppIcon.appiconset folder to iosAppCompose/iosApp/Assets.xcassets. Return to Xcode, and you should now see the Kodeco logo in AppIcon.

To confirm that your iOS app is ready, compile the project. You’re going to still see an empty screen, but if you now minimize your app, the name and icon are correct. :]

Fig. B.3 — iOS App icon
Fig. B.3 — iOS App icon

Updating Your Project Structure

To share your UI, you’ll need to create a new Kotlin Multiplatform module. This is required because different platforms have different specifications — which means you’ll need to write some platform-specific code. This is similar to what you’ve done throughout this book.

jvm()
compileSdk = libs.versions.android.sdk.compile.get().toInt()
minSdk = libs.versions.android.sdk.min.get().toInt()
val xcfName = "shared-ui"
Fig. B.4 — Project view hierarchy
Res. T.4 — Fkaratt deiy jeufohmct

Sharing Your UI Code

Although the code of both platforms is quite similar, the Android app uses platform-specific libraries. Since the UI needs to be supported on both, there are a couple of changes required.

Migrating Your Android UI Code to Multiplatform

Start by moving all the directories inside androidApp/ui into shared-ui/commonMain/ui. Don’t move the MainActivity.kt file, since activities are Android-specific.

com.kodeco.learn.utils
package com.kodeco.learn.utils

public const val TIME_FORMAT: String = "yyyy/MM/dd"

expect fun converterIso8601ToReadableDate(date: String): String
package com.kodeco.learn.utils

private const val TAG = "Utils"

@SuppressLint("ConstantLocale")
private val simpleDateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())

actual fun converterIso8601ToReadableDate(date: String): String {
  return try {
    val instant = Instant.parse(date)
    val millis = Date(instant.toEpochMilliseconds())
    return simpleDateFormat.format(millis)
  } catch (e: Exception) {
    Logger.w(TAG, "Error while converting dates. Error: $e")
    "-"
  }
}
api(project(":shared-logger"))

implementation(libs.kotlinx.datetime)
@Suppress("ConstantLocale")
package com.kodeco.learn.utils

actual fun converterIso8601ToReadableDate(date: String): String {
  val dateFormatter = NSDateFormatter()
  dateFormatter.dateFormat = TIME_FORMAT

  val nsdate = NSISO8601DateFormatter().dateFromString(date)
  return dateFormatter.stringFromDate(nsdate ?: NSDate())
}
import platform.Foundation.NSDate
import platform.Foundation.NSDateFormatter
import platform.Foundation.NSISO8601DateFormatter
implementation(project(":shared-ui"))

Compose Multiplatform

Jetpack Compose was initially introduced for Android as the new UI toolkit where one could finally leave the XML declarations and the findViewById calls behind and shift towards a new paradigm – declarative UI.

Gaftusu Ovatofaot Toygacu Nusupoav 2 Zicsehi Dekiqeuk Luvmule Feiwmisuad Xohjoki II Rovmuko Fehgasiz Pulnega Nuqtuve Heytuso IU Lauglav (Iyjpeip)
Beh. R.4 — Fiqxinq Necwobu tadx-quyes lienlog

Migrating to Compose Multiplatform

Open the BookmarkContent.kt file from shared-ui. Here you’ll see that the imports to androidx.compose* are not being resolved.

alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.jetbrains.compose.compiler)
api(compose.foundation)
api(compose.material)
api(compose.material3)
api(compose.runtime)
api(compose.ui)

Updating Your Shared UI Dependencies

Now that shared-ui contains your app UI, it’s time to add the missing libraries. Open the build.gradle.kts file from this module and look for commonMain/dependencies. Update it to include:

api(project(":shared"))

Migrating Your UI Components

Not all classes in an Android Compose project are available in Compose Multiplatform. Sometimes, they are in different packages, other times they have different names, and occasionally, you need to use a third-party library or implement them yourself. In HorizontalPagerIndicator.kt file, you’ll encounter all three scenarios.

import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material3.LocalContentColor
activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentColor.current.alpha),
inactiveColor: Color = activeColor.copy(DisabledAlpha),
private const val DisabledAlpha = 0.38f

Using Third-Party Libraries

Although Compose Multiplatform is taking its first steps, the community is following closely, releasing libraries that help make the bridge between Android and desktop apps.

Fetching Images

There are currently two libraries commonly used to fetch images in Compose Multiplatform:

coil = "3.1.0"
image-coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }`
image-coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
api(libs.image.coil)
api(libs.image.coil.network)
androidMain {
  dependencies {
    implementation(libs.ktor.client.android)
  }
}

jvmMain {
  dependencies {
    implementation(libs.ktor.client.jvm)
  }
}

iosMain {
  dependencies {
    implementation(libs.ktor.client.ios)
  }
}
ktor-client-jvm = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
import coil3.compose.rememberAsyncImagePainter

Using LiveData and ViewModels

learn was built using LiveData and ViewModels that are available in Android through the runtime-livedata library. Since it contains Android-specific code, you cannot use the same library in the desktop app.

api(libs.jetbrains.compose.lifecycle)
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
private val _items = MutableStateFlow<List<KodecoEntry>>(emptyList())
val items: StateFlow<List<KodecoEntry>> = _items.asStateFlow()
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import androidx.lifecycle.MutableLiveData
private val _profile = MutableStateFlow<GravatarEntry>(GravatarEntry())
val profile: StateFlow<GravatarEntry> = _profile.asStateFlow()
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
val profile = feedViewModel.profile.collectAsState()
val bookmarks = bookmarkViewModel.items.collectAsState()
import androidx.compose.runtime.livedata.observeAsState
java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
kotlinx-coroutines = "1.10.1"
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
implementation(libs.kotlinx.coroutines.swing)

Handling Navigation

There are multiple libraries that handle navigation between different screens. In previous editions of this book, you used precompose.

api(compose.material3AdaptiveNavigationSuite)
//1
NavigationSuiteScaffold(
  //2
  navigationSuiteItems = {
    bottomNavigationItems.forEach { screen ->
      item(
        icon = {
          screen.icon()
        },
        label = {
          Text(
            text = stringResource(screen.title)
          )
        },
        selected = it == currentDestination.value,
        onClick = { currentDestination.value = screen }
      )
    }
  },
  content = {
    //3
    Column(
      modifier = Modifier.padding(it)
    ) {
      MainTopAppBar(
        profile = profile
      )

      MainContent(
        destination = currentDestination.value,
        coroutineScope = coroutineScope,
        bottomSheetScaffoldState = bottomSheetScaffoldState,
        selected = selected,
        feeds = feeds,
        bookmarks = bookmarks,
        onOpenEntry = onOpenEntry
      )
    }
  }
)
val currentDestination = remember {
  mutableStateOf<BottomNavigationScreens>(BottomNavigationScreens.Home)
}
import androidx.compose.material3.Scaffold
import androidx.navigation.compose.rememberNavController
destination: BottomNavigationScreens,
Column {
  MainScreenNavigationConfigurations(
    destination = destination,
    coroutineScope = coroutineScope,
    bottomSheetScaffoldState = bottomSheetScaffoldState,
    selected = selected,
    feeds = feeds,
    bookmarks = bookmarks,
    onOpenEntry = onOpenEntry
  )
}
when(destination) {
  BottomNavigationScreens.Home -> {
    HomeContent(
      selected = selected,
      items = feeds,
      coroutineScope = coroutineScope,
      bottomSheetScaffoldState = bottomSheetScaffoldState,
      onOpenEntry = onOpenEntry
    )
  }
  BottomNavigationScreens.Bookmark -> {
    BookmarkContent(
      selected = selected,
      items = bookmarks,
      coroutineScope = coroutineScope,
      bottomSheetScaffoldState = bottomSheetScaffoldState,
      onOpenEntry = onOpenEntry
    )
  }
  BottomNavigationScreens.Latest -> {
    LatestContent(
      items = feeds,
      onOpenEntry = onOpenEntry
    )
  }
  BottomNavigationScreens.Search -> {
    SearchContent(
      selected = selected,
      items = feeds,
      coroutineScope = coroutineScope,
      bottomSheetScaffoldState = bottomSheetScaffoldState,
      onOpenEntry = onOpenEntry
    )
  }
}

Handling Resources

All platforms handle resources quite differently. Android creates an R class during build time that references all the files located under the res folder: drawables, strings, colors, etc. Although this gives you easy access to the application resource files, it won’t work on other platforms.

Configuring moko-resources

Start by opening libs.versions.toml file, located inside the gradle folder. Inside the [versions] section add the latest moko-resources version:

moko-resources = "0.24.5"
moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" }
moko-resources-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko-resources" }
moko-multiplatform-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko-resources" }
alias(libs.plugins.moko.multiplatform.resources) apply false
alias(libs.plugins.moko.multiplatform.resources)
multiplatformResources {
  resourcesPackage.set("com.kodeco.learn.ui")
}
api(libs.moko.resources)
api(libs.moko.resources.compose)

Loading Local Images

You’ll write the logic to load local images in Kotlin Multiplatform. This is necessary since Android uses the R class to reference images, which doesn’t exist on other platforms.

androidTarget {
  compilations.all {
    compileTaskProvider.configure {
      compilerOptions {
        jvmTarget.set(JvmTarget.JVM_17)
      }
    }
  }
}
alias(libs.plugins.android.library)
android {
  namespace = "com.kodeco.learn.ui"

  compileSdk = libs.versions.android.sdk.compile.get().toInt()

  defaultConfig {
    minSdk = libs.versions.android.sdk.min.get().toInt()
  }

  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
  }
}
import dev.icerock.moko.resources.compose.painterResource
val resource = painterResource(MR.images.ic_more)
import com.kodeco.learn.ui.MR
import com.kodeco.learn.R
import androidx.compose.ui.res.painterResource
val resource = painterResource(MR.images.ic_brand)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.res.painterResource
import com.kodeco.learn.R
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.res.painterResource
import com.kodeco.learn.R
painter = painterResource(MR.images.ic_home),
painter = painterResource(MR.images.ic_bookmarks),
painter = painterResource(MR.images.ic_latest),
painter = painterResource(MR.images.ic_search),
val resource = painterResource(MR.images.ic_search)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.res.painterResource
import com.kodeco.learn.R

Using Custom Fonts

The font that the three apps use is OpenSans. Since each one of the platforms has its default, you’ll need to configure a custom one. You’ll use, once again, moko-resources to load the new font.

<fontFamily>-<fontStyle>
OpenSans-Bold.ttf
OpenSans-ExtraBold.ttf
OpenSans-Light.ttf
OpenSans-Regular.ttf
OpenSans-SemiBold.ttf
fontFamilyResource(MR.fonts.opensans_regular)
MR.fonts.opensans_regular.asFont()
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.kodeco.learn.android.R

private val OpenSansFontFamily = FontFamily(
  Font(R.font.opensans_bold, FontWeight.Bold),
  Font(R.font.opensans_extrabold, FontWeight.ExtraBold),
  Font(R.font.opensans_light, FontWeight.Light),
  Font(R.font.opensans_regular, FontWeight.Normal),
  Font(R.font.opensans_semibold, FontWeight.SemiBold),
)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.fontFamilyResource
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)
fontFamily = fontFamilyResource(MR.fonts.opensans_regular)

Sharing Strings

Once again, you’re going to use the moko-resouces library to share strings across all platforms.

text = stringResource(MR.strings.empty_screen_bookmarks)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import com.kodeco.learn.android.R
text = stringResource(MR.strings.app_kodeco),
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
val description = stringResource(MR.strings.description_more)
import androidx.compose.ui.res.stringResource
val description = stringResource(MR.strings.description_preview_error)
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
val text = if (item.value.bookmarked) {
  stringResource(MR.strings.action_remove_bookmarks)
} else {
  stringResource(MR.strings.action_add_bookmarks)
}
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
text = stringResource(MR.strings.action_share_link),
import androidx.compose.ui.res.stringResource
import com.kodeco.learn.android.R
AddEmptyScreen(stringResource(MR.strings.empty_screen_loading))
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import com.kodeco.learn.android.R
val title: StringResource,
title = MR.strings.navigation_home,
contentDescription = stringResource(MR.strings.navigation_home)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.stringResource
title = MR.strings.navigation_bookmark,
contentDescription = stringResource(MR.strings.navigation_bookmark)
title = MR.strings.navigation_latest,
contentDescription = stringResource(MR.strings.navigation_latest)
title = MR.strings.navigation_search,
contentDescription = stringResource(MR.strings.navigation_search)
import androidx.annotation.StringRes
import androidx.compose.ui.res.stringResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
text = stringResource(MR.strings.app_name),
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
contentDescription = stringResource(MR.strings.description_profile)
import androidx.compose.ui.res.stringResource
import com.kodeco.learn.android.R
text = stringResource(MR.strings.search_hint),
val description = stringResource(MR.strings.description_search)
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource

What’s Missing?

With all of these changes done, you’re almost done. Open the desktopApp project and:

implementation(project(":shared-ui"))
implementation(project(":shared-action"))

implementation(compose.desktop.currentOs)
implementation(project(":shared-ui"))
implementation(project(":shared-action"))

implementation(libs.android.material)
implementation(libs.androidx.navigation.compose)
import com.kodeco.learn.R
import com.kodeco.learn.ui.R
val view = LocalView.current
if (!view.isInEditMode) {
  SideEffect {
    val window = (view.context as Activity).window
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
      window.decorView.setOnApplyWindowInsetsListener { view, insets ->
        view.setBackgroundColor(colorScheme.surface.toArgb())
        insets
      }
    } else {
      window.statusBarColor = colorScheme.surface.toArgb()
    }
    WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
  }
}
import android.app.Activity
import android.os.Build
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
KodecoTheme {

  val view = LocalView.current
  if (!view.isInEditMode) {
    val color = colorScheme.surface.toArgb()
    val darkTheme = isSystemInDarkTheme()

    SideEffect {
      val window = (view.context as Activity).window
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
        window.decorView.setOnApplyWindowInsetsListener { view, insets ->
          view.setBackgroundColor(color)
          insets
        }
      } else {
        window.statusBarColor = color
      }
      WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
  }
}
import com.kodeco.learn.resources.Res
import com.kodeco.learn.resources.app_name
import org.jetbrains.compose.resources.stringResource
import com.kodeco.learn.ui.MR
import dev.icerock.moko.resources.compose.stringResource
title = stringResource(MR.strings.app_name)
Fig. B.6 — Feed in Android App
Gif. V.0 — Raok uf Ibhwiap Obs

Fig. B.7 — Feed in Desktop App
Fik. F.2 — Toed em Dimlmun Axl

val xcfName = "SharedUIKit"

iosX64 {
  binaries.framework {
    baseName = xcfName
    linkerOpts.add("-lsqlite3")
  }
}

iosArm64 {
  binaries.framework {
    baseName = xcfName
    linkerOpts.add("-lsqlite3")
  }
}

iosSimulatorArm64 {
  binaries.framework {
    baseName = xcfName
    linkerOpts.add("-lsqlite3")
  }
}
package com.kodeco.learn.ui

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
import androidx.lifecycle.viewmodel.compose.viewModel
import com.kodeco.learn.data.model.KodecoEntry
import com.kodeco.learn.ui.bookmark.BookmarkViewModel
import com.kodeco.learn.ui.home.FeedViewModel
import com.kodeco.learn.ui.main.MainScreen
import com.kodeco.learn.ui.theme.KodecoTheme
import platform.Foundation.NSLog
import platform.Foundation.NSURL
import platform.UIKit.UIApplication

private lateinit var bookmarkViewModel: BookmarkViewModel
private lateinit var feedViewModel: FeedViewModel

fun MainViewController() = ComposeUIViewController {
  Surface(modifier = Modifier.fillMaxSize()) {

    bookmarkViewModel = viewModel {
      BookmarkViewModel()
    }

    feedViewModel = viewModel {
      FeedViewModel()
    }

    feedViewModel.fetchAllFeeds()
    feedViewModel.fetchMyGravatar()
    bookmarkViewModel.getBookmarks()

    val items = feedViewModel.items
    val profile = feedViewModel.profile.collectAsState()
    val bookmarks = bookmarkViewModel.items.collectAsState()

    KodecoTheme {
      MainScreen(
        profile = profile.value,
        feeds = items,
        bookmarks = bookmarks,
        onUpdateBookmark = { updateBookmark(it) },
        onShareAsLink = {},
        onOpenEntry = { openLink(it) }
      )
    }
  }
}

private fun updateBookmark(item: KodecoEntry) {
  if (item.bookmarked) {
    removedFromBookmarks(item)
  } else {
    addToBookmarks(item)
  }
}

private fun addToBookmarks(item: KodecoEntry) {
  bookmarkViewModel.addAsBookmark(item)
  bookmarkViewModel.getBookmarks()
}

private fun removedFromBookmarks(item: KodecoEntry) {
  bookmarkViewModel.removeFromBookmark(item)
  bookmarkViewModel.getBookmarks()
}

private fun openLink(url: String) {
  val application = UIApplication.sharedApplication
  val nsurl = NSURL(string = url)
  if (!application.canOpenURL(nsurl)) {
    NSLog("Unable to open url: $url")
    return
  }

  application.openURL(nsurl)
}
cd "$SRCROOT/.."
./gradlew :shared-ui:embedAndSignAppleFrameworkForXcode
import SharedUIKit
func makeUIViewController(context: Context) -> UIViewController {
  Main_iosKt.MainViewController()
}
.ignoresSafeArea(.all, edges: .all)
Fig. B.8 — Feed in iOS App (dark mode)
Jaj. F.1 — Qiid uk oIZ Iwl (qebd qefa)

Fig. B.9 — Feed in iOS App (light mode)
Fol. Z.0 — Teiy aj oEM Ixw (valbm babo)

Where to Go From Here?

Congratulations! You just finished Kotlin Multiplatform by Tutorials. What a ride! Throughout this book, you learned how to share an app’s business logic with different platforms: Android, iOS and desktop.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now