Blog Infos
Author
Published
Topics
, , , ,
Published

In this blog post, We’ll delve into the powerful trio of Jetpack Compose, Ktor, and Koin, exploring how they synergize to streamline and enhance modern Android app development. Here’s what we’ll cover:

  1. Introduction to Jetpack Compose: We’ll integrate of Jetpack Compose, Google’s declarative UI toolkit for building native Android app. Link
  2. Getting Started with Ktor: Next, we’ll dive into Ktor, a lightweight web framework for building asynchronous servers and clients in Kotlin. We’ll explore its intuitive API, asynchronous programming model, and seamless integration with other Kotlin libraries. Link
  3. Understanding Koin for Dependency Injection: Dependency injection is crucial for building scalable and maintainable Android apps. We’ll introduce Koin, a pragmatic lightweight dependency injection framework for Kotlin. We’ll cover its core concepts, such as modules, components, and scoped dependencies, and demonstrate how it simplifies dependency management in our projects. Link
  4. Integration and Interoperability: We’ll explore how Jetpack Compose, Ktor, and Koin can seamlessly integrate with each other. We’ll discuss best practices for integrating Ktor APIs into Jetpack Compose apps and leveraging Koin for dependency injection in both UI and backend layers.
  5. Building a Sample Application: To tie everything together, we’ll walk through the development of a sample Android application using Jetpack Compose for the UI, Ktor for networking, and Koin for dependency injection. We’ll demonstrate how these technologies work in harmony to create a robust and efficient mobile app.

By the end of this blog post, you’ll have a comprehensive understanding of how to leverage Jetpack Compose, Ktor, and Koin to build modern, efficient, and maintainable Android applications. Let’s dive in!

Before delving into our exploration of Compose Multiplatform for networking, I’d like to briefly revisit my previous blog post (Part 2). In that piece, I covered the intricacies of dependency injection with Ktor. If you haven’t had the chance to read it yet, you can find it here. Understanding the fundamentals of dependency injection will provide valuable context for our discussion ahead.”. this article is for KMP but here we are focusing only for Android !!

https://proandroiddev.com/compose-multiplatform-networking-with-ktor-koin-part-2-ea394158feb9?source=post_page—–05b9f28b4cd8——————————–

for Jetpack compose navigation see article

https://medium.com/proandroiddev/jetpack-compose-navigation-with-mvvm-dependency-injection-koin-ceee45658c86

Lets start quick implementation

declare dependencies inside build.gradle

dependencies {

    implementation("androidx.activity:activity-compose:1.8.2")
    implementation(platform("androidx.compose:compose-bom:2023.08.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    val lifecycle_version ="2.6.2"
    val coroutine_version="1.7.3"
    val koin_version="3.4.0"

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    //Koin
    implementation ("io.insert-koin:koin-android:$koin_version")
    implementation ("io.insert-koin:koin-androidx-compose:$koin_version")


    //Lifecycle
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
    implementation ("androidx.lifecycle:lifecycle-extensions:2.2.0")
    implementation( "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
    implementation ("androidx.activity:activity-ktx:1.8.2")

    //Coroutines
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version")
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version")
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutine_version")


    //Compose Navigation
    implementation ("androidx.navigation:navigation-compose:2.7.7")

    //Koil Image Dependency
    implementation("io.coil-kt:coil-compose:2.4.0")

    //Ktor & kotlin Serialization
    implementation("io.ktor:ktor-client-android:2.3.10")
    implementation("io.ktor:ktor-client-serialization:2.3.10")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
    implementation("io.ktor:ktor-client-logging-jvm:2.3.10")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.10")
}

for Ktor integration with Koin, lets break down step by step

val networkModule = module {
    single {
        HttpClient {
            install(ContentNegotiation) {
                json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
            }
            install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        Log.v("Logger Ktor =>", message)
                    }

                }
                level = LogLevel.ALL
            }
            install(ResponseObserver) {
                onResponse { response ->
                    Log.d("HTTP status:", "${response.status.value}")
                }
            }
            install(DefaultRequest) {
                header(HttpHeaders.ContentType, ContentType.Application.Json)
            }
        }
    }
}
val networkModule = module {
  • This line defines a Koin module named networkModule. Modules in Koin are used to organize and configure dependencies.
single {
        HttpClient {

this declares a singleton dependency using Koin’s single function. It creates an instance of the HttpClient class provided by Ktor.

 install(ContentNegotiation) {
                json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
            }

This block installs the ContentNegotiation feature in the HTTP client, allowing it to automatically serialize and deserialize JSON data. It configures JSON serialization with ignoreUnknownKeys set to true, meaning that the JSON parser will ignore any unknown keys encountered during deserialization.

install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        Log.v("Logger Ktor =>", message)
                    }
                }
                level = LogLevel.ALL
            }
  • This block installs the Logging feature in the HTTP client, enabling logging of HTTP requests and responses. It configures a custom logger that logs messages to Android’s Logcat with a verbose level (Log.v). The logging level is set to LogLevel.ALL, which logs all messages.
install(ResponseObserver) {
                onResponse { response ->
                    Log.d("HTTP status:", "${response.status.value}")
                }
            }

This block installs the ResponseObserver feature in the HTTP client, allowing it to observe and log HTTP responses. It defines a callback function that logs the HTTP status code of the response

            install(DefaultRequest) {
                header(HttpHeaders.ContentType, ContentType.Application.Json)
            }
  • This block installs the DefaultRequest feature in the HTTP client, setting default request headers. It adds a header specifying that the content type of requests should be JSON.

for DI modules

val networkModule = module {
    single {
        HttpClient {
            install(ContentNegotiation) {
                json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
            }
            install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        Log.v("Logger Ktor =>", message)
                    }

                }
                level = LogLevel.ALL
            }
            install(ResponseObserver) {
                onResponse { response ->
                    Log.d("HTTP status:", "${response.status.value}")
                }
            }
            install(DefaultRequest) {
                header(HttpHeaders.ContentType, ContentType.Application.Json)
            }
        }
    }
}

val apiServiceModule= module {
    factory { ApiService(get()) }
}

val repositoryModule = module {
    factory {  Repository(get()) }
}

val viewModelModule= module {
    viewModel{ MainViewModel(get(),get()) }
}

val remoteDataSourceModule= module {
    factory {  RemoteDataSource(get()) }
}

Application class

startKoin {
            androidContext(this@MyApplication)
            androidLogger()
            modules(networkModule,
                apiServiceModule, remoteDataSourceModule, repositoryModule, viewModelModule)
        }

ApiService class

class RemoteDataSource(private val apiService: ApiService) {

    suspend fun getReceipes() = apiService.getReceipes()
    suspend fun getReceipesDetail(id:Int?) = apiService.getReceipeDetails(id)
}

class ApiService(private val httpClient: HttpClient) {

    val recipes="recipes/"
    suspend fun getReceipes(): Receipes = httpClient.get("${Constants.BASE_URL}$recipes").body<Receipes>()
    suspend fun getReceipeDetails(id:Int?): Receipes.Recipe = httpClient.get("${Constants.BASE_URL}$recipes$id").body<Receipes.Recipe>()
}

This ApiService class acts as a client for making HTTP requests to the remote server. It uses the get() function provided by the HttpClient to execute the request. The response body is deserialized into a Receipes object using Kotlinx.serialization’s body() function.

Repository class

class Repository(private val remoteDataSource: RemoteDataSource) {

    suspend fun getReceipes(context: Context): Flow<UiState<Receipes?>> {
        return toResultFlow(context){
            remoteDataSource.getReceipes()
        }
    }

    suspend fun getReceipesDetail(context: Context,id:Int?): Flow<UiState<Receipes.Recipe?>> {
        return toResultFlow(context){
            remoteDataSource.getReceipesDetail(id)
        }
    }

}

this Repository class acts as an intermediary between the UI layer and the remote data source, providing a clean API for fetching recipes data while encapsulating the details of how the data is fetched and processed.

Common BaseviewModel for Api requests

open class BaseViewModel(application: Application) : AndroidViewModel(application) {
  protected val context
    get() = getApplication<Application>()

  suspend fun <T> fetchData(uiStateFlow: MutableStateFlow<UiState<T?>>, apiCall: suspend () -> Flow<UiState<T?>>) {
      uiStateFlow.value = UiState.Loading
    try {
       apiCall().collect {
         uiStateFlow.value = it
      }
    } catch (e: Exception) {
       uiStateFlow.value = UiState.Error(e.message?:"")
    }
  }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

extend this BaseviewModel into your any ViewModel class

class MainViewModel(private val repository: Repository, application: Application): BaseViewModel(application) {

    val _uiStateReceipeList = MutableStateFlow<UiState<Receipes?>>(UiState.Loading)
    val uiStateReceipeList: StateFlow<UiState<Receipes?>> = _uiStateReceipeList

    val _uiStateReceipeDetail = MutableStateFlow<UiState<Receipes.Recipe?>>(UiState.Loading)
    val uiStateReceipeDetail: StateFlow<UiState<Receipes.Recipe?>> = _uiStateReceipeDetail


    fun getReceipesList() = viewModelScope.launch {
        fetchData(_uiStateReceipeList) { repository.getReceipes(context) }
    }

    fun getReceipeDetail(id: Int?) = viewModelScope.launch {
        fetchData(_uiStateReceipeDetail,) { repository.getReceipesDetail(context, id) }
    }
}

Generic function for handle response

function provides a convenient way to handle data fetching operations asynchronously and emit different states (loading, success, error) as the result of the operation. It encapsulates common error handling and internet connectivity checks.

fun <T> toResultFlow(context: Context, call: suspend () -> T?) : Flow<UiState<T?>> {
    return flow<UiState<T?>> {
        if(Utils.hasInternetConnection(context)) {
            emit(UiState.Loading)
            val c = call.invoke()
            c.let { response ->
                try {
                    emit(UiState.Success(response))
                } catch (e: Exception) {
                    emit(UiState.Error(e.toString()))
                }
            }
        }else{
            emit(UiState.Error(Constants.API_INTERNET_MESSAGE))
        }
    }.flowOn(Dispatchers.IO)
}

Finally call your viewmodel function inside composable functions

@Composable
fun RecipesScreen(navigation: NavController, mainViewModel: MainViewModel) {
    Scaffold(
        topBar = {
            CustomToolbarScreen(navController = navigation, title = "Home", false)
        }
    )
    { innerPadding ->
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .padding(10.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            //add your code
            LaunchedEffect(key1 = Unit) {
                getReceipesListAPI(mainViewModel)
            }
            val state = mainViewModel.uiStateReceipeList.collectAsState()
            when (state.value) {
                is UiState.Success -> {
                    ProgressLoader(isLoading = false)
                    (state.value as UiState.Success<Receipes>).data?.let {
                        it.recipes?.let { it1 ->
                            RecipeList(recipes = it1) { recipe ->
                                // Handle recipe click here
                                navigation.navigate(Routes.getSecondScreenPath(recipe.id))
                            }
                        }
                    }
                }

                is UiState.Loading -> {
                    ProgressLoader(isLoading = true)
                }

                is UiState.Error -> {
                    ProgressLoader(isLoading = false)
                    //Handle Error
                    SimpleAlertDialog(message =  ((state.value as UiState.Error<Receipes>).message))
                }
            }
        }
    }


}

@Composable
fun RecipeListCard(recipe: Receipes.Recipe, onRecipeClick: (Receipes.Recipe) -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onRecipeClick(recipe) },
        shape = RoundedCornerShape(10),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 4.dp
        )
    ) {
        Row(
            modifier = Modifier.padding(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Image(
                painter = rememberAsyncImagePainter(recipe.image),
                contentDescription = null,
                modifier = Modifier
                    .size(100.dp)
                    .clip(RoundedCornerShape(10.dp)),
                contentScale = ContentScale.Crop
            )
            Column(
                modifier = Modifier
                    .padding(start = 8.dp)
                    .weight(1f)
            ) {
                Text(
                    text = recipe.name ?: "",
                    fontWeight = FontWeight.Bold,
                    fontSize = 18.sp
                )
                Text(
                    text = "Prep Time: ${recipe.prepTimeMinutes} mins",
                    fontSize = 14.sp,
                    color = Color.Black
                )
                Text(
                    text = "Cook Time: ${recipe.cookTimeMinutes} mins",
                    fontSize = 14.sp,
                    color = Color.Black
                )
                Text(
                    text = "Servings: ${recipe.servings}",
                    fontSize = 14.sp,
                    color = Color.Black
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecipeList(recipes: List<Receipes.Recipe>, onRecipeClick: (Receipes.Recipe) -> Unit) {
    LazyColumn {
        items(recipes) { recipe ->
            RecipeListCard(recipe = recipe, onRecipeClick = onRecipeClick)
        }
    }
}

Full Implementation on GitHub

Certainly! Here are some suggestions for sentences to include after wrapping up:

  1. Thank you for taking the time to read article. I hope you found it informative and useful for your Android development projects.”
  2. If you have any questions, feedback, or topics you’d like us to cover in future articles, please don’t hesitate to reach out to us.”
  3. Stay tuned for more insightful content on modern Android development techniques and best practices.”
  4. “Don’t forget to follow me for updates on upcoming articles and other interesting content.”
  5. We appreciate your support and look forward to sharing more knowledge with you in the future. Happy coding !!”

Nimit Raja — LinkedIn

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu