TechStacks Mobile Android App written in Kotlin
Kotlin Java

README.md

TechStacks Kotlin Android App

To demonstrate Kotlin Native Types in action we've ported the Swift TechStacks iOS App to a native Kotlin Android App to showcase the responsiveness and easy-of-use of leveraging Kotlin Add ServiceStack Reference in Android Projects.

The Android TechStacks App can be downloaded for free from the Google Play Store:

Data Binding

As there's no formal data-binding solution in Android we've adopted a lightweight iOS-inspired Key-Value-Observable-like data-binding solution in Android TechStacks in order to maximize knowledge-sharing and ease porting between native Swift iOS and Java/Kotlin Android Apps.

Similar to the Swift TechStacks iOS App, all web service requests are encapsulated in a single App.kt class and utilizes Async Service Client API's in order to maintain a non-blocking and responsive UI.

Registering for Data Updates

In iOS, UI Controllers register for UI and data updates by implementing *DataSource and *ViewDelegate protocols, following a similar approach, Android Activities and Fragments register for Async Data callbacks by implementing the Custom interface AppDataListener below:

interface AppDataListener {
    fun onUpdate(data: AppData, dataType: DataType)
}

Where Activities or Fragments can then register itself as a listener when they're first created:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    App.data.addListener(this)
}

Data Binding Async Service Responses

Then in onCreateView MainActivity calls the AppData singleton to fire off all async requests required to populate it's UI:

override fun onCreateView(inflater:LayoutInflater?, container:ViewGroup?, 
                          savedInstanceState:Bundle?): View? {
    App.data.loadAppOverview()
    ...
}

Where loadAppOverview() makes an async call to the AppOverview Service, storing the result in an AppData instance variable before notifying all registered listeners that DataType.AppOverview has been updated:

fun loadAppOverview(): AppData {
    if (appOverviewResponse != null) {
        onUpdate(DataType.AppOverview)
    }
    client.getAsync(AppOverview(), AsyncSuccess<AppOverviewResponse> {
        appOverviewResponse = it
        onUpdate(DataType.AppOverview)
    })
    return this
}

Returning this allows expression chaining, reducing the boilerplate required to fire off multiple requests

Calling onUpdate() simply invokes the list of registered listeners with itself and the enum DataType of what was changed, i.e:

fun onUpdate(dataType: DataType) {
    for (listener in listeners) {
        listener.onUpdate(this, dataType)
    }
}

The Activity can then update its UI within the onUpdate() callback by re-binding its UI Controls when relevant data has changed, in this case when AppOverview response has returned:

override fun onUpdate(data: App.AppData, dataType: App.DataType) {
    when (dataType) {
        App.DataType.AppOverview -> {
            val spinner = categorySpinner
            if (spinner != null) {
                val categories = data.appOverviewResponse!!.AllTiers.map { it.Title }
                spinner.adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, categories)
            }

            val list = topRatedListView
            if (list != null) {
                refreshTopTechnologies(data, list)
            }
        }
    }
}

In this case the MainActivity home screen re-populates the Technology Category Spinner (aka Picker) and the Top Technologies ListView controls by assigning a new Android ArrayAdapter.

Images and Custom Binary Requests

The TechStacks Android App can take advantage of the Custom Service Client API's to download images asynchronously. As images can be fairly resource and bandwidth intensive they're stored in a simple Dictionary Cache to minimize any unnecessary CPU and network resources, i.e:

internal var imgCache = HashMap<String, Bitmap>()
fun loadImage(imgUrl: String, callback: ImageResult) {
    val img = imgCache[imgUrl]
    if (img != null) {
        callback.success(img)
        return
    }

    client.getAsync(imgUrl, AsyncSuccess<ByteArray> {
        val img = AndroidUtils.readBitmap(it)
        imgCache.put(imgUrl, img)
        callback.success(img)
    })
}