Unit 5 notes

Get Data from the Internet

Introduction to Coroutines in Kotlin playground

Concurrency: performing multiple tasks in your app at the same time. Example: get data from a server or save user data on the device, while responding to user input events and updating the UI accordingly.

Coroutine

Coroutine: A function that can “pause” and “resume”. It is conceptually similar to a thread since it takes a block of code to run that works concurrently with the rest of the code, but it is not bound to any particular thread. A coroutine may suspend its execution in one thread and resume in another one.

  • Coroutines make it easier to write asynchronous code, which means one task doesn’t need to finish completely before starting the next task, enabling multiple tasks to run concurrently.
  • Co in Coroutine means cooperative and routine means a set of instructions like a function. The code cooperates to share the underlying event loop when it suspends to wait for something, which allows other work to be run in the meantime.

Suspending functions

Suspending function: similar to a regular function, but it can be suspended (paused) and resumed again later. To do this, suspend functions can only be called from other suspend functions that make this capability available.

  • A suspending function may contain zero or more suspension points. A suspension point is where the execution of the function can suspend.

Synchronous code: Only one conceptual task is in progress at a time. It is like a sequential linear path. One task must finish completely before the next one is started.

Delay function

delay(...): It is a special suspending function since the execution of the function where delay(...) is called will be paused (suspended) at this point, and them resume after the delay is over.

runBlocking function

runBlocking(): Runs an event loop, which can handle multiple tasks at once by continuing each task where it left off when it’s ready to be resumed. It is synchronous since it will not return until all work within its lambda block is completed.

  • Note: Only use runBlocking() within a main() function for learning purposes. In your Android app code, you do not need runBlocking() because Android provides an event loop for your app to process resumed work when it becomes ready. runBlocking() can be useful in your tests, however, and can let your test await specific conditions in your app before invoking the test assertions.

Asynchronous code

launch

launch(): Function that launches a new coroutine. To execute tasks concurrently, add multiple launch().

  • Fire and forget: you fire off a new coroutine and don´t care about its result.

Structured concurrency

Coroutines follow the Structured concurrency principle: Code is sequential by default and cooperates with an underlying event loop, unless the programmer explicitly asks for concurrent execution (with launch() for instance).

  • A function should finish its work completely by the time it returns regardless of how many coroutines it may have used.
  • Even if a function fails with an exception, once the exception is thrown, there are no more pending tasks from the function.
  • More about structured concurrency
  • Structured concurrency meaning: We can write (and read) async code in a structured manner - meaning top to bottom, without having to jump around to follow the control flow of the code.

async

async(): Function similar to launch(), but it cares about the value returned by the coroutine. Returns an object of type Deferred, which is like a promise that the result will be in there when it’s ready. You can access the result on the Deferred object using await().

Parallel decomposition: Taking a problem and breaking it into smaller subtasks that can be solved in parallel. The subtasks results’ can be combined.

CoroutineScope

coroutineScope(): Function that creates a local scope. The coroutines launched in this scope are grouped together within this scope.

  • coroutineScope() will only return once all its work, including any coroutines it launched, have completed.
  • With coroutineScope(), even though the function is internally doing work concurrently, it appears to the caller as a synchronous operation because coroutineScope won’t return until all work is done. (structured concurrency)

Exceptions and cancellations

Exception: unexpected event that happens during execution of your code.

There is a parent-child relationship among coroutines. You can launch a coroutine (known as the child) from another coroutine (parent).

  • When one of the child coroutines fails with an exception, it gets propagated upwards. The parent coroutine is cancelled, which in turn cancels any other child coroutines.

Handling exceptions in coroutines is the same as in synchronous code: by using a try-catch block.

Note: Exceptions are propagated differently for coroutines started with launch() versus async(). Within a coroutine started by launch(), an exception is thrown immediately so you can surround code with a try-catch block if it’s expected to throw an exception.

Cancellation: The cancellation of coroutines is typically user-driven, when an event has caused the app to cancel work that it had previously started. It can be done by using cancel() function on a coroutine.

  • A coroutine can be cancelled, but it won’t affect other coroutines in the same scope and the parent coroutine will not be cancelled.

(More) coroutine concepts

Job

