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 "?"
liketop-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 useRequestBody
.@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>

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)
}
}
}
}
}
}
}
}
}