Building a Compose Multiplatform Image Gallery App for Android and iOS
Apps|Apr 23, 2023|Last edited: May 4, 2023
Tags:
Jetpack Compose UI
Compose Multiplatform
iOS
App
Android
KMM VIewModel

type
Post
status
Published
date
Apr 23, 2023
slug
compose-multiplatform-images-midjourney-android-ios-app
summary
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.
tags
Jetpack Compose UI
Compose Multiplatform
iOS
App
Android
KMM VIewModel
category
Apps
icon
password
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 https://mj.mbakgun.com/
Architecture
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
iosApp/iosApp/iOSApp.swift
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.kotlin/com/mbakgun/mj/ui/MainActivity.kt
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.iosApp/iosApp/ContentView.swift
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.kotlin/main.ios.kt
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
.kotlin/util/DispatcherProvider.kt
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.
shared/src/commonMain/kotlin/util/ImageLoader.kt
package util import com.seiko.imageloader.ImageLoader expect fun generateImageLoader(): ImageLoader
shared/src/iosMain/kotlin/util/ImageLoader.kt
@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
.shared/src/androidMain/kotlin/util/ImageLoader.kt
@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.Reference
Code:
midjourney-images-compose-multiplatform
mbakgun • Updated May 24, 2023