Skip to main content

6 posts tagged with "kotlin"

View All Tags

· 16 min read
Boris Nikolov

 Kotlin Multiplatform Mobile including Android and iOS

Introduction to Kotlin Multiplatform Mobile

Understanding Kotlin Multiplatform Mobile

What is KMM?

Kotlin Multiplatform Mobile is an extension of the Kotlin programming language that enables the sharing of code between different platforms, including Android and iOS. Unlike traditional cross-platform frameworks that rely on a common runtime, KMM allows developers to write platform-specific code while sharing business logic and other non-UI code.

Key Advantages of KMM

  1. Code Reusability: With KMM, you can write and maintain a single codebase for your business logic, reducing duplication and ensuring consistency across platforms.
  2. Native Performance: KMM leverages the native capabilities of each platform, providing performance comparable to writing platform-specific code. All your KMM code is built to platform-specific code before running on any device following all the latest best practices eventually providing users peak native capabilities.
  3. Interoperability: KMM seamlessly integrates with existing codebases and libraries, allowing developers to leverage platform-specific features when needed.
  4. Incremental Adoption: You can introduce KMM gradually into your projects, starting with shared modules and gradually expanding as needed.

KMM vs. Flutter

While KMM and Flutter do have a lot in common in terms of functionality and end result, they have very different approaches to reaching it:

  1. Programming language - KMM uses Kotlin, a language known for its conciseness, safety features and strong null-safety. Flutter on the other hand uses Dart, developed by Google and specifically targeted at building UIs through a reactive programming model
  2. Architecture - KMM focuses on sharing business logic between platforms and encourages a modular architecture by mixing sharing of core business logic modules with platform specific UI implementations. Flutter embraces a reactive and declarative UI framework with a widget-based architecture. The entire UI in Flutter is expressed as a hierarchy of widgets and doesn’t have a clear separation between business logic and UI.
  3. UI Framework - KMM doesn’t have a UI framework of its own, but rather leverages native UI frameworks like Jetpack Compose for Android and SwiftUI for iOS. Flutter proposes a custom UI framework that is equipped with a rich set of customisable widgets. The UI is rendered via the Skia graphics engine which is aimed at delivering a consistent look and feel across all supported platforms.
  4. Community and ecosystem - KMM is actively developed by JetBrains and has been gaining a lot of traction since inception by drawing many benefits from the Kotlin community. Flutter is maintained by Google and has a large and active community. It’s constantly growing its ecosystem of packages and plugins.
  5. Integration with native code - KMM seamlessly integrates with native codebases making its adoption effortless. Flutter relies on a platform channel mechanism to communicate with native code. It can invoke platform-specific functionality, but requires additional setup.
  6. Performance - Kotlin compiles to native code, providing near-native performance. Flutter uses a custom rendering engine (Skia) and introduces an additional layer between the app and the platform, potentially affecting performance in graphic-intensive applications.
  7. Platform support - KMM currently supports Android and iOS devices with planned support for other platforms in the future. Flutter has a broader range of supported platforms including Android, iOS, web, desktop (yet in experimental stage) and embedded devices.

The choice between KMM and Flutter still remains mostly subjective and is still dependent on language and architecture preferences, specific project requirements and of course - personal choice.

Creating a New KMM Project

Creating a new KMM project is a straightforward process:

  1. Open Android Studio:
    • Select "Create New Project."
    • Choose the "Kotlin Multiplatform App" template.
  2. Configure Project Settings:
    • Provide a project name, package name, and choose a location for your project.
  3. Configure Platforms:
    • Choose names for the platform-specific and shared modules (Android, iOS and shared).
    • Configure the Kotlin version for each platform module.
  4. Finish:
    • Click "Finish" to let Android Studio set up your KMM project.

If you don’t see the “Kotlin Multiplatform App” template then open Settings > Plugins, type “Kotlin Multiplatform Mobile”, install the plugin and restart your IDE.

“Kotlin Multiplatform Mobile plugin IDE

Project Structure and Organization

Understanding the structure of a KMM project is crucial for efficient development:

MyKMMApp
|-- shared
| |-- src
| |-- commonMain
| |-- androidMain
| |-- iosMain
|-- androidApp
|-- iosApp
  • shared: Contains code shared between Android and iOS.
  • commonMain: Shared code that can be used on both platforms.
  • androidMain: Platform-specific code for Android.
  • iosMain: Platform-specific code for iOS.
  • androidApp: Android-specific module containing code and resources specific to the Android platform.
  • iosApp: iOS-specific module containing code and resources specific to the iOS platform.

Shared Code Basics: Writing Platform-Agnostic Logic

Now that you have your Kotlin Multiplatform Mobile (KMM) project set up, it's time to dive into the heart of KMM development—writing shared code. In this chapter, we'll explore the fundamentals of creating platform-agnostic logic that can be used seamlessly across Android and iOS.

Identifying Common Code Components

The essence of KMM lies in identifying and isolating the components of your code that can be shared between platforms. Common code components typically include:

  • Business Logic: The core functionality of your application that is independent of the user interface or platform.
  • Data Models: Definitions for your application's data structures that remain consistent across platforms.
  • Utilities: Helper functions and utilities that don't rely on platform-specific APIs.

Identifying these shared components sets the foundation for maximizing code reuse and maintaining a consistent behavior across different platforms.

Writing Business Logic in Shared Modules

In your KMM project, the commonMain module is where you'll write the majority of your shared code. Here's a simple example illustrating a shared class with business logic:

// shared/src/commonMain/kotlin/com.example.mykmmapp/Calculator.kt

package com.example.mykmmapp

class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}

fun multiply(a: Int, b: Int): Int {
return a * b
}
}

In this example, the Calculator class provides basic mathematical operations and can be used across both Android and iOS platforms.

Ensuring Platform Independence

While writing shared code, it's crucial to avoid dependencies on platform-specific APIs. Instead, use Kotlin's expect/actual mechanism to provide platform-specific implementations where necessary.

Here's an example illustrating the use of expect/actual for platform-specific logging. In order to stay consistent while writing your code it’s recommended to use the same service provider on both platforms, for example Shipbook’s logger providing all required dependencies for both platforms. For the sake of simplicity, the example given below is using the native loggers of each platform.

Code in shared module:

// shared/src/commonMain/kotlin/com.example.mykmmapp/Logger.kt

package com.example.mykmmapp

expect class Logger() {
fun log(message: String)
}

Code in Android’s module:

// shared/src/androidMain/kotlin/com.example.mykmmapp/AndroidLogger.kt

package com.example.mykmmapp

actual class Logger actual constructor() {
actual fun log(message: String) {
android.util.Log.d("MyKMMApp", message)
}
}

Code in iOS’s module:

// shared/src/iosMain/kotlin/com.example.mykmmapp/IOSLogger.kt

package com.example.mykmmapp

import platform.Foundation.NSLog

actual class Logger actual constructor() {
actual fun log(message: String) {
NSLog("MyKMMApp: %@", message)
}
}

By employing expect/actual declarations, you ensure that the shared code can utilize platform-specific features without compromising the platform independence of the core logic.

Platform-Specific Code: Adapting for Android

Now that you've laid the groundwork with shared code, it's time to explore the intricacies of adapting your Kotlin Multiplatform Mobile (KMM) project for the Android platform.

Leveraging Platform-Specific APIs

One of the advantages of KMM is the ability to seamlessly integrate with platform-specific APIs. In Android development, you can use the Android-specific APIs in the androidMain module. Here's an example of using the Android Toast API:

// shared/src/androidMain/kotlin/com.example.mykmmapp/Toaster.kt

package com.example.mykmmapp

import android.content.Context
import android.widget.Toast

actual class Toaster(private val context: Context) {
actual fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}

In this example, the Toaster class is designed to display Toast messages on Android. The class takes an Android Context as a parameter, allowing it to interact with Android-specific features.

Managing Platform-Specific Dependencies

When working with platform-specific code, it's common to have dependencies that are specific to each platform. KMM provides a mechanism to manage platform-specific dependencies using the expect and actual declarations. For example, if you need a platform-specific library for Android, you can declare the expected behavior in the shared module and provide the actual implementation in the Android module.

Here is a shared class and function intended to fetch data from an online source making a HTTP request:

// shared/src/commonMain/kotlin/com.example.mykmmapp/NetworkClient.kt

package com.example.mykmmapp

expect class NetworkClient() {
suspend fun fetchData(): String
}

Android-specific implementation:

//shared/src/androidMain/kotlin/com.example.mykmmapp/AndroidNetworkClient.kt

package com.example.mykmmapp

import okhttp3.OkHttpClient
import okhttp3.Request

actual class NetworkClient actual constructor() {
private val client = OkHttpClient()

actual suspend fun fetchData(): String {
val request = Request.Builder()
.url("https://api.example.com/data")
.build()

val response = client.newCall(request).execute()
return response.body?.string() ?: "Error fetching data"
}
}

In this example, the NetworkClient interface is declared in the shared module, and the Android-specific implementation is provided in the androidMain module using the OkHttp library.

Building UI with Kotlin Multiplatform

User interfaces play a pivotal role in mobile applications, and with Kotlin Multiplatform Mobile (KMM), you can create shared UI components that work seamlessly across Android and iOS. In this chapter, we'll explore the basics of building UI with KMM, creating shared UI components, and handling platform-specific UI differences.

Overview of KMM UI Capabilities

KMM provides a unified approach to UI development, allowing you to share code for common UI elements while accommodating platform-specific nuances. The shared UI code resides in the “commonMain” module, and platform-specific adaptations are made in the “androidMain” and “iosMain” modules. A more convenient, but advanced approach to designing shared components would be to use a multiplatform composer tool, like the one provided by JetBrains named Compose Multiplatform. While still young in its development, it already provides powerful approach to writing UI logic reusable in many platforms like:

  • Android (including Jetpack Compose, hence the name “Compose Multiplatform)
  • iOS (currently in Alpha, but unfortunately without support for SwiftUI)
  • Desktop (Windows, Mac and Linux)
  • Web (but still in Experimental stage)

Creating Shared UI Components

Let's consider a simple example of creating a shared button component:

// shared/src/commonMain/kotlin/com.example.mykmmapp/Button.kt

package com.example.mykmmapp

expect class Button {
fun render(): Any
}

In this example, the Button interface is declared in the shared module, and the actual rendering implementation is provided in the platform-specific modules.

Android Implementation

// shared/src/androidMain/kotlin/com.example.mykmmapp/AndroidButton.kt

package com.example.mykmmapp

import android.widget.Button

actual class Button actual constructor(private val text: String) {
actual fun render(): Button {
val button = Button(AndroidContext.appContext)
button.text = text
return button
}
}

iOS Implementation

// shared/src/iosMain/kotlin/com.example.mykmmapp/IOSButton.kt

package com.example.mykmmapp

import platform.UIKit.UIButton
import platform.UIKit.UIControlStateNormal

actual class Button actual constructor(private val text: String) {
actual fun render(): UIButton {
val button = UIButton()
button.setTitle(text, UIControlStateNormal)
return button
}
}

In these platform-specific implementations, we use Android's “Button” and iOS's “UIButton” to render the button with the specified text.

Storing Platform-Specific Resources

To manage platform-specific resources such as layouts or styles, you can utilize the “androidMain/res” and “iosMain/resources” directories. This allows you to tailor the UI experience for each platform without duplicating code.

Interoperability: Bridging the Gap Between Kotlin and Native Code

Kotlin Multiplatform Mobile (KMM) doesn't exist in isolation; it seamlessly integrates with native code on each platform, allowing you to leverage platform-specific libraries and functionalities. In this chapter, we'll explore the intricacies of interoperability, incorporating platform-specific libraries, communicating between shared and platform-specific code, and addressing data serialization/deserialization challenges.

Incorporating Platform-Specific Libraries

One of the strengths of KMM is its ability to integrate with existing platform-specific libraries. This allows you to leverage the rich ecosystems of Android and iOS while maintaining a shared codebase. Let's consider an example where we integrate an Android-specific library for image loading.

Shared Code Interface

// shared/src/commonMain/kotlin/com.example.mykmmapp/ImageLoader.kt

package com.example.mykmmapp

expect class ImageLoader {
fun loadImage(url: String): Any
}

Android Implementation

// shared/src/androidMain/kotlin/com.example.mykmmapp/AndroidImageLoader.kt

package com.example.mykmmapp

import android.widget.ImageView
import com.bumptech.glide.Glide

actual class ImageLoader actual constructor() {
actual fun loadImage(url: String): ImageView {
val imageView = ImageView(AndroidContext.appContext)
Glide.with(AndroidContext.appContext).load(url).into(imageView)
return imageView
}
}

In this example, we've integrated the popular Glide library for Android to load images. The ImageLoader interface is declared in the shared module, and the actual implementation utilizes Glide in the Android-specific module.

Communicating Between Shared and Platform-Specific Code

Effective communication between shared and platform-specific code is crucial for building cohesive applications. KMM provides mechanisms for achieving this, including the use of interfaces, callbacks, and delegation.

Callbacks and Delegation

// shared/src/commonMain/kotlin/com.example.mykmmapp/CallbackListener.kt

package com.example.mykmmapp

interface CallbackListener {
fun onResult(data: String)
}

Usage in Android-specific module

//shared/src/androidMain/kotlin/com.example.mykmmapp/AndroidCallbackHandler.kt

package com.example.mykmmapp

actual class AndroidCallbackHandler {
private var callback: CallbackListener? = null

fun setCallback(callback: CallbackListener) {
this.callback = callback
}

fun performCallback(data: String) {
callback?.onResult(data)
}
}

In this example, the “AndroidCallbackHandler” class in the Android-specific module utilizes the shared callback interface and acts as an intermediary for callback communication between shared code and Android-specific code.

Handling Data Serialization/Deserialization

When dealing with shared data models, KMM provides tools for efficient data serialization and deserialization. The “kotlinx.serialization” library simplifies the process of converting objects to and from JSON, facilitating seamless communication between shared and platform-specific code.

Add Serialization Dependency

Ensure that your shared module has the kotlinx.serialization dependency added to its “build.gradle.kts” or “build.gradle” file:

commonMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
}
}

Define Serializable Data Class:

Create a data class that represents the structure of your serialized data. Annotate it with “@Serializable”:

// shared/src/commonMain/kotlin/com.example.mykmmapp/User.kt

package com.example.mykmmapp

import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Int, val name: String, val email: String)

Serialize Data to JSON:

Use the “Json.encodeToString” function to serialize an object to JSON:

// shared/src/commonMain/kotlin/com.example.mykmmapp/UserService.kt

package com.example.mykmmapp

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class UserService {
fun getUserJson(user: User): String {
return Json.encodeToString(user)
}
}

Deserialize JSON to Object:

Use the “Json.decodeFromString” function to deserialize JSON to an object:

// shared/src/commonMain/kotlin/com.example.mykmmapp/UserService.kt

package com.example.mykmmapp

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

class UserService {
fun getUserFromJson(json: String): User {
return Json.decodeFromString(json)
}
}

Debugging and Testing in a Kotlin Multiplatform Project

Debugging and testing are critical aspects of the software development lifecycle, ensuring the reliability and quality of your Kotlin Multiplatform Mobile (KMM) project. In this chapter, we'll explore strategies for debugging shared code, writing tests for shared and platform-specific code, and running tests on Android.

Writing Tests for Shared Code

Testing shared code is crucial for ensuring its correctness and reliability. KMM supports writing tests that can be executed on both Android and iOS platforms. The “kotlin.test” framework is commonly used for writing tests in the shared module.

Sample Test in the Shared Module

// shared/src/commonTest/kotlin/com.example.mykmmapp/CalculatorTest.kt

package com.example.mykmmapp

import kotlin.test.Test
import kotlin.test.assertEquals

class CalculatorTest {
@Test
fun testAddition() {
val calculator = Calculator()
val result = calculator.add(3, 4)
assertEquals(7, result)
}

@Test
fun testMultiplication() {
val calculator = Calculator()
val result = calculator.multiply(2, 5)
assertEquals(10, result)
}
}

Running Tests on Android

Running tests on Android and iOS involves using Android Studio's and xCode’s testing tools. Ensure that your Android and iOS test configurations are set up correctly, and then execute your tests as you would with standard Android and iOS tests.

Testing Platform-Specific Code

While shared code tests focus on business logic, platform-specific code tests ensure the correct behavior of platform-specific implementations. Write tests for Android and iOS code using their respective testing frameworks.

Android Unit Test Example

// shared/src/androidTest/kotlin/com.example.mykmmapp/AndroidImageLoaderTest.kt

package com.example.mykmmapp

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class AndroidImageLoaderTest {
@Test
fun testImageLoading() {
val imageLoader = ImageLoader()
val imageView = imageLoader.loadImage("https://example.com/image.jpg")
assertTrue(imageView is android.widget.ImageView)
}
}

iOS Unit Test Example

// shared/src/iosTest/kotlin/com.example.mykmmapp/IosImageLoaderTest.kt

import XCTest
import MyKmmApp // Assuming this is your Kotlin Multiplatform module name

class IosImageLoaderTest: XCTestCase {

func testImageLoading() {
let imageLoader = ImageLoader()
let imageView = imageLoader.loadImage("https://example.com/image.jpg")
XCTAssertTrue(imageView is UIImageView)
}
}

integrating Kotlin Multiplatform Mobile with Existing Android Projects

Integrating Kotlin Multiplatform Mobile (KMM) with existing Android projects allows you to gradually adopt cross-platform development while leveraging your current codebase. In this chapter, we'll explore the process of adding KMM modules to existing projects, sharing code between new and existing modules, and managing dependencies.

Adding KMM Modules to Existing Projects

  1. Add KMM Module

    • Navigate to "File" > "New" > "New Module..."
    • Choose "Kotlin Multiplatform Shared Module"
    • Follow the prompts to configure the module settings.
  2. Configure Dependencies

    Ensure that your Android module and KMM module are appropriately configured to share code and dependencies. Update the settings.gradle and build.gradle files as needed.

    // settings.gradle

    include ':app', ':shared', ':kmmModule'
    // app/build.gradle

    dependencies {
    implementation project(":shared")
    implementation project(":kmmModule")
    }
  3. Sharing Code

    You can now share code between the Android module and the KMM module. Place common code in the “commonMain” source set of the KMM module.

    // shared/src/commonMain/kotlin/com.example.mykmmapp/CommonCode.kt

    package com.example.mykmmapp

    fun commonFunction() {
    println("This function is shared between Android and KMM.")
    }
  4. Run and Test

    Run your Android project, ensuring that the shared code functions correctly on both platforms.

Managing Dependencies

Shared Dependencies

Ensure that dependencies required by shared code are included in the KMM module's “build.gradle.kts” file.

// shared/build.gradle.kts

kotlin {
android()
ios()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
// Add other shared dependencies
}
}
}
}

Platform-Specific Dependencies

For platform-specific dependencies, declare them in the respective source sets.

// shared/build.gradle.kts

kotlin {
android()
ios()
sourceSets {
val androidMain by getting {
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.9.0")
// Add other Android-specific dependencies
}
}
val iosMain by getting {
dependencies {
// Add iOS-specific dependencies
}
}
}
}

Conclusion

As we conclude our exploration of Kotlin Multiplatform Mobile (KMM), it's evident that this technology has emerged as a powerful solution for cross-platform mobile app development. By seamlessly bridging the gap between Android and iOS, KMM empowers developers to build robust applications with efficiency and code reusability at its core.

