2017-04-25 55 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