Previously I’ve published a tutorial about building a RESTful API with kTor. But you know what kTor is multiplatform, that means we can also consume the RESTful APIs using kTor; and in this kTor Android Client Tutorial we will learn performing a simple HTTP GET request using ktor android client.
Check the updated kotlin ktor android client tutorial here.Â
In this project I will build a movie list application as shown below.
Table of Contents
TMDB API
To get the list of movies I am using TMDB API here. To create this project you will require a TMDB API Key to authenticate the api, so signup for a TMDB Account here and get your api key first.
Get the Starter Project
Before moving ahead you need my starter project. You can clone the project from this repository. Make sure you are in the branch 1_starter_project of this repository.
This project contains the UI Design, Basic Setup and all the dependencies. As this tutorial is about ktor android client, below you can see the specific dependencies that are required to add ktor android client in your project.
1 2 3 4 5 6 7 8 9 |
//kotlinx serialization implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" //ktor client, serialization and logging implementation "io.ktor:ktor-client-android:1.5.0" implementation "io.ktor:ktor-client-serialization:1.5.0" implementation "io.ktor:ktor-client-logging-jvm:1.5.0" |
The starter project contains many other dependencies as well; for example hilt, coroutines etc.
Once you have cloned the project (make sure you are at 1_starter_project branch) run the project to make sure everything is working fine.
If you are getting the result as shown in the above screenshot you can move ahead.
Creating an HTTP Client (kTor Android Client)
Let’s create a class named TmdbHttpClient , and this class will contain a function getHttpClient() and this function will return the HttpClient .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class TmdbHttpClient @Inject constructor() { fun getHttpClient() = HttpClient(Android) { install(JsonFeature) { serializer = KotlinxSerializer(kotlinx.serialization.json.Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }) engine { connectTimeout = TIME_OUT socketTimeout = TIME_OUT } } install(Logging) { logger = object : Logger { override fun log(message: String) { Log.v(TAG_KTOR_LOGGER, message) } } level = LogLevel.ALL } install(ResponseObserver) { onResponse { response -> Log.d(TAG_HTTP_STATUS_LOGGER, "${response.status.value}") } } install(DefaultRequest) { header(HttpHeaders.ContentType, ContentType.Application.Json) parameter("api_key", TmdbApiKeys.API_KEY) } } companion object { private const val TIME_OUT = 10_000 private const val TAG_KTOR_LOGGER = "ktor_logger:" private const val TAG_HTTP_STATUS_LOGGER = "http_status:" } } |
The code is pretty much self explanatory, in case you have any problem understanding it let’s discuss it in the comments below.
Make sure you have added your tmdb api key in place of TmdbApiKeys.API_KEYÂ in the above code.
TMDB API URL
Create one more file, I’ve created a file named BaseUrl.kt and here I’ve defined the TMDB API Base URL.
1 2 3 |
const val BASE_URL = "https://api.themoviedb.org/3/movie" |
Creating a Resource Wrapper
Now, I will create a Resource Wrapper. The reason of this wrapper is, when we perform an api call we may get an error as well. Also while performing the api we need to show a progressbar in the UI as well. So we can see we have three states for the api result; Loading, Success and Failure. We need the wrapper to maintain these three states.
To create the wrapper we can use sealed class.
1 2 3 4 5 6 7 |
sealed class Resource<out R> { data class Success<out R>(val result: R): Resource<R>() data class Failure(val exception: Exception): Resource<Nothing>() object Loading: Resource<Nothing>() } |
Model Classes
I forget to tell you that, what api we are going to use in this project. So I am going to use the Popular Movies API.
If you go to the api doc, you can see this api will return the following result.
So we will create two classes to map the above result into kotlin data class.
First create a class for Movie .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@Serializable data class Movie( @SerialName("adult") val adult: Boolean, @SerialName("backdrop_path") val backdropPath: String, @SerialName("genre_ids") val genreIds: List<Int>, @SerialName("id") val id: Int, @SerialName("original_language") val originalLanguage: String, @SerialName("original_title") val originalTitle: String, @SerialName("overview") val overview: String, @SerialName("popularity") val popularity: Double, @SerialName("poster_path") val posterPath: String, @SerialName("release_date") val releaseDate: String, @SerialName("title") val title: String, @SerialName("video") val video: Boolean, @SerialName("vote_average") val voteAverage: Double, @SerialName("vote_count") val voteCount: Int ) { val fullPosterPath: String get() = "https://image.tmdb.org/t/p/original/$posterPath" } |
Then we will create one more class named PopularMovies .
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Serializable data class PopularMovies( @SerialName("page") val page: Int, @SerialName("results") val movies: List<Movie>, @SerialName("total_pages") val totalPages: Int, @SerialName("total_results") val totalResults: Int ) |
Creating Movie Repository
Now let’s create a repository interface. The implementation of this interface will perform the network request.
1 2 3 4 5 |
interface MoviesRepository { suspend fun getPopularMovies(): Resource<List<Movie>> } |
We need only one function here that will get the popular movies from the tmdb api. Now let’s define the implementation of this interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
private const val POPULAR_MOVIES = "${BASE_URL}/popular" class MoviesRepositoryImpl @Inject constructor( private val httpClient: HttpClient ) : MoviesRepository { override suspend fun getPopularMovies(): Resource<List<Movie>> { return try { Resource.Success( httpClient.get<PopularMovies> { url(POPULAR_MOVIES) }.movies ) } catch (e: Exception) { e.printStackTrace() Resource.Failure(e) } } } |
We have the repository ready that will perform the actual api request.
Hilt Module
Now let’s define a hilt module that will provide the http client and repository.
1 2 3 4 5 6 7 8 9 10 11 12 |
@InstallIn(SingletonComponent::class) @Module class AppModule { @Provides fun getHttpClient(httpClient: TmdbHttpClient): HttpClient = httpClient.getHttpClient() @Provides fun getMoviesRepository(impl: MoviesRepositoryImpl): MoviesRepository = impl } |
Creating ViewModel
Let’s create a ViewModel now. The ViewModel will consume the repository to get the popular movies data from the API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@HiltViewModel class MainViewModel @Inject constructor( private val repository: MoviesRepository ) : ViewModel() { private val _movies = MutableStateFlow<Resource<List<Movie>>?>(null) val movies: StateFlow<Resource<List<Movie>>?> = _movies init { viewModelScope.launch { _movies.value = Resource.Loading _movies.value = repository.getPopularMovies() } } } |
Now we can simply collect the movies flow to render the movies list in the UI.
Displaying Movie List
For this project I am using Jetpack Compose and the UI related code is already there in the starter project; you just need to make few modifications to display the movie list that we got from the api call.
The MainActivity.kt will be like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel by viewModels<MainViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val context = LocalContext.current val movies = viewModel.movies.collectAsState() AppTheme { movies.value?.let { when (it) { is Resource.Failure -> { context.toast(it.exception.message!!) } Resource.Loading -> { FullScreenProgressbar() } is Resource.Success -> { MovieList(it.result) } } } } } } } |
Then modifie the MovieList()Â composable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Composable fun MovieList(movies: List<Movie>) { Surface( modifier = Modifier .fillMaxSize() ) { LazyColumn { items(movies) { item -> MovieItem(item) } } } } |
Also the MovieItem()Â composable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
@Composable fun MovieItem(movie: Movie) { val spacing = MaterialTheme.spacing Box( modifier = Modifier .background( brush = Brush.linearGradient( colors = listOf(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.surface), start = Offset(0f, Float.POSITIVE_INFINITY), end = Offset(Float.POSITIVE_INFINITY, 0f) ) ) .fillMaxWidth() .wrapContentHeight() .padding(spacing.extraSmall) .clip(RoundedCornerShape(spacing.small)) .shadow(elevation = 1.dp) ) { Row( modifier = Modifier .padding(spacing.small) .fillMaxWidth() ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(movie.fullPosterPath) .crossfade(true) .build(), placeholder = painterResource(R.drawable.bg_image_placeholder), contentDescription = movie.title, contentScale = ContentScale.Fit, modifier = Modifier.weight(0.4f) ) Column( modifier = Modifier .weight(0.6f) .padding(start = spacing.medium) ) { Text( text = movie.originalTitle, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.size(spacing.medium)) Text( text = movie.overview, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 7, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.size(spacing.medium)) Text( text = "IMDB ${movie.voteAverage}", style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold, modifier = Modifier .clip(RoundedCornerShape(spacing.extraSmall)) .background(Color.Yellow) .padding( start = spacing.small, end = spacing.small, top = spacing.extraSmall, bottom = spacing.extraSmall ) ) } } } } |
And that’s it, now you can try running the application and it should work.
The complete code for this project is in the branch 2_final_project ; you can switch to this branch in the same project to get the complete source code.
So that is all for this kTor Android Client Tutorial; in case you have any question or confusion you can leave it in the comments below. Thank You 🙂
JsonFeature is not found while creating client. May I know from where this attribute is calling from ?
In the new version of ktor client, there are some changes.
Check this updated post: https://www.simplifiedcoding.net/kotlin-ktor-android-client/
How To integrate Ktor in java based application