Daily dispatch #10 - Ktor is a Delight

Daily dispatch #10 - Ktor is a Delight

I recently started changing the project’s API client from Retrofit to Ktor.

And it’s been a delight. As a reference here’s the before and after code.

class MoviesAPIService {  
    private val API_KEY_MOVIEDB = "It's Mine You can't have it"  
    private val BASE_URL_MOVIEDB = "https://api.themoviedb.org/3/"  
    private val api = Retrofit.Builder()  
        .baseUrl(BASE_URL_MOVIEDB)  
        .addConverterFactory(GsonConverterFactory.create()) // Converts JSON to GSON  
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // creates Single from GSON  
        .build()  
        .create(MoviesAPI::class.java)  
  
    fun getMovies(): Single<ManyMoviesResponse> {  
        return api.getMovies()  
    }  
  
    fun getMovie(id: String): Single<JsonObject> {  
        return api.getMovie(id)  
    }  
}  
  
data class ManyMoviesResponse(  
    val page: Int,  
    val results: List<Movie>,  
    val total_pages: Int,  
    val total_results: Int  
)  
typealias SingleMovie = Single<Movie>  
  
interface MoviesAPI {  
    @GET("discover/movie?primary_release_year=2021&amp;sort_by=vote_average.desc&amp;$apiKey")  
    fun getUnpopularMovies(): Single<ManyMoviesResponse>  
  
    @GET("discover/movie?sort_by=popularity.desc&amp;$apiKey")  
    fun getMovies(): Single<ManyMoviesResponse>  
  
    @GET("movie/{movieId}?$apiKey&amp;append_to_response=credits")  
    fun getMovie(@Path("movieId", encoded = true) movieId: String): Single<JsonObject  
}  
  
  
class MovieDetailViewModel(application: Application) : BaseViewModel(application) {  
    ...  
    fun fetchMovieFromRemote(id: Int) {  
        // loading.value = true  
        disposable.add(  
            moviesService.getMovie(id.toString())  
                .subscribeOn(Schedulers.newThread())  
                .observeOn(AndroidSchedulers.mainThread())  
                .subscribeWith(object : DisposableSingleObserver<JsonObject>() {  
                    override fun onSuccess(movie: JsonObject) {  
                        val newMovie = Movie(  
                            title = (movie["title"] as JsonPrimitive).asString,  
                            id = (movie["id"] as JsonPrimitive).asString,  
                            releaseDate = (movie["release_date"] as JsonPrimitive).asString,  
                            duration = (movie["runtime"] as JsonPrimitive).asString,  
                            director = getDirectorFromCredits(movie),  
                            description = (movie["overview"] as JsonPrimitive).asString,  
                            score = (movie["vote_average"] as JsonPrimitive).asString,  
                            imageUrl = (movie["poster_path"] as JsonPrimitive).asString,  
                        )       
                        movieLiveData.value = newMovie  
                        Toast.makeText(getApplication(), "Movie retrieved", Toast.LENGTH_SHORT).show()  
                    }  
  
                    override fun onError(e: Throwable) {  
                        // moviesLoadError.value = true  
                        // loading.value = false                        
                        e.printStackTrace()  
                    }  
                })  
        )  
    }  
  
    fun getDirectorFromCredits(creditsJsonObject: JsonObject): String {  
        try {  
            val credits =  
                creditsJsonObject["credits"]       
            val crew: JsonArray = (credits as JsonObject)["crew"] as JsonArray      
            var director = ""       
for(i in 0 until crew.size()) {  
                val currCrew: JsonObject =  
                    crew[i] as JsonObject       
                val currJob = currCrew["job"].asString       
                if(currJob == "Director") {  
                    director = currCrew["name"].asString       
                    break  
                }  
            }       
            return director  
        } catch (e: Exception) {  
            return ""  
        }  
    }  
}

And I’d have to create those Single responses from RxJava2. Then there was the @GET annotations that made it a mess to put query parameters. Ugly as hell.

Then enters Ktor:

internal expect val ApplicationDispatcher: CoroutineDispatcher  
  
class KTorSimpleClient {  
    private val movieApiAddress = "https://api.themoviedb.org/3"   
	private val apiKey = "It's Mine you can't have it"  
    val client = HttpClient() {  
        install(ContentNegotiation) {  
            Json {  
                prettyPrint = true isLenient = true  
            }  
        }    
    }      
    
	suspend fun getMovies(): MovieListResult {  
        val httpResponse =  
            client.get { url("$movieApiAddress/discover/movie") parameter ("sort_by", "popularity.desc")     parameter("api_key", apiKey) }     return Json.decodeFromString(  
            httpResponse.bodyAsText()  
        )  
    } 
    
