Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Second Edition · Android 14, iOS 17, Desktop · Kotlin 1.9.10 · Android Studio Hedgehog

7. App Architecture
Written by Saeed Taheri

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

In the previous chapter, you started creating the Organize app. However, you didn’t make it to the organization part. In this chapter, you’ll lay the groundwork for implementing a maintainable and scalable app.

Anyone who has ever played with LEGO bricks has tried to make the highest tower possible by putting all the bricks on top of each other. While this may work in specific scenarios, your tower will fall down at even the slightest breeze.

That’s why architects and civil engineers never create a building or tower like that. They plan extensively, so their creations stay stable for decades. The same applies to the software world.

If you remember your first days of learning to program, there’s a high chance that you wrote every piece of your program’s code inside a single file. That was cool until you needed to add a few more features or address an issue.

Although the term software architecture is relatively new in the industry, software engineers have applied the fundamental principles since the mid-1980s.

Design Patterns

The broad heading of software architecture consists of numerous subtopics. One of these is architectural styles — otherwise known as software design patterns. This topic is so substantial that many people use software design patterns to refer to software architecture itself.

Depending on how long you’ve been programming, you may have heard of or utilized a handful of those patterns, such as Clean Architecture, Model-View-ViewModel (MVVM), Model-View-Controller (MVC) and Model-View-Presenter (MVP).

When incorporating KMP, you’re free to use any design pattern you see fit for your application.

If you come from an iOS background, and you’re mostly comfortable with MVC, KMP will embrace you. If you’re mainly an Android developer, and you follow Google’s recommendation on using MVVM, you’ll feel right at home as well.

There is no best or worst way to do it.

Next, you’ll find an introduction to some design patterns many developers take advantage of.

Model-View-Controller

The MVC pattern’s history goes back to the 1970s. Developers have commonly used MVC for making graphical user interfaces on desktop and web applications.

apgoco otuh asbeeq iqtaga doqivm vedcjivtac folik maop
Noq. 9.4 — MGP hoonmar

Model-View-ViewModel

As the name implies, MVVM is a great fit for applications with views or user interfaces. Since the concept of bindings is prominent in this pattern, some people also call it Model-View-Binder.

Clean Architecture

In 2012, Robert C. Martin, also known as Uncle Bob, published a post in his blog at https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html explaining the details of a new design pattern he came up with based on Hexagonal Architecture, Onion Architecture and many more.

Coliwex Sujatabx Egdoyboz ixsebmenav Whigogkatr Ciyvhudzafv Iyi Vokos Yol SK IA Ulhijaer
Nuj. 2.8 — Hveaq Enmcadalwine Bqujc

Sharing Business Logic

KMP shines when you try to minimize the duplicated code you write. In the previous chapter, you wrote the logic for the About Device page twice. That code could easily be inside the shared module and all the platforms would be able to take advantage of it.

Creating ViewModels

Open the starter project in Android Studio. It’s mostly the final project of the previous chapter.

expect abstract class BaseViewModel()
Fig. 7.3 — Alt+Enter on expect class name
Vap. 7.4 — Ezx+Uypoc az iynedk rhumv vodu

actual abstract class BaseViewModel : ViewModel()
import androidx.lifecycle.ViewModel
actual abstract class BaseViewModel actual constructor()

Creating AboutViewModel

Now that you have a base viewmodel, it’s time to create the concrete versions. Start by creating a file named AboutViewModel.kt in the commonMain folder inside the presentation directory.

class AboutViewModel: BaseViewModel() {
}
private val platform = Platform()
data class RowItem(
  val title: String,
  val subtitle: String,
)
private fun makeRowItems(platform: Platform): List<RowItem> {
  val rowItems = mutableListOf(
    RowItem("Operating System", "${platform.osName} ${platform.osVersion}"),
    RowItem("Device", platform.deviceModel),
    RowItem("CPU", platform.cpuType),
  )
  platform.screen?.let {
    rowItems.add(
      RowItem(
        "Display",
        "${
          max(it.width, it.height)
        }×${
          min(it.width, it.height)
        } @${it.density}x"
      ),
    )
  }
  return rowItems
}
val items: List<RowItem> = makeRowItems(platform)

Using AboutViewModel in the View Layer

Android

Open AboutView.kt inside the androidApp module.

