Building a Compose Multiplatform Image Gallery App for Android and iOS

Apps|Apr 23, 2023|Last edited: May 4, 2023
  • Jetpack Compose UI
  • Compose Multiplatform
  • iOS
  • App
  • Android
  • KMM VIewModel
  • Building a Compose Multiplatform Image Gallery App for Android and iOS
    Apr 23, 2023
    Learn how to build a Compose Multiplatform Android, iOS app that utilizes Jetpack Compose and Koin for dependency injection to display images using an API call.
    Jetpack Compose UI
    Compose Multiplatform
    KMM VIewModel
    The rise of Kotlin Multiplatform has opened up new possibilities for developers to build apps that can run across multiple platforms, such as Android, iOS, desktop, and web. In this article, we'll walk through the steps of building a multiplatform image gallery app with Kotlin, using Jetpack Compose for the UI, Ktor for networking, and Koin for dependency injection targeting Android and iOS devices.
    Compose Multiplatform is a new framework from Jetbrains that allows you to build native mobile apps with Kotlin. With Compose Multiplatform, you can write your UI code once and deploy it to both Android and iOS devices. This can save you a lot of time and effort, and it can also help you to create more consistent and high-quality apps.

    Overview of the MidJourney Image Gallery App

    The image gallery app consists of a grid of images, loaded from a remote data source. The app will be able to handle different states, such as loading, error, and content, and will support the lazy loading of images as the user scrolls down the grid. The app also implements image caching to improve performance powered by Compose ImageLoader library.
    Note: The app does not directly make requests to MidJourney API. It fetches data from


    The app follows the MVVM architecture pattern, with the following layers:
    • Data layer: responsible for fetching data from the network or local storage
    • Domain layer: responsible for processing the data and converting it to a format that the UI layer can use
    • UI layer: responsible for rendering the user interface and handling user interactions
    To implement the MVVM pattern, we'll use the following libraries:
    • Ktor: Multiplatform networking library that supports HTTP, WebSocket, and other protocols
    • Kotlinx Serialization: Multiplatform serialization library that allows us to convert JSON responses to Kotlin objects
    • Jetpack Compose: Modern UI toolkit for Android and the web
    • Koin: Dependency injection library that allows us to inject dependencies into our classes
    • Compose ImageLoader: Compose Image library for Kotlin Multiplatform.
    • KMM-ViewModel: Library that allows you to share ViewModels between Android and iOS.

    Source Code Explanation: MidJourney Images App


    import SwiftUI @main struct iOSApp: App { var body: some Scene { WindowGroup { ContentView() } } }
    This is a Swift code that is used in iOS development using SwiftUI framework. The @main attribute tells the compiler that this is the main entry point for the app.


    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setComposable() } private fun setComposable() { setContent { MjImagesApp( viewModel = get() ) } } }
    The setComposable method sets the content of the activity to an instance of the MjImagesApp composable. It receives the viewModel parameter, which is obtained by calling the get method of the Koin dependency injection container.


    import UIKit import SwiftUI import shared struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { let viewModel = MjImagesViewModel.init(fetchUseCase: MjImagesFetchUseCase.init()) return Main_iosKt.MainViewController(viewModel: viewModel) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } struct ContentView: View { init() { KoinModuleKt.doInitKoin() } var body: some View { ComposeView().ignoresSafeArea(.keyboard) } }
    This is a Swift code that uses both UIKit and SwiftUI frameworks, along with Kotlin Multiplatform code. import shared is importing Kotlin Multiplatform code into the project.
    The ComposeView struct conforms to the UIViewControllerRepresentable protocol, which allows us to use a UIKit view controller in a SwiftUI view.
    The ContentView struct is the root view of the app. It initializes the Koin dependency injection framework by calling doInitKoin method defined in the Kotlin Multiplatform code. Then it creates a ComposeView and sets the ignoresSafeArea modifier to .keyboard, which tells SwiftUI to ignore the safe area around the keyboard when laying out the view.


    import androidx.compose.ui.window.ComposeUIViewController import platform.UIKit.UIViewController import ui.MjImagesApp import ui.MjImagesViewModel fun MainViewController(viewModel: MjImagesViewModel): UIViewController = ComposeUIViewController { MjImagesApp(viewModel) }
    The MainViewController function is defined to create a UIKit view controller that hosts a Compose UI. It takes a viewModel parameter of type MjImagesViewModel, which is used to initialize the MjImagesApp.
    The ComposeUIViewController class is used to wrap the MjImagesApp in a UIKit view controller. It creates a bridge between the Compose UI and the UIKit framework, allowing the Compose UI to be embedded in an iOS app.
    ComposeUIViewController wraps @Composable function to UIViewController.


    package util import kotlinx.coroutines.Dispatchers internal actual fun getDispatcherProvider(): DispatcherProvider = IosDispatcherProvider() private class IosDispatcherProvider : DispatcherProvider { override val main = Dispatchers.Main override val io = Dispatchers.Default override val unconfined = Dispatchers.Unconfined }
    Defines a platform-specific implementation, in this case iOS for providing dispatchers in a Kotlin Multiplatform project.


    package util import com.seiko.imageloader.ImageLoader expect fun generateImageLoader(): ImageLoader


    @Composable actual fun generateImageLoader(): ImageLoader = ImageLoader( requestCoroutineContext = rememberCoroutineScope().coroutineContext ) { logger = DebugLogger(LogPriority.DEBUG) components { setupDefaultComponents(imageScope) } interceptor { diskCacheConfig { directory(FileSystem.SYSTEM_TEMPORARY_DIRECTORY) } } }
    Inside the function, the ImageLoader constructor is called to create a new instance. The rememberCoroutineScope function is used to retrieve the current coroutine scope to use as the requestCoroutineContext.


    @Composable actual fun generateImageLoader(): ImageLoader { val context = LocalContext.current return ImageLoader { logger = DebugLogger(LogPriority.DEBUG) components { setupDefaultComponents(context) } interceptor { diskCacheConfig { directory(context.cacheDir.resolve("image_cache").toOkioPath()) maxSizeBytes(1024 * 1024 * 100) } } } }
    The generateImageLoader() function uses the LocalContext provided by Jetpack Compose. It retrieves the Context from the current Composition and uses it to create the ImageLoader instance.


    mbakgunUpdated May 24, 2023
    Jetpack Compose library that enables contents zooming with pinch gestureCreate a Stunning Animated Navigation Bar in Jetpack Compose