In this previous section, we shared a single, simple class that exposed a platform-specific string. This demonstrated how we can leverage the expect and actual modifiers, but the overall example was quite simple. In this section, let's look at a slightly more interesting example of how we can architect common code and consume it across the different projects we've created.
For this, we perform the following steps:
- First, we're going to create a data class called ViewState to represent the data we want displayed to the screen. We'll define that class in a new file, core/src/commonMain/kotlin/ViewState.kt:
data class ViewState(val title: String, val subtitle: String)
ViewState will simply hold two strings: a title and a subtitle. We'll be using those shortly to update our UI.
- Next, we're going to create a Presenter class within our :core module. This Presenter class will expose a listener that will provide instances of ViewState, and it will have a method for handling clicks:
class Presenter {
private var count = 0
private var viewState = ViewState(Platform.name,
"Clicked ${++count} times")
set(value) {
field = value
viewStateListener?.invoke(value)
}
var viewStateListener: ((ViewState) -> Unit)? = null
set(value) {
field = value
value?.invoke(viewState)
}
fun onClick() {
viewState = viewState.copy(subtitle =
"Clicked ${++count} times")
}
}
With this Presenter class, we can define how state and interactions are managed in a platform-independent way, and then reuse this class across our different build targets.
- Now, we're going to make use of our new Presenter class within our :androidapp module. We'll update MainActivity.kt to use the presenter to respond to click events and to update our UI:
class MainActivity : AppCompatActivity() {
lateinit var title: TextView
lateinit var subtitle: TextView
lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title = findViewById(R.id.titleText)
subtitle = findViewById(R.id.subtitleText)
button = findViewById(R.id.button)
val presenter = Presenter().apply {
viewStateListener = {
title.text = it.title
subtitle.text = it.subtitle
}
}
button.setOnClickListener { presenter.onClick() }
}
}
We've updated MainActivity.kt to work with three views: two TextViews and a Button. We create an instance of Presenter, and then assign viewStateListener so that we can respond to changes in the ViewState by updating our UI. We then set a click listener on the button that calls Presenter.onClick().
The result is that when the button is clicked, UI should be updated to display the number of times the button has been clicked.
- To demonstrate the fact that Presenter can be used across various targets, we'll now update our iOS app to use Presenter in the same way as the Android app. To do this, we'll update iosapp/iosapp/ViewController.swift:
class ViewController: UIViewController {
let presenter = Presenter()
override func viewDidLoad() {
super.viewDidLoad()
let title = UILabel(frame: CGRect(x: 0, y: 0,
width: 300, height: 21))
title.center = CGPoint(x: 160, y: 285)
title.textAlignment = .center
title.font = title.font.withSize(25)
view.addSubview(title)
let subtitle = UILabel(frame: CGRect(x: 0, y: 0,
width: 300, height: 21))
subtitle.center = CGPoint(x: 160, y: 385)
subtitle.textAlignment = .center
subtitle.font = subtitle.font.withSize(25)
view.addSubview(subtitle)
let button = UIButton(frame: CGRect(x: 0, y: 0,
width: 300, height: 21))
button.center = CGPoint(x: 160, y: 485)
button.setTitle("Click Me", for: .normal)
button.backgroundColor = UIColor(red: 0, green: 50/255,
blue: 51/255, alpha: 0.5)
view.addSubview(button)
presenter.viewStateListener = {
title.text = $0.title
subtitle.text = $0.subtitle
return KotlinUnit()
}
button.addTarget(self, action: #selector(buttonClicked),
for: .touchUpInside)
}
@objc func buttonClicked() {
presenter.onClick()
}
}
Once again, we are using Presenter to listen to changes in ViewState, and to respond to click events. Notice that the changes to the iOS project look very similar to that of the Android project. With a Kotlin multiplatform project, if you can define common abstractions in the shared code, then the work remaining for every specific platform is mostly to perform simple binding of the data to the UI. Because the general pattern becomes unified across platforms, the ultimate implementation of the UI layer across all platforms will likely be quite similar.
In the next section, we'll see how we can use available multiplatform libraries to simplify the development process and to help make our common architecture unique across all platforms.