People have been asking the question "Retrofit vs OkHttp" for years. Most of the answers would be a feature comparison, pros and cons, then they'll tell you Retrofit saves you so much time.
Now it is time for a different answer.
I'm gonna convince you Retrofit is better than OkHttp when you follow MVVM.
Suppose you are writing a post detail screen in a blog app. All you need to do is call 1 api and display the data.
How would you write the same feature with different ideas applied.
God Activity Architecture
Let's start basic, remember the good old days when we write everything in Activity
?
Typically, these are what you do
- create a
getPost(id: String)
function accept a post id as a parameter - concat String to create a url
- make a network request directly with OkHttp
- onResponse callback, parse response and display the data
- onFailure callback, show some sort of failure message
class PostDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_details)
getPost(intent.getStringExtra("POST_ID")!!)
}
fun getPost(id: String) {
val okhttp = OkHttpClient.Builder().build()
val request = Request.Builder().get().url("https://somewh.ere/api/v1/posts/$id").build()
okhttp.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body!!.string()
val post = Gson().fromJson(body, Post::class.java)
setView(post)
}
override fun onFailure(call: Call, e: IOException) {
Toast.makeText(this@PostDetailActivity, e.message, Toast.LENGTH_SHORT).show()
}
})
}
fun setView(post: Post) {
//TODO
}
}
MVVM
Now we apply Separation of Concern or else you get frowned upon.
- M-Model,
PostApi
- put implementation details of network call and deserialization here
- wrap the asynchronous callback with coroutine because it's 2022
- or you might go with old school callback and declare the function
like
fun getPost(id: String, callback: Callback<Post>)
, doesn't really matter
- VM-ViewModel,
PostDetailViewModel
- accept
PostApi
as a constructor parameter, easy DI, testable - holds the data
- accept
- V-View,
MvvmPostDetailActivity
- UI manipulation
- observe data from
PostDetailViewModel
- an instance of
PostDetailViewModel
should be obtained through a DI framework but let's leave it like this for simplicity
class PostApi {
suspend fun getPost(id: String): Post = suspendCoroutine { continuation ->
val okhttp = OkHttpClient.Builder().build()
val request = Request.Builder().get().url("https://somewh.ere/api/v1/posts/$id").build()
okhttp.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body!!.string()
val post = Gson().fromJson(body, Post::class.java)
continuation.resume(post)
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
}
}
class PostDetailViewModel(val postApi: PostApi) : ViewModel() {
val postLiveData: MutableLiveData<Post> = MutableLiveData()
val errorLiveData: MutableLiveData<String> = MutableLiveData()
fun getPost(id: String) {
viewModelScope.launch {
try {
val p = postApi.getPost(id)
postLiveData.value = p
} catch (e: Exception) {
errorLiveData.value = e.message
}
}
}
}
class MvvmPostDetailActivity : AppCompatActivity() {
val viewModel: PostDetailViewModel = PostDetailViewModel(PostApi())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_details)
viewModel.postLiveData.observe(this) { post: Post ->
setView(post)
}
viewModel.errorLiveData.observe(this) { message: String ->
Toast.makeText(this@MvvmPostDetailActivity, message, Toast.LENGTH_SHORT).show()
}
viewModel.getPost(intent.getStringExtra("POST_ID")!!)
}
}
Code Against Interfaces, Not Implementations
I have no idea why this principle(?) exits, isn't that just how interface works? Anyway, I'm sure you have been in a situation where you develop frontend/mobile app but the API is not ready, and you have to make due with mock data.
- convert
PostApi
to an interface withsuspend fun getPost(id: String): Post
RealPostApi
implementsPostApi.getPost()
and call the actual APIMockPostApi
implementsPostApi.getPost()
but return mock data of your choice- you can now swap between real data and mock data with just one line of code (where you create ViewModel)
- if you have proper DI setup, no code changes in the Activity needed (the change will be in your DI setup instead)
interface PostApi {
suspend fun getPost(id: String): Post
}
class RealPostApi : PostApi {
override suspend fun getPost(id: String): Post = suspendCoroutine { continuation ->
val okhttp = OkHttpClient.Builder().build()
val request = Request.Builder().get().url("https://somewh.ere/api/v1/posts/$id").build()
okhttp.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body!!.string()
val post = Gson().fromJson(body, Post::class.java)
continuation.resume(post)
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
}
}
class MockPostApi : PostApi {
override suspend fun getPost(id: String): Post {
delay(2000)
if (id == "404") {
throw Exception("A post with id = $id does not exist")
} else {
return Post(id, "Peace Treaty", "Ceres Fauna")
}
}
}
class MvvmPostDetailActivity : AppCompatActivity() {
val viewModel: PostDetailViewModel = PostDetailViewModel(MockPostApi())
// val viewModel: PostDetailViewModel = PostDetailViewModel(RealPostApi())
}
Retrofit
Man, hand-written http request and deserialization are such tedious tasks, and you have to do this for every single api call.
But, wait a minute... this looks familiar somehow.
Right, we can just annotate our PostApi
interface and have a fully functional "Real"PostApi
instance created by Retrofit
without ever have to write RealPostApi
ourselves!
interface PostApi {
@GET("https://somewh.ere/api/v1/posts/{id}")
suspend fun getPost(@Path("id") id: String): Post
}
class MvvmPostDetailActivity : AppCompatActivity() {
val viewModel: PostDetailViewModel =
PostDetailViewModel(Retrofit.Builder().build().create(PostApi::class.java))
// val viewModel: PostDetailViewModel = PostDetailViewModel(MockPostApi())
}
Since PostDetailViewModel
still depends on an interface PostApi
, the flexibility of swap the mock implementation is still there, still easy to test. And if later on you decide to hand-write RealPostApi
yourself, you can still do that without changing any line of code inside PostDetailViewModel
.
Yeah, the Retrofit creation code is kind of ugly in the Activity, so you should have a proper DI setup.
Conclusion
Even you started out using OkHttp, when you adopt MVx architecture, write testable code and use interfaces wisely, Retrofit can replace your hand-written API call implementation. I guess this is what it means by Retrofit turns your HTTP API into a Java interface as written in the first line on the website.