Content Index
- Incorporating Retrofit and dependencies into our project
- Initial Configuration
- 1. Data Classes and Modeling
- 2. Sealed Classes: Enumerated types on steroids
- Advantages of Sealed Classes
- 3. Retrofit Client and Singleton
- Service Interface
- Implementation and Main Screen
- API Call and State Management
- Displaying Results and Loading Images
- Complete Code
- Conclusion
Retrofit is a REST client for developing Android applications; it is relatively easy to use (as expected in Java environments), and it allows adding custom converters to map data obtained from a REST API in XML or the famous JSON format into an object of a custom class through a deserializer.
Previously, we saw how to create a custom notification in Android.
To work with Retrofit in our Android Studio project, we need:
- A REST API created on another platform from which we can get HTTP; we have previously seen how to create a REST API with CodeIgniter.
- A model class that allows mapping/casting the JSON/XML returned by the REST API into our object, which is known as a deserializer.
- An adapter class that allows specifying which objects will be deserialized.
- An interface to define our URLs, annotations (GET, POST...), which allows centralizing the requests made to our REST API.
- Actually, the connection with the REST API.
With these elements mentioned above, we can work perfectly with Retrofit in our Android project.
Incorporating Retrofit and dependencies into our project
To consume data from an API, such as JSONs, and convert them into objects within our Android app, we need to add some dependencies to our project. This is done in the build.gradle file (at the module level).
Nowadays, dependency management in Android is done using implementation instead of the old compile method (considered legacy). Below are the updated dependencies for Retrofit and the Gson converter:
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'You can check the latest version and the official documentation on the official Retrofit website.
Other libraries for processing JSONs
Retrofit is flexible and supports several libraries for processing JSON. Depending on your needs, you can use one of the following:
- Gson:
implementation 'com.squareup.retrofit2:converter-gson:3.0.0' - Moshi:
implementation 'com.squareup.retrofit2:converter-moshi:3.0.0' - Jackson:
implementation 'com.squareup.retrofit2:converter-jackson:3.0.0'
In this experiment, we will use Gson for its popularity and simplicity, but you are free to choose the one that best suits your project.
Initial Configuration
The first thing is to install the necessary dependencies in our build.gradle file:
- Retrofit: The main library for requests.
- Gson Converter: To automatically convert the JSON format to Kotlin objects.
- Coil: To load images from an internet URL (like Pokémon sprites).
Don't forget to enable the internet permission in your AndroidManifest.xml file:
<uses-permission android:name="android.permission.INTERNET" />Backend and APIs
In a real application for a client, you will usually have to implement or consume a Backend layer. Whether using technologies like Laravel (PHP) or Python (Django, FastAPI, Flask), the goal is to create a REST API that your Android application can query.For this example, we will use a well-known public and free API: the PokeAPI. When we send it the name of a Pokémon, it returns a JSON format with its characteristics, which we must translate into data models in our code.
1. Data Classes and Modeling
Our goal will be to create a model that represents that response and display some data on the screen. This will be a minimal example, as in a real application there are many more layers and files involved for everything to work properly.
The first thing we have is a data class. Here we define an identifier, a name, and the sprite (the Pokémon's image). Remember that the purpose of data classes is to model data structures, especially when working with responses from the Internet.
In this case, the Pokémon is a real-world object that has an ID, a name, and an image. Since we don't need inheritance, polymorphism, or complex logic, data classes are perfect for this scenario.
The sprite can be optional, which is why we use the question mark (?). Additionally, we define another class for the sprite, where we are only interested in the front image to keep the example simple.
data class PokemonResponse(
val id: Int,
val name: String,
val sprites: PokemonSprites?
)
data class PokemonSprites(
val front_default: String?
)2. Sealed Classes: Enumerated types on steroids
To manage the user interface, we use a Sealed Class (PokemonUiState). Unlike traditional ENUMs, these allow us to have a closed but flexible structure to handle the connection states:
- Idle: Initial state.
- Loading: When the request is in progress.
- Success: When the response is successful.
- Error: When the connection fails or the resource is not found.
Sealed classes are similar to enums, but more powerful. They are closed classes, which gives us more control and security in the code. Unlike comparing strings or using traditional enums, here we avoid errors such as misspelled values or inconsistencies.
This type of class is usually used together with a when, and later we will see how Kotlin forces us to handle all possible cases, which is a great advantage.
Advantages of Sealed Classes
An important detail is that sealed classes are very well integrated with Kotlin. For example, if we use a when and do not cover all the defined states, the compiler will mark an error. This protects us from runtime errors and forces us to handle all possible cases.
/ --- UI STATES (SEALED CLASS) ---
sealed class PokemonUiState {
object Idle : PokemonUiState()
object Loading : PokemonUiState()
data class Success(val pokemon: PokemonResponse) : PokemonUiState()
data class Error(val message: String) : PokemonUiState()
}3. Retrofit Client and Singleton
Next, we have the Retrofit client, which is implemented as a Singleton. We only need one instance of an internet connection, so it makes no sense to create several.
Here we configure the baseUrl, which is the root URL of the Pokémon API. We also declare the service instance, which contains the available endpoints. In this case, we only have one, to get the detail of a Pokémon, but in a real CRUD, you would have many more.
Retrofit automatically concatenates the baseUrl with the path defined in the service. In addition, we configure the JSON converter, which is responsible for transforming JSON responses into Kotlin objects. That's why we previously installed the Gson package.
object RetrofitClient {
private const val BASE_URL = "https://pokeapi.co/api/v2/"
val instance: PokeApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(PokeApiService::class.java)
}
}Service Interface
The service interface has no direct implementation. It simply defines the available routes and requests. Retrofit takes care of generating the implementation internally.
interface PokeApiService {
@GET("pokemon/{name}")
suspend fun getPokemonInfo(@Path("name") name: String): PokemonResponse
}Implementation and Main Screen
We arrive at the MainActivity, where we create the PokemonScreen component. Here we use advanced Jetpack Compose concepts:
- Coroutines: Internet connections are asynchronous by nature. We use rememberCoroutineScope to launch the search without blocking the interface.
- Reactive State: We use mutableStateOf so that the screen reloads automatically when we receive the Pokémon's information or if an error occurs.
API Call and State Management
When the user presses the button, we call the Retrofit service and update the state:
- We go to Loading before making the call.
- If the response is successful, we go to Success.
- If an error occurs, we go to Error.
These states are directly reflected in the graphical interface, showing a loading indicator, an error message, or the Pokémon's information.
@Composable
fun PokemonScreen() {
val scope = rememberCoroutineScope()
var pokemonName by remember { mutableStateOf("") }
var uiState by remember { mutableStateOf<PokemonUiState>(PokemonUiState.Idle) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = pokemonName,
onValueChange = { pokemonName = it },
label = { Text("Pokémon Name") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
uiState = PokemonUiState.Loading
scope.launch {
try {
val response = RetrofitClient.instance.getPokemonInfo(pokemonName.lowercase().trim())
uiState = PokemonUiState.Success(response)
} catch (e: Exception) {
Log.e("POKEMON_APP", e.toString())
uiState = PokemonUiState.Error("Pokémon not found")
}
}
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Search")
}
Spacer(modifier = Modifier.height(20.dp))
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Enter a name to start")
}
}
}Finally, we use a when structure to evaluate the state of our sealed class. If the state is successful, we call the PokemonDetailCard component, which uses Coil (AsyncImage) to paint the Pokémon's image directly from the URL.
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Enter a name to start")
}Displaying Results and Loading Images
When the state is successful, we display a custom composable with the Pokémon's information. We use a Card, a Column, informational texts, and an image loaded from the Internet.
To load images, we use another additional library that we had already installed. This allows us to display the image directly from the sprite's URL.
@Composable
fun PokemonDetailCard(pokemon: PokemonResponse) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = pokemon.sprites?.front_default,
contentDescription = "Image of ${pokemon.name}",
modifier = Modifier.size(150.dp)
)
Text(text = "ID: #${pokemon.id}", style = MaterialTheme.typography.labelMedium)
Text(text = pokemon.name.uppercase(), style = MaterialTheme.typography.headlineMedium)
}
}
}
Complete Code
package com.example.myproyectandroid
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
// --- DATA MODELS ---
data class PokemonResponse(
val id: Int,
val name: String,
val sprites: PokemonSprites?
)
data class PokemonSprites(
val front_default: String?
)
// --- UI STATES (SEALED CLASS) ---
sealed class PokemonUiState {
object Idle : PokemonUiState()
object Loading : PokemonUiState()
data class Success(val pokemon: PokemonResponse) : PokemonUiState()
data class Error(val message: String) : PokemonUiState()
}
// --- RETROFIT CLIENT (SINGLETON) ---
object RetrofitClient {
private const val BASE_URL = "https://pokeapi.co/api/v2/"
val instance: PokeApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(PokeApiService::class.java)
}
}
interface PokeApiService {
@GET("pokemon/{name}")
suspend fun getPokemonInfo(@Path("name") name: String): PokemonResponse
}
// --- MAIN ACTIVITY ---
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyProyectAndroidTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
PokemonScreen()
}
}
}
}
}
}
@Composable
fun PokemonScreen() {
val scope = rememberCoroutineScope()
var pokemonName by remember { mutableStateOf("") }
var uiState by remember { mutableStateOf<PokemonUiState>(PokemonUiState.Idle) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = pokemonName,
onValueChange = { pokemonName = it },
label = { Text("Pokémon Name") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
uiState = PokemonUiState.Loading
scope.launch {
try {
val response = RetrofitClient.instance.getPokemonInfo(pokemonName.lowercase().trim())
uiState = PokemonUiState.Success(response)
} catch (e: Exception) {
Log.e("POKEMON_APP", e.toString())
uiState = PokemonUiState.Error("Pokémon not found")
}
}
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Search")
}
Spacer(modifier = Modifier.height(20.dp))
when (val state = uiState) {
is PokemonUiState.Loading -> CircularProgressIndicator()
is PokemonUiState.Success -> PokemonDetailCard(state.pokemon)
is PokemonUiState.Error -> Text(state.message, color = Color.Red)
is PokemonUiState.Idle -> Text("Enter a name to start")
}
}
}
@Composable
fun PokemonDetailCard(pokemon: PokemonResponse) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = pokemon.sprites?.front_default,
contentDescription = "Image of ${pokemon.name}",
modifier = Modifier.size(150.dp)
)
Text(text = "ID: #${pokemon.id}", style = MaterialTheme.typography.labelMedium)
Text(text = pokemon.name.uppercase(), style = MaterialTheme.typography.headlineMedium)
}
}
}You can consult the official documentation and more examples at the following link: Retrofit.
Conclusion
In this simple way, you learned to make your first Internet connection in Android using Retrofit, as well as applying important concepts such as data classes, sealed classes, coroutines, and reactive state with Jetpack Compose. All of this forms a solid foundation for creating modern and scalable applications.
The next step is to learn how to use the Canvas API in Android Studio.