@Composable
fun AboutView(
  viewModel: AboutViewModel = AboutViewModel(),
  onUpButtonClick: () -> Unit
)
@Composable
private fun ContentView(items: List<AboutViewModel.RowItem>) {
  LazyColumn(
    modifier = Modifier.fillMaxSize(),
  ) {
    items(items) { row ->
      RowView(title = row.title, subtitle = row.subtitle)
    }
  }
}
ContentView(items = viewModel.items)
Fig. 7.4 — The About Device page of Organize on Android built using ViewModel.
Yiy. 0.7 — Xqo Evooc Pojuqa belu ed Exkasesi uw Ojfliel yoabl uceyh DiivHacac.

iOS

Open the Xcode project and switch to AboutView.swift.

import Shared
@State private var viewModel = AboutViewModel()
let items: [AboutViewModel.RowItem]
AboutListView(items: [AboutViewModel.RowItem(title: "Title", subtitle: "Subtitle")])
AboutListView(items: viewModel.items)
Fig. 7.5 — The About Device page of Organize on iOS built using ViewModel.
Jir. 9.9 — Sya Igeir Fikuxi tihe iz Uzrezuhu et oEM nuiwq ogoqd BiapBojas.

Desktop

You’re now familiar with the process. Since you created the desktop app using Jetpack Compose, even the function names you need to change are the same or very similar to the Android version. Remove the unneeded function for generating the data and replace the ContentView method in AboutView.kt in the desktopApp module.

@Composable
fun AboutView(viewModel: AboutViewModel = AboutViewModel()) {
  ContentView(items = viewModel.items)
}

@Composable
private fun ContentView(items: List<AboutViewModel.RowItem>) {
  LazyColumn(
    modifier = Modifier.fillMaxSize(),
  ) {
    items(items) { row ->
      RowView(title = row.title, subtitle = row.subtitle)
    }
  }
}
Fig. 7.6 — The About Device page of Organize on desktop built using ViewModel.
Gug. 2.7 — Gci Ureeg Temagi hapu ar Alnipuqu ir tebnrij goamq osagn PeehFaqor.

Creating Reminders Section

Until now, you were working on a supplementary page of the app. There was a reason for this: You wanted to avoid redoing everything for all platforms. However, now you know what the app’s structure is and where you could put the shared business logic.

Repository Pattern

A first idea for implementing the RemindersViewModel might involve directly creating, updating and deleting reminders and exposing the data and the actions to RemindersView. This design works, but by using it, the app becomes more and more difficult to maintain as it grows. It gives too much responsibility to the RemindersViewModel class, which violates the separation of concerns principle.

private val _reminders: MutableList<Reminder> = mutableListOf()
data class Reminder(
  val id: String,
  val title: String,
  val isCompleted: Boolean = false,
)
fun createReminder(title: String) {
  val newReminder = Reminder(
    id = UUID().toString(),
    title = title,
    isCompleted = false
  )
  _reminders.add(newReminder)
}
fun markReminder(id: String, isCompleted: Boolean) {
  val index = _reminders.indexOfFirst { it.id == id }
  if (index != -1) {
    _reminders[index] = _reminders[index].copy(isCompleted = isCompleted)
  }
}
val reminders: List<Reminder>
  get() = _reminders

Creating RemindersViewModel

Inside the presentation directory of commonMain module, create a new class named RemindersViewModel. Update it with the following:

class RemindersViewModel : BaseViewModel() {
  //1
  private val repository = RemindersRepository()

  //2
  private val reminders: List<Reminder>
    get() = repository.reminders

  //3
  var onRemindersUpdated: ((List<Reminder>) -> Unit)? = null
    set(value) {
      field = value
      onRemindersUpdated?.invoke(reminders)
    }

  //4
  fun createReminder(title: String) {
    val trimmed = title.trim()
    if (trimmed.isNotEmpty()) {
      repository.createReminder(title = trimmed)
      onRemindersUpdated?.invoke(reminders)
    }
  }

  //5
  fun markReminder(id: String, isCompleted: Boolean) {
    repository.markReminder(id = id, isCompleted = isCompleted)
    onRemindersUpdated?.invoke(reminders)
  }
}

Updating View on Android

Open RemindersView.kt inside the androidApp module. In the beginning, add a parameter with a default value for the RemindersViewModel to the RemindersView function. Then, pass viewModel into the ContentView method.

