Adding API Integration to AI-Generated Android Apps with Retrofit
When AI-generated Android apps are deployed, they're often basic templates with hardcoded data. To make them production-ready, you need to integrate real APIs. Retrofit is the industry standard for network operations in Kotlin, and combined with Coroutines and proper architecture patterns, it transforms template apps into robust services.
Why Retrofit for AI-Generated Apps?
AI code generators typically produce UI scaffolds with mock data. The missing piece is always the backend integration. Retrofit excels here because:
- Type-safe requests: Compile-time checking of API contracts
- Coroutine-native: Suspending functions for async/await style code
- Interceptors: Handle authentication, logging, and request/response modification
- Sealed classes: Elegant error handling that the AI can generate cleanly
- ViewModel integration: Ensures API calls survive configuration changes
Your AI-generated app will be tested against real data within minutes of adding Retrofit.
Step 1: Define Your API Interface
Retrofit converts your HTTP API into a Kotlin interface. Here's a practical example for a weather app:
import retrofit2.http.*
import kotlinx.coroutines.flow.Flow
data class WeatherResponse(
val temp: Double,
val description: String,
val humidity: Int
)
data class ForecastResponse(
val city: String,
val forecasts: List<WeatherResponse>
)
interface WeatherApi {
@GET("weather")
suspend fun getWeather(
@Query("city") city: String,
@Query("apiKey") apiKey: String
): WeatherResponse
@GET("forecast")
suspend fun getForecast(
@Query("city") city: String,
@Query("days") days: Int = 7
): ForecastResponse
@POST("subscribe")
suspend fun subscribeToAlerts(
@Body request: SubscriptionRequest
): SubscriptionResponse
@DELETE("subscription/{id}")
suspend fun cancelSubscription(
@Path("id") subscriptionId: String
): Unit
}
data class SubscriptionRequest(
val email: String,
val city: String
)
data class SubscriptionResponse(
val subscriptionId: String,
val message: String
)
The suspend keyword signals that these are Coroutine-compatible. Retrofit automatically handles the threading.
Step 2: Configure Retrofit with Interceptors
Before making any request, you need to configure the HTTP client with authentication and logging:
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.Interceptor
object RetrofitClient {
private const val BASE_URL = "https://api.weather.example.com/v1/"
private const val API_KEY = "your_api_key_here"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val authInterceptor = Interceptor { chain ->
val originalRequest = chain.request()
val requestWithAuth = originalRequest.newBuilder()
.header("Authorization", "Bearer $API_KEY")
.header("User-Agent", "MyWeatherApp/1.0")
.build()
chain.proceed(requestWithAuth)
}
private val httpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val weatherApi: WeatherApi = retrofit.create(WeatherApi::class.java)
}
The interceptors automatically add headers to every request. The logging interceptor is invaluable for debugging—remove it in production or use BuildConfig.DEBUG.
Step 3: Error Handling with Sealed Classes
Network requests fail. A robust app handles them gracefully:
sealed class ApiResult<T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error<T>(val exception: Exception, val message: String) : ApiResult<T>()
class Loading<T> : ApiResult<T>()
}
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): ApiResult<T> = try {
ApiResult.Success(apiCall())
} catch (e: HttpException) {
val errorBody = e.response()?.errorBody()?.string() ?: "Unknown error"
ApiResult.Error(e, "HTTP ${e.code()}: $errorBody")
} catch (e: IOException) {
ApiResult.Error(e, "Network error: ${e.message}")
} catch (e: Exception) {
ApiResult.Error(e, "Unexpected error: ${e.message}")
}
Call it like this:
val result = safeApiCall { RetrofitClient.weatherApi.getWeather("London", API_KEY) }
when (result) {
is ApiResult.Success -> updateUI(result.data)
is ApiResult.Error -> showError(result.message)
is ApiResult.Loading -> showLoadingSpinner()
}
Step 4: Repository Pattern with ViewModel
Separate data fetching from UI logic:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class WeatherRepository(private val api: WeatherApi = RetrofitClient.weatherApi) {
suspend fun fetchWeather(city: String): ApiResult<WeatherResponse> {
return safeApiCall { api.getWeather(city, API_KEY) }
}
suspend fun fetchForecast(city: String, days: Int = 7): ApiResult<ForecastResponse> {
return safeApiCall { api.getForecast(city, days) }
}
}
class WeatherViewModel(
private val repository: WeatherRepository = WeatherRepository()
) : ViewModel() {
private val _weatherState = MutableStateFlow<ApiResult<WeatherResponse>>(ApiResult.Loading())
val weatherState: StateFlow<ApiResult<WeatherResponse>> = _weatherState
private val _forecastState = MutableStateFlow<ApiResult<ForecastResponse>>(ApiResult.Loading())
val forecastState: StateFlow<ApiResult<ForecastResponse>> = _forecastState
fun getWeather(city: String) {
viewModelScope.launch {
_weatherState.value = ApiResult.Loading()
_weatherState.value = repository.fetchWeather(city)
}
}
fun getForecast(city: String) {
viewModelScope.launch {
_forecastState.value = ApiResult.Loading()
_forecastState.value = repository.fetchForecast(city)
}
}
}
In your Composable or Fragment:
@Composable
fun WeatherScreen(viewModel: WeatherViewModel = viewModel()) {
val weatherState by viewModel.weatherState.collectAsState()
LaunchedEffect(Unit) {
viewModel.getWeather("Tokyo")
}
when (val state = weatherState) {
is ApiResult.Loading -> CircularProgressIndicator()
is ApiResult.Success -> {
Text("${state.data.temp}°C - ${state.data.description}")
}
is ApiResult.Error -> {
Text("Error: ${state.message}", color = Color.Red)
}
}
}
Step 5: Pagination and Caching
For large datasets, implement pagination:
interface PaginatedApi {
@GET("items")
suspend fun getItems(
@Query("page") page: Int,
@Query("limit") limit: Int = 20
): ItemsResponse
}
data class ItemsResponse(
val items: List<Item>,
val hasMore: Boolean,
val page: Int
)
class PaginatedRepository(private val api: PaginatedApi) {
private var currentPage = 0
private val allItems = mutableListOf<Item>()
suspend fun loadNextPage(): ApiResult<List<Item>> = safeApiCall {
val response = api.getItems(page = currentPage, limit = 20)
allItems.addAll(response.items)
if (response.hasMore) currentPage++
allItems
}
}
Step 6: Testing with Mock Responses
Mock your API for testing:
class MockWeatherApi : WeatherApi {
override suspend fun getWeather(city: String, apiKey: String) =
WeatherResponse(22.5, "Partly cloudy", 65)
override suspend fun getForecast(city: String, days: Int) =
ForecastResponse("Tokyo", emptyList())
override suspend fun subscribeToAlerts(request: SubscriptionRequest) =
SubscriptionResponse("sub_123", "Subscribed successfully")
override suspend fun cancelSubscription(subscriptionId: String) {}
}
Best Practices Summary
- Always use suspend functions for Coroutine compatibility
- Centralize configuration in a RetrofitClient singleton
- Use sealed classes for type-safe error handling
- Implement Repository pattern to decouple data from UI
- Add logging interceptors during development
- Handle timeouts with okhttp3 client configuration
- Test with mocks before deploying
- Use StateFlow to manage UI state reactively
Conclusion
Adding Retrofit to your AI-generated Android app transforms it from a prototype into production-ready software. The pattern—API interface → Retrofit client → Repository → ViewModel → UI—is the Android standard. Master this flow and your apps will scale from MVP to millions of users.
Get my 8 Android app templates and extend them with APIs. https://myougatheaxo.gumroad.com
Top comments (0)