Android/iOS Kotlin Multiplatform — Serialization and views

Antonio Acién
6 min readAug 12, 2019

--

This is the new chapter on the Multiplatform quest Carlos and I are doing at Mobgen. Here are the previous ones:

Chapter 1: Android/iOS Kotlin Multiplatform quick start guide

Chapter 2: Android/iOS Kotlin Multiplatform — Basics

Chapter 3: Android/iOS Kotlin Multiplatform — API and threading

Introduction

After figuring out the basics of the multiplatform stuff, as we narrated in our previous story, we felt compelled to do a somewhat more real example (keeping in mind this is all but a production environment, we are just testing and playing). Thus, we worked on a simple idea and built an app from there, which led us to libraries, dependencies, project structures and all the stuff that comes with creating a proper application.

The concept

The idea behind the project is as simple as it gets: a search bar where you input a keyword, and it searches recipes containing that word in an API. The API we used, for the sake of simplicity, is RecipePuppy, where you can just attach the keywords to the URL as a query. It is public, it is free, it is easy, it is just what we needed.

Only the bare minimum.

Serialization

Like in any project worth its salt, we needed some libraries to make our life a bit easier. In a multiplatform project, this leads us to search for multiplatform libraries, which support what we are trying to do. Surprisingly, we found out that there were plenty of libraries for most of what we needed.

At this point, we needed to mess up a bit more with the Gradle file. Earlier in the project, we had decided to stick to a single Gradle file, where the different modules are declared, rather than a several files linked, each belonging to a module.

For parsing JSON objects to Kotlin data classes, we added the Kotlinx.Serialization library, which was pretty much a cakewalk, as specified in their Github page. Simply add the common library in the common module, and then the JVM and the Native implementation in their own modules. The result looked like this. Notice the difference on the names of the implemented library on each module.

As you can see, the libraries used in the common part of the app need to have their implementation on each platform in order to run (Ktor, Coroutines, Serialization…), but we can have dependencies exclusive to each module, such as the RecyclerView or CardView in androidMain. The rest of dependencies were explained thoroughly in the previous chapter.

The app: common REST client code

The process is fairly simple. We’ll make a quick recap from the previous story, just in case: from the presenter down, it’s all common code in Kotlin. We simply have a presenter that retrieves the keyword we input previously, and through a use case and a repository, sends a request to the API. We make the call using Ktor, and the JSON response gets mapped to a Kotlin data class through the Kotlinx.Serialization library parsing. Then, we pass this class as a display to the presenter, which sets the view. Fairly easy and straightforward.

In order to make the serialization, we need to annotate the Kotlin data classes with “@Serializable”, and then, we just call the Serializer to parse the JSON when we recieve the API response. Just like this:

import kotlinx.serialization.Serializable

@Serializable
data class RecipesEntity(
val href: String,
val results: List<Result>,
val title: String,
val version: Double
) {
@Serializable
data class Result(
val href: String,
val ingredients: String,
val thumbnail: String,
val title: String
)
}

And this is the parsing:

suspend fun getRecipe(search: String): RecipesEntity =
Json(JsonConfiguration.Stable).let { json ->
json.parse(RecipesEntity.serializer(), client.get {
url(queryUrl + search)
})
}

The app: the views

Tasty multiplatform gazpacho recipes.

So, naturally, the one thing left to do was to make the Activities and layouts on Android, and the ViewControllers and Storyboards on iOS. The later was the trickiest part, mostly because our user experience with XCode was unpleasant to say the least. But the cool thing is that once you have the visual part ready, everything is all set, you just need to call the presenter, implement the interface which shows the data, et voilà. Any change on the backend will be done in both platforms without having to change anything platform-specific (aside from the look and feel).

So, in Android we have a simple activity to show the list of recipes:

class MainActivity : AppCompatActivity(), RecipeView {

private val recipeList: RecyclerView by lazy { findViewById<RecyclerView>(R.id.rv_cardList) }

override fun showState(state: RecipeState) {
try {
recipeList.adapter = RecipeAdapter(state.recipes)
} catch (e: Exception) {
print(e.message)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.recipe_view)

recipeList.layoutManager = LinearLayoutManager(this)

val p = RecipePresenter(this)
try {
val searchQuery = intent.getStringExtra("SEARCH")
if(!searchQuery.isNullOrEmpty()){
p.start(searchQuery)
}else {
p.start()
}
} catch (e: Exception) {
print(e.message)
}
}
}

And a simple adapter to use with the RecyclerView:

class RecipeAdapter(private val display: RecipesDisplay) : RecyclerView.Adapter<RecipeAdapter.RecipeViewHolder>() {

override fun onCreateViewHolder(root: ViewGroup, p1: Int): RecipeViewHolder {
return RecipeViewHolder(LayoutInflater.from(root.context).inflate(R.layout.card_view, root, false))
}

override fun getItemCount(): Int {
return display.results.size
}

override fun onBindViewHolder(holder: RecipeViewHolder, pos: Int) {
holder.title.text = display.results[pos].title
holder.ingredients.text = display.results[pos].ingredients
}

class RecipeViewHolder(v: View) : RecyclerView.ViewHolder(v) {
var img: ImageView = v.findViewById(R.id.iv_img)
var title: TextView = v.findViewById(R.id.tv_title)
var ingredients: TextView = v.findViewById(R.id.tv_ingredients)
}

}

I am leaving out the layouts and the manifest because you can check it in the code repository easily! The ViewController in iOS is as follows:

import UIKit
import app

class RecipeTableViewController: UITableViewController, RecipeView {

var searchQuery: String?

var recipes = [RecipesDisplay.Result]()

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipes.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "RecipeCellTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? RecipeCellTableViewCell else {
fatalError("The dequeued cell is not an instance of RecipeCellTableViewCell.")
}
let recipe = recipes[indexPath.row]
cell.label.text = recipe.title
cell.ingredients.text = recipe.ingredients
return cell
}

func showState(state: RecipeState) {
recipes = state.recipes.results
tableView.reloadData()
}

override func viewDidLoad() {
super.viewDidLoad()
let presenter = RecipePresenter(view: self)
presenter.start(search: searchQuery ?? "")

}
}

And this is the simple Cell for the TableView:

import UIKit

class RecipeCellTableViewCell: UITableViewCell {

//MARK: Properties

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}
@IBOutlet weak var label: UILabel!
@IBOutlet weak var ingredients: UILabel!
}

I am leaving out as well the segues and the storyboards because there is no easy visible way to attach them, but you can also check them in the repo.

Conclusions

It was quite surprising to find that there are multiplatform libraries for almost everything we needed, well-supported and fully functional. And if there are not, you can implement your own expect/actual functionality on each platform.

The coroutines on iOS are working but not having dispatchers for the background thread defeats the purpose. Luckily, Ktor does the work in the background thread on its own, so the app works as it should.

It would be interesting to see this very same project, but not done with the structure that IntelliJ makes for you, but instead, creating your own modules, and having a separate Gradle file for each.

All in all, we can see that Kotlin Multiplatform is a really powerful tool that could potentially save tons of work in development. Making a single change and having it applied to all the platforms really shortens the coding time, but it would be also interesting to see how this would work in a production environment, or in a substantially bigger app. Would we have a repo for each platform? A common one for them all? Would developers work only on their own platform, or only on the common code, or touch everything? This would depend largely on the approach of every team, but it is worth considering.

You can check our code here.

--

--