0

I've written a small test that reproduces my problem. I want to nest cached observables and to reflect all changes done to the underlying data structure being done to the ORIGINAL object, but with nested caching this seems to fail.

Is it possible to solve the problem? I have written what I expect under problem...

Test

public static void test()
{
    // --------------------
    // TEST 1 - simple caching
    // --------------------

    List<Data> emptyData1 = getEmptyData()
            .toBlocking()
            .single();

    L.d("TEST 1a - Size: %d == %d", emptyData1.size(), 0);

    // add 1 empty data
    emptyData1.add(new Data(0, false));
    L.d("TEST 1b - Size: %d == %d (Loaded: %b)", emptyData1.size(), 1, emptyData1.get(0).loaded);

    List<Data> emptyData2 = getEmptyData()
            .toBlocking()
            .single();

    L.d("TEST 1c - Size: %d == %d (Loaded: %b)", emptyData2.size(), 1, emptyData2.get(0).loaded);

    // --------------------
    // TEST 2 - nested caching
    // --------------------

    List<Data> loadedData1 = getLoadedData()
            .toBlocking()
            .single();

    L.d("TEST 2a - Size: %d == %d (Loaded: %b)", loadedData1.size(), 1, loadedData1.get(0).loaded);

    // add a second data, this time we add a loaded data
    loadedData1.add(new Data(1, true));

    List<Data> loadedData2 = getLoadedData()
            .toBlocking()
            .single();

    L.d("TEST 2b - Size: %d == %d (Loaded: %b, %b)", loadedData1.size(), 2, loadedData2.get(0).loaded, loadedData2.get(1).loaded);

    // --------------------
    // TEST 3 - test if empty observable is holding the loaded data as well now
    // --------------------

    List<Data> testEmptyStateData = getEmptyData()
            .toBlocking()
            .single();

    L.d("TEST 3a - Size: %d == %d", testEmptyStateData.size(), 2);

    // I don't expect this, but this will happen
    if (testEmptyStateData.size() == 1)
    {
        L.d("TEST 3b1 - Size: %d == %d (Loaded: %b)", testEmptyStateData.size(), 2, testEmptyStateData.get(0).loaded);
    }
    // I expect this, but it won't be true
    if (testEmptyStateData.size() == 2)
    {
        L.d("TEST 3b - Size: %d == %d (Loaded: %b, %b)", testEmptyStateData.size(), 2, testEmptyStateData.get(0).loaded, testEmptyStateData.get(1).loaded);
    }
}

Result

TEST 1a - Size: 0 == 0
TEST 1b - Size: 1 == 1 (Loaded: false)
TEST 1c - Size: 1 == 1 (Loaded: false)

TEST 2a - Size: 1 == 1 (Loaded: true)
TEST 2b - Size: 2 == 2 (Loaded: true, true)

TEST 3a - Size: 1 == 2 // <= UNDESIRED RESULT!!!
TEST 3b1 - Size: 1 == 2 (Loaded: true) // <= at least the object in the list is correct! But I would expect that the empty observalbe would hold 2 items, both loaded! "Test 3b2" should be printed instead

Problem

I would expect that all observables will just pass the ORIGINAL List down the stream and that I always get the original object and that all my changes to this object will be reflected in the base list and therefore at the end, the empty observable should return 2 loaded data items, but that is not true for this nested caching scenario.

What I need

  • each operation should only run once! No matter if someone is subscribed to an observable or not! That's whyI decided to use cache()
  • observables must be shareable and must be reused

Code

Helper functions and data

private static  Observable<List<Data>> mEmptyObservable = null;
private static  Observable<List<Data>> mLoadedObservable = null;

public static Observable<List<Data>> getEmptyData()
{
    if (mEmptyObservable == null)
    {
        // simple test data
        List<Data> values = new ArrayList<>();
        mEmptyObservable = Observable.just(values)
                // cache and share observable
                .cache().replay().refCount();
    }
    return mEmptyObservable;
}

