DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Adding API Integration to AI-Generated Android Apps with Retrofit

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
)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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}")
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

Best Practices Summary

  1. Always use suspend functions for Coroutine compatibility
  2. Centralize configuration in a RetrofitClient singleton
  3. Use sealed classes for type-safe error handling
  4. Implement Repository pattern to decouple data from UI
  5. Add logging interceptors during development
  6. Handle timeouts with okhttp3 client configuration
  7. Test with mocks before deploying
  8. 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)