    suspend fun getMovie(id: String): Movie {  
        val httpResponse =  
            client.get { 
	            url("$movieApiAddress/movie/634649") 
	            parameter ("append_to_response", "credits")     
	            parameter("api_key", apiKey) 
	        }  
        val movie = Json.decodeFromString<Movie>(httpResponse.bodyAsText())  
        val director = getDirectorFromCredits(movie)   
        movie.director = director   
        return movie  
    } 
    
    private fun getDirectorFromCredits(movie: Movie): String {  
        return try {  
            val credits = movie.credits  
            val crew = credits.crew  
            var director = ""       
			for (element in crew) {  
                val currJob = element.job       
                if (currJob == "Director") {  
                    director = element.name     
                    break  
                }  
            }   
            director  
        } catch (e: Exception) {  
            ""  
        }  
    }  
}

It just looks delightful, it’s clean, it has it’s own Domain Specific Language, so very little Annotations required.


Why I love Ktor

Let’s go over why I love Ktor more than retrofit.

First of all Ktor is multiplatform.

Second, it uses Kotlin Coroutines instead of rxjava2.

So we get to develop a KMM module for our app straight out of the box.

Json to Data Class Plugin

Another useful thing I’ve found is the Json plug-in for Android Studio.

You give it a json response from an api and it generates kotlin data classes for ya.

Check out what it generated for me from this JSON:

{
   "page":1,
   "results":[
      {
         "adult":false,
         "backdrop_path":"/EnDlndEvw6Ptpp8HIwmRcSSNKQ.jpg",
         "genre_ids":[
            14,
            35,
            12
         ],
         "id":425909,
         "original_language":"en",
         "original_title":"Ghostbusters: Afterlife",
         "overview":"When a single mom and her two kids arrive in a small town, they begin to discover their connection to the original Ghostbusters and the secret legacy their grandfather left behind.",
         "popularity":1136.17,
         "poster_path":"/sg4xJaufDiQl7caFEskBtQXfD4x.jpg",
         "release_date":"2021-11-11",
         "title":"Ghostbusters: Afterlife",
         "video":false,
         "vote_average":7.7,
         "vote_count":2102
      },
      {
         "adult":false,
         "backdrop_path":"/8pgKccb5PfE1kWB9qqiXJem83VC.jpg",
         "genre_ids":[
            28,
            53
         ],
         "id":522016,
         "original_language":"en",
         "original_title":"The 355",
         "overview":"A group of top female agents from American, British, Chinese, Columbian and German government agencies are drawn together to try and stop an organization from acquiring a deadly weapon to send the world into chaos.",
         "popularity":1183.696,
         "poster_path":"/uQt2dJFMnJmAp9zLAWNfGilK0BW.jpg",
         "release_date":"2022-01-05",
         "title":"The 355",
         "video":false,
         "vote_average":6.1,
         "vote_count":265
      },
      {
         "adult":false,
         "backdrop_path":"/cTTggc927lEPCMsWUsdugSj6wAY.jpg",
         "genre_ids":[
            28,
            12
         ],
         "id":335787,
         "original_language":"en",
         "original_title":"Uncharted",
         "overview":"A young street-smart, Nathan Drake and his wisecracking partner Victor “Sully” Sullivan embark on a dangerous pursuit of “the greatest treasure never found” while also tracking clues that may lead to Nathan’s long-lost brother.",
         "popularity":1133.259,
         "poster_path":"/tlZpSxYuBRoVJBOpUrPdQe9FmFq.jpg",
         "release_date":"2022-02-10",
         "title":"Uncharted",
         "video":false,
         "vote_average":7.2,
         "vote_count":451
      }
   ],
   "total_pages":32509,
   "total_results":650173
}

The generated Data Classes:


import kotlinx.serialization.SerialName    
import kotlinx.serialization.Serializable        

@Serializable    
data class MovieListResult(     
    @SerialName("page")     
    val page: Int,     
    @SerialName("results")     
    val results: List<Result>,     
    @SerialName("total_pages")     
    val totalPages: Int,     
    @SerialName("total_results")     
    val totalResults: Int    
)        
@Serializable    
data class Result(     
	@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    
)  

Pretty Cool hm?


Manga Web Portal

On a more entrepreneurial note. I’ve been thinking about a simple app to buy and sell Manga with verified users and sellers.

Could be called Golduru.

I think I could set up a website on firebase and backend on it as well.

Maybe eventually link account with a block chain wallet to help verify scammers as well.

I think there is enough demand in this market to be able to sell this service as an yearly subscription.