public static Observable<List<Data>> getLoadedData()
{
    if (mLoadedObservable == null)
    {
        mLoadedObservable = getEmptyData()
                .flatMap(new Func1<List<Data>, Observable<Data>>()
                {
                    @Override
                    public Observable<Data> call(List<Data> datas)
                    {
                        return Observable.from(datas);
                    }
                })
                .map(new Func1<Data, Data>()
                {
                    @Override
                    public Data call(Data data)
                    {
                        data.load();
                        return data;
                    }
                })
                .toList()
                // cache and share observable
                .cache().replay().refCount();
    }
    return mLoadedObservable;
}

Data class

static class Data
{
    int index;
    boolean loaded;

    public Data(int index, boolean processed)
    {
        this.index = index;
        this.loaded = processed;
    }

    public void load()
    {
        if (!loaded)
        {
            // do some have operation... once only per data!
        }
        loaded = true;
    }
}
prom85
  • 16,896
  • 17
  • 122
  • 242

1 Answers1

1

You're using both cache() and replay(). Try something like

private static  Observable<List<Data>> mEmptyObservable = null;
private static  Observable<List<Data>> mLoadedObservable = null;

public static Observable<List<Data>> getEmptyData()
{
    if (mEmptyObservable == null)
    {
        // simple test data
        List<Data> values = new ArrayList<>();
        mEmptyObservable = Observable.just(values)
                // share and replay observable
                .share().replay();
    }
    return mEmptyObservable;
}

public static Observable<List<Data>> getLoadedData()
{
    if (mLoadedObservable == null)
    {
        mLoadedObservable = getEmptyData()
                .flatMap(new Func1<List<Data>, Observable<Data>>()
                {
                    @Override
                    public Observable<Data> call(List<Data> datas)
                    {
                        return Observable.from(datas);
                    }
                })
                .map(new Func1<Data, Data>()
                {
                    @Override
                    public Data call(Data data)
                    {
                        data.load();
                        return data;
                    }
                })
                .toList()
                // share and replay observable
                .share().replay();
    }
    return mLoadedObservable;
}

There's more information in this answer.

Edit: The real issue however seems to be that you're creating a different list in getLoadedData(). If you want the same list try something like

public static Observable<List<Data>> getLoadedData()
{
    if (mLoadedObservable == null)
    {
        mLoadedObservable = getEmptyData().
                .map(new Func1<List<Data>, List<Data>>()
                {
                    @Override
                    public Data call(List<Data> data)
                    {
                        for (Data item : data) {
                           item.load();
                        }
                        return data;
                    }
                })
                // share and replay observable
                .share().replay();
    }
    return mLoadedObservable;
}
Community
  • 1
  • 1
JohnWowUs
  • 3,053
  • 1
  • 13
  • 20
  • I've read the link now and it seems like `replay().autoConnect()` could be a solutuion for what I need (I need caching, I want each step to be executed only once + I need sharing of observables), but this will lead to the same result as the solution I posted. Your suggestion won't execute and block in first call to `getEmptyData()` already. Based on your suggestion I tried using `share().replay().autoConnect()` for caching and sharing (that's what you wanted to suggest I think), but this again results in the same result as the solution I posted. – prom85 Nov 22 '16 at 10:01
  • You're right about including `autoConnect`. You might still have an ordering issue though. Have you tried `share().autoConnect().replay()`? – JohnWowUs Nov 22 '16 at 10:39
  • `share()` does not return a `ConnectableObserver`... So no, as it is not possible. I tried everything I could think of, most of the time I get the same result... Only solution I can think of is a custom data structure that handles the caching (I have used something like this in the past already) but I think this should not be necessary... – prom85 Nov 22 '16 at 10:42
  • 1
    The problem isn't the caching as far as I can see. `getLoadedData()` is creating a new list. It's not the same as the one created and cached in `getEmptyData()` even if they share elements in common. – JohnWowUs Nov 22 '16 at 11:48
  • That's true and I'm aware of this, but I think this is only a problem if I would create `Observable` observables, but I'm not doing so. I observe a list as one single object. So the emitted items should be the same, shouldn't they? – prom85 Nov 22 '16 at 11:55
  • You should put something about the problem into your answer, than I can accept it. Thanks for helping, I just did not see this problem... – prom85 Nov 22 '16 at 13:56