【问题标题】:Repository data cache management with RxJava and switchIfEmpty()使用 RxJava 和 switchIfEmpty() 管理存储库数据缓存
【发布时间】:2017-04-26 12:04:00
【问题描述】:

我有两个使用公共存储库的片段。

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

这个想法是: 其中一个片段被加载,它调用getData()方法,该方法使用getDataFromNetwork()对远程JSON Api进行网络调用,获取结果并将其作为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 时,网络调用就完成了......

===== EDIT 发现问题但未找到解决方案 ==========

事情是在我的构造函数中,我初始化了我的污染ApiService。 这对 JSON 请求使用 Dagger 并返回一个 Observable :

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

我不知道这一切的细节是如何运作的,但我是这样解释的。 Dagger 创建一个 PollutionApiService 提供者,它是一个 Observable。当我做return cachedData 时,这个 Observable 被订阅了,所以网络调用完成了......但不知道如何修复它。事实上,每次我发出return cachedData 时,都会有一个网络调用。

【问题讨论】:

  • 你想缓存给定参数的所有请求吗?如果您首先使用 city: 'Hamburg' 和 auth: X 调用 getData,然后再调用 'Berlin'、'X',这两个请求是否都应该缓存 60 秒?在您当前的实现中,您只需为每个请求添加元素到缓存中。
  • 我的两个调用中的参数都是一样的。

标签: android caching rx-java repository-pattern


【解决方案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);
}

【讨论】:

  • 感谢您的回答,但我只需要缓存 10 个整数的 Arraylist ......应该很容易做到这一点,而无需使用充满我不需要的东西的外部库,因为它是用于处理更复杂的缓存任务。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-15
  • 1970-01-01
  • 1970-01-01
  • 2017-02-26
  • 1970-01-01
  • 2017-08-19
相关资源
最近更新 更多