Kotlin Multiplatform Mobile stands as a testament to the evolving landscape of mobile app development. By embracing the principles of code reusability, adaptability, and continuous improvement, you are well-equipped to navigate the complexities of cross-platform development.

· 11 min read
Petros Efthymiou

From Android Views to Jetpack Compose

Jetpack Compose and why it matters

Jetpack Compose is a revolutionary UI toolkit introduced by Google for building native Android applications. Unlike traditional Android Views, Jetpack Compose adopts a declarative approach to UI development, allowing developers to create user interfaces using composable functions.

This paradigm shift simplifies UI development by eliminating the need for complex view hierarchies and manual view updates. With Jetpack Compose, developers can express the desired UI state and let the framework handle the rendering and updating automatically. This results in cleaner and more readable code, improved productivity, and faster UI development cycles.

Jetpack Compose offers a modern and intuitive way to build UIs, enabling developers to create beautiful, responsive, and highly interactive Android applications with ease. Its importance lies in providing a more efficient and enjoyable development experience, enabling developers to focus on crafting exceptional user experiences while reducing boilerplate code and increasing code maintainability.

And the cherry on top? No more Android Fragments! We all had our fair share of pain trying to comprehend and debug the complex Fragment lifecycle. With Jetpack Compose, we can put an end to it! That’s right, Composables can take the Fragments’ place as reusable UI components that are tied up to an Activity.

Declarative UI building is the way that all front-facing applications are moving towards. It was first introduced by React in 2013. After its successful adoption in the web, it later moved to cross platform mobile platforms such as React Native and Flutter. Realizing its advantages, both native platforms, Android and iOS, have recently made a similar move by introducing Jetpack Compose and SwiftUI. Soon all other UI-creating tools will be a thing of the past.

Understanding RecyclerView and its Limitations

RecyclerView has long been a popular component in Android app development for efficiently displaying lists and grids. It offers flexibility and performance optimizations by recycling views as users scroll through the list, reducing memory consumption and improving scrolling smoothness. However, RecyclerView also comes with its limitations. Managing view recycling, implementing complex adapter logic, and supporting different view types for diverse list items can often lead to boilerplate code and increased development effort.

Additionally, RecyclerView lacks built-in support for animations and complex layout transitions, making it challenging to create dynamic and visually engaging user interfaces. These limitations have prompted developers to seek alternative solutions that offer a more streamlined and intuitive approach to building user interfaces. The Jetpack Compose Column and Lazy Column are coming to the rescue.

Analyzing the Existing RecyclerView Implementation

We are creating an application that fetches a list of playlists and displays them on the screen. The initial implementation is based on Android Fragment and Recycler View. Let's take a closer look at the code structure and components involved:

class PlaylistFragment : Fragment() {

private val viewModel: PlaylistViewModel by viewModels()
@Injected
var playlistAdapter: PlaylistAdapter

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_playlist, container, false)

val playlistsRecyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
playlistsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
playlistsRecyclerView.adapter = playlistAdapter

lifecycleScope.launchWhenStarted {
viewModel.playlists.collect { playlists ->
playlistAdapter.submitList(playlists)
}
}

return view
}
}

Our Fragment depends on the ViewModel, which exposes a Kotlin StateFlow that emits a list of playlists. We observe this StateFlow using the collect method, and upon receiving the updated list, we populate the RecyclerView with the playlist items by calling submitList. The RecyclerView is set up with a custom adapter that extends the RecyclerView Adapter and holds a list of playlists as its data source.

Below is the respective code for the RecyclerView Adapter:

class PlaylistAdapter : RecyclerView.Adapter<PlaylistAdapter.PlaylistViewHolder>() {

private var playlistItems: List<Playlist> = emptyList()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_playlist, parent, false)
return PlaylistViewHolder(itemView)
}

override fun onBindViewHolder(holder: PlaylistViewHolder, position: Int) {
val playlist = playlistItems[position]
holder.bind(playlist)
}

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

inner class PlaylistViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
private val descriptionTextView: TextView = itemView.findViewById(R.id.descriptionTextView)

fun bind(playlist: Playlist) {
titleTextView.text = playlist.title
descriptionTextView.text = playlist.description
}
}

fun submitList(playlists: List<Playlist>) {
playlistItems = playlists
notifyDataSetChanged()
}
}

Within the adapter, we override the necessary methods, such as onCreateViewHolder, onBindViewHolder, and getItemCount to handle view creation, data binding, and determining the item count respectively. The item layout XML file defines the visual representation of each playlist item, containing the necessary views and bindings.

As we explained earlier, RecyclerView implementations require a lot of boilerplate and repetitive code.

Jetpack Compose Column vs Lazy Column

Before we jump into improving our implementation with Jetpack Compose, let’s discuss the differences between the Column and LazyColumn components.

In Jetpack Compose, both Column and LazyColumn are composable functions used to display vertical lists of UI elements. The primary difference lies in their behavior and performance optimization. The Column is suitable for a small number of items or when the entire list can fit on the screen. It lays out all its children regardless of whether they are currently visible on the screen, which may lead to performance issues with large lists. For short lists, rendering the items from the start offers increased performance.

On the other hand, LazyColumn is optimized for handling large lists efficiently. It loads only the visible items on the screen and recycles the off-screen items, similar to the traditional RecyclerView. This approach reduces memory consumption and enhances scrolling performance for long lists. Therefore, LazyColumn is the preferred choice when dealing with extensive datasets or dynamic content, ensuring a smooth and responsive user experience.

Setting Up Jetpack Compose in the Project

In order to use Jetpack Compose in our project, we need to complete the following setup steps:

Step 1: Add the Jetpack Compose dependency in build.gradle

plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
// ...
buildFeatures {
compose true // Enable Jetpack Compose
}

composeOptions {
kotlinCompilerExtensionVersion = “$version”
}
// ...
}

dependencies {
implementation "androidx.compose.ui:ui:$compose_version" // Check for the latest version
implementation "androidx.compose.material:material:$material_version" // Check for the latest version
implementation "androidx.activity:activity-compose:$compose_version" // Check for the latest version
// ...
}

Step 2: Initialize Jetpack Compose In your Application class, in the onCreate method.

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) // Optional: Disable dark mode
}
}

You can now start adding Composables inside your MainActivity and leverage the power of Jetpack Compose!

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//TODO add a composable
}
}
}

Migrating RecyclerView to Lazy Column

Jetpack Compose belongs to the declarative UI family. In declarative UI, we receive the state of the data that needs to be displayed, and we programmatically create the views. The views are immutable, and their state cannot change. Every time the data state changes, everything is being redrawn on the screen, and the views recreated from scratch. Practically, behind the scenes, there are smart diffing mechanisms that don’t redraw elements that their data hasn’t changed. But we, as developers, must write code as if everything is being redrawn when the data changes.

Let’s see how we can refactor the playlists screen with Jetpack Compose.

As we promised earlier, with Jetpack Compose, we can get rid of the Android Fragments. Everything is Jetpack Compose, from a whole screen to a small UI element, is a composable. The composables are functions instead on objects. This reflects one of the paradigm shifts that the declarative UI introduces. We are moving towards stateless functional programming instead of stafeful object oriented programming.

Let’s start by replacing our PlaylistFragment with a screen composable.

@Composable
fun PlaylistScreen(viewModel: PlaylistViewModel) {
val playlists by viewModel.playlists.collectAsState()

LazyColumn {
items(playlists) { playlist ->
PlaylistItem(playlist = playlist)
}
}
}

The PlaylistScreen composable represents the screen where the playlists are displayed. It collects the playlists from the PlaylistViewModel using collectAsState to recompose the composable whenever the playlist data changes automatically. The main component in the PlaylistScreen is the LazyColumn, which is a Jetpack Compose equivalent of RecyclerView. It handles view recycling and renders only the visible items on the screen. Every time the playlist StateFlow emits another result, the composable function PlaylistScreen will automatically recompose, and the UI be redrawn with the updated data.

Each list item is described by the composable below:

@Composable
fun PlaylistItem(playlist: Playlist) {
// Custom composable for rendering an individual playlist item
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = playlist.title,
style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = playlist.description)
}
}

The PlaylistItem composable represents an individual playlist item. We use a Column composable to stack the title and description texts vertically. We apply styling and padding.

With Jetpack Compose's LazyColumn, we achieve a more concise and declarative way of displaying the list of playlists without the need for a separate adapter or view holder logic. The composable functions automatically handle the UI rendering and updates based on the provided state. This refactoring results in cleaner, moer reuseable and more maintainable code, making UI development more intuitive and efficient. Furthermore, we don’t have to handle the Fragment’s complex lifecycle while retaining the benefit of reusable UI components.

The playlist with Compose&#39;s LazyColumn

Figure: The playlist with Compose's LazyColumn

Handling Clicks

Handling clicks in the Jetpack Compose Column component is super easy, we simply need to add the ‘clickable’ modifier and call the code that we want to execute when the respective list item is clicked. We have access to selected playlist model info.

 @Composable
fun PlaylistItem(playlist: Playlist) {
// Custom composable for rendering an individual playlist item
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { /* Handle item click here */ }
.padding(16.dp)
) {
Text(
text = playlist.title,
style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = playlist.description)
}
}

Testing

As good engineers, we should always include automated tests that verify that our code works correctly. With Jetpack Compose, UI testing is much easier than before. Let’s see how we can test the PlaylistScreen after we migrate it to Jetpack Compose.

@ExperimentalCoroutinesApi
@get:Rule
val composeTestRule = createComposeRule()

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun playlistScreen_RenderList_Success() {
// Dummy data for testing
val playlists = listOf(
Playlist("Playlist 1", "Description 1"),
Playlist("Playlist 2", "Description 2"),
Playlist("Playlist 3", "Description 3")
)

// Create a TestCoroutineDispatcher to be used with Dispatchers.Main
val testDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testDispatcher)

// Launch the composable with TestCoroutineScope
testCoroutineScope.launch {
composeTestRule.setContent {
PlaylistScreen(viewModel = PlaylistViewModel(playlists))
}
}

