Android 使用RxJava和switchIfEmpty()进行存储库数据缓存管理
我有两个使用公共存储库的片段 我正在尝试为这个存储库实现一个缓存管理系统 这个想法是: 加载一个片段,它调用Android 使用RxJava和switchIfEmpty()进行存储库数据缓存管理,android,caching,rx-java,repository-pattern,Android,Caching,Rx Java,Repository Pattern,我有两个使用公共存储库的片段 我正在尝试为这个存储库实现一个缓存管理系统 这个想法是: 加载一个片段,它调用getData()方法,此方法使用getDataFromNetwork()对远程JSON Api进行网络调用,获取结果并将其作为列表(我的代码中的数据变量)放入缓存中 加载下一个片段。如果发生在60秒之前,则没有网络调用,数据直接来自我的数据列表中的缓存,使用getDataFromMemory() RxJavaObservable.switchIfEmpty()用于知道Observable
getData()
方法,此方法使用getDataFromNetwork()
对远程JSON Api进行网络调用,获取结果并将其作为列表
(我的代码中的数据
变量)放入缓存中
加载下一个片段。如果发生在60秒之前,则没有网络调用,数据直接来自我的数据列表中的缓存,使用getDataFromMemory()
RxJavaObservable.switchIfEmpty()
用于知道Observable(我的ArrayList)是否为空,并调用正确的方法
我不知道如何首次亮相,所以我只是在我的主布局上放了一个按钮。当我启动我的应用程序时,第一个片段会自动加载,getData()
第一次被调用。当我按下这个按钮时,它会加载第二个片段,getData()
被再次调用
如果我在60秒之前按下这个按钮,我就不会有对JSON api的网络调用,但是。。。我有一个,我总是接到第二个网络电话,我的缓存数据没有被使用。我的代码怎么了
public class CommonRepository implements Repository {
private static final String TAG = CommonRepository.class.getSimpleName();
private long timestamp;
private static final long STALE_MS = 60 * 1000; // Data is stale after 60 seconds
private PollutionApiService pollutionApiService;
private ArrayList<Aqicn> data;
public CommonRepository(PollutionApiService pollutionApiService) {
this.pollutionApiService = pollutionApiService;
this.timestamp = System.currentTimeMillis();
data = new ArrayList<>();
}
@Override
public Observable<Aqicn> getDataFromNetwork(String city, String authToken) {
Observable<Aqicn> aqicn = pollutionApiService.getPollutionObservable(city, authToken)
.doOnNext(new Action1<Aqicn>() {
@Override
public void call(Aqicn aqicn) {
data.add(aqicn);
}
});
return aqicn;
}
private boolean isUpToDate() {
return System.currentTimeMillis() - timestamp < STALE_MS;
}
@Override
public Observable<Aqicn> getDataFromMemory() {
if (isUpToDate()) {
return Observable.from(data);
} else {
timestamp = System.currentTimeMillis();
data.clear();
return Observable.empty();
}
}
@Override
public Observable<Aqicn> getData(String city, String authToken) {
return getDataFromMemory().switchIfEmpty(getDataFromNetwork(city, authToken));
}
}
我不知道这些细节是如何运作的,但我是这样解释的。Dagger创建一个可观察的服务提供商。当我执行
返回cachedData
时,此可观察对象已订阅,因此网络调用已完成。。。但不知道怎么做,也不知道怎么解决。事实上,每次我执行返回cachedData
时,都会有一个网络调用。我使用以下类实现了缓存行为
为了使用缓存类,您需要以下依赖项:
接口存储库{
单个getData(字符串param1、字符串param2);
}
类RepositoryImpl实现存储库{
私有最终缓存;
私人最终功能2 calculateKey;
RepositoryImpl(缓存){
this.cache=cache;
this.calculateKey=(s,s2)->s+s2;
}
@凌驾
公共单getData(字符串param1、字符串param2){
可能networkFallback=getFromNetwork(param1,param2,calculateKey).toMaybe();
返回getFromCache(参数1、参数2、calculateKey)。switchIfEmpty(网络回退)
.toSingle();
}
专用单getFromNetwork(字符串参数1、字符串参数2、函数2 calculateKey){
返回Single.fromCallable(结果::new)
.doOnSuccess(结果->{
如果(!cache.containsKey(calculateKey.apply(param1,param2))){
System.out.println(“保存在缓存中”);
字符串apply=calculateKey.apply(param1,param2);
cache.put(应用,结果);
}
})//模拟网络请求
.延迟(50,时间单位毫秒);
}
private可能是getFromCache(字符串参数1、字符串参数2、函数2 calculateKey){
可能返回。延迟(()->{
String key=calculateKey.apply(param1,param2);
if(cache.containsKey(键)){
System.out.println(“从缓存获取”);
返回Maybe.just(cache.get(key));
}否则{
返回Maybe.empty();
}
});
}
}
班级成绩{
}
测试行为:
@Test
// Call getData two times with equal params. First request gets cached. Second request requests from network too, because cash has already expired.
void getData_requestCashed_cashExpiredOnRequest() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hans", "wurst");
// Action
data1.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
data2.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate second Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
}
@Test
// Call getData two times with different params for each request. Values cashed but only for each request. Second request will hit network again due to different params.
void getData_twoDifferentRequests_cacheNotHit() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hansX", "wurstX");
// Action
data1.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
// Action
data2.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate second Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
}
@Test
// Call getData two times with equal params. First request hit network. Second request hits cache. Cache does not expire between two requests.
void getData_twoEqualRequests_cacheHitOnSecond() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hans", "wurst");
// Action
data1.test()
.await();
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(0))
.get(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> true);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
TestObserver<Result> sub2 = data2.test()
.await()
.assertNoErrors()
.assertValueCount(1)
.assertComplete();
// Validate second subscription: load from cache
inOrder.verify(cacheMock, times(1))
.containsKey(anyString());
inOrder.verify(cacheMock, times(0))
.put(anyString(), any());
inOrder.verify(cacheMock, times(1))
.get(anyString());
sub2.assertResult(result);
}
@测试
//使用相等的参数调用getData两次。第一个请求被缓存。第二个请求也来自网络,因为现金已经过期。
void getData\u requestCashed\u cashExpiredOnRequest()引发异常{
//安排
Cache cacheMock=mock(Cache.class);
inoorder inoorder=Mockito.inoorder(cacheMock);
Repository=newrepositoryimpl(cacheMock);
结果=新结果();
当(cacheMock.containsKey(anyString())。然后回答(调用->false);
当(cacheMock.get(anyString())。然后回答(调用->结果);
单个数据1=rep.getData(“hans”、“wurst”);
单个数据2=rep.getData(“hans”、“wurst”);
//行动
data1.test()
.等待
.assertValueAt(0,r->r!=结果);
//验证第一个订阅:保存到缓存
顺序验证(cacheMock,次(2))
.containsKey(anyString());
顺序验证(cacheMock,次(1))
.put(anyString(),any());
数据2.test()
.等待
.assertValueAt(0,r->r!=结果);
//验证第二个订阅:保存到缓存
顺序验证(cacheMock,次(2))
.containsKey(anyString());
顺序验证(cacheMock,次(1))
.put(anyString(),any());
}
@试验
//对每个请求使用不同的参数调用getData两次。值已兑现,但仅适用于每个请求。由于参数不同,第二个请求将再次命中网络。
void getData\u twoDifferentRequests\u cacheNotHit()引发异常{
//安排
Cache cacheMock=mock(Cache.class);
inoorder inoorder=Mockito.inoorder(cacheMock);
Repository=newrepositoryimpl(cacheMock);
结果=新结果();
当(cacheMock.containsKey(anyString())。然后回答(调用->false);
当(cacheMock.get(anyString())。然后回答(调用->结果);
单个数据1=rep.getData(“hans”、“wurst”);
单个数据2=rep.getData(“hansX”、“wurstX”);
//行动
data1.test()
.等待
.assertValueAt(0,r->r!=结果);
//验证第一个订阅:保存到缓存
顺序验证(cacheMock,次(2))
.containsKey(anyString());
顺序验证(cacheMock,次(1))
.put(anyString(),any());
//行动
数据2.test()
.等待
.assertValueAt(0,r->r!=结果);
//验证第二个订阅:保存到缓存
顺序验证(cacheMock,次(2))
.包含
public interface PollutionApiService {
@GET("feed/{city}/")
Observable<Aqicn> getPollutionObservable(@Path("city") String city, @Query("token") String token);
}
interface Repository {
Single<Result> getData(String param1, String param2);
}
class RepositoryImpl implements Repository {
private final Cache<String, Result> cache;
private final Function2<String, String, String> calculateKey;
RepositoryImpl(Cache<String, Result> cache) {
this.cache = cache;
this.calculateKey = (s, s2) -> s + s2;
}
@Override
public Single<Result> getData(String param1, String param2) {
Maybe<Result> networkFallback = getFromNetwork(param1, param2, calculateKey).toMaybe();
return getFromCache(param1, param2, calculateKey).switchIfEmpty(networkFallback)
.toSingle();
}
private Single<Result> getFromNetwork(String param1, String param2, Function2<String, String, String> calculateKey) {
return Single.fromCallable(Result::new)
.doOnSuccess(result -> {
if (!cache.containsKey(calculateKey.apply(param1, param2))) {
System.out.println("save in cache");
String apply = calculateKey.apply(param1, param2);
cache.put(apply, result);
}
}) // simulate network request
.delay(50, TimeUnit.MILLISECONDS);
}
private Maybe<Result> getFromCache(String param1, String param2, Function2<String, String, String> calculateKey) {
return Maybe.defer(() -> {
String key = calculateKey.apply(param1, param2);
if (cache.containsKey(key)) {
System.out.println("get from cache");
return Maybe.just(cache.get(key));
} else {
return Maybe.empty();
}
});
}
}
class Result {
}
@Test
// Call getData two times with equal params. First request gets cached. Second request requests from network too, because cash has already expired.
void getData_requestCashed_cashExpiredOnRequest() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hans", "wurst");
// Action
data1.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
data2.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate second Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
}
@Test
// Call getData two times with different params for each request. Values cashed but only for each request. Second request will hit network again due to different params.
void getData_twoDifferentRequests_cacheNotHit() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hansX", "wurstX");
// Action
data1.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
// Action
data2.test()
.await()
.assertValueAt(0, r -> r != result);
// Validate second Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
}
@Test
// Call getData two times with equal params. First request hit network. Second request hits cache. Cache does not expire between two requests.
void getData_twoEqualRequests_cacheHitOnSecond() throws Exception {
// Arrange
Cache<String, Result> cacheMock = mock(Cache.class);
InOrder inOrder = Mockito.inOrder(cacheMock);
Repository rep = new RepositoryImpl(cacheMock);
Result result = new Result();
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> false);
Single<Result> data1 = rep.getData("hans", "wurst");
Single<Result> data2 = rep.getData("hans", "wurst");
// Action
data1.test()
.await();
// Validate first Subscription: save to cache
inOrder.verify(cacheMock, times(2))
.containsKey(anyString());
inOrder.verify(cacheMock, times(0))
.get(anyString());
inOrder.verify(cacheMock, times(1))
.put(anyString(), any());
when(cacheMock.containsKey(anyString())).thenAnswer(invocation -> true);
when(cacheMock.get(anyString())).thenAnswer(invocation -> result);
TestObserver<Result> sub2 = data2.test()
.await()
.assertNoErrors()
.assertValueCount(1)
.assertComplete();
// Validate second subscription: load from cache
inOrder.verify(cacheMock, times(1))
.containsKey(anyString());
inOrder.verify(cacheMock, times(0))
.put(anyString(), any());
inOrder.verify(cacheMock, times(1))
.get(anyString());
sub2.assertResult(result);
}