Skip to content

Instantly share code, notes, and snippets.

@marcelpinto
Last active May 18, 2018 13:41
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save marcelpinto/635d23353e472e9f95ef to your computer and use it in GitHub Desktop.
Save marcelpinto/635d23353e472e9f95ef to your computer and use it in GitHub Desktop.
Cache RxJava observables on Android approach to reuse and avoid multiple calls.
This is my approach to reuse observables and avoid multiple calls made by the user or because of configuration change or going to pause and then on resume again, etc...
The idea is to use the .cache() operator from Rxjava to cache the observable so if you subrcirbe again to the same obs, this does not start from the begining and gets the result of the previouse one. The problem of cache is that does not clear when the observable finished so if you keep subscribing to it you will get the same result every time.
In order to avoid that, I created the observable manager to keep in memory the observables and play with them. Using the TTL. So you can decide when and observable should be created again or not.
Example 1:
Class A does getStores() --> the network call starts --> Class B does getStores() again --> subscribe to the same observable --> network finish send result --> the result is send to all the subscribers so Class A and B get the same result coming from the same network call.
Example 2:
Activity A getStores() --> network call --> User turns screen --> Activity A unsubscribe because we go to onPause() --> Activity A goes to onResum() --> we call again getStores() --> We subscribe to the same observable that was doing the request --> network call finished, our Activity gets the result of the first network call
Exmple 3:
The same situation as example 2 but the network call finished while we were not subscribed. When we subscribe again if the TTL stablished by the getStores() method if smaller we will get the network call result as soon as we subscribe to the cached obsevable.
Just for the record, I think you can use BeheivorSubject to do something similar. But I'm not sure how to do it. I've using this approach and works fine for me.
private ObservableManager obsManager = new ObservableManager(EventBus.getDefault());
@Override
public Subscription getStores() {
// Get the observable if exists and is not too old
Observable<StoresList> observable = obsManager.get(ObservableManager.Types.STORES);
if (observable == null) {
// If is null create it and us cache to keep it in memeroy
observable = api.getStoresList()
.compose(applySchedulers(api.getStoresList()))
.cache();
// Put it inside our Manager to access it again
obsManager.put(ObservableManager.Types.STORES.getKey(), observable);
}
// Subscribe to it.
return observable.subscribe(
getDefaultSubscriber(StoresList.class, ObservableManager.Types.STORES.getKey()));
}
/**
* Global operators to apply to all or most of the observables, return the observable with the
* default operators applied.
*
* @param toBeResumed origin observable to retry in case of fail
*/
private <T> Observable.Transformer<T, T> applySchedulers(Observable<T> toBeResumed) {
return observable -> observable.subscribeOn(Schedulers.io())
.retry((attempts, throwable) -> throwable instanceof SocketTimeoutException
&& attempts < Globals.RETRY_NETWORK_FAIL)
// Global method to refresh token, you can ignore if you don't need auth
.onErrorResumeNext(refreshTokenAndRetry(toBeResumed))
.observeOn(AndroidSchedulers.mainThread());
}
private <T extends BaseModel<T>> Subscriber<T> getDefaultSubscriber(final Class<T> tClass, final Integer key) {
return new Subscriber<T>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
// Delete the Cached observable from our Manager because it was a fail and we want to executed again
obsManager.remove(key);
// Handle error
}
@Override
public void onNext(T t) {
// Do what you need to do with the data
}
};
}
/**
* Created by marcel on 08/04/15.
*/
public abstract class BaseModel<T> implements Serializable {
private int code;
private ErrorBody errorBody;
private long createdTime;
public BaseModel() {
}
public BaseModel(int code) {
this.code = code;
}
public static <T extends BaseModel<T>> T getInstance(Class<T> clazz, int code) {
return BaseModel.getInstance(clazz, code, null);
}
public static <T extends BaseModel<T>> T getInstance(Class<T> clazz, int code,
ErrorBody errorBody) {
try {
T instance = clazz.newInstance();
instance.setCode(code);
instance.setCreatedTime(System.currentTimeMillis());
instance.setErrorBody(errorBody);
return instance;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public long getCreatedTime() {
return createdTime;
}
public void setCreatedTime(long createdTime) {
this.createdTime = createdTime;
}
public ErrorBody getErrorBody() {
return errorBody;
}
public void setErrorBody(ErrorBody errorBody) {
this.errorBody = errorBody;
}
}
/**
* Class to manage all the observables created by the core in order to cache them so there are no
* duplicated request on the same time, and avoid calls being made too much frequently.
*
* The time to live (ttl) for each call can be set in the {@link ObservableManager.Types}
* <br><br>
* Created by marcel on 01/06/15.
*/
public class ObservableManager {
private final EventBus bus;
private HashMap<Integer, Observable<? extends BaseModel>> obsMap;
// I use event bus to keep all my model in one place so I can get the sticky events from memory. I have a BaseModel.java that keeps
// The result and the created time so Obsevable manager can retreived. Another approach would be to keep the reference of the created time
// here in the ObservableManager.
public ObservableManager(EventBus bus) {
this.bus = bus;
this.obsMap = new HashMap<>();
}
public void put(Integer key, Observable<? extends BaseModel> observable) {
obsMap.put(key, observable);
}
public <T extends BaseModel> Observable<T> get(Types type) {
Object obj = bus.getStickyEvent(type.getType());
if (obj == null) {
return (Observable<T>) obsMap.get(type.getKey());
}
if (((BaseModel) obj).getCreatedTime() > 0
&& System.currentTimeMillis() - ((BaseModel) obj).getCreatedTime() > type.getTtl()) {
((BaseModel) obj).setCreatedTime(0); // This makes sure to invalidate the created time
obsMap.remove(type.getKey());
return null;
}
return (Observable<T>) obsMap.get(type.getKey());
}
public void remove(Integer key) {
obsMap.remove(key);
}
public void clear() {
obsMap.clear();
}
/**
* Internal class where all types of request are bounded to specific class and a TTL if set
*/
public enum Types {
// TODO fill up with every call we need
STORES(100, StoresList.class, 1000),
USER(101, User.class),
ORDER(102, OrdersList.class);
int key;
Class type;
long ttl;
Types(int key, Class type) {
this.key = key;
this.type = type;
}
Types(int key, Class type, long ttl) {
this.key = key;
this.type = type;
this.ttl = ttl;
}
public int getKey() {
return key;
}
public Class getType() {
return type;
}
public long getTtl() {
return ttl == 0 ? Globals.DEFAULT_CACHED_TTL : ttl;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment