2017-04-25 88 views
0

我有两个使用公共存储库的片段。使用RxJava和switchIfEmpty管理存储库数据缓存()

我正在尝试为此存储库实现缓存管理系统。

的理念是: 其中一个片段被加载,它调用getData()方法,这个方法使使用getDataFromNetwork()网络调用远程JSON API时,得到的结果,并把它放在高速缓存为List<Aqicn>(中data变量在我的代码中)。

下一个片段被加载。如果它发生在60秒之前,那么没有网络呼叫,数据直接来自我的数据列表中的缓存,使用getDataFromMemory()

RxJava Observable.switchIfEmpty()用于了解Observable(my 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)); 
    } 
} 

=======编辑:我简化了我的代码,以最小===========

public class CommonRepository implements Repository { 
    private PollutionApiService pollutionApiService; 
    private static Observable<Aqicn> cachedData = null; 


    public CommonRepository(PollutionApiService pollutionApiService) { 
     this.pollutionApiService = pollutionApiService; 
    } 

    @Override 
    public Observable<Aqicn> getDataFromNetwork(String city, String authToken) { 
     Observable<Aqicn> aqicn = pollutionApiService.getPollutionObservable(city, authToken); 
     cachedData = aqicn; 
     return aqicn; 
    } 

    @Override 
    public Observable<Aqicn> getData(String city, String authToken) { 
     if(cachedData == null) { 
      return getDataFromNetwork(city, authToken); 
     } 
     return cachedData; 
    } 
} 

而就意识到,不管是什么我这样做,当我做return cachedData网络通话后...

=====编辑发现的问题,但没有解决发现==========

在我的构造函数中,我启动了我的污染服务。 这种使用匕首的JSON请求,并返回一个可观察:

public interface PollutionApiService { 
    @GET("feed/{city}/") 
    Observable<Aqicn> getPollutionObservable(@Path("city") String city, @Query("token") String token); 
} 

我不知道这一切是如何工作的细节,但我interprate这样。 Dagger创建一个可观察的PollutionApiService提供者。当我做return cachedData这Observable订阅,所以网络通话已完成...但不知道如何解决它。事实是,我每次做return cachedData都有网络电话。

+0

你希望缓存给定PARAMS所有的请求?如果你打电话给城市第一个getData:'Hamburg'和auth:X,然后'Berlin','X',这两个请求是否应该被缓存60秒?在您当前的实现中,您只需将每个请求的元素添加到缓存中。 –

+0

这两个调用中的参数都相同。 – Laurent

回答

1

我用下面的类实现了缓存行为。

为了使用Cache类,则需要以下依赖性:https://cache2k.org/docs/1.0/user-guide.html#android

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); 
} 
+0

谢谢你的回答,但我只需要缓存10个整数的Arraylist ......应该很容易做到这一点,而不需要使用一个外部库,它充满了我不需要的东西,因为它被实现来处理更复杂的缓存任务。 – Laurent