job: When you launch a coroutine with the launch() function, it returns an instance of Job. The Job holds a handle, or reference, to the coroutine, so you can manage its lifecycle.

  • The Deferred object that is returned from a coroutine started with the async() function is a Job as well, and it holds the future result of the coroutine.
  • The job can be used to control the life cycle, or how long the coroutine lives for, such as cancelling the coroutine if you don’t need the task anymore.
  • With a job, you can check if it’s active, cancelled, or completed.
  • The job is completed if the coroutine and any coroutines that it launched have completed all of their work.
  • Note: the coroutine could have completed due to a different reason, such as being cancelled, or failing with an exception, but the job is still considered completed at that point.
  • Jobs also keep track of the parent-child relationship among coroutines.

Job hierarchy

Job hierarchy: Have the parent-child relationships between the coroutines. This parent-child relationship dictates certain behavior for the child and parent, and other children belonging to the same parent:

  • If a parent job gets cancelled, then its child jobs also get cancelled.
  • When a child job is canceled using job.cancel(), it terminates, but it does not cancel its parent.
  • If a job fails with an exception, it cancels its parent with that exception. This is known as propagating the error upwards (to the parent, the parent’s parent, and so on).

CoroutineScope

  • Coroutines are typically launched into a CoroutineScope. This ensures that we don’t have coroutines that are unmanaged and get lost, which could waste resources.
  • launch() and async() are extension functions on CoroutineScope. Call launch() or async() on the scope to create a new coroutine within that scope.
  • A CoroutineScope is tied to a lifecycle, which sets bounds on how long the coroutines within that scope will live.
    • If a scope gets cancelled, then its job is cancelled, and the cancellation of that propagates to its child jobs.
    • If a child job in the scope fails with an exception, then other child jobs get cancelled, the parent job gets cancelled, and the exception gets re-thrown to the caller.
  • A CoroutineScope is declared as an interface containing a CoroutineContext as a variable. The launch() and async() functions create a new child coroutine within that scope and the child also inherits the context from the scope.
CoroutineContext
  • The CoroutineContext provides information about the context in which the coroutine will be running in. The CoroutineContext is essentially a map that stores elements where each element has a unique key. These are not required fields, but here are some examples of what may be contained in a context:
    • name: name of the coroutine to uniquely identify it (default: “coroutine”)
    • job: controls the lifecycle of the coroutine (default: no parent job)
    • dispatcher: dispatches the work to the appropriate thread (default: Dispatchers.Default)
    • exception handler: handles exceptions thrown by the code executed in the coroutine (default: no exception handler)
  • Each of the elements in a context can be appended together with the + operator.
    • For example, one CoroutineContext could be defined as follows: Job() + Dispatchers.Main + exceptionHandler. The default coroutine name will be used.
  • Within a coroutine, if you launch a new coroutine, the child coroutine will inherit the CoroutineContext from the parent coroutine, but replace the job specifically for the coroutine that just got created.
    • You can also override any elements that were inherited from the parent context by passing in arguments to the launch() or async() functions for the parts of the context that you want to be different. Example: scope.launch(Dispatchers.Default) { ... }
  • More about CoroutineContext

Dispatcher

  • Coroutines use dispatchers to determine the thread to use for its execution

    • A thread can be started, does some work (executes some code), and then terminates when there’s no more work to be done.
  • App starts -> Android creates a proces and a single thread called main thread. The main thread handles system events, drawing the UI, handling user input events, etc…

  • Threading behaviour can be blocking or non-blocking. A regular function blocks the calling thread while an asynchronous function does not block it.

  • In an app, one should only call blocking code on the main thread if it will execute fairly quickly. The goal is to keep the main thread unblocked, so that it can execute work immediately if a new event is triggered.

    • If the main thread needs to execute a long-running block of work, then the screen won’t update as frequently and the user will see an abrupt transition (known as “jank”) or the app may hang or be slow to respond.
  • Long-running work items should be handled in additional threads (and not on the main thread). Hence, even if one of those threads is blocked, the main thread is not and can respond to UI and user events.

  • There are three types of dispatchers:

    • Dispatchers.Main: Runs a coroutine in the main thread. Used primarly for handling UI updates and interactions, and performing quick work.
    • Dispatchers.IO: Optimized to perform disk or network I/O outside of the main thread. Example: Read or write to files and execute any network operations.
    • Dispatchers.Default: Used when calling launch() and async(), when no dispatcher is specified in their context. Can be used to perform computationally-intensive work outside of the main thread. Example: processing a bitmap image file.
  • withContext(): Suspending function that changes the CoroutineContext that the coroutine is executed in. For example, one can change the dispatcher. (e.g. withContext(Dispatchers.Default) { ... })

  • When working with popular libraries like Room and Retrofit , specifying the dispatcher might not be necessary because the library already assigns the correct coroutine dispatcher to a coroutine. Hence, the suspend function provided by those libraries may already be main-safe (meaning that they can be called from a coroutine running on the main thread).