@Composable
fun RemindersView(
  viewModel: RemindersViewModel = RemindersViewModel(),
  onAboutButtonClick: () -> Unit,
) {
  Column {
    Toolbar(onAboutButtonClick = onAboutButtonClick)
    ContentView(viewModel = viewModel)
  }
}
@Composable
private fun ContentView(viewModel: RemindersViewModel) {
}
var reminders by remember {
  mutableStateOf(listOf<Reminder>(), policy = neverEqualPolicy())
}
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember
viewModel.onRemindersUpdated = {
  reminders = it
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
  //1
  items(items = reminders) { item ->

    //2
    val onItemClick = {
      viewModel.markReminder(id = item.id, isCompleted = !item.isCompleted)
    }

    //3
    ReminderItem(
      title = item.title,
      isCompleted = item.isCompleted,
      modifier = Modifier
        .fillMaxWidth()
        .clickable(enabled = true, onClick = onItemClick)
        .padding(horizontal = 16.dp, vertical = 4.dp)
    )
  }
}
item {
  //1
  val onSubmit = {
    viewModel.createReminder(title = textFieldValue)
    textFieldValue = ""
  }

  //2
  NewReminderTextField(
    value = textFieldValue,
    onValueChange = { textFieldValue = it },
    onSubmit = onSubmit,
    modifier = Modifier
      .fillMaxWidth()
      .padding(vertical = 8.dp, horizontal = 16.dp)
    )
}
var textFieldValue by remember { mutableStateOf("") }
Fig. 7.7 — The Reminders first page on Android
Xas. 0.4 — Mco Saqahsumf xuyww deqa af Edtxioz

Updating the View on iOS

For the reactive nature of data binding to function effectively, SwiftUI heavily relies on the Combine framework or, as of iOS 17, the Observation framework. If you are targeting iOS 16 and earlier, you may have used @State to annotate value data types, and @StateObject or @StateObject for external reference model data. With the advent of iOS 17, Apple has simplified state management. With the assistance of the Observation framework, you can now utilize @State for all data types, whether they are value types or reference types.

//1
import Observation
import Shared

//2
@Observable
final class RemindersViewModelWrapper {
  //3
  let viewModel = RemindersViewModel()

  //4
  private(set) var reminders: [Reminder] = []

  init() {
    //5
    viewModel.onRemindersUpdated = { [weak self] items in
      self?.reminders = items
    }
  }
}
struct RemindersView: View {
  //1
  @State private var viewModelWrapper = RemindersViewModelWrapper()

  //2
  @State private var textFieldValue = ""

  var body: some View {
    //3
    List {
      //4
      if !viewModelWrapper.reminders.isEmpty {
        Section {
          ForEach(viewModelWrapper.reminders, id: \.id) { item in
            //5
            ReminderItem(title: item.title, isCompleted: item.isCompleted)
              .onTapGesture {
                //6
                withAnimation {
                  viewModelWrapper.viewModel.markReminder(
                    id: item.id,
                    isCompleted: !item.isCompleted
                  )
                }
              }
          }
        }
      }

      //7
      Section {
        NewReminderTextField(text: $textFieldValue) {
          withAnimation {
            viewModelWrapper.viewModel.createReminder(title: textFieldValue)
            textFieldValue = ""
          }
        }
      }
    }
    .navigationTitle("Reminders")
  }
}
Fig. 7.8 — The Reminders first page on iOS
Rev. 9.5 — Lda Foleghigg ketwb rota og eIP

Updating View on Desktop

Since the desktop app is using Jetpack Compose, you can literally copy and paste the code from RemindersView.kt in the androidApp module to the same file in the desktopApp module.

Fig. 7.9 — The Reminders first page on desktop
Veq. 6.2 — Xse Xuyamcits hufrr reza ux caskloj

Sharing Tests and UI

By sharing business logic, you reduce the code you need to write for each platform to their respective UI code.

Challenge

Here’s a challenge for you to see if you mastered this chapter. The solution is waiting for you inside the materials for this chapter.

Challenge: Moving Page Titles to Viewmodels

As viewmodels are responsible for making everything ready for views to show, you can make the viewmodels provide the page title to their respective views. This way, you can transfer one other point of code duplication to the shared platform and prevent wrong titles for pages or typos.

Key Points

  • You can use any design pattern you see fit with Kotlin Multiplatform.
  • You got acquainted with the principal concepts of MVC, MVVM and Clean Architecture.
  • Sharing data models, viewmodels and repositories between platforms using Kotlin Multiplatform is straightforward.
  • You can share business logic tests using Kotlin Multiplatform.
  • Although possible, it isn’t always the best decision to share UI between platforms.
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.
© 2024 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