Modtion logo
Modtion
android

Android : Consuming API with Basic of Retrofit for Android Compose

Android : Consuming API with Basic of Retrofit for Android Compose

Retrofit

Retrofit is a powerful, efficient, and flexible library for retrieving data from APIs on Android. With easy integration with OkHttp, Gson, Coroutines, and RxJava, Retrofit is highly recommended for modern application development. Many apps in the Playstore using it to consume their APIs. Lets start to consume APIs.

Step 1

Implement retrofit to your project.

libs.versions.toml

[versions]
...
retrofitVersion = "2.9.0"
loggingInterceptor = "4.11.0"

[libraries]
...
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitVersion" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }

[bundles]
retrofit = [
    "logging-interceptor",
    "retrofit",
    "converter-gson",
]

app/build.gradle.kts

dependencies {
    ...
    implementation(libs.bundles.retrofit)
}

Don't forget to sync the Project.

Step 2

Create API Interface. In this file you will set API Declaration like Request Method, Endpoint, URL Manipulation, Body, Response, Header, etc. ApiService.kt

import retrofit2.http.GET
import retrofit2.http.Query

interface ApiService {

    @GET("latest")
    suspend fun getLatest(
        @Query("language") language: String = "us",
        @Query("apikey") apikey: String = ""
    ): NewsResponse
}

Register to https://newsdata.io and create your own apikey.

Additional Information

Request Method

Every method must have an HTTP annotation that provides the request method and relative URL. There are eight built-in annotations:

  • @HTTP

  • @GET

  • @POST

  • @PUT

  • @PATCH

  • @DELETE

  • @OPTIONS

  • @HEAD

    Set this annotation above the method then add the endpoint @GET("top-headlines")

URL Manipulation

A request URL can be updated dynamically using replacement blocks and parameters on the method.

  • @Path Parameter

If your URL Endpoint need dynamic data or alphanumeric string. Set replacement block surrounded by { and } then > must be annotated with @Path using the same string. For example the endpoint with dynamic id.

@GET("category/{id}/articles")
suspend fun category(@Path("id") articleId: Int): List<Article>
  • @Query Parameter

If your URL Endpoint have query parameter parameter after "?" like top-headlines?country=us&category=business. Set query for country and category with annotation @Query.

@GET("articles")
suspend fun articles(@Query("country") country: String, @Query("category") category: String ): List<Article>
  • @QueryMap Parameter

For complex query parameter combinations, a Map can be used.

@GET("articles")
suspend fun articles(@QueryMap options: Map<String, String>): List<Article>

Request Body

An object can be specified for use as an HTTP request body with the @Body annotation.

@POST("articles/new")
suspend fun createArticle(@Body article: Articles): Article

Form Encoded

Form-encoded data is sent when @FormUrlEncoded annotated in a method. Use annotation @Field in method parameter that contain key name and object as value.

@FormUrlEncoded
@POST("articles/new")
suspend fun createArticle(@Field("title") String title, @Field("content") String content): Article

Multipart

Multipart is used when you have to send file like image, txt, etc in your API Request. For file you can use MultipartBody.Part and for primitive data like string, integer, etc can use RequestBody.

@Multipart
@POST("articles/new")
suspend fun createArticle(
    @Part image: MultipartBody.Part,
    @Part("title") title: RequestBody,
    @Part("content") content: RequestBody,
): Article

Header Manipulation

If your request need to change the header, use @Header annotation that contain header key and an string as value or use @HeaderMap for complex header combination. In case some API need authorization token to access database.

@GET("my-article")
suspend fun getMyArticle(@Header("Authorization") authorization: String ): List<Article>

@GET("user")
suspend fun getUser(@HeaderMap Map<String, String> headers): List<User>

Step 3

Create Retrofit class that handle Retrofit Configuration. You can setup like API base URL, logging, client, header, etc. We will use news api from https://newsdata.io.

