Returning the result of a long-running function at a later time is an important aspect of programming. A lot of high-level programming languages have the asynchronousity build in (e.g. C# with delegates) or it’s part of the language nature that it even leads to a callback hell (Node.js). Java/Android doesn’t have first class support and has to rely on callbacks via interfaces. In this tutorial, we’ll take a closer look what they are and how Retrofit utilizes them.
Retrofit Series Overview
- Getting Started and Creating an Android Client
- Basics of API Description
- Creating a Sustainable Android Client
- URL Handling, Resolution and Parsing
- How to Change API Base Url at Runtime
- Multiple Server Environments (Develop, Staging, Production)
- Share OkHttp Client and Converters between Retrofit Instances
- Upgrade Guide from 1.9
- Beyond Android: Retrofit for Java Projects
- How to use OkHttp 3 with Retrofit 1
- Synchronous and Asynchronous Requests
- Send Objects in Request Body
- Add Custom Request Header
- Manage Request Headers in OkHttp Interceptor
- Dynamic Request Headers with @HeaderMap
- Multiple Query Parameters of Same Name
- Optional Query Parameters
- Send Data Form-Urlencoded
- Send Data Form-Urlencoded Using FieldMap
- How to Add Query Parameters to Every Request
- Add Multiple Query Parameter With QueryMap
- How to Use Dynamic Urls for Requests
- Constant, Default and Logic Values for POST and PUT Requests
- Cancel Requests
- Reuse and Analyze Requests
- Optional Path Parameters
- How to Send Plain Text Request Body
- Customize Network Timeouts
- How to Trust Unsafe SSL certificates (Self-signed, Expired)
- Dynamic Endpoint-Dependent Interceptor Actions
- How to Update Objects on the Server (PUT vs. PATCH)
- How to Delete Objects on the Server
- Introduction to (Multiple) Converters
- Adding & Customizing the Gson Converter
- Implementing Custom Converters
- How to Integrate XML Converter
- Access Mapped Objects and Raw Response Payload
- Supporting JSON and XML Responses Concurrently
- Handling of Empty Server Responses with Custom Converter
- Send JSON Requests and Receive XML Responses (or vice versa)
- Unwrapping Envelope Responses with Custom Converter
- Wrapping Requests in Envelope with Custom Converter
- Define a Custom Response Converter
- Callbacks
- Annotations
- Fluent Interface with Builders
Java Callbacks
Before we go into the details, let’s start with an easy description of callbacks from Paul Jakubik:
Callbacks are most easily described in terms of the telephone system. A function call is analogous to calling someone on a telephone, asking her a question, getting an answer, and hanging up; adding a callback changes the analogy so that after asking her a question, you also give her your name and number so she can call you back with the answer. Paul Jakubik
Hopefully this quote explains fairly well what callbacks are and how we use them in programming. Retrofit doesn’t make any phone calls, but has to fulfill a very similar function: network requests. Because those go to a server somewhere on the Internet, there is no guarantee if and when we’ll get a response. Thus, it makes sense to start the request (phone call) and rather take a notification when the response (answer phone call) is available than wait for the result.
This is important on Android in particular due to the fact that Android 4.0 or newer crashes the app when you execute network operations on the main thread! You’ve to move to some kind of asynchronous model. Unfortunately, there are not that many options on Android. Java doesn’t support lambda expressions or method references until Java 8. You, me and all of the Android developers won’t be able to use those for a long time. Thus, there’s currently only one common way of implementing callbacks: via interfaces.
Callbacks via Interfaces
Before we look at Retrofit’s implementation, let’s look at a simplified example. The scenario is the following: we’re implementing an admin app for the Future Studio platform. On the screen we’re currently working on we want to display how many students are enrolled in our University. We can look that up in the database, which is already part of the admin app. We only have to query it. Followingly, our code is simple:
public class UniversityAdminActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_university_admin);
int studentCount = DatabaseModule.getStudentCount();
displayStudentCount(studentCount);
}
private void displayStudentCount(int studentCount) {
// todo implement the display of the data
}
...
public static class DatabaseModule {
public static int getStudentCount() {
// todo: implement real logic, this seems fake ;)
Thread.sleep(5000);
return 42;
}
}
}
However, we’ve noticed that the query is not very fast and takes a couple of seconds, which causes a noticeable UI freeze. It’s time to fix that by changing to an asynchronous model!
The new approach is the following: we’ll request the data from our database module and instead of blocking everything until we get a response, we’ll just update the data when we get the data asynchronously at some point in the future.
Because Java doesn’t have native callbacks, only callback interfaces, we need to define an interface, which will describe the result callback method:
public interface DatabaseQuery {
// this is the method that will be called when the database is done
// the result (studentCount) will be passed back to our activity as a parameter
void studentCountResult(int studentCount);
}
Of course, we also have to change our DatabaseModule
to return the result asynchronously. Our method doesn’t have a direct return type (int
) anymore. Instead, we’ll return void
and accept a callback interface as parameter. We return the result indirectly via the callback interface method studentCountResult()
:
public static class DatabaseModule {
public static void getStudentCount(DatabaseQuery callback) {
// todo: implement real logic, this seems fake ;)
Thread.sleep(5000);
callback.studentCountResult(42);
}
}
Finally, when we request the data from our DatabaseModule
we’ve to pass an instance of our callback interface. Often, the code is cleaner to just use an anonymous declaration instead of initializing a full (field) variable:
public class UniversityAdminActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_university_admin);
// we pass an (anonymous) instance of the DatabaseQuery interface
DatabaseModule.getStudentCount(new DatabaseQuery() {
@Override
public void studentCountResult(int studentCount) {
// whenever the result is ready we can display it
displayStudentCount(studentCount);
}
});
}
private void displayStudentCount(int studentCount) {
// todo implement the display of the data
}
...
public static class DatabaseModule {
public static void getStudentCount(DatabaseQuery callback) {
// todo: implement real logic, this seems fake ;)
Thread.sleep(5000);
callback.studentCountResult(42);
}
}
}
Hopefully this example made it clearer how you can implement callback interfaces in Java and what elements are necessary. You’ll see them again in the next section, when we talk about the Retrofit callbacks.
Retrofit’s Request Callbacks
Due to the nature of mobile networks and the Internet, there is no guarantee of knowing when a server responds to your request. As explained earlier, callbacks are the only way of implementing network requests, if you start them on the UI thread. Consequently, Retrofit (and every other request library) utilizes callbacks. Let’s look at them in more detail.
The Call
class is the starting point for every network request with Retrofit. Usually, you’ll want to execute the request asynchronously with the enqueue
method. The method expects a typed implementation of Retrofit’s Callback
class, which expects the implementation of two methods (onResponse
and onFailure
). We won’t go into more detail what those two methods mean, you can learn about them in our getting started with Retrofit tutorial.
public class User {
private int id;
private String email;
private String username;
}
...
Call<User> call = userService.login();
call.enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// todo deal with returned data (user)
}
public void onFailure(Call<User> call, Throwable t) {
// todo deal with the failed network request
}
});
Retrofit uses two different callback methods for the two possible outcomes of a network requests: either a failure or a successful request. Retrofit will call the appropriate callback method depending on the result. If the request was successful, Retrofit will also pass you the response of the server. Fantastic!
Summary
In this tutorial you’ve learned a lot of background on callbacks and how they’re implemented in Java. It’s necessary to comprehend the logic behind Java’s callback interfaces. You’ve seen the components in action in our Future Studio University admin app. Afterwards, you’ve gained a deeper understanding in Retrofit’s callbacks.
If you’ve feedback or a question, let us know in the comments or on twitter @futurestud_io.
Make it rock & enjoy coding!