可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Imagine the following scenario.
I am using Retrofit as my API client and I am going to use a caching mechanism to persist some responses locally in let's say SQLite database, so some of the data is obtained only once. It seems like a perfect case to create a custom implementation for the following interface which is going to fetch the data from local database when network is not available.
public interface CommentsService {
@GET("posts/{postId}/comments")
Call<List<Comment>> getCommentsOfPost(@Path("postId") int id);
}
The problem is that all service methods in retrofit2 should be wrapped in a Call
object, since it prevents passing the callback as a parameter like in previous versions of retrofit.
I would like to know if it is a good practice to create a custom implementation of this interface, if yes then how to treat with the Call object ?
回答1:
In one of my projects I had a similar problem and could not use RxJava. I solved this by using a Command Pattern.
First, I have created a custom Callback class with a clear API. That way I can handle HTTP codes more concisely (such as 400 or 500).
public abstract class RestCallback<T> implements Callback<T> {
@Override
public void onResponse(Response<T> response, Retrofit retrofit) {
if (response != null) {
if (response.isSuccess()) {
onSuccess(response, retrofit);
} else {
onError(response, retrofit);
}
} else {
onFailure(null);
}
}
abstract public void onSuccess(Response<T> response, Retrofit retrofit);
abstract public void onError(Response<T> response, Retrofit retrofit);
abstract public void onFailure(Throwable t);
}
Then I created a Command interface:
public interface Command {
// New network hit
void fresh(RestCallback callback);
// First cached if available then fresh
void all(RestCallback callback);
// Cached response only
void cached(RestCallback callback);
// If cache exists return it, otherwise return a fresh response
void get(RestCallback callback);
// Cancel the request
void cancel();
}
This way, instead of working directly with Retrofit objects you will implement the Command interface and work with that. For example, you could create a GetCommentsOfPostCommand and let it take care of the Retrofit Call object, encapsulating request logic.
This way you can work as you like; implementing cache, requests, cancellations, etc. while exposing a well-defined interface.
Hope this helps.
回答2:
If I'm understanding your question right, you'd like to fetch data from your SQLite just in case there's no network connection.
You have this interface, where you define your endpoint:
public interface CommentsService {
@GET("posts/{postId}/comments")
Call<List<Comment>> getCommentsOfPost(@Path("postId") int id);
}
Then you should have another interface, which you use from another part of your application:
public interface GetCommentsService {
void getComments(int id, CommentsServiceResultListener listener);
}
Where CommentsServiceResultListener is the listener you're passing to the implementation of the previous interface. Let's define it as follows:
public interface CommentsServiceResultListener {
void onResponse(List<Comment> response);
void onError(String errorMessage);
}
The, you need to implement your GetCommensService interface, in order to actually get the data. You can do it like:
public class GetCommensServiceImpl implemens GetCommensService {
private static final String TAG = BuildingsBaseServiceImpl.class.getSimpleName();
@Override
public void getComments(int id, CommentsServiceResultListener listener) {
CommentsService service = getService();
Call<List<Comment>> request = service.getCommentsOfPost(id);
request.enqueue(new Callback<List<Comment>>(){
@Override //if this method is executed, the actual call has been made
public void onResponse(Call<List<Comment>> call, Response<List<Comment>> response) {
if (response.isSuccessful()) {
listener.onResponse(response.body());
} else {
//TODO check here if the call wans't successful because a network problem. In that case, fetch from your SQLite
//Default unsuccessful call management
Log.e(TAG, response.code() + ": " + response.message());
listener.onError(response.message());
}
}
@Override //maybe the call couldn't be made because of lack of connection.
public void onFailure(Call<List<Comment>> call, Throwable t) {
//TODO check the failure cause, then decide if there's need to fetch from your SQLite.
//Default failure management
Log.e(TAG, t.getMessage() + "", t);
listener.onError(t.getMessage() + "");
}
});
}
private CommentsService getService() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://your.base.url/")
.addConverterFactory(GsonConverterFactory.create()) //assuming JSON
.build();
return retrofit.create(CommentsService.class);
}
}
Hope this helps!!
回答3:
In retrofit2, you can use Call.enqueue(Callback)
to specify the callback. For example,
getCommentsService.getCommentsOfPost(myInt).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
// do something with the response
}
}
If you want a synchronous execution, use Call.execute()
.
回答4:
If you have experience with RxJava, It'll become easier because you can combine multiple task while getting data, and show it as quickly as possible. So, the first thing you have to do is save the api result into your database. The next time you want to show the data, you can call concat method to concatenate multiple source request (from cache, disk, or server) to retrieves data as quickly as possible.
Here is the good explanation about that.
http://blog.danlew.net/2015/06/22/loading-data-from-multiple-sources-with-rxjava/
https://medium.com/@murki/chaining-multiple-sources-with-rxjava-20eb6850e5d9#.2u8zssice
回答5:
Since you only want to check the local database when network isn't available, what you could also is implement a callback interface and pass it as a parameter to the function which is making the retrofit call. This way, you can make your function return void, and from inside the onResonse or onFailure method of your retrofit call, you can decide whether to load from the successful response(onResponse) or from local database(onFailure), i.e., make the required call to the required function of your custom interface. This will mimic the synchronous behavior of your retrofit call in an asynchronous call, while giving you the flexibility to make decisions even when you receive improper response.
回答6:
So ok , lets imagine that Retrofit also provides mechanism (for ex. by passing special callback if error - load from DB). It would be very specific, because:
- If I want to update token, for security data
- If I want to recheck connection manually
- If I want show special data
- ...
Primary task of using Retrofit it's getting result from connection, and status of work. And that is all.
Also answering your question. You may provide your own behavior, for case where is no net connection, and call onDataBaseLoadIfError(). And than call in your overridden Retrofit class - Call.
public interface CommentsServiceListener {
void onResponse(List<Comment> response);
void onError(String errorMessage);
void onDataBaseLoadIfError(String errorMessage);
}