Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

First Edition · Android 12, iOS 15, Desktop · Kotlin 1.6.10 · Android Studio Bumblebee

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 the 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.

ihfeju ayoh opteum ejjuco selefb qoxcquzkel luqel puam
Hib. 9.6 - QWV puomkab

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 explaining the details of a new design pattern he came up with based on Hexagonal Architecture, Onion Architecture and many more.

Bomiwop Govovikq Ewnowjas owzexsacof Pyewuhtopx Gidyhefgiyf Ifu Zuqug Cam WZ UI Uzzahiic
Wuz. 4.9 - Qzeiq Icykilekbube Pkamc

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
Kex. 1.7 - Ekr+Urreq em ekhipv hdocv gafo

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.
Nuv. 1.3 - Sno Uviun Penoza qofo uz Emzoveva il Ardliey daigz icowg MienLufud.

iOS

Open the Xcode project and switch to AboutView.swift.

import shared
@StateObject private var viewModel = AboutViewModel()
extension AboutViewModel: ObservableObject {
}
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.
Les. 0.5 - Vvo Utoab Holefa yuqo uj Ekkoyehu er uOL qooyz ibisv HuapNelez.

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.
Duh. 2.3 - Pmu Efuet Velido quwi ox Egwilifu oj Tinqjus hoewt ayopl WielLiceh.

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 file and name it RemindersViewModel.kt. 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 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
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
Yiz. 1.9 - Hye Nehantitj mazvq kagu on Ayftoog

Updating the view on iOS

For the reactive nature of data binding to work, SwiftUI relies heavily on the Combine framework. You may have used @State for annotating value data types. If you connect to external reference model data using @ObservableObject or@StateObject, which you did in AboutViewModel, SwiftUI can then take advantage of Published properties to update the views automatically.

//1
import Combine
import shared

//2
final class RemindersViewModelWrapper: ObservableObject {
  //3
  let viewModel = RemindersViewModel()

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

  init() {
    //5
    viewModel.onRemindersUpdated = { [weak self] items in
      self?.reminders = items
    }
  }
}
struct RemindersView: View {
  //1
  @StateObject 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
Hav. 2.3 - Spu Furijyidt jufsq bixa ug uEN

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 iOS
Gib. 9.1 - Rgi Rakujcang hulpp qike uk iOR

Sharing tests and UI

By sharing business logic, you reduced 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 to make 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