Build a Simple Dictionary App Using Jetpack Compose and Retrofit
Creating a dictionary app is an excellent project for learning Android development using Jetpack Compose. This tutorial will guide you through building a fully functional dictionary app that allows users to search for word definitions. We'll use Retrofit for network operations and Jetpack Compose for UI design.
What You'll Learn
- Setting up a Jetpack Compose project.
- Integrating Retrofit for API calls.
- Designing a responsive user interface with Jetpack Compose.
- Handling API responses and displaying word details.
- Managing state using ViewModel.
What is Retrofit?
Retrofit is a type-safe HTTP client for Android and Java developed by Square. It simplifies the process of making network requests and handling API responses. It is commonly used to interact with RESTful APIs.
Why Use Retrofit?
Ease of Use
- It abstracts the complexity of HTTP calls.
- You define APIs as simple interfaces using annotations (@GET, @POST, etc.).
Data Parsing
- Automatically parses JSON or XML responses into Kotlin/Java objects using converters like Gson or Moshi.
Error Handling
- Handles HTTP errors (e.g., 404 or 500) effectively.
Scalability
- Suitable for large projects with complex APIs.
Kotlin Support
- Works seamlessly with Kotlin and coroutines for asynchronous calls.
Why Choose Retrofit for a Dictionary App?
A dictionary app communicates with a REST API to fetch word details. Retrofit simplifies the process by:
- Converting JSON responses directly into objects (e.g., ApiResponse).
- Allowing you to make asynchronous calls using coroutines, keeping the UI thread responsive.
- Handling common issues like timeouts, errors, and retries effectively.
Can I Build Without Retrofit?
Yes, You can achieve the same functionality using alternatives such as:
Ktor
- A modern framework for Kotlin applications, also supporting HTTP requests.
HttpURLConnection
- The native way to make HTTP requests in Android. However, it's verbose and harder to use.
OkHttp
- A lower-level HTTP client (Retrofit is built on top of OkHttp).
Volley
- An older library for networking in Android. It's more suited for apps that require frequent updates like chat apps.
Step 1: Setting Up the Project
1. Create a New Project:
- Open Android Studio and create a new project.
- Choose the "Empty Compose Activity" template.
- Name your project (e.g., DictionaryApp).
2. Add Dependencies:
Open the build.gradle file and add the following dependencies:
Sync your project after adding these dependencies.implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
Step 2: Set Up Retrofit
Retrofit simplifies network operations by allowing you to define APIs as interfaces. For this project, we'll use the free Dictionary API.
1. Create Data Models:
Create a new Kotlin file named ApiResponse.kt and define the models:
package com.codingbihar.dictionaryappdemo
data class ApiResponse(
val word: String,
val meanings: List<Meaning>
)
data class Meaning(
val partOfSpeech: String,
val definitions: List<Definition>,
val synonyms: List<String> = emptyList(),
val antonyms: List<String> = emptyList()
)
data class Definition(
val definition: String
)
data class WordDetail(
val word: String,
val partOfSpeech: String,
val synonyms: List<String> = emptyList(),
val antonyms: List<String> = emptyList(),
val meanings: List<String>
)
2. Define API Interface:
Create a new Kotlin file named DictionaryApi.kt:
package com.codingbihar.dictionaryappdemo
import retrofit2.http.GET
import retrofit2.http.Path
interface DictionaryApi {
@GET("entries/en/{word}")
suspend fun getDefinition(@Path("word") word: String): List<ApiResponse>
}
3. Set Up Retrofit Instance:
Create a new Kotlin file named RetrofitInstance.kt:
package com.codingbihar.dictionaryappdemo
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitInstance {
val api: DictionaryApi by lazy {
Retrofit.Builder()
.baseUrl("https://api.dictionaryapi.dev/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DictionaryApi::class.java)
}
}
Step 3: Design the UI with Jetpack Compose
Compose provides an easy way to design modern UIs.
Main UI:
Create a new file named DictionaryApp.kt and define the UI:
package com.codingbihar.dictionaryappdemo
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun DictionaryApp(viewModel: DictionaryViewModel = viewModel()) {
val searchQuery = remember { mutableStateOf("") }
val wordDetails by viewModel.wordDetails.observeAsState(emptyList())
val error by viewModel.error.observeAsState("")
val loading by viewModel.loading.observeAsState(false)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
SearchBar(
query = searchQuery.value,
onQueryChange = { query ->
searchQuery.value = query
viewModel.searchWord(query)
}
)
if (loading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
} else {
if (error.isNotEmpty()) {
Text(
text = error,
color = Color.Red,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
} else {
WordDetailList(wordDetails = wordDetails)
}
}
}
}
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier.fillMaxWidth(),
label = { Text("Search for a word") }
)
}
@Composable
fun WordDetailList(wordDetails: List<WordDetail>) {
LazyColumn {
items(wordDetails) { detail ->
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = "${detail.word} (${detail.partOfSpeech})",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(bottom = 4.dp)
)
if (detail.synonyms.isNotEmpty()) {
Text(text = "Synonyms: ${detail.synonyms.joinToString(", ")}")
}
if (detail.antonyms.isNotEmpty()) {
Text(text = "Antonyms: ${detail.antonyms.joinToString(", ")}")
}
detail.meanings.forEach { meaning ->
Text(text = "• $meaning")
}
}
}
}
}
Step 4: Implement ViewModel
The ViewModel will handle API calls and manage UI state.
Create ViewModel:
Create a new Kotlin file named DictionaryViewModel.kt:
Here, the searchWord function fetches word details from the API and updates the state accordingly.package com.codingbihar.dictionaryappdemo
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class DictionaryViewModel : ViewModel() {
private val _wordDetails = MutableLiveData<List<WordDetail>>()
val wordDetails: LiveData<List<WordDetail>> = _wordDetails
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun searchWord(query: String) {
if (query.isBlank()) {
_wordDetails.value = emptyList()
_error.value = ""
_loading.value = false
return
}
viewModelScope.launch {
_loading.value = true
try {
val response = RetrofitInstance.api.getDefinition(query)
val extractedDetails = response.flatMap { apiResponse ->
apiResponse.meanings.map { meaning ->
WordDetail(
word = apiResponse.word,
partOfSpeech = meaning.partOfSpeech,
synonyms = meaning.synonyms,
antonyms = meaning.antonyms,
meanings = meaning.definitions.map { it.definition }
)
}
}
_wordDetails.value = extractedDetails
_error.value = ""
} catch (e: HttpException) {
_error.value = "HTTP error occurred."
} catch (e: IOException) {
_error.value = "Network error. Please check your connection."
} catch (e: Exception) {
_error.value = "An unexpected error occurred."
} finally {
_loading.value = false
}
}
}
}
Step 5: Run the App
- Build and run the app on an emulator or physical device.
- Enter a word in the search bar to fetch its details.
- The app displays the word's part of speech, synonyms, antonyms, and definitions.