JSON feed reader app with Kotlin Native
Having spent some time exploring and learning more about Kotlin Native, time has come to start building an app where I can use Kotlin Native in a real world use case. After a lot of ideas, I finally decided to build a feed reader app for this blog.
It seems that such an app would be an ideal first project as I will have the opportunity to explore features like networking, de-serializing, storing user preference and much more that I will find out as I move on. At the same time, it doesn’t sound too demanding both in respect of time and effort.
For the sake of this app, I will use the JSON feed that I added on my Jekyll site, the process of which is documented on another post.
Furthermore, I will use a template that I have built as a base for any Kotlin Native project, and which already includes the required setup for unit tests, linting and CI. The whole process of how I created this template has been recorded in a series of posts.
So, are you ready to move on the implementation? Let’s go!
Implementation
First thing first, let’s start with the dependencies.
The feed reader will have to make an HTTP request to fetch the feed and then to transform the JSON response to a Kotlin object. To implement those tasks, we are going to use coroutines, ktor and the kotlinx serialization.
To begin with, let’s define the version of the dependencies on the gradle.properties
file, like below:
coroutines_version=1.2.2
serialization_version=0.11.1
ktor_version=1.2.4
Next, we will have to edit build.gradle
and add the following line on the dependencies
block:
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
After this, we can move to the shared/build.gradle
file.
First we have to add the following line to apply the kotlinx-serialization:
apply plugin: 'kotlinx-serialization'
and then we can define our dependencies for each target like in the following snippet:
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
androidMain {
dependencies {
implementation kotlin('stdlib')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
}
}
androidTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
iosMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
implementation "io.ktor:ktor-client-ios:$ktor_version"
implementation "io.ktor:ktor-client-core-native:$ktor_version"
implementation "io.ktor:ktor-client-serialization-native:$ktor_version"
}
}
iosTest {
}
Lastly, we have to add another line on the settings.gradle
for the coroutines:
enableFeaturePreview("GRADLE_METADATA")
All these changes can be found on this GitHub commit.
Having all the dependencies in place, we are ready to move on to the actual library.
Let’s navigate to the common
module inside the shared
folder. There, we will create a package, which will host our feed reader. In my case the package name is io.github.diamantidis.feedreader
and that will be the working directory for the rest of the post.
Models
Inside this package folder we will create a new directory named model
, where we will place the models that will be used to map the JSON feed to a Kotlin object.
Based on the JSON feed file we are going to need three classes, so let’s create three files inside the model
directory: Author.kt
, Feed.kt
and Item.kt
and add the following snippets respectively.
// model/Author.kt
package io.github.diamantidis.feedreader.model
import kotlinx.serialization.Serializable
@Serializable
data class Author (
val name: String,
val url: String
)
// model/Feed.kt
package io.github.diamantidis.feedreader.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Feed (
val version: String,
val title: String,
@SerialName("home_page_url")
val homePageURL: String,
@SerialName("feed_url")
var feedURL: String,
val description: String,
val icon: String? = null,
val favicon: String? = null,
var expired: Boolean? = null,
val author: Author,
val items: List<Item>
)
// model/Item.kt
package io.github.diamantidis.feedreader.model
import io.ktor.util.date.GMTDate
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import io.github.diamantidis.feedreader.utils.parseDate
@Serializable
data class Item (
val id: String,
val url: String,
val title: String,
@SerialName("date_published")
val datePublishedStr: String? = null,
@SerialName("date_modified")
val dateModifiedStr: String? = null,
val author: Author? = null,
val summary: String? = null,
@SerialName("content_html")
val contentHtml: String? = null
) {
@Transient
val datePublished: GMTDate?
get() = datePublishedStr?.parseDate()
@Transient
val dateModified: GMTDate?
get() = dateModifiedStr?.parseDate()
}
As you can see from these snippets, we are using the kotlinx.serialization
and the @Serializable
and @SerialName
annotations for the de-serialization. Some properties are declared optional, as they may not be on the response we are going to get from the feed. Furthermore, we make use of the @Transient
annotation alongside a helper function named parseDate()
and ktor’s GMTDate
to get a date representation of the dates which are plain string properties on the JSON feed.
For this helper function, and for any potential likewise functionality, I have created another folder named utils
. Inside this directory a file named DateFormatter.kt
is added with the following content:
package io.github.diamantidis.feedreader.utils
import io.ktor.util.date.GMTDate
import io.ktor.util.date.Month
internal fun String.parseDate(): GMTDate {
val dateRegex = "^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\\\.[0-9]+)?(Z)?".toRegex()
val matchResult = dateRegex.find(this)
matchResult?.groupValues?.let {
val year = it.get(1).toInt()
val month = it[2].toInt()
val day = it[3].toInt()
val hour = it[4].toInt()
val minute = it[5].toInt()
val second = it[6].toInt()
return GMTDate(second, minute, hour, day, Month.from(month - 1), year)
} ?: run {
throw Error("Error while parsing $this")
}
}
The helper function makes use of a regex to parse each component of a date field and then create an instance of GMTDate
.
All these changes can be found on this GitHub commit.
And that’s it with the model layer. We can now move on to the Ktor
implementation which will be responsible for the network request and the de-serialization of the response.
Networking
For the network we are going to create another package named network
and inside it we are going add a new file named Api.kt
. This file will have the following content:
package io.github.diamantidis.feedreader.network
import io.ktor.client.HttpClient
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.http.takeFrom
import kotlinx.serialization.json.Json
import io.github.diamantidis.feedreader.model.Feed
class Api {
private val client: HttpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(Json.nonstrict).apply {
setMapper(Feed::class, Feed.serializer())
}
}
}
private fun HttpRequestBuilder.apiUrl(path: String) {
url {
takeFrom("https://diamantidis.github.io/")
encodedPath = path
}
}
suspend fun fetchFeed(): Feed = client.get {
apiUrl("feed.json")
}
}
First we create the HttpClient and define the serializer that will transform the JSON response to a Kotlin object. Then, we define a helper function to construct the URL and lastly we define the coroutine function that will make the request and return an object of the class Feed
that we have previously mentioned.
All these changes can be found on this GitHub commit.
Coroutines
As we said before, for our network processes, we are going to make use of coroutines
.
Generally it’s a good practice to not use the GlobalScope
but rather provide a custom instance of CoroutineScope
. Since we are working on objects with a lifecycle, we should cancel all the operation related to these objects, when they are destroyed.
Using GlobalScope
we have to do this manual for each operation, whereas with the use of an instance of CoroutineScope
we can cancel all the operations of this scope by calling the function cancel
. For more info you can refer to Coroutine’s documentation.
To define a custom CoroutineScope
provider, let’s create a file named common.kt
and we add the following content:
package io.github.diamantidis.feedreader
import kotlinx.coroutines.CoroutineScope
internal expect fun ApplicationScope(): CoroutineScope
Basically, with this snippet, we are stating that we expect for a native implementation of a CoroutineScope
provider function from both the iOS and Android module.
So, on the Android module, we are going to add an ApplicationScope.kt
file containing the following snippet where we provide the actual implementation:
package io.github.diamantidis.feedreader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
internal actual fun ApplicationScope(): CoroutineScope = MainScope()
Contrary to the Android implementation, on the iOS module, we have a little more work to do. We have to create our own dispatcher, since iOS doesn’t provide a default one like Android.
Currently there is a known open issue regarding support for multi-threaded coroutines on Kotlin/Native and for this reason our custom dispatcher will use Grand Central Dispatch to run the asynchronous code on the main thread.
For this implementation, we are going to create a new file named ApplicationScope.kt
inside the iOS module and place the following code in it:
package io.github.diamantidis.feedreader
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Runnable
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
import platform.darwin.dispatch_queue_t
import kotlin.coroutines.CoroutineContext
import kotlin.native.concurrent.freeze
internal actual fun ApplicationScope(): CoroutineScope = CoroutineScope(MainDispatcher(dispatch_get_main_queue()))
internal class MainDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue.freeze()) {
block.run()
}
}
}
All these changes can be found on this GitHub commit.
Now, that we are done with the model, the network and the coroutines, let’s take some time to focus on the presentation.
Presentation
For the presentation logic, we are going to create yet another folder named presentation
. Since we have to perform a network request, every view that is going to fetch this feed will have to handle three states: Loading the data, a potential error and of course the successful scenario, in which case the feed info is fetched.
To cater for all these scenarios we are going to define an interface that our native views will have to implement. For this reason, let’s create a file named FeedView.kt
and add the following as a content:
package io.github.diamantidis.feedreader.presentation
import io.github.diamantidis.feedreader.model.Feed
interface FeedView {
fun showData(feed: Feed)
fun showError(error: Throwable)
fun showLoading()
}
We simply declare the three functions that we are going to call depending on the state of the request.
All these changes can be found on this GitHub commit.
And now it’s time to connect all these pieces together. We are following the MVP pattern, so we will define a presenter that will interact with the view both to fetch the data and to update the UI depending on the response of the network request.
Presenter
Let’s create a new folder named presenter
and a new file named FeedPresenter.kt
. Next, add the following snippet in this file.
package io.github.diamantidis.feedreader.presenter
import kotlinx.coroutines.*
import io.github.diamantidis.feedreader.ApplicationScope
import io.github.diamantidis.feedreader.model.Feed
import io.github.diamantidis.feedreader.network.Api
import io.github.diamantidis.feedreader.presentation.FeedView
class FeedPresenter(
private val view: FeedView,
private val api: Api = Api(),
private val mainScope: CoroutineScope = ApplicationScope()
) {
fun loadData() {
view.showLoading()
mainScope.launch {
try {
val result: Feed = api.fetchFeed()
view.showData(result)
} catch (error: Throwable) {
view.showError(error)
}
}
}
fun destroy() = mainScope.cancel()
}
In this class we inject the view that will present the data, using the interface we created earlier. We also inject the dependencies like the CoroutineScope
and the Api
, providing some default values.
For now, the main functionalities of this class are just two. One to initialize the process of fetching the feed and one for cancelling any pending process. The first function, loadData
, is responsible for triggering the network request and notifying the view about the state changes by calling the corresponding function declared in the FeedView
interface.
The destroy
function will be called to terminate all the operations on the injected scope when the caller object is going to be destroyed.
All these changes can be found on this GitHub commit.
With the above implementation of FeedPresenter
we should be done and ready to use our library from an iOS or Android app. Though on Android it is possible to create a new instance of the presenter by just calling private val presenter: FeedPresenter by lazy { FeedPresenter(this) }
from an activity that implements the FeedView
interface, on iOS is not so easy. Default constructors are not working as expected, so we have to instantiate a new instance of Api
and CoroutineScope
. Using a factory class would be a good alternative.
Factory
For this reason, let’s create a new directory named factory
and place a file named PresenterFactory.kt
inside it. Then, we put the following snippet as content to this file:
package io.github.diamantidis.feedreader.factory
import io.github.diamantidis.feedreader.presentation.FeedView
import io.github.diamantidis.feedreader.presenter.FeedPresenter
class PresenterFactory {
companion object {
fun createFeedPresenter(view: FeedView) = FeedPresenter(view)
}
}
In this file, we just declare a method on the companion object to instantiate the Presenter.
Then we can use this class to create our presenter on both iOS and Android, like in the following snippets:
// Android
private val presenter: FeedPresenter by lazy { PresenterFactory.createFeedPresenter(this) }
// iOS
private lazy var presenter = PresenterFactory.Companion().createFeedPresenter(view: self)
All these changes can be found on this GitHub commit.
Now, our JSON feed reader library is ready to be used on the apps. Let’s see how!
Android app
For the Android app we have to implement the FeedView
interface on our MainActivity
, lazy initialize the FeedPresenter
and then by calling the function loadData
to fetch the feed. We then make use of an ArrayAdapter
to populate a ListView
with the titles of the post items.
A dummy activity could be like the following example:
package io.github.diamantidis.androidApp
import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.*
import io.github.diamantidis.feedreader.factory.PresenterFactory
import io.github.diamantidis.feedreader.model.Feed
import io.github.diamantidis.feedreader.model.Item
import io.github.diamantidis.feedreader.presentation.FeedView
import io.github.diamantidis.feedreader.presenter.FeedPresenter
class MainActivity : AppCompatActivity(), FeedView {
override fun showData(feed: Feed) {
adapter.clear()
adapter.addAll(feed.items)
}
override fun showLoading() {
print("Loading")
}
override fun showError(error: Throwable) {
print("show error: ${error.message}")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val listView = ListView(this)
adapter = FeedAdapter(this, mutableListOf())
listView.adapter = adapter
presenter.loadData()
this.setContentView(listView)
}
private lateinit var adapter: FeedAdapter
private val presenter: FeedPresenter by lazy { PresenterFactory.createFeedPresenter(this) }
private inner class FeedAdapter(context: Context, items: List<Item>) :
ArrayAdapter<Item>(context, -1, -1, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val item = super.getItem(position)
val listLayout = LinearLayout(context)
listLayout.layoutParams = AbsListView.LayoutParams(
AbsListView.LayoutParams.WRAP_CONTENT,
AbsListView.LayoutParams.WRAP_CONTENT
)
val listText = TextView(context)
listText.setPadding(40, 40, 0, 40)
listText.text = item?.title
listLayout.addView(listText)
listLayout.setTag(item)
return listLayout
}
}
}
Besides that, a few more changes are required on the build.gradle
and the AndroidManifest.xml
. All these changes can be found on this GitHub commit.
iOS app
Similarly for iOS, we follow a similar procedure; implement the FeedView
interface from our UIViewController
, lazy initialize the FeedPresenter
and then call the loadData
function, though instead of a ListView
we make use of the equivalent on iOS, which is a UITableView
.
A dummy UIViewController could be like the following example:
import UIKit
import shared
class ViewController: UIViewController, FeedView {
func showError(error: KotlinThrowable) {
activityIndicatorView.stopAnimating()
}
func showData(feed: Feed) {
activityIndicatorView.stopAnimating()
self.tableView.separatorStyle = .singleLine
self.posts = feed.items
self.tableView.reloadData()
}
func showLoading() {
self.tableView.separatorStyle = .none
activityIndicatorView.startAnimating()
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor)
])
tableView.backgroundView = activityIndicatorView
presenter.loadData()
}
private var posts = [Item]()
private lazy var presenter = PresenterFactory.Companion().createFeedPresenter(view: self)
private lazy var activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
private lazy var tableView: UITableView = {
var tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return posts.isEmpty ? 0 : 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath)
cell.accessoryType = .disclosureIndicator
cell.textLabel!.text = posts[indexPath.row].title
return cell
}
}
Again, all the changes required to build a dummy iOS app that will use the JSON feed reader library, can be found on this GitHub commit.
Conclusion
To sum up, this post describes how to implement a JSON feed reader library with Kotlin Native and how to use it from both an Android and an iOS app. While building this library, we have seen how to use ktor
to implement the network request, utilize kotlinx-serialization
’s power to de-serialize the JSON response into a Kotlin object, learn how to work with coroutines
and take advantage of the MVP
pattern to communicate between the Kotlin Native library and the native application.
Definitely the process of building this app, though it still lacks some basic functionality, helped me a lot to get more acquainted with Kotlin Native. Next step? To iterate over this implementation and add some more features like presenting the content of the post or adding some caching layer!!
Thanks for reading and should you have any questions, suggestions or comments, just let me know on Twitter or email me!!