Skip to content

hadiyarajesh/flower

Repository files navigation

Flower is a Kotlin multi-platform (originally, Android) library that makes networking and database caching easy. It enables developers to fetch network resources and use them as is OR combine them with local database at single place with fault-tolerant architecture.

release contributors

Special thanks

A special thanks to JetBrains for sponsoring the tools required to develop this project.

Why Flower?

  • It helps you to handle different states (Loading, Success, EmptySuccess, Error) of resources efficiently.
  • It helps you to use local data in case of network unavailability.
  • It provides a fluid app experience by not blocking the main thread when accessing network/database resources.

Flower is recognised by Google Dev Library, a showcase of open-source projects.

Installation

Flower is primarily available in two modules, one for Ktorfit and the other for Retrofit.

If you want to handle networking yourself, you can also use the core module.

$flowerVersion=3.1.0
$ktorFitVersion=1.0.0-beta16
$retrofitVersion=2.9.0

Ktorfit

This is a multiplatform module. It is suitable for use in Kotlin multiplatform projects, Android (Apps/Libraries), the JVM in general, Kotlin-JS, and so on...

It uses and provides Ktorfit and you must use KSP in your project.

Apply the KSP Plugin to your project:

plugins {
  id("com.google.devtools.ksp") version "1.7.10-1.0.6"
}

Multiplatform example

dependencies {
    implementation("io.github.hadiyarajesh.flower-ktorfit:flower-ktorfit:$flowerVersion")

    add("kspCommonMainMetadata", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
    add("kspJvm", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
    add("kspAndroid", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
    add("kspIosX64", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
    add("kspJs", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
    add("kspIosSimulatorArm64", "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
}

Android example

dependencies {
    implementation("io.github.hadiyarajesh.flower-ktorfit:flower-ktorfit:$flowerVersion")
    //Ktorfit library
    implementation("de.jensklingenberg.ktorfit:ktorfit-lib:$ktorFitVersion")
    ksp("de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorFitVersion")
}

Retrofit

This is an Android-only module, so it can only be used in Android Apps/Libs.

dependencies {
    implementation("io.github.hadiyarajesh.flower-retrofit:flower-retrofit:$flowerVersion")
    // Retrofit library
    implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
}

Core

This module only contains the core code and allows you to handle the networking yourself.

We Highly recommend you to use either Ktorfit or Retrofit module. Only use this if you don't want to rely on Ktorfit or Retrofit.

dependencies {
    implementation("io.github.hadiyarajesh:flower-core:$flowerVersion")
}

Usage

Assume you have a model class called MyModel that you are retrieving from the network.

data class MyModel(
    val id: Long,
    val data: String
)

Prerequisite

  • If you want to save network response in a local database, your database caching system function must return a value of type Flow<MyModel>.

Android Room example:

@Dao
interface MyDao {
    @Query("SELECT * FROM Data")
    fun getLocalData(): Flow<MyModel>
}
  • Return type of networking api function must be ApiResponse<MyModel> ( or Flow<ApiResponse<MyModel>> if you're retrieving a flow of data from server)

Ktorfit/Retrofit example:

interface MyApi {
    @GET("data")
    suspend fun getRemoteData(): ApiResponse<MyModel>
    // OR
    @GET("data")
    fun getRemoteData(): Flow<ApiResponse<MyModel>>
}



1. Add CallAdapterFactory/ResponseConverter in networking client

Ktorfit Add FlowerResponseConverter as ResponseConverter in Ktorfit builder.

Ktorfit.Builder()
  .baseUrl(BASE_URL)
  .httpClient(ktorClient)
  .responseConverter(FlowerResponseConverter())
  .build()

Retrofit Add FlowerCallAdapterFactory as CallAdapterFactory in Retrofit builder

Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(okHttpClient)
    .addCallAdapterFactory(FlowerCallAdapterFactory.create())
    .build()



2. Make network request (and save data) in Repository

2.1 If you want to fetch network resources and save into local database, use dbBoundResource() higher order function (or dbBoundResourceFlow() function if you're retrieving a flow of data from server). It takes following functions as parameters.

  • fetchFromLocal - A function to retrieve data from local database
  • shouldMakeNetworkRequest - Decides whether or not to make network request
  • makeNetworkRequest - A function to make network request
  • processNetworkResponse - A function to process network response (e.g., saving response headers before saving actual data)
  • saveResponseData - A function to save network response (MyModel) to local database
  • onNetworkRequestFailed - An action to perform when a network request fails
fun getMyData(): Flow<Resource<MyModel>> {
    return dbBoundResources(
        fetchFromLocal = { myDao.getLocalData() },
        shouldMakeNetworkRequest = { localData -> localData == null },
        makeNetworkRequest = { myApi.getRemoteData() },
        processNetworkResponse = { },
        saveResponseData = { myDao.saveMyData(it) },
        onNetworkRequestFailed { errorMessage, statusCode -> }
    ).flowOn(Dispatchers.IO)
}

OR

2.2 If you only want to fetch network resources without saving it in local database, use networkResource() higher order function. (or networkResourceFlow() function if you're retrieving a flow of data from server)

fun getMyData(): Flow<Resource<MyModel>> {
    return networkResource(
        makeNetworkRequest = { myApi.getRemoteData() },
        onNetworkRequestFailed { errorMessage, statusCode -> }
    ).flowOn(Dispatchers.IO)
}



3. Collect Flow to observe different state of resources (Loading, Success, Error) In ViewModel

// A model class to re-present UI state
sealed class UiState<out T> {
    object Empty : UiState<Nothing>()
    data class Loading(val data: T?) : UiState<out T>()
    data class Success<out T>(val data: T & Any) : UiState<T & Any>()
    data class Error(val msg: String?) : UiState<Nothing>()
}
private val _myData: MutableStateFlow<UiState<MyModel>> = MutableStateFlow(UiState.Empty)
val myData: StateFlow<UiState<MyModel>> = _myData.asStateFlow()

init {
    viewModelScope.launch {
        getMyData()
    }
}

suspend fun getMyData() = repository.getMyData().collect { response ->
    when (response.status) {
        is Resource.Status.Loading -> {
            val status = response.status as Resource.Status.Loading
            _myData.value = UiState.Loading(status.data)
        }
      
        is Resource.Status.Success -> {
            val status = response.status as Resource.Status.Success
            _myData.value = UiState.Success(status.data)
        }
      
        // EmptySuccess is for potentially body-less successful HTTP responses like 201, 204
        is Resource.Status.EmptySuccess -> {
            _myData.value = UiState.Empty
        }
      
        is Resource.Status.Error -> {
            val status = response.status as Resource.Status.Error
            _myData.value = UiState.Error(status.message)
        }
    }
}



4. Observe data in Activity/Fragment/Composable function to drive UI changes

lifecycleScope.launchWhenStarted {
    viewModel.myData.collect { data ->
        when (data) {
            is UiState.Loading -> {
                // Show loading
            }
            is UiState.Success -> {
                // Show success
            }
            is UiState.Error -> {
                // Show error
            }
            else -> {}
        }
    }
}

Sample

Two sample apps are provided in this repository

  1. XML View based app - It fetch random quote from remote api and save it to local database in order to display it on UI.
  2. Jetpack Compose based app - It fetches unsplash images from Picsum and display it on UI.

License

Apache License 2.0