2017-04-25 6 views
0

공통 저장소를 사용하는 두 개의 조각이 있습니다.RxJava 및 switchIfEmpty()를 사용한 저장소 데이터 캐시 관리

이 저장소에 대한 캐시 관리 시스템을 구현하려고합니다.

아이디어는 다음과 같습니다 조각 중 하나가로드 , 그것은 getData() 메소드를 호출,이 방법은 getDataFromNetwork()을 사용하여 원격 JSON API에 대한 네트워크 호출을하기 위해선, 결과를 얻을 수 및 (A List<Aqicn>로 캐시에 넣어 내 코드에서 변수 data).

다음 조각이로드됩니다. 60 초가 지난 후 네트워크 호출이 없으면 getDataFromMemory()을 사용하여 데이터가 내 데이터 목록의 캐시에서 직접 가져옵니다.

RxJava Observable.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)); 
    } 
} 

======= 편집 : 최소 ===========

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을 할 때 나는

===== 편집은 문제 발견하지만 해결책은 ==========

발견 ...,하고있어 0

것은 내 생성자 내 pollutionApiService 이니셜입니다. json으로 요청이 사용 단검과는 관찰 가능한을 반환

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

나는이 모든 세부 사항에서 작동하는 방법을 알고하지 않습니다하지만이 같은 interprate. Dagger는 Observable 인 PollutionApiService 공급자를 만듭니다. 내가 할 때 return cachedData Observable Observable이 가입되어 네트워크 콜이 완료되었지만 어떻게 해결할 것인가 전혀 모른다. 사실 내가 return cachedData 네트워크 통화가있을 때마다입니다.

+0

당신이 주어진 PARAMS 모든 요청을 캐시 하시겠습니까? 첫 번째 getData를 city : 'Hamburg'및 auth : X로 호출 한 후 'Berlin', 'X'두 요청을 모두 60 초 동안 캐시해야합니까?현재 구현에서는 각 요청에 대해 캐시에 요소를 추가하기 만하면됩니다. –

+0

두 매개 변수 모두 매개 변수가 동일합니다. – Laurent

답변

1

다음과 같은 클래스로 캐시 동작을 구현했습니다. 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 만 캐시해야합니다 ... 복잡한 libc를 처리하기 위해 구현 된 이후로는 필요하지 않은 것들로 가득 찬 외부 lib를 사용하지 않고 이것을 수행하는 것이 쉽습니다 작업. – Laurent