Skip to main content

Overview

NASA Explorer uses Retrofit to communicate with the NASA APOD (Astronomy Picture of the Day) API. Retrofit is a type-safe HTTP client that makes it easy to consume RESTful web services by converting API interfaces into callable Kotlin functions.
Retrofit turns your HTTP API into a Java/Kotlin interface, providing compile-time validation of API requests and automatic serialization/deserialization.

Why Retrofit?

Type Safety

Compile-time verification of API calls and responses

Easy Integration

Works seamlessly with Kotlin coroutines and suspend functions

Automatic Parsing

Converts JSON responses to Kotlin data classes automatically

Flexible

Supports multiple converters and interceptors for customization

Project Setup

Retrofit and its dependencies are configured in the build file:
build.gradle.kts
dependencies {
    // Retrofit for network calls
    implementation(libs.retrofit2.retrofit)
    implementation(libs.converter.gson)  // JSON converter using Gson
    
    // Coil for image loading (works with Retrofit URLs)
    implementation(libs.coil.compose)
}

NASA API Configuration

The API key is securely loaded from local.properties:
build.gradle.kts
// Read local.properties
val localProperties = Properties().apply {
    load(project.rootProject.file("local.properties").inputStream())
}

// Get API key from local.properties
val nasaApiKey: String = localProperties.getProperty("nasaApiKey") ?: ""

android {
    defaultConfig {
        // Define buildConfigField with the API key
        buildConfigField("String", "NASA_API_KEY", "\"$nasaApiKey\"")
    }
    
    // Enable buildConfig
    buildFeatures {
        buildConfig = true
    }
}
Never commit your API key to version control. Add local.properties to .gitignore.

Hilt Module Configuration

Retrofit is provided as a singleton through Hilt dependency injection:
ApiNetworkModule.kt
package com.ccandeladev.nasaexplorer.data.di

import com.ccandeladev.nasaexplorer.data.api.NasaApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ApiNetworkModule {

    private const val BASE_URL = "https://api.nasa.gov/"

    @Singleton
    @Provides
    fun provideNasaApiService(retrofit: Retrofit): NasaApiService {
        return retrofit.create(NasaApiService::class.java)
    }

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)              // Associate HTTP client
            .baseUrl(BASE_URL)                 // Base URL for API
            .addConverterFactory(GsonConverterFactory.create())  // JSON converter
            .build()
    }

    @Singleton
    @Provides
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}
Retrofit instances are expensive to create. By using @Singleton, the app creates only one instance that’s shared throughout the application.

API Service Interface

Define API endpoints as Kotlin interface methods with Retrofit annotations:
NasaApiService.kt
package com.ccandeladev.nasaexplorer.data.api

import retrofit2.http.GET
import retrofit2.http.Query

interface NasaApiService {

    // Get image of the day (optionally for a specific date)
    @GET("planetary/apod")
    suspend fun getImageOfTheDay(
        @Query("api_key") apiKey: String,
        @Query("date") date: String? = null
    ): NasaResponse

    // Get images in a date range
    @GET("planetary/apod")
    suspend fun getImagesInRange(
        @Query("api_key") apiKey: String,
        @Query("start_date") startDate: String,
        @Query("end_date") endDate: String? = null
    ): List<NasaResponse>

    // Get random images
    @GET("planetary/apod")
    suspend fun getRandomImages(
        @Query("api_key") apiKey: String,
        @Query("count") count: Int,
    ): List<NasaResponse>
}
The suspend keyword makes these functions work with Kotlin coroutines, allowing async network calls without blocking threads.

Response Data Class

API responses are automatically parsed into Kotlin data classes:
NasaResponse.kt
package com.ccandeladev.nasaexplorer.data.api

import com.ccandeladev.nasaexplorer.domain.NasaModel

data class NasaResponse(
    val copyright: String?,
    val date: String,
    val explanation: String,
    val hdurl: String?,
    val media_type: String,
    val service_version: String,
    val title: String,
    val url: String
) {
    // Convert API response to domain model
    fun toNasaModel(): NasaModel {
        return NasaModel(
            title = title,
            url = url,
            explanation = explanation
        )
    }
}

Example API Response

{
  "copyright": "John Doe",
  "date": "2024-03-06",
  "explanation": "This stunning image shows...",
  "hdurl": "https://apod.nasa.gov/apod/image/2403/example_hd.jpg",
  "media_type": "image",
  "service_version": "v1",
  "title": "A Beautiful Galaxy",
  "url": "https://apod.nasa.gov/apod/image/2403/example.jpg"
}

Repository Pattern

The repository encapsulates API calls and handles data transformation:
NasaRepository.kt
package com.ccandeladev.nasaexplorer.data.api

import com.ccandeladev.nasaexplorer.BuildConfig
import com.ccandeladev.nasaexplorer.domain.NasaModel
import javax.inject.Inject

class NasaRepository @Inject constructor(
    private val nasaApiService: NasaApiService
) {
    companion object {
        private const val API_KEY = BuildConfig.NASA_API_KEY
    }

    // Get image of the day, optionally for a specific date
    suspend fun getImageOfTheDay(date: String? = null): NasaModel {
        val response = nasaApiService.getImageOfTheDay(apiKey = API_KEY, date = date)
        return response.toNasaModel()  // Convert to domain model
    }

    // Get images in a date range
    suspend fun getImagesInRange(startDate: String, endDate: String? = null): List<NasaModel> {
        val response = nasaApiService.getImagesInRange(
            apiKey = API_KEY,
            startDate = startDate,
            endDate = endDate
        )
        return response.map { it.toNasaModel() }
    }

    // Get a specific number of random images
    suspend fun getRandomImages(count: Int): List<NasaModel> {
        val response = nasaApiService.getRandomImages(apiKey = API_KEY, count = count)
        return response.map { it.toNasaModel() }
    }
}

Usage in ViewModel

ViewModels call repository functions within coroutines:
DailyImageViewModel.kt
@HiltViewModel
class DailyImageViewModel @Inject constructor(
    private val nasaRepository: NasaRepository,
    private val firebaseAuth: FirebaseAuth,
    private val firebaseDatabase: FirebaseDatabase
) : ViewModel() {

    private val _dailyImage = MutableStateFlow<NasaModel?>(null)
    val dailyImage: StateFlow<NasaModel?> = _dailyImage

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    fun loadDailyImage(date: String? = null) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                // Call repository which uses Retrofit
                val result = nasaRepository.getImageOfTheDay(date = date)
                _dailyImage.value = result
                _errorMessage.value = null
            } catch (e: Exception) {
                _errorMessage.value = "Sin conexión a internet. Conéctate a una red Wi-Fi"
                _dailyImage.value = null
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Request Flow

Here’s how a typical API request flows through the app:

Error Handling

Retrofit calls are wrapped in try-catch blocks to handle network errors:
fun loadDailyImage(date: String? = null) {
    viewModelScope.launch {
        _isLoading.value = true
        try {
            val result = nasaRepository.getImageOfTheDay(date = date)
            _dailyImage.value = result
            _errorMessage.value = null
        } catch (e: Exception) {
            // Handle network errors, timeouts, parsing errors, etc.
            _errorMessage.value = "Sin conexión a internet. Conéctate a una red Wi-Fi"
            _dailyImage.value = null
        } finally {
            _isLoading.value = false
        }
    }
}

Query Parameters

Retrofit makes it easy to build URLs with query parameters:
// Calling this:
getImageOfTheDay(apiKey = "YOUR_KEY", date = "2024-03-06")

// Generates this URL:
https://api.nasa.gov/planetary/apod?api_key=YOUR_KEY&date=2024-03-06

// With optional parameters:
getImagesInRange(
    apiKey = "YOUR_KEY",
    startDate = "2024-03-01",
    endDate = "2024-03-05"
)

// Generates:
https://api.nasa.gov/planetary/apod?api_key=YOUR_KEY&start_date=2024-03-01&end_date=2024-03-05

Benefits in NASA Explorer

Retrofit validates API calls at compile time, catching errors before runtime. The Kotlin type system ensures correct parameters and return types.
Suspend functions integrate seamlessly with Kotlin coroutines, enabling clean async code without callbacks.
Gson converter automatically transforms JSON responses into Kotlin data classes, eliminating manual parsing.
Base URL, converters, and interceptors are configured once in the Hilt module, making it easy to modify.

Best Practices

1

Use Suspend Functions

Define API methods as suspend functions for seamless coroutine integration.
2

Repository Pattern

Wrap Retrofit calls in a repository layer to separate API logic from ViewModels.
3

Secure API Keys

Store API keys in local.properties and use BuildConfig to access them securely.
4

Handle Errors Gracefully

Wrap API calls in try-catch blocks and provide meaningful error messages to users.
5

Use Singleton Scope

Configure Retrofit as a singleton to avoid creating multiple instances.

Advanced Features

Interceptors

Add logging, authentication headers, or modify requests/responses

Custom Converters

Support different serialization formats (Moshi, kotlinx.serialization)

Call Adapters

Integrate with RxJava, Flow, or other reactive frameworks

Request/Response Types

Handle different body types (JSON, XML, Form data)

Troubleshooting

Common Issues:
  • Missing Gson converter causes serialization errors
  • Incorrect BASE_URL (must end with /)
  • API key not configured in local.properties
  • Network calls on main thread (use suspend functions)
  • Missing internet permission in AndroidManifest.xml

Resources

Retrofit Documentation

Official Retrofit documentation

NASA APOD API

NASA API documentation and registration

OkHttp

Underlying HTTP client used by Retrofit

Gson

JSON serialization library used by Retrofit

Build docs developers (and LLMs) love