// Wait for recomposition
composeTestRule.waitForIdle()

// Check if each playlist item is rendered correctly
playlists.forEach { playlist ->
composeTestRule.onNode(hasText(playlist.title)).assertIsDisplayed()
composeTestRule.onNode(hasText(playlist.description)).assertIsDisplayed()
}
}

In this test, we use the createComposeRule to set up the Compose test rule. We also create a TestCoroutineDispatcher and a TestCoroutineScope to simulate the background coroutine execution. Then, we launch the PlaylistScreen composable with dummy data for testing. After the recomposition, we use onNode to check if each playlist item title and description is correctly displayed. Note that we are testing UI, therefore this is an instrumentation test that must be inserted under the AndroidTest folder.

Let’s now see how we can test the PlaylistItem in isolation:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun playlistItem_Render_Success() {
val playlist = Playlist("Playlist 1", "Description 1")

composeTestRule.setContent {
PlaylistItem(playlist = playlist)
}

composeTestRule.onNode(hasText(playlist.title)).assertIsDisplayed()
composeTestRule.onNode(hasText(playlist.description)).assertIsDisplayed()
}

In this test, we use the createComposeRule to set up the Compose test rule. We then render the PlaylistItem composable with a dummy Playlist object. After rendering, we use onNode to check if the playlist title and description are correctly displayed.

These automated tests use Jetpack Compose's testing libraries to verify if the PlaylistScreen and PlaylistItem composables render as expected. They help ensure that the UI is correctly displayed and the appropriate data is rendered, providing confidence in the correctness of your composable functions. Remember to import the necessary dependencies and adapt the test code to your specific project setup.

Conclusion

Declarative UI is the future both in the web and mobile platforms. All major players have already adopted it, and it looks like all the other UI generation tools will eventually become deprecated.

It introduces a paradigm shift in building the UI where the views are immutable, and their state cannot change. When the data state changes, the views are recreated from scratch and are put to display the data updates.

Declarative UI building and Jetpack Compose specifically offer advantages such as simpler code that is easier to read, write and maintain. As a bonus, we can get rid of Fragments while maintaining the advantage of reusable UI components.

Shipbook offers fantastic Jetpack Compose debugging capabilities. You can add logs to monitor any UI rendering errors. Those will enable you to track, trace and fix every issue efficiently and effectively.

The sooner you start getting your hands on it, the better!

· 15 min read
Kustiawanto Halim

Exception Handling

In modern mobile applications, it is common that the application is required to perform network calls. The developer needs to make sure this heavy work is not running in the main thread of the application, as this will block the UI, leading the application to be unresponsive and triggering an Application Not Responsive (ANR) error.

There have been many approaches to preventing apps from blocking, including threading, callbacks, futures, promises, and most importantly, coroutines.

Kotlin handles this issue effectively by delegating most functions to libraries and integrating coroutine support at the language level. A coroutine is a concurrency design concept that may be used on Android to facilitate asynchronous function operation. Based on the existing principles from other languages, coroutines were added to Kotlin in version 1.3.

Coroutines not only enable asynchronous programming—often known as non-blocking programming—but also offer up plenty of additional possibilities, such as concurrency and actors. Coroutines on Android manage lengthy operations that will otherwise render your app unresponsive by blocking the main thread.

This article discusses how to leverage coroutines in Kotlin to solve asynchronous programming challenges, which allows you to create clearer and more efficient code.

Alternatives to Coroutines

Before we get into coroutines, let's have a look at some of the alternative solutions.

Threading, or using a separate thread for long-running functions is the most well-known approach to avoid blocking the UI. However, it has several drawbacks: threading is costly, the number of threads is limited by your operating system, and debugging threads becomes yet another issue in multi-threaded programming.

Callbacks are another popular solution to concurrency problems. A callback is used to provide one function as a parameter to another function, which will then be called after the process is finished. Although it seems like a possible solution, It may cause callback hell which will make it hard to propagate and handle errors.

A future or promise (other languages use other names) entails a promise object that can be operated on after a function call. Using a promise requires us to use a specific design pattern and API to handle chaining calls. We also need to introspect the promise object to be able to get the real value being promised.

Kotlin's approach for working with asynchronous code is to adopt coroutines, instances of suspendable computations, in which a function can postpone its execution and restart it later. Promise and Coroutines differ primarily in that Promise returns an explicit result object, whereas Coroutines yields (which allows other functions to take control of the running thread), enabling us to suspend or resume a Coroutines. Coroutines aren’t really a new idea; in fact, they’ve been around for years and are common in several other programming languages, like Go.

One benefit of using a coroutine is that writing non-blocking code is almost the same as writing blocking code for developers. The programming paradigm itself does not change. Most of the functionality is delegated to libraries, and you won’t have to learn an entirely new set of APIs.

Kotlin Coroutines

Notable Features

There are several benefits to using coroutines:

  • Lightweight: Since suspending is supported, you can execute multiple coroutines on a single thread without blocking the thread where the coroutine is operating. Suspending, as opposed to blocking, saves memory while allowing for several concurrent tasks.
  • Fewer memory leaks: This is because coroutines run operations using structured concurrency inside a scope.
  • Built-in cancellation support: Cancellations seamlessly propagate across the running coroutine tree.
  • Jetpack integration: Various Jetpack libraries have extensions that offer comprehensive coroutine access; some libraries additionally offer their native coroutine scope for structured concurrency.

Coroutine Concepts

There are four main elements of a coroutine: dispatchers, CoroutineScope, jobs, and CoroutineContext.

Starting a Coroutine

There are two ways to start a coroutine:

  • launch creates a new coroutine but does not return the result to the caller; you can use it to start any job that is presumed "fire and forget."
  • async creates a new coroutine and lets you return a result using the await suspend method.

Because a normal function cannot call await, you should use the launch function to create a new coroutine from it. Only use async within another coroutine or within a suspend function.

Dispatchers

Coroutines in Kotlin require dispatchers to specify which threads will be used to execute a coroutine. You need to set Kotlin coroutines to the default or IO Dispatchers to run code outside of the main thread.

Kotlin offers three dispatchers that you can use to designate where the coroutine should run:

  • Dispatchers.Main launches a coroutine on the main Android thread; you can only use this for dealing with the user interface and executing short operations, e.g., updating the value of a TextView.
  • Dispatchers.IO is designed to handle disk and network I/O independently of the main thread, e.g., running any network activity.
  • Dispatchers.Default executes coroutines for CPU-intensive tasks outside of the main thread. E.g., parsing a huge JSON object.

CoroutineScope

CoroutineScope keeps track of each coroutine it generates using launch or async. In Android, several KTX libraries provide their CoroutineScope for specific lifecycle types. ViewModel, for example, has a viewModelScope, whereas Lifecycle has a lifecycleScope. However, unlike a Dispatcher, a CoroutineScope does not run the coroutine.

The following code will demonstrate how to create your CoroutineScope:

class MyCoroutineScope {

// To create a scope, we will combine a Job and Dispatchers.
val myScope = CoroutineScope(Job() + Dispatchers.Main)

fun printNumberAfterDelay() {
// Launch a new coroutine with given scope
myScope.launch {
// Delay the function for oneseveral seconds
delay(1000L)
// Print desire number
println(97)
}
}

fun cancel() {
// To cancel ongoing coroutines work, simply cancel the scope
myScope.cancel()
}
}

A scope that has been canceled cannot launch any new coroutine. As a result, you should always use scope.cancel() when the class that controls its lifecycle is about to be destroyed. When you use viewModelScope, the ViewModel class automatically cancels the scope in the ViewModel's onCleared() function.

Job

A Job is the coroutine's handler. Each coroutine you create using launch or async produces a Job instance that has a unique identifier and maintains the coroutine's lifecycle. You also could provide a Job to a CoroutineScope to control its lifecycle, as illustrated in the example below:

class MyCoroutineScope {

fun printNumberAfterDelay() {
// Newly launched coroutines will be assigned to job
val job = myScope.launch {
// Long running task
}

if (...) {
// If we cancel the coroutine launched above,
// it will not affect the scope where the coroutine launched in
job.cancel()
}
}
}

CoroutineContext

The behavior of coroutines can be defined using CoroutineContext. Several things you can configure within CoroutineContext are:

  • A job, to control the coroutine’s lifecycle
  • Dispatchers, to control in which thread the coroutine runs
  • A name for the coroutine, for easier debugging process
  • A CoroutineExceptionsHandler, to catch exceptions
class MyCoroutineScope {

val myScope = CoroutineScope(Job() + Dispatchers.Main)

fun printNumberAfterDelay() {
// Launch coroutine on Dispatchers.Main
val job1 = myScope.launch {
// CoroutineName = "coroutine" (default)
delay(1000L)
println(98)
}

// Launch a new coroutine on Dispatchers.IO
val job2 = myScope.launch(Dispatchers.IO + "RunningOnIO") {
// CoroutineName = "RunningOnIO" (overridden)
delay(2000L)
println(99)
}
}
}

A new Job instance is assigned to a new coroutine launched within a Scope, while the other CoroutineContext properties are inherited from the parent Scope. By providing a new CoroutineContext to the launch or async function, you can override the inherited properties.

Now, let's discuss how to use coroutines with a real example.

Creating a Background Task

A network request is a common use case for all applications. We cannot call a network request on the main thread because it will block it and cause an Application Not Responding (ARN) error. That's why we need to call network requests in the background thread, and to do that, we can use a coroutine.

Take a look at the following example of repository code:

class DownloadFileRepository(private val fileHelper: FileHelper) {

// Function that makes the network request, blocking the current thread
fun makeDownloadRequest(url: String): Result<String> {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}

The makeDownloadRequest(url:) is a synchronous function that blocks the calling of the main thread. If you are not familiar with Result, it is basically just an encapsulation of the success value of generic type T or failure with a throwable exception.

Kotlin already provides the Result class, as defined in its documentation:

sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}

A ViewModel will have a function that calls the network request when the user performs an action, like that below:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
repository.makeDownloadRequest(url)
}
}

Calling DownloadViewModel.download(url:) will block the UI thread from which you’re making the request. We must avoid doing this since it will freeze the application UI so that the user cannot interact.

To make the download(url:) function run outside the main thread, create a new coroutine and run the function on an IO dispatcher as follows:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
// Launch network request on I/O Dispatchers
viewModelScope.launch(Dispatchers.IO) {
repository.makeDownloadRequest(url)
}
}
}

viewModelScope is the CoroutineScope included in the ViewModel KTX Extension. If you use ViewModel class, it’s recommended to start a coroutine in viewModelScope, which will be canceled; any running coroutine calls are also canceled when the ViewModel is destroyed.

When you call the download(url:) function from the View layer of the main thread, the launch function will create a new coroutine with IO dispatchers as the thread resolver. The download(url:) function will continue running until you get a success or failure response.

You already made sure that the download(url:) function does not block the main thread, but you still need to ensure that makeDownloadRequest(url:) will always be called from the IO dispatchers each time you want to use it. So, let’s refactor our code to make sure makeDownloadRequest(url:) is also a main-safe function.

Making Coroutine Main-Safe Functions

A function that does not block UI updates on the main thread is a main-safe function. makeDownloadRequest(url:) is not main-safe, as calling it from the main thread blocks the UI update.

To make this function main-safe, use the withContext(Dispatchers.IO) function provided by the coroutine to change the resolver thread to an IO Thread. Adding withContext will also make the makeDownloadRequest(url:) function become a suspend function. The keyword “suspend” is the way Kotlin forces us to call the function marked within a coroutine call.

Refactoring the code will look like the following:

class DownloadFileRepository(private val fileHelper: FileHelper) {

suspend fun makeDownloadRequest(url: String): Result<String> {
// Add coroutine context to IO Dispatchers
withContext(Dispatchers.IO) {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
}

Now we do not need to specifically define the dispatcher in ViewModel layer, since we already moved the execution to a repository layer. Take a look at the refactored code:

class DownloadViewModel(
private val repository: DownloadFileRepository
): ViewModel() {

fun download(url: String) {
// We do not need to specify thread anymore
viewModelScope.launch {
val result = repository.makeDownloadRequest(url)

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

With the refactored code, we do not specify the dispatchers on the download(url:) function anymore. The coroutine called from viewModelScope is now running in the main thread but it is not blocking the UI update because makeDownloadRequest(url:) is running in the IO Dispatchers.

We also add some response handlers to the network request function, but is it safe from exception?

Exception Handling

Our repository layer can still throw an exception because it has a function to write network responses to a file. We can add try-catch function to handle this kind of exception as follows:

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

fun download(url: String) {
viewModelScope.launch {
// Add try-catch to handle exceptions
val result = try {
repository.makeDownloadRequest(url)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

Now, any unexpected error thrown from the repository layer will be treated as "Network request failed" and can be handled by the UI layer safely.

Best Practices

In this section, we’ll discuss the best practices for using coroutines to make your applications scalable and testable.

Suspend Function Must Be Main-Safe

A function must be main-safe if it is to be treated as a suspend function. If a class is responsible for long-running operations like network calls, that class is also responsible to move its execution from the main thread using withContext. In our example above, we already refactored our repository class (which is responsible for network calls) to call its function from the IO Thread.

Coroutines in ViewModel

If you use the ViewModel class in your applications, it is responsible for creating and launching the coroutine, instead of exposing it and launching it in the view layer. You should also use viewModelScope to create the coroutine since it will handle its lifecycle scope based on the ViewModel lifecycle scope:

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

// DO THIS, create coroutine based on viewModelScope
fun download(url: String) {
viewModelScope.launch {
repository.makeDownloadRequest(url)
}
}

// DON'T DO THIS, do not make download suspend
suspend fun download(url: String) =
repository.makeDownloadRequest(url)
}

Making Coroutines Cancelable

A coroutine is not canceled when its job is canceled. If you block an operation in a coroutine call, you must make sure the coroutine is cancelable. To make sure it’s not canceled before calling it, we can add an ensureActive function.

class DownloadViewModel(
private val repository: DownloadRepository
): ViewModel() {

fun download(url: String) {
viewModelScope.launch {
// Add try-catch to handle exceptions
val result = try {
ensureActive() // Make sure not canceled
repository.makeDownloadRequest(url)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}

when (result) {
is Result.Success<DownloadResponse> -> // handle success response
else -> // handle error response
}
}
}
}

The Dispatcher Should Be Injected

When creating a new coroutine or calling it inside withContext, it is recommended to inject the dispatchers instead of hardcoding it. This will make your code testable when you want to test the coroutine by changing it to TestDispatchers, which we will discuss in a later section.

Your repository layer code will be refactored as follows:

class DownloadFileRepository(
Private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val fileHelper: FileHelper
) {

suspend fun makeDownloadRequest(url: String): Result<String> {
// now we call inside injected dispatchers
withContext(dispatcher) {
val url = URL(url)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "GET"
doOutput = true
fileHelper.save(inputStream)
return Result.Success(fileHelper.destinationPath)
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
}

Testing Coroutines

When testing the coroutine function, you need to inject TestDispatcher into the coroutine dispatcher.

kotlinx-coroutine-test has two implementations of TestDispatcher:

  • StandartTestDispatcher will run the coroutine with a scheduler when the test thread is ready. Use this dispatcher to simulate a queue like a “real” dispatcher.
  • UnconfinedTestDispatcher will run the coroutine eagerly and write the test more easily. However, it gives you less control when the coroutine is executed during the test.

The runTest function tests a coroutine, while the runTest function will use a TestCoroutineScheduler to skip the delay in the suspend function tested. Take a look at the following sample test class:

    class DownloadFileRepositoryTest {

@Test
fun testMakeDownloadRequest() = runTest {
// Create a test dispatcher and inject it to the repository
val testDispatcher = UnconfinedTestDispatcher(testScheduler)

val fileHelper = FakeFileHelper()
val repository = DownloadFileRepository(
testDispatcher,
fileHelper
)

repository.makeDownloadRequest("http://www.shipbook.io")
assertThat(fileHelper.destinationPath.isNotEmpty())
}
}

When creating a test class, make sure to share TestDispatcher with the same scheduler. Doing this will allow your coroutine to run on a single thread to make your test deterministic.

Conclusion

Coroutines offer a lightweight and easy-to-use way to handle multi-threaded programming in Kotlin, as well as in Android development. Since coroutines are fully supported by Android and Kotlin itself, it is highly recommended that you use coroutines when dealing with asynchronous programming. Remember, although it is convenient to use coroutines, you also need to make sure your application handles the suspend function in the correct way.

Due to the fact that we can't really determine which instructions are being executed at any particular time, asynchronous programming can occasionally produce errors. If we want to debug asynchronous code, we may utilize logs. Shipbook captures your application logs and exceptions, allowing you to remotely gather, monitor, and evaluate your user mobile-app logs and crashes in the cloud on a per-user and session basis. This will help you to quickly examine relevant data and solve errors.

· 9 min read
Nikita Lazarev-Zubov

Exception Handling

The first version of Java was released in 1995 based on the great idea of WORA (“write once, run anywhere”) and a syntax similar to C++ but simpler and human-friendly. One notable language invention was checked exceptions—a model that later was often criticized.

Let’s see if checked exceptions are really that harmful and look at what’s being used instead in contemporary programming languages, such as Kotlin and Swift.

Good Ol’ Java Way

Java has two types of exceptions, checked and unchecked. The latter are runtime failures, errors that the program is not supposed to recover from. One of the most notable examples is the notorious NullPointerException.

The fact that the exception is unchecked doesn’t mean you can’t handle it:

Object object = null;
try {
System.out.println(object.hashCode());
} catch (NullPointerException npe) {
System.out.println("Caught!");
}

The difference between a checked and unchecked exception is that if the former is raised, it must be included in the method’s declaration:

void throwCustomException() throws CustomException {
throw new CustomException();
}

static class CustomException extends Exception { }

The compiler will make sure that it’s handled— sooner or later. The developer must wrap the throwCustomException() with a try-catch block:

try {
throwCustomException();
} catch (CustomException e) {
System.out.println(e.getMessage());
}

Or pass it further:

void rethrowCustomException() throws CustomException {
throwCustomException();
}

What’s Wrong with the Model

Checked exceptions are criticized for forcing people to explicitly deal with every declared exception, even if it’s known to be impossible. This results in a large amount of boilerplate try-catch blocks, the only purpose of which is to silence the compiler.

Programmers tend to work around checked exceptions by either declaring the method with the most general exception:

void throwCustomException() throws Exception {
if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 2 == 0) {
throw new EvenDayException();
} else {
throw new OddDayException();
}
}

Or handling it using a single catch-clause (also known as Pokémon exception handling):

void throwCustomException()
throws EvenDayException, OddDayException {
// ...
}

try {
throwCustomException();
} catch (Exception e) {
System.out.println(e.getMessage());
}

Both ways lead to a potentially dangerous situation, when all possible exceptions are sifted together, including everything that is not supposed to be dismissed. Error-handling blocks of code also become meaningless, fictitious, if not empty.

Even if all exceptions are meticulously dealt with, public methods swarm with various throws declarations. This means all abstraction levels are aware of all exceptions that are thrown around it, compromising the principle of information hiding.

In some parts of the system, where multiple throwing APIs meet, a problem with scalability might emerge. You call one API that raises one exception, then call another that raises two more, and so on, until the method must deal with more exceptions than it reasonably can. Consider a method that must deal with these two:

void throwsDaysExceptions() throws EvenDayException, OddDayException  {
// …
}
void throwsYearsExceptions() throws LeapYearException {
// …
}

It's doomed to have more exception-handling code than business logic:

void handleDate() {
try {
throwsDaysExceptions();
} catch (EvenDayException e) {
// ...
} catch (OddDayException e) {
// ...
}
try {
throwsYearsExceptions();
} catch (LeapYearException e) {
// ...
}
}

And finally, the checked exception approach is claimed to have a problem with versioning. Namely, adding a new exception to the throws section of a method declaration breaks client code. Consider the throwing method from the example above. If you add another exception to its throws declaration, the client code will stop compiling:

void throwException()
throws EvenDayException, OddDayException, LeapYearException {
// ...
}

try {
// Unhandled exception: LeapYearException
} catch (EvenDayException e) {
// ...
} catch (OddDayException e) {
// ...
}

The Kotlin Way

Sixteen years after Java was first released, in 2011, Kotlin was born from the efforts of JetBrains, a Czech company founded by three Russian software engineers. The new programming language aimed to become a modern alternative to Java, mitigating all its known flaws.

I don’t know any programming language that followed Java in implementing checked exceptions, Kotlin included, despite the fact it targeted JVMs. In Kotlin, you can throw and catch exceptions similarly to Java, but you’re not required to indicate an exception in a method’s declaration. (In fact, you can’t):

class CustomException: Exception()

fun throwCustomException() {
throw CustomException()
}

fun rethrowCustomException() {
try {
throwCustomException()
} catch (e: CustomException) {
println(e.message)
}
}

Even catching is up to the programmer:

fun rethrowCustomException() {
throwCustomException() // No compilation errors.
}

For interoperability with Java (and some other programming languages), Kotlin introduced the @Throws annotation. Although it’s optional and purely informative, it’s required for calling a throwing Kotlin method in Java:

@Throws(CustomException::class)
fun throwCustomException() {
throw CustomException()
}

From One Extreme to Another

It may seem that programmers can finally breathe easy, but, personally, I think by solving the original problem, this new approach—Kotlin’s exceptions model—creates another. Unscrupulous developers are free to entirely ignore all possible exceptions. Nothing stops them from quickly wrapping a handful of exceptions with a try-catch expression and shipping the result to their end users, with a prayer. Or not covered exceptions are going to be discovered by end users.

Even if you’re a disciplined engineer, you’re not safe: Neither the compiler nor API will alert you to exceptions lurking inside. There’s no reliable way to make sure that all possible errors are being properly handled.

You can only guard yourself from your own code, patiently annotating your methods with @Throws. Though, even in this case, the compiler will tell you nothing and it’s easy to forget one exception or another.

The Swift Way

Swift first appeared publicly a little later, in 2014. And again, we saw something new. The error-handling model itself lies somewhere between Java’s and Kotlin’s, but how it works together with the language’s optionals is incredible. But first things first.

Of course, Swift has runtime, “unchecked”, errors—an array index out of range, a force-unwrapped optional value turned out to be nil, etc. But unlike Java or Kotlin, you can’t catch them in Swift. This makes sense since runtime exceptions can only happen because of a programming mistake, or intentionally (for instance, by calling fatalError()).

The rest of exceptions are errors that are explicitly thrown in code. All methods that throw anything must be marked with the throws keyword, and all code that calls such methods must either handle errors or propagate them further. Looks familiar, doesn’t it? But there’s a catch.

Fly in the Ointment

Let’s look at an example from above:

func throwError() throws {
if (Calendar.current.component(.day, from: Date()) % 2 == 0) {
throw EvenDayError()
} else {
throw OddDayError()
}
}

As you can see, you don’t declare specific errors that a method can throw; you’re only required to mark it as throwing something. The consequence of this is that you, again, don’t really know what to catch.

Unfortunately, the code below won’t compile:

do {
/*
Errors thrown from here are not handled because the enclosing
catch is not exhaustive
*/
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is OddDayError {
print(String(describing: EvenDayError.self))
}

You always have to add Pokémon handling:

do {
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is OddDayError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}

In fact, the Swift compiler doesn’t care about specific error types that you try to catch. You can even add a handler for something entirely irrelevant:

do {
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is IrrelevantError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}

Or you can have only one default catch block that covers everything:

    do {
try throwError()
} catch {
print(error)
}

Another bad thing about the approach is that, without a workaround, you can’t catch one error and propagate another. The only way to implement such behavior is to catch the error you’re interested in and throw it again:

func rethrow() throws {
do {
try throwError()
} catch is EvenDayError {
throw EvenDayError() // Here's the trick.
} catch is IrrelevantError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}
}

Ointment

In my opinion, Swift’s strongest merit is its optionals system that cooperates with all aspects of the language. If you don’t care about thrown errors, instead of fictitious catch-blocks, you can always write try? Execution of the method will stop the moment the error is thrown, without propagating it further:

try? throwError()

If you’re feeling bold, you can use try! instead of try?, which won’t suppress the error if it’s thrown, but will let you omit the do-catch block:

try! throwError()

This method also allows converting a throwing call to a value. try? will give you an optional one, whereas try! has an effect similar to force-unwrapping:

func intOrError() throws -> Int {
// …
}

let optionalInt = try? intOrError() // Optional(Int)
let dangerousCall = try! intOrError() // Int or die!

Conclusion

Personally, I find Kotlin’s way, ahem, a failure. I can understand why Kotlin developers decided not to follow Java in its way of checked exceptions, but ignoring exceptions entirely, without a hint of static checks, is too much.

On the other hand, is the Java way really that harmful? No mechanism can defend software from undisciplined programmers. Even the best idea can be distorted and misused. But applying Java’s principles as designed can lead to good results.

Connecting two levels of abstraction, you can catch errors from one level and re-throw new types of errors to propagate them to the next level. You can catch several types of errors, “combine” them into one another, and throw them for further handling. This can help mitigate problems with encapsulation and scalability. For instance:

void throwCustomException() throws CustomException {
try {
throwDayException();
} catch (EvenDayException | OddDayException e) {
throw new CustomException();
}
}

What Java lacked from the very beginning is Swift’s optionality system and a syntax to bind exception handling and optional values. I believe, coupled with entirely static checks of thrown exceptions, this would build a very strong model that can satisfy the grouchiest programmers. Although, in any aforementioned programming language, this would require breaking changes, I personally believe it would be a game-changing improvement of code safety.

And if you want to improve your app stability right now, Shipbook is already here for you! It proactively inspects your app, catches exceptions and allows you to analyze them even before your users stumble upon the problem.

· 13 min read
Donald Le

Unit Testing in Android Development

Introduction

Unit testing entails the testing of the smallest parts of software, such as methods or classes. The main role of unit testing is to make sure the isolated part works as expected without integrating with third-party software, databases, or any dependency. To achieve this, software developers implement multiple testing techniques, like using stubs, mocks, dummies, and spies.

This post will show you why you should perform unit testing and how to implement it in your Android development project.

Benefits of Unit Testing

Unit testing allows you to catch software bugs early in the software development process, instead of QA finding them in the integration phase or end-to-end-testing, or, even worse, in the production environment. Moreover, as you develop your product, more features are added, meaning integration tests and end–to-end tests alone cannot cover all the corner cases. With unit testing, more corner cases are covered, which ensures your product meets the expected quality.

Benefits of Test-Driven Development (TDD)

Unit testing often goes along with the test-driven development (TDD) methodology, where developers first write the test, then write the feature code. At first, the tests will fail because the feature is not yet implemented. When the feature code is implemented, the tests will become green.

The huge benefit of TDD is that a software team can make sure the product is built and will meet the expected requirements, as demonstrated by the tests. Moreover, because developers write the tests first, they need to spend more time thinking about the product and what features the product has to cover; this way, the product being built will tend to have a higher quality.

Also, writing tests before writing product code will prevent developers from needing to refactor the code just to be able to write tests for it. For example, in the Go language, if the developers do not implement code with an interface, it’s very hard to write tests later on.

Example Application to Demonstrate Unit Testing

To better understand how to apply proper testing techniques for Android applications, let’s get your hands dirty by building a real application and then write tests for it. The application will show a list of popular movies for users to choose from as suggestions for their weekly movie night. Check out this GitHub repository for the full application code.

After opening the application, users will see a list of popular movies:

The movie suggestion application shown on a virtual device

Figure 1: The movie suggestion application shown on a virtual device

You can then tap on a movie for details like its plot summary and cast:

Details for the movie “Black Rock”

Figure 2: Details for the movie “Black Rock”

Unit Testing (Local Testing)

The unit test of our application will be run by a popular test runner called JUnit, a unit-testing framework that uses JVM languages like Java or Kotlin. If you’re not familiar with JUnit, you can learn more about it here. It helps you structure your tests, like what needs to be done first, what will be done last to clean data, and which data should be collected for the test report.

An Example of a Simple Unit Test

Okay, now let’s write an example unit test for the application.

We have the MovieValidator class in the utils package, which has the function isValidMovie:

import android.text.Editable
import android.text.TextWatcher
import java.util.regex.Pattern

class MovieValidator : TextWatcher {
internal var isValid = false
override fun afterTextChanged(editableText: Editable) {
isValid = isValidMovie(editableText)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
companion object {
private val MOVIE_PATTERN = Pattern.compile("^[a-zA-Z]+(?:[\\s-][a-zA-Z]+)*\$")
fun isValidMovie(movie: CharSequence?): Boolean {
return movie != null && MOVIE_PATTERN.matcher(movie).matches()
}
}
}

To write the unit test for the function isValidMovie, we will first create a test class called MovieValidatorTest in the test folder. Then, we will need to import the MovieValidator class to test the isValidMovie in it.

The MovieValidatorTest will look like the following:

import com.fernandocejas.sample.core.functional.MovieValidator
import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse
import org.junit.Test
import mu.KotlinLogging
class MovieValidatorTest {
private val logger = KotlinLogging.logger {}
@Before
fun setUp() {
logger.info { "Starting the isValidMovie test" }
}
@Test
fun isValidMovie() {
assertTrue(MovieValidator.isValidMovie("The lord of the rings"))
assertFalse(MovieValidator.isValidMovie("name@email"))
}
@After
fun tearDown(){
logger.info { "Finishing the isValidMovie test" }
}
}

In the test file above, we implemented one test case to check the validity of the movie name. We also apply Before and After annotations to adding logging information so that we know when the test is about to start and when it is about to finish.

The Before and After annotations, help us structure our test scenario better. The Before annotation will be executed before every test, and the After annotation will be executed after every test. Developers often use these for setting up data for tests and then cleaning it up after testing is complete.

Notes: In order to install the logger library, we need to add the following code into our gradle configuration file.

implementation 'io.github.microutils:kotlin-logging-jvm:2.0.11'

When we run the test, we will see results as below:

Tests passed for movie validator test case

Figure 3: Tests passed for movie validator test case

The example unit test we just went over is very simple. But in real-world applications, you’ll need to deal with all kinds of dependencies and third-party APIs. How can we write tests for functions that interact with third-party dependencies?

When implementing unit testing, the best practice is to not deal with the real thing, like the real database, the real response from another API we take as input for the function, or any third-party dependencies. The reason for this is that when it comes to unit testing, we want to isolate the tests so that each test will test each unit. We could test the database or the third-party dependencies, but this will lead to flakiness in the tests. Instead, we’ll use “test doubles,” objects that stand in for the real objects when we implement the test. There are five types of test doubles: fake, dummy, stub, spy, and mock.

In this article, we’ll review the stub and mock types and use them for our example application.

  • Stubs provide fake data to the test.
  • Mocks check whether the expectation of the unit we are testing has been met.

How to Create Stubs and Mocks in a Sample Project

To better understand how to use a stub and a mock, let’s apply these techniques for writing unit tests in our movie suggestion app using MockK.

MockK is the well-known mock library in Kotlin, which provides native support for the Kotlin language. Users who are fond of the syntactic sugar of Kotlin will still be able to enjoy it with MockK. Moreover, since the default class and properties in Kotlin are final, using Mockito is considerably hard when mocking in Kotlin. But with MockK’s support, users won’t have to deal with that challenge anymore. To learn more about the benefits of using MockK over Mockito, check out this article.

To include the MockK library in your Android project, we need to add this line into the build.gradle.kts file:

testImplementation(TestLibraries.mockk)

The TestLibraries.mockk value is defined in Dependencies.kt as:

const val mockk = "io.mockk:mockk:${Versions.mockk}"
const val mockk = "1.10.0"

And that’s it.

So, let’s say we’re trying to test the class GetMovieDetails.

Initially, we usually implement the code without dependency injection like the following:

class GetMovieDetails : UseCase<MovieDetails, Params>() {
private val moviesRepository = MoviesRepository()
override fun run(params: Params) = moviesRepository.movieDetails(params.id)
data class Params(val id: Int)
}

The MovieRepository class is as defined below:

class MoviesRepository {
lateinit var context: Context
lateinit var retrofit: Retrofit
private val networkHandler = NetworkHandler(context)
private val service = MoviesService(retrofit)
fun movieDetails(movieId: Int): Either<Failure, MovieDetails> {
return when (networkHandler.isNetworkAvailable()) {
true -> request(
service.movieDetails(movieId),
{ it.toMovieDetails() },
MovieDetailsEntity.empty
)
false -> Left(NetworkConnection)
}
}
}

But writing code like this makes writing unit tests for this class impossible since we cannot mock dependency for the MoviesRepository class. Well, actually, we can write unit tests literally, but we’d need to use the real movie database, and this would lead to slower tests and make your test couple with third-party dependencies. Moreover, the problem with third-party dependencies is that they might not be working for other reasons, not because of our code.

The best practice when it comes to writing code that can be tested is applying dependency injection, which you can learn more about here.

First, we need to change the class MovieRepository to an interface type. The code for the interface MovieRepository will be changed as below:

interface MoviesRepository {
fun movies(): Either<Failure, List<Movie>>
fun movieDetails(movieId: Int): Either<Failure, MovieDetails>
class Network
@Inject constructor(
private val networkHandler: NetworkHandler,
private val service: MoviesService
) : MoviesRepository {
override fun movieDetails(movieId: Int): Either<Failure,
MovieDetails> {
return when (networkHandler.isNetworkAvailable()) {
true -> request(
service.movieDetails(movieId),
{ it.toMovieDetails() },
MovieDetailsEntity.empty
)
false -> Left(NetworkConnection)
}
}
}
..
}

Then, the class GetMovieDetails will be written as below, with the constructor MovieRepository:

class GetMovieDetails {
@Inject constructor(private val
moviesRepository:MoviesRepository):
UseCase < MovieDetails, Params > () {
override fun run(params: Params) = moviesRepository.movieDetails(params.id)
data class Params(val id: Int)
}
}

In order to test this class without calling the real database, we need to mock the MoviesRepository class using MockK:

@MockK private lateinit var moviesRepository: MoviesRepository

The test function for the movieDetails function will be written as below:

class GetMovieDetailsTest : UnitTest() {
private lateinit var getMovieDetails: GetMovieDetails
@MockK private lateinit var moviesRepository:
MoviesRepository
@Before fun setUp() {
getMovieDetails = GetMovieDetails(moviesRepository)
every { moviesRepository.movieDetails(MOVIE_ID) } returns
Right(MovieDetails.empty)
}
@Test fun `should get data from repository`() {
getMovieDetails.run(GetMovieDetails.Params(MOVIE_ID))
verify(exactly = 1) {
moviesRepository.movieDetails(MOVIE_ID)
}
}
companion object {
private const val MOVIE_ID = 1
}
}

In the setUp step, with @Before annotation, we initialize the getMovieDetails variable.

Then in the test function, we call the run function, with the input as GetMovieDetails.Params(MOVIE_ID. After that, we use the verify function, provided by MockK to check whether or not the call was actually made exactly one time.

Now, we will run the test to see whether it works or not. To run the test in Android Studio, click on the green button on the test method:

Log for the unit test run when testing GetMovieDetails class

Figure 4: Log for the unit test run when testing GetMovieDetails class

Advantages and Disadvantages of Unit Testing

With unit tests in place, we can be confident that our logic is met and we will be notified if any changes are made that break the existing logic. In addition, the unit tests are run blazingly fast. Still, we’re not sure if users can interact with the application as we expect.

That’s where UI testing comes into play.

UI Testing (Instrumentation Testing)

Traditionally, automated end-to-end testing is usually done in a blackbox way, meaning we create another project for automated end-to-end testing of the application. We need to find the locator of the elements in our application and find a way to interact with it via a framework such as Appium or UIAutomator. However, this approach is more time-consuming since we have to redefine the locators of the elements in our application; also, Appium is pretty slow when interacting with the real mobile application.

To be able to resolve the drawbacks of Appium, we’ll apply instrumentation tests with the help of the Espresso and AndroidX frameworks.

How to Implement UI in a Project

Let’s say we want to check whether the movie list button is shown and is clickable.

The MoviesActivity is defined as following:

class MoviesActivity : BaseActivity() {
companion object {
fun callingIntent(context: Context) = Intent(context, MoviesActivity::class.java)
}
override fun fragment() = MoviesFragment()
}

The actual logic and how the movies page is rendered is defined in the MoviesFragment class:

@AndroidEntryPoint
class MoviesFragment : BaseFragment() {
...
private fun loadMoviesList() {
emptyView.invisible()
movieList.visible()
showProgress()
moviesViewModel.loadMovies()
}

private fun renderMoviesList(movies: List<MovieView>?) {
moviesAdapter.collection = movies.orEmpty()
hideProgress()
}
...
}

The test class will be written like the following:

class MainApplicationTest {
@get:Rule
val mActivityRule = ActivityTestRule(MoviesActivity::class.java, true, false)

@Before
fun setUp() {
mActivityRule.launchActivity(null)
Intents.init();
}

@After
fun tearDown() {
Intents.release();
}

@Test
fun clickMovieListButton() {
val movieListButton = onView(withId(R.id.movieList))
movieListButton.perform(click())
val moviePoster = onView(withId(R.id.moviePoster))
moviePoster.check(matches(isDisplayed()))
}
}

In the test class, we need to specify the activity of the application we want to run, in this case, MovieActivity.

  @get:Rule
val mActivityRule = ActivityTestRule(MoviesActivity::class.java, true, false)

Before the test is run, the activity will be initialized.

  @Before
fun setUp() {
mActivityRule.launchActivity(null)
Intents.init();
}

Then after the test is done, we will close the activity.

  @After
fun tearDown() {
Intents.release();
}

For the test itself, we find the movieList element, and click on it.

  @Test
fun clickMovieListButton() {
val movieListButton = onView(withId(R.id.movieList))
movieListButton.perform(click())
val moviePoster = onView(withId(R.id.moviePoster))
moviePoster.check(matches(isDisplayed()))
}

After running the test by clicking on the green button, we can see the test has passed:

Test result for instrumentation testing

Figure 5: Test result for instrumentation testing

Advantages and Disadvantages of Instrumentation Tests

So, with instrumentation tests, we can be confident that users can interact with the UI and the functionalities work as expected per our business requirements. And the speed is pretty amazing.

But the drawback of instrumentation tests is that after every change in production code, you will need to change the test code since the test is affected by both the user interface and the business logic.

Conclusion

Creating a working Android application is not a hard task. But to be able to create a high-quality application that’s reliable over time is very difficult. You need to run a lot of tests, from unit tests and integration tests to end-to-end tests. Each test has its own role to play in the success of your product. Creating tests not only ensures high quality, but also gives developers the confidence they need to add new features later on without worrying that new code will break existing functionality. So make sure you implement all of them before releasing your application on the market.

Still, writing tests is a daunting task, so you also need to take your time implementing them. Moreover, debugging tests to know why they failed requires much time and effort too. If you’re having a hard time debugging your tests, or even get stuck in them, check out Shipbook, a logging platform that can help you quickly debug issues in your tests. Shipbook provides numerous resources and documents to help you test your applications, along with logs to easily discover the root cause of that bug you’re struggling with.

· 13 min read
Yossi Elkrief
Elisha Sterngold

Yossi Elkrief

Interview with Mobile Lead at Nike, Author, Google Developers Group Beer Sheva Cofounder, Yossi Elkrief

Thank you for being with us today Yossi, would you like to begin with sharing a little bit about your position at Nike, and what you do?

I joined Nike for a bit more than two years now. I am head of mobile development in the Digital Studio of innovation. It is a bit different from regular app development but we still work closely with all the teams in WHQ, Nike headquarters in the US as well as Europe, China, and Japan. We really work across the globe, and we do some pretty cool things in the realm of innovation. We develop new technologies and try to find ways to harness new technologies or algorithms to help Nike provide the best possible service to our consumers.

I have experience in mobile for the past 13, almost 14 years now. I’ve been involved in Android development since their very first Beta release, even a bit before that. I also worked on iOS throughout the years, and I’ve been involved in a couple of pretty large consumer based companies and startups.

At Nike we have a few apps, such as: Nike Training Club (NTC), Nike Running Club (NRC), and the Nike app made for consumers, where you can purchase Nike’s products.

We work with all of those teams and other teams within Nike, on various apps as well as in-house apps that are specific creations of our studio, where we work on creating new innovative features for Nike.

One major project that is currently working on completing roll out is Nike Fit, recently launched in China and Japan. Nike Fit, is aimed at helping people shop Nike shoes online and hopefully for other apparels in the near future.

How is it working for Nike, as a clothing company, with a background of working mainly for tech companies?

Nike is a company with so much more technology than people realize. We are not just a shoe company or a fashion company.

Our mission is to bring inspiration and innovation to every athlete1 in the world.

We use a tremendous amount of technology to transform a piece of fabric into a piece in the collection of the Nike brand. Nike may be more faceforward than companies that I’ve worked for in the past, but there is a vast array of technologies that we work with in Nike, or work on building upon, to make Nike the choice brand for our customers, now and in the future.

One of the highest priorities at Nike is the athlete consumers. Because Nike is a brand that is specifically designed and geared toward athletes. We therefore try to keep all of Nike’s athletes at the forefront in terms of their importance to the company. Consumer facing, most of Nike’s products are not the apps. All of my previous experiences in app companies or technical companies that provide a service are pretty different from what I focus on now at Nike. So everything we do at Nike, all the services we provide, are to help serve athletes in their day to day activities, whether this be in sports for professional athletes, or for people with hobbies like running, football, or cycling and so on.

Everything I focus on has to do with providing athletes with better service while choosing their shoes, pants, or all the equipment they need, and that Nike provides so they can best utilize their skills.

Can you tell us a bit about what went into writing your book “Android 6 Essentials”? Do you feel that writing the book improved your own skills as a developer?

I write quite a lot. I don’t get to write as many technical manuals as I’d like, but I do write quite a few documentations, technical documents, and blog posts. Writing the book was a different process, but I really wanted to engage a technical audience, as this audience is very different from that of a poem, or story, which is less for use and more for enjoyment.

Writing the book made me a better person in general because I was working full time in the capacity of my position at the company that I was with at the time, and then on top of all of my regular responsibilities, in order to be able to keep to schedule and hit all of the milestones, and points that I wanted to cover in my book. I had to be very organised and devoted to the project. I had to juggle work, and family, and all of my other responsibilities as well, so I divided my time to make sure I could meet all of my goals. The process was really quite fun because in the end I had something that I built and created from scratch.

I would recommend it, because it gives you an added value that no one else will have, and in the end you have a final product that you can show someone, and say that it was your creation. I think the whole process makes you a better developer, and it helps you understand technology better, because you need to understand technology at a level and to a degree of depth in order to then explain it in writing to someone else.

You also took part in co-founding Google Developers Group Beer Sheva, which is also about sharing knowledge and bettering yourselves as developers, can you tell us a little about that process?

The main aim of Google Developers Group is sharing knowledge. When we share knowledge we can learn from everyone. Even if I built each of the pieces of a machine myself, when I share it with someone else, they can always bring to light something that I was unaware of; some new and interesting way of using it. Sharing with people helps more than just the basic value of assistance. Finding a group of peers that share the same desire or passion for technology, knowledge, and information, this is a key concept in growth, for everyone in general.

On that note, we are seeing an interesting trend in development: even though mobile apps are becoming increasingly more complex, the industry has succeeded in reducing the occurrence of crashes. Is that your experience as well and if so, what are, in your eyes, the main reasons for this shift?

It’s really a two part answer.

Firstly, both Google and Apple are providing a lot more information, and are focusing a lot more on user experience in terms of crashes, app not responding, bad reviews etc. Users are more likely to write a good review if you provide more information, or create a better service with more value for them. Consumers in general are more interested in using the same app, the same experience, if they love it. So they will happily provide you with more information so that you can solve its issues, and keep using your app rather than trying something new. We call them Power Users or Advanced Users. With their help, we can keep the app updated and solve issues faster.

The second part of the answer is that all of the tools, ID integrations, shared knowledge, documentation, has been vastly improved. People understand now that they need to provide a service that runs smoothly with as little interference as possible for the user and they do their part to make sure that these issues remain as low as possible in the apps. We want a crash rate lower than 0.1%. So we work 90% of our time to build an infrastructure that will remain robust and maintain top quality, with a negligible amount of crashes, exceptions, and app issues, in general, that will harm and affect the user experience.

Do you believe that all bugs should always be fixed? If not, do you have ways of defining which ones do not need to be fixed?

As a perfectionist, yes, we want to solve all of the app issues. But in terms of real life, we work with a simple process. We look at the impact of the bug. How many users are being impacted? What is the extent to the impact? What does the user have to do in order to use the service? Is it just a simple work around or is it preventing the user from using an important part of the app?

Do you close insignificant issues, or are they kept open in a back office somewhere?

No, so we are very careful and organized about all of the issues that we have in the system. We document every issue with as much information as possible. Sometimes you can fix an issue with dependency and provide a new version for some dependencies and then because of all the interactions of the code versions you have some issues being solved even though you didn’t do anything. So for example, this doesn’t happen much, but sometimes we have issues in the backlog that can remain unsolved for more than a month.

What is your view on QA teams? Some companies have come out saying that they don’t use QA teams and instead move that responsibility to the developer team. Do you believe that there should be a QA Team?

I believe that companies should have a Quality Assurance team, which is sometimes also called QE, Quality Engineering. I think as a developer, working on various platforms, when you implement a new feature or service, give or take on the architecture of the technology, the actual issue can be quite difficult to find. This requires a different point of view than the developer. When you develop or write the code, you have a different point of view in mind then users often have when it comes to using the app. 90% of the time users will actually often behave differently than developers anticipated when writing the code. So when we design the feature, sometimes we need to understand a bit better how users will interact. We have a product team that we involve and engage on an hourly basis. The same goes for QA. We use QA in our Innovation Studio as well, but the same goes for our apps. We are constantly engaging QA to see how to both resolve issues and understand better how the user will interact with the app.

What is your position on Android Unit testing: How are the benefits compared to the efforts?

With testing in general, some will say it's not necessary at all and will just rely on QA. I don’t side with either. I think it is a mix. You don't need to unit test every line of code. I think that is excessive. Understanding the architecture is more important than unit testing. It's more important to understand how and why the pieces of the puzzle interact- to understand why to choose one flow over another, than to just unit test every function. Sometimes pieces of the puzzle are better understood with unit testing, but it is not necessary to unit test everything. That said, the majority of our code does undergo UI and UX testing.

What do you think about the fact that with Kotlin, you don’t state the exception in the function, this is unlike Java or Swift, which both require it. Which approach do you prefer?

I think for each platform there are different methods of working. Both approaches are fine with me. I think the Kotlin approach for Android, or for Kotlin in general, gives the developer more responsibility as to what can go wrong. You need to better understand the code and the reasons behind what can go wrong with exceptions when working with Kotlin. You can solve it using annotations and documentation, but in general people need to understand that if something can go wrong it will. They need to understand then how to solve it within the runtime code that they are writing, or building. If you are using an API, then API documentation will provide you with a bit more knowledge as to what is happening under the surface, and in terms of architecture, yes you need to know that when using an API function call or whichever function you are using within your own classes, you still need to interact with them properly, so it drives you to write better code handling for all exceptions.

Do you feel the fragmentation of devices or versions in Android is a real difficulty?

Yes, we see different behaviors across devices and different versions, and making sure that the app runs smoothly across all platforms can be a bit rough. But even so, it is a lot better than what we had in the past. I hope that as we progress in time, more and more devices will be upgraded to use an API level that is safer to use, and will mitigate fragmentation. Right now, some of the features that we are building, for example API 24 and above, have major progress in comparison to API 21 and above.

As a final question, which feature would you dream that Android or Kotlin would have?

I never thought of that, because, a week ago I would say camera issues on Android. But a month ago I would say, running computer vision in AI on Android on different devices. Camera issues are due to different hardwares. Google is doing a relatively good job in trying to enforce a certain level of compliance and testing on all devices. You have quite a few tests the device has to pass both in hardware and in API. But we still see many devices attempt to bypass, or give false results to the tests.

I would say giving us support for actual devices as far back as five to seven years, instead of three, and giving an all around better camera experience over all devices.

Thank you very much to Yossi Elkrief for your time and expertise!


Shipbook gives you the power to remotely gather, search and analyze your user logs and exceptions in the cloud, on a per-user & session basis.

Footnotes

  1. If you have a body, you are an athlete.