Retrofit for API Calls with Hilt Dependency Injection
In the preceding article, we successfully incorporated Room
accessing and storing local data using a Local repository. Now, we’ll focus on making API calls using Retrofit
to establish a Remote Repository. Using a combination of Local and Remote Data Sources in your application is possible based on your needs.
In many applications, accessing data from a server via API calls is a necessity. There exist various approaches for this purpose, but we’ll adopt Retrofit
and Hilt
for this tutorial. Retrofit is a widely embraced HTTP
client library on Android, simplifying interactions with RESTful APIs. Conversely, Hilt
serves as a dependency injection library for Android, aiding in the management and injection of dependencies within your application.
Our objective will be to utilize the TMDB API
as the Remote data source. Obtaining an Authentication API key is straightforward; you can easily acquire it from https://developer.themoviedb.org/reference/intro/authentication#api-key-quick-start. Remember to include this key in your project as ACCESS_TOKEN
within the Constant
class.
Let’s begin the game, including the basic libraries in the project.
//Hilt dependencies. Use latest versions for better experience
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-compiler:2.48.1")
//Retrofit Dependecies.
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
Create an Application class and annotate your Application class @HiltAndroidApp
to enable Hilt in your application. Then create an interface that defines your API endpoints. Here, we are only defining two of them. In functionfetchMovies()
API now_playing
has been called and for functiongetDetail
API movie_id
has been called.
interface MyApiService {
@GET("now_playing")
suspend fun fetchMovies(): Response<MovieResponse>
@GET("{movie_id}")
suspend fun getDetail(
@Path("movie_id") movieId: String,
@Query("language") language: String = "en-US"
): Response<MovieDetails>
}
Write your AppModule
class that will be used by the Hilt
that provides various dependencies required for network-related operations, like Retrofit
, OkHttp
, Gson
, and an authentication interceptor
.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideBaseUrl(): String {
return Constant.BASE_URL
}
@Provides
fun provideGson(): Gson {
return GsonBuilder()
.create()
}
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(
provideAuthInterceptor()
)
.build()
}
@Provides
fun provideAuthInterceptor(): AuthInterceptor {
return AuthInterceptor(Constant.ACCESS_TOKEN)
}
@Provides
fun provideRetrofit(
baseUrl: String,
gson: Gson,
okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): MyApiService =
retrofit.create(MyApiService::class.java)
}
In the above code
provideBaseUrl()
Provides the base URL,provideGson()
Provides a Gson instance for JSON serialization/deserialization ,provideOkHttpClient()
Provides an OkHttpClient instance and adds anAuthInterceptor
to it for token authentication.provideAuthInterceptor()
Provides an instance ofAuthInterceptor
with the access token provided viaConstant.ACCESS_TOKEN
.provideRetrofit()
Constructs and provides the Retrofit instance using the base URL, Gson instance, and OkHttpClient.provideApiService()
Provides an instance of theMyApiService
interface using Retrofit. With authInterceptor code below:
class AuthInterceptor(private val authToken: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.addHeader("accept", "application/json")
.addHeader("Authorization", "Bearer $authToken")
val request = requestBuilder.build()
return chain.proceed(request)
}
}
Now define MovieRepository
class which seems to encapsulate the interaction with apiService
(an instance of MyApiService
) to fetch movies and retrieve details of a particular movie based on its ID. The structure is typical of a repository pattern where data retrieval from an API is abstracted from the rest of the application. (Single Source Of Truth)
class MovieRepository @Inject constructor(
private val apiService: MyApiService
) {
suspend fun fetchMovies() = apiService.fetchMovies()
suspend fun getDetail(movieId: String) = apiService.getDetail(movieId)
}
Finally, theMovieViewModel
class uses the MovieRepository
to interact with the data layer and manage the UI-related data using LiveData, where Hilt provides the movieRepository
instance through@Inject
. The ViewModel fetches a list of movies and retrieves details of a specific movie based on its ID, returning the List<Movie>
and MovieDetails
as the Response Objects.
MovieViewModel @Inject constructor(
private val movieRepository: MovieRepository
) : ViewModel() {
private val _moviesLiveData = MutableLiveData<List<Movie>>()
val moviesLiveData: LiveData<List<Movie>> get() = _moviesLiveData
private val _movieDetailLiveData = MutableLiveData<MovieDetails>()
val detailLiveData: LiveData<MovieDetails> get() = _movieDetailLiveData
init {
fetchMovies()
}
private fun fetchMovies() {
viewModelScope.launch {
try {
val newItem = withContext(Dispatchers.IO) {
movieRepository.fetchMovies()
}
_moviesLiveData.postValue(newItem.body()?.results)
Log.e("element", newItem.toString())
} catch (e: Exception) {
// Handle exceptions, if any
Log.e("getDetail", "Error fetching details: ${e.message}")
}
}
}
fun getDetail(movieId: String) {
viewModelScope.launch {
try {
val response = movieRepository.getDetail(movieId)
_movieDetailLiveData.value = response.body()
} catch (e: Exception) {
}
}
}
As we are using TMDB
API so baseurl
and thumbnailUrl
will look like this including ACCESS_TOKEN
in the Constant
file of the application. For further details on the API, you can check https://developer.themoviedb.org/docs.
object Constant {
const val BASE_URL = "https://api.themoviedb.org/3/movie/"
const val THUMBNAIL_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w154/"
const val POSTER_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/original"
const val ACCESS_TOKEN = "Your Access Token for the TMDB API"
}
MovieResponse
class provides data classes representing the structure of movie-related data received from an API response. You can use the attributes as required and write your MovieDetail
class for Movie Detail Response.
data class MovieResponse(
val dates: DateRange,
val page: Int,
val results: List<Movie>,
val total_pages: Int,
val total_results: Int
)
data class DateRange(
val maximum: String,
val minimum: String
)
data class Movie(
val adult: Boolean,
val backdrop_path: String?,
val genre_ids: List<Int>,
val id: Int,
val original_language: String,
val original_title: String,
val overview: String,
val popularity: Double,
val poster_path: String?,
val release_date: String,
val title: String,
val video: Boolean,
val vote_average: Double,
val vote_count: Int
)
At the end, we have the UI Layer where we will implement the fragment or an Activity depending upon the requirement of our application. Annotate your Main Activity Class as @AndroidEntryPoint
or Fragment in which you are using the ViewModel
. Get the instance of the viewModel
through activityViewModel
or hiltViewModel
depending on the use of the UI.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//....
private val movie: MovieViewModel by activityViewModels()
You can continue the UI layer from this article https://medium.com/@anumshafiq89/creating-a-vertical-list-with-jetpack-compose-cb42e1d021cf. And you have successfully implemented the Retrofit
using Hilt
.
In the article, I demonstrate the utilization of the TMDB
API, but the core principles can be applied to any API server by modifying the function names and endpoints. Any feedback on this approach is greatly appreciated. Enjoy coding!