CoroutineScope in Android Apps:

  • Android provides coroutine scope support in entities that have a well-defined lifecycle, such as Activity (lifecycleScope) and ViewModel (viewModelScope). Coroutines that are started within these scopes will adhere to the lifecycle of the corresponding entity, such as Activity or ViewModel.

    • For example, if you start a coroutine in an Activity with lifecycleScope, then when the activity gets destroyed, the lifecycleScope will get canceled. All its child coroutines will automatically get canceled too.
  • LaunchedEffect(): Used to call suspend functions safely from inside a composable. LaunchedEffect() composable runs the provided suspending function for as long as it remains in the composition

Get data from the Internet

  • Retrofit is a library used to make requests to a web server

    • Based on the content of the web service, it creates the code to call and consume this service for you, including critical details, such as running the requests on background threads.
  • Most web servers use a common stateless web architecture known as REST (REpresentational State Transfer). They are called RESTful services.

  • Requests are made to RESTful web seervices using URIs (Uniform Resource Identifiers). URIs identifies a resource in the server by name, without implying its location or how to access it.

    • A URL (Uniform Resource Locator) is a subset of a URI that specifies where a resource exists and the mechanism for retrieving it.
  • An endpoint is a URL that allows you to access a web service running on a server.

  • Web service requests use the HTTP protocol used in web browser. Common HTTP operations include:

    • GET: Retrieving server data
    • POST: Creating new data on the server
    • PUT: Updating existing data on the server
    • DELETE: Deleting data from the server
  • Response from a web service is in XML (eXtensible Markup Language) or JSON (JavaScript Object Notation). The JSON format represents structured data in key-value pairs. An app communicates with the REST API using JSON.

  • viewModelScope can be used to launch a coroutine and make a web service request in the background. Since the viewModelScope belongs to the ViewModel, the request continues even if the app goes through a configuration change.

  • Declaring INTERNET permission to allow the app to access the Internet: <uses-permission android:name="android.permission.INTERNET" />

  • To represent the status of a web request, one can use a sealed interface. A sealed interface in Kotlin is an interface that cannot be extended by any other interface. Example:

sealed interface ExampleUiState {
   data class Success(...) : ExampleUiState
   data class Loading : ExampleUiState
   data class Error : ExampleUiState
}
  • Serialization is the process of converting data used by an application to a format that can be transferred over a network. As opposed to serialization, deserialization is the process of reading data from an external source (like a server) and converting it into a runtime object.
  • In an app, one should add @Serializable annotation to the model class (which is a data class) to make the class serializable. Each field name corresponds to a JSON key with the same name. If the field name and the JSON key have different names, then we put in the line above the field the annotation @SerialName(value = "...") where the string inside the value parameter is the name of the JSON key.

Load and display images from the internet

Repository

Data layer

Data layer: It has one or more repositories and one or more data sources. A repository can contain zero or more data sources.

  • Best practice: the app should have a repository for each type of data source used by the app.

Repository

Repository: Class that:

  • Exposes data to the rest of the app
  • Centralizes changes to data
  • Solves conflicts between multiple data sources
  • Abstracts sources of data from the rest of the app
  • Contains business logic

A repository helps make the code retrieving the data loosely coupled from ViewModel. Being loosely coupled allows changes to be made to the ViewModel or the repository without adversely affecting the other (except if the function names of the repository are changed).

  • We are now able to make changes to the implementation inside the repository without affecting the caller.

Dependency injection

  • When a class requires another class, the required class is called a dependency.

  • There are two ways for a class to get these required objects:

    • The class instantiates the required object itself
      • makes the code inflexible and more difficult to test as the class and the required object are tightly coupled
      • The calling class needs to call the object’s constructor. If the constructor changes, the calling code needs to change, too.
    • The class receives the object as an argument (dependency injection, also called inversion of control)
      • More flexible code
  • Dependency injection: when a dependency is provided at runtime instead of being hardcoded into the calling class.

  • Advantages:

    • Helps with the reusability of code. Code is not dependent on a specific object, which allows for greater flexibility.
    • Makes refactoring easier. Code is loosely coupled, so refactoring one section of code does not impact another section of code.
    • Helps with testing. Test objects can be passed in during testing. Example: A fake web service.

To make the ViewModel testable, instead of having the ViewModel creating the repository, we implement an application container that provides the repository to the ViewModel.

An application container contains the dependencies that the app requires. These dependencies are used across the whole app, so they need to be in a common place that all activities can use. You can create a subclass of the Application class and store a reference to the container.

To attach an application container to the app, one must create a subclass of Application() class and in AndroidManifest.xml, add the following in <application> tag: android:name=".MarsPhotosApplication".

  • The Application class in Android is the base class within an Android app that contains all other components such as activities and services. The Application class, or any subclass of the Application class, is instantiated before any other class when the process for your application/package is created. This class is primarily used for initialization of global state before the first Activity is displayed.

The Factory pattern is a creational pattern used to create objects. The ViewModel.Factory object uses the application container to retrieve the repository, and then passes this repository to the ViewModel when the ViewModel object is created.

  • A companion object helps us by having a single instance of an object that is used by everyone without needing to create a new instance of an expensive object. This is an implementation detail, and separating it lets us make changes without impacting other parts of the app’s code.

Load and display images from the Internet

  • Coil: Download, buffer, decode, and cache your images. Needs two things: URL of image you want to load and display; an AsyncImage composable to actually display that image. Example:
AsyncImage(
    model = ImageRequest.Builder(context = LocalContext.current)
        .data(<URL_OF_IMG>)
        .crossfade(true)
        .build(),
    error = painterResource(R.drawable.ic_broken_image),
    placeholder = painterResource(R.drawable.loading_img),
    contentDescription = stringResource(R.string.mars_photo),
    contentScale = ContentScale.Crop
)

Example of app using Retrofit, Coil and the correct architecture (Mars app):

App container:

/**
 * Dependency Injection container at the application level.
 */
interface AppContainer {
    val marsPhotosRepository: MarsPhotosRepository
}

/**
 * Implementation for the Dependency Injection container at the application level.
 *
 * Variables are initialized lazily and the same instance is shared across the whole app.
 */
class DefaultAppContainer : AppContainer {
    private val baseUrl = "https://android-kotlin-fun-mars-server.appspot.com/"

    /**
     * Use the Retrofit builder to build a retrofit object using a kotlinx.serialization converter
     */
    private val retrofit: Retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(baseUrl)
        .build()

    /**
     * Retrofit service object for creating api calls
     */
    private val retrofitService: MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }

    /**
     * DI implementation for Mars photos repository
     */
    override val marsPhotosRepository: MarsPhotosRepository by lazy {
        NetworkMarsPhotosRepository(retrofitService)
    }
}

Repository:

/**
 * Repository that fetch mars photos list from marsApi.
 */
interface MarsPhotosRepository {
    /** Fetches list of MarsPhoto from marsApi */
    suspend fun getMarsPhotos(): List<MarsPhoto>
}

/**
 * Network Implementation of Repository that fetch mars photos list from marsApi.
 */
class NetworkMarsPhotosRepository(
    private val marsApiService: MarsApiService
) : MarsPhotosRepository {
    /** Fetches list of MarsPhoto from marsApi*/
    override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}

API service:

/**
 * A public interface that exposes the [getPhotos] method
 */
interface MarsApiService {
    /**
     * Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
     * The @GET annotation indicates that the "photos" endpoint will be requested with the GET
     * HTTP method
     */
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}

ViewModel:

/**
 * UI state for the Home screen
 */
sealed interface MarsUiState {
    data class Success(val photos: List<MarsPhoto>) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}

class MarsViewModel(private val marsPhotosRepository: MarsPhotosRepository) : ViewModel() {
    /** The mutable State that stores the status of the most recent request */
    var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
        private set

    /**
     * Call getMarsPhotos() on init so we can display status immediately.
     */
    init {
        getMarsPhotos()
    }

    /**
     * Gets Mars photos information from the Mars API Retrofit service and updates the
     * [MarsPhoto] [List] [MutableList].
     */
    fun getMarsPhotos() {
        viewModelScope.launch {
            marsUiState = MarsUiState.Loading
            marsUiState = try {
                MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
            } catch (e: IOException) {
                MarsUiState.Error
            } catch (e: HttpException) {
                MarsUiState.Error
            }
        }
    }

    /**
     * Factory for [MarsViewModel] that takes [MarsPhotosRepository] as a dependency
     */
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as MarsPhotosApplication)
                val marsPhotosRepository = application.container.marsPhotosRepository
                MarsViewModel(marsPhotosRepository = marsPhotosRepository)
            }
        }
    }
}

Application:

class MarsPhotosApplication : Application() {
    /** AppContainer instance used by the rest of classes to obtain dependencies */
    lateinit var container: AppContainer
    override fun onCreate() {
        super.onCreate()
        container = DefaultAppContainer()
    }
}