ApiConfig.kt

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ApiConfig {

    private const val BASE_URL = "https://newsdata.io/api/1/"

    private val client = OkHttpClient.Builder().build()

    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

Step 4

Create Model class NewsResponse.kt

data class NewsResponse(
    val nextPage: String?,
    val results: List<Article>?
)

Article.kt

data class Article(
    val article_id: String?,
    val title: String?,
    val description: String?,
    val content: String?,
    val pubDate: String?,
    val source_name: String?,
    val image_url: String?
)

Step 5

Create View Model class NewsViewModel.kt

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class NewsViewModel : ViewModel() {

    private val _articles = MutableStateFlow<List<Article>>(emptyList())
    val articles: StateFlow<List<Article>> = _articles.asStateFlow()

    init {
        fetchNews()
    }

    private fun fetchNews() {
        viewModelScope.launch {
            try {
                val response = ApiConfig.api.getLatest()
                _articles.value = response..results ?: emptyList()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}

Step 6

Create Screen NewsScreen.kt

@Composable
fun NewsScreen(viewModel: NewsViewModel) {

    val articles by viewModel.articles.collectAsState()

    LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        items(
            count = articles.size,
        ) { index ->
            Card(modifier = Modifier.fillMaxWidth().padding(8.dp), elevation = CardDefaults.cardElevation(4.dp)) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(text = articles[index].title.toString(), style = MaterialTheme.typography.headlineSmall)
                    articles[index].description?.let {
                        Text(text = it, style = MaterialTheme.typography.bodyMedium)
                    }
                }
            }
        }
    }
}

Step 7

Setup your MainActivity MainActivity.kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeRetrofitTheme {
                val newsViewModel = NewsViewModel()
                NewsScreen(newsViewModel)
            }
        }
    }
}

Setup internet permission. AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>
Gif

Lets recreate it with MVI Pattern

You need to create 2 more file to create in MVI Pattern, namely action and state.

NewsState.kt

data class NewsState(
    val isLoading: Boolean = false,
    val isError: Boolean = false,
    val news: List<Article> = emptyList()
)

state will save all of your data like a model.

NewsAction.kt

sealed interface NewsAction {
    data object GetNews: NewsAction
}

all action from ui will be set inside this interface action.

NewsViewModel.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class NewsViewModel : ViewModel() {

    var state by mutableStateOf(NewsState())
        private set

    fun onAction(action: NewsAction){
        when(action){
            NewsAction.GetNews -> fetchNews()
        }
    }

    private fun fetchNews() {
        viewModelScope.launch {
            state = state.copy(
                isLoading = true
            )
            try {
                val response = RetrofitInstance.api.getLatest()
                state = state.copy(
                    news = response.results ?: emptyList()
                )
            } catch (e: Exception) {
                e.printStackTrace()
                state = state.copy(
                    isError = true
                )
            }

            state = state.copy(
                isLoading = false
            )
        }
    }
}

Set the NewsState with mutableStateOf() and you can get the state data from screen ui with val state = viewModel.state. Set the NewsAction with method fun onAction(action: NewsAction), handle all action inside this method and you can call the action from screen ui like viewModel.onAction(NewsAction.GetNews).

NewsScreen.kt

@Composable
fun NewsScreen(viewModel: NewsViewModel) {

    val state = viewModel.state

    LaunchedEffect(true) {
        viewModel.onAction(NewsAction.GetNews)
    }

    Scaffold(
        modifier = Modifier
    ) { innerPadding ->
        Box(
            modifier = Modifier.padding(innerPadding)
                .fillMaxSize(),
            contentAlignment = Alignment.Center
        ){
            if (state.isLoading) {
                CircularProgressIndicator()
            }

            if (state.news.isNotEmpty()) {
                LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
                    items(
                        count = state.news.size,
                    ) { index ->
                        Card(modifier = Modifier.fillMaxWidth().padding(8.dp), elevation = CardDefaults.cardElevation(4.dp)) {
                            Column(modifier = Modifier.padding(16.dp)) {
                                Text(text = state.news[index].title.toString(), style = MaterialTheme.typography.headlineSmall)
                                state.news[index].description?.let {
                                    Text(text = it, style = MaterialTheme.typography.bodyMedium)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}