Kotlin Multiplatform: A Shopping List for Android & iOS including Flow

Kaan K.
6 min readMar 24, 2022

Day by day Kotlin is getting more powerful and many new announcements are encouraging us to see beyond Android development. One of them is of course the promising Kotlin Multiplatform Mobile Framework. Unlike other cross-platform approaches, KMM allows us to just abstract the data layer of an application and simply integrate it to our Native UI.

Overview of Kotlin Multiplatform Mobile

With this project, I aim to build a very simple shopping list for iOS and Android with KMM, where the UI is built in SwiftUI/Jetpack Compose of course 😌. In the common Kotlin module used in both platforms we will provide a repository, where we will handle all data-related logic with a SQLite database — so we just need the implementation of the UI on the platform itself.

Caution #1: Mind that several features used in this post are still experimental. KMM is planned to turn into beta in Spring 2022.

Caution #1: I have 0 experience with SwiftUI and also I am not that much skilled in Jetpack Compose, so the UI parts below may not be the best case, however building similar UI components with both frameworks will of course help us compare these 2 platforms. 🙃

Setting up Kotlin Multiplatform Mobile

I created the project with the wizard provided in Android Studio (New Project/Kotlin Multiplatform App). Initially I tried to use CocoaPods as the dependency manager for iOS, however I had build issues on the initial Hello World project, which led me to try the Regular framework — where the KMM module is integrated via internal Gradle task and Xcode — which worked like a charm. The result of the wizard is a single screen app with Hello World for both platforms. Note that you are now able to run the iOS application in Xcode as well!

Project Tree

The root of the project tree consists a module for each platform and also an additional one called shared where we plan to have our repository with our database. Additional libraries we will use are kotlinx-coroutines-core and kotlinx-datetime. To avoid memory/concurrency issues with coroutines we will use the version 1.6.0-native-mt, which has the new memory model of Kotlin.

We will start with our data layer — the ShoppingListRepository! This will connect our platforms to our database and allows us to read the items with Kotlin Flow, and insert/update/delete items. After setting up the database and binding the platform drivers (like here) we can create a class for our database — ShoppingListDb — which will consist any interaction with the database, as following:

class ShoppingListDb(databaseDriverFactory: DatabaseDriverFactory) {
private val db = AppDatabase(databaseDriverFactory.createDriver())
private val dbQueries = db.appDatabaseQueries
fun getAllItems(): Flow<List<ShoppingListItem>> {
return dbQueries
.selectAllItems { id, name, timestamp, checked ->
ShoppingListItem(
id = id,
name = name,
timestamp = LocalDateTime.parse(timestamp),
isChecked = checked ?: false

)
}
.asFlow()
.mapToList()
}
fun insertItems(shoppingListItem: ShoppingListItem) {
dbQueries.insertItem(
id = shoppingListItem.id,
name = shoppingListItem.name,
timestamp = shoppingListItem.timestamp.toString(),
checked = shoppingListItem.isChecked

)
}
fun updateItemState(item: ShoppingListItem) {
dbQueries.updateItemCheckedState(!item.isChecked, item.id)
}
...
}

Besides the self-explaining insert/update functions, we use for the select function some important methods: .asFlow() turns our query into a Kotlin Flow and .mapToList() will map our database query into a List.

Our repository has even a simpler logic than our database, since we just have a single datasource and the repo will serve as a gateway between the database and the platforms:

class ShoppingListRepo(databaseDriverFactory:DatabaseDriverFactory){
private val db = ShoppingListDb(databaseDriverFactory)
// insert, update, delete, get}

Note that we will use this repo directly on our platforms and since the parameter DatabaseDriverFactory is client-specific, we need to pass it from there.

Android ↔ KMM

On Android, we listen to our database with Kotlin Flow and collect it as a state for our Compose UI — which means passing this to our Composable with LazyColumn is enough and the UI will refresh automatically at any change. Inserting/Deleting/Updating is pretty much straight forward. In the following snippet you can find all the calls to our repository bold styled.

class MainActivity : AppCompatActivity() {
private val repo = ShoppingListRepo(DatabaseDriverFactory(this))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...

Scaffold(
...,
content = {
// receive from items in Flow from our repository
val shoppingListItems: List<ShoppingListItem>
by repo.getShoppingListItems()
.collectAsState(initial = emptyList())
ShoppingList(shoppingListItems = shoppingListItems) AddItemDialog(show = showAddItemDialog) { textValue ->
addNewItem(textValue)
}
},
...
)
}
@Composable
fun ShoppingList(shoppingListItems: List<ShoppingListItem>) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(shoppingListItems) { shoppingListItem ->

val rememberDismiss = rememberDismissState()

if(rememberDismiss.isDismissed(DismissDirection.EndToStart)){
// delete dismissed item with our repository
repo.deleteShoppingListItem(shoppingListItem)

}

SwipeToDismiss(
state = rememberDismiss,
directions = setOf(DismissDirection.EndToStart),
background = {},
dismissThresholds = { FractionalThreshold(0.05f) },
dismissContent = {
ShoppingListItemRow(item = shoppingListItem)
Divider(startIndent = 16.dp, thickness = Dp.Hairline)
}
)
}
}
}
@Composable
fun ShoppingListItemRow(item: ShoppingListItem) {
Row(...) {
Checkbox(
checked = item.isChecked,
onCheckedChange = { repo.updateItemState(item) }
)

Text(item.name, fontSize = 16.sp)
}
}
private fun addNewItem(textValue: String) {
repo.insertShoppingListItemWithTextonly(
id = UUID.randomUUID().toString(),
text = textValue
)

}
}

Eventually, you can check the linked sample below to see all the Compose elements in harmony. 👯

iOS ↔ KMM

Now it is time to combine the mighty Kotlin with the beautiful SwiftUI. SwiftUI is built declarative as Jetpack Compose, but somehow it evolved much better over the years, which makes it IMHO more enjoyable.

So after the initial setup the module iosApp has already the shared module integrated and KMM already does the mapping of any primitive data types, Strings, collections, etc. to Swift accordingly, but what happens with types like Flow? As you already know, we’re listening to our database with Kotlin Flow and unfortunately there is no native support yet in KMM for this. While looking for ways, I stumbled upon 2 similar solutions, which were — broadly defined — just swift-supported wrappers around Flow (see #1 and #2). Besides them, I found a library called KMP-NativeCoroutines, which is adding the needed support for coroutines to KMM and allows us to decide even between Async, Combine or RxSwift.

First, our beloved UI:

var body: some View {
NavigationView {
List {
ForEach(viewModel.items) { item in
HStack{
Button(action: {
viewModel.updateItemState(item: item)
}) {
Image(...)
}
Text(item.name)
}
}
.onDelete { indexSet in
viewModel.removeItem(at: indexSet)
}
}
.navigationBarTitle("kmmShopper")
.navigationBarItems(trailing: Button( action: {
self.showAddItemModal = true
}){
Image(systemName: "plus")
})
.sheet(isPresented: $showAddItemModal) {
AddItemModalView { text in
viewModel.addNewItem(text: text)
}
}
}
}

Above you can see the action functions at our viewmodel boldly styled, and also the usage of the property viewModel.items, which should consist the current state of our database or to be more precise of the Flow in our repository. Next up, you can find the ShoppingListViewModel, which uses our repository from the shared module:

class ShoppingListViewModel: ObservableObject {
@Published var items = [ShoppingListItem]()

private var pollShoppingListItemsTask: Task<(), Never>? = nil
private let repository: ShoppingListRepo
init(repository: ShoppingListRepo) {
self.repository = repository
pollShoppingListItemsTask = Task {
do {
let stream = asyncStream(
for: repository.getShoppingListItemsNative()
)
for try await data in stream {
self.items = data
}
} catch {
print("Failed with error: \(error)")
}
}
}
func addNewItem(text:String) {
repository.insertShoppingListItemWithTextonly(
text: text, id: NSUUID.init().uuidString
)

}
func updateItemState(item: ShoppingListItem){
repository.updateItemState(item: item)
}
func removeItem(at offsets: IndexSet) {
let item = items[offsets[offsets.startIndex]]
repository.deleteShoppingListItem(item: item)
}
}

As you can see, the transactions are again pretty straight forward. But unlike these action functions, ‘getShoppingListItems’ has a suffix ‘Native’, which is the generated function by the library mentioned above — this helps us to use coroutines without minding any freeze/concurrency issues.

That’s it! We now have apps working for both platforms with native UI and single business logic. The code has of course still room for improvement regarding a proper design pattern or even lack of a dependency injection, but all in all it feels great for a Kotlin developer to jump into other platforms than Android that easily, even though there are some difficulties as seen with coroutines. Before trying out KMM, mind that it is still in early stages and you may not find enough sources to solve the issues you encounter. It will be probably also nice to see sophisticated KMM projects, e.g. using the camera 📸 or even Bluetooth. Networking should be straight forward with ktor.

Note: You can checkout the whole project here: https://github.com/k-kaan/KMM-ShoppingList

--

--