1

Please look at the picture beforehand. Imagine I've got the following methods:

Observable<FootballPlayer> loadPlayersForTeam(int teamId)

for example, Barcelona FC has a teamId = 13213, so loadPlayersForTeam(13213) will return an Observable that emits FootballPlayers objects: FootballPlayer("Messi", 10), FootballPlayer("Suarez", 9), etc.

This observable will eventually emit every football player of the current squad of Barcelona FC. This corresponds to the regions #1 + #2 at the picture.

And then I've got another methods:

Observable<Coach> loadCoachesForTeam(int teamId)
Single<FootballPlayer> loadPlayer(int playerId)

I hope the names of the object speak for themselves. And here's the thing: there's favoritePlayerId field under class Coach, so effectively every Coach maps to some playerId. For example, Coach('Pep Guardiola') will have .favoritePlayerId = 9 which stands for Messi (note that we could have downloaded FootballPlayer("Messi", 10) from Observable loadPlayersForTeam(int teamId).

And it may happen that Coach('Ronald Koeman').favoritePlayerId = 99 and there's no FootballPlayer with .favoritePlayerId = 99 under current squad of Barcelona FC (so we won't get it using loadPlayersForTeam()) and because we're really huge supporters and we have to use loadPlayer(99) to get that old FootballPlayer. A region of players that we can download using loadCoachesForTeam() and then calling loadPlayer() for each emitted coach is region #3.

Here's a question: FootballPlayer objects are really heavy, that's why we don't want to call loadPlayerSingle(int playerId) for region #2 and only to call for region #3, for example: we don't want to call loadPlayer(9) – load Messi using this method, cause we've already received him in the observable from loadPlayersForTeam().

What I want is to return a new Observable<FootballPlayer> – it should emit both current + favorites of the coaches (note: FootballPlayers themselves and not their Ids only) and only call loadPlayer() for the Ids which are not in the current squad, cause these calls are really expensive: if my playerIds from FootballPlayer objects from loadPlayersForATeam are 9, 10, 21, 1 and the .favoritePlayerId of loadCoachesForTeam() are 1, 3, 44, 5, 1 I want to make sure I won't call loadPlayer(1): again, because we can get it from the 1st observable (which is loadPlayersForTeam(Barcelona.teamId)). The ids in the 1st observable is unique, and you can't say so about the 2nd one (loadCoachesForTeam(Barcelona.teamId). So what I tried is to use distinct() and mergeWith() for this 2 observables. But distinct() only operates on the prefix of the events so far and doesn't wait for the observable to comlpete: it may happen that we get 1 from the 2nd observable.

How can I call distinct for the whole Observable and then add there some more values without using Subjects (call loadPlayer() for region #3 only and take FootballPlayers from #2 from the 1st observable)?

Thanks.

Alex Kokorin
  • 459
  • 5
  • 15
  • You can't solve this without Subjects. You can use [this subject caching](https://stackoverflow.com/a/34545490/61158) mechanism and do some `flatMapping` with it. – akarnokd Jan 10 '18 at 20:27
  • OK, could you give me a high level template for solving this problem then? – Alex Kokorin Jan 10 '18 at 20:55

1 Answers1

0

You can use this subject caching mechanism and do some flatMapping with it. On a high level, I'd do the following:

RxCache<Integer, Observable<FootballPlayer>> cache = 
    new RxCache<>(id -> loadPlayer(id).toObservable());

Observable<Integer> teamIds = ...

teamIds
.flatMap(team -> 
    Observable.concat(
       loadPlayersForTeam(team)
           doOnNext(player -> cache.putIfAbsent(player.id, player))
       ,
       loadCoachesForTeam(team)
           .flatMap(coach -> cache.get(coach.favoritePlayerId)),
)
.subscribe(player -> { /* ... */ });

Where RxCache is:

public class RxCache<K, V> {

    final ConcurrentHashMap<K, AsyncSubject<V>> cache;
    final Func1<K, Observable<V>> valueGenerator;

    public RxCache(Func1<K, Observable<V>> valueGenerator) {
        this.valueGenerator = valueGenerator;
        this.cache = new ConcurrentHashMap<>();
    }

    public Observable<V> get(K key) {
        AsyncSubject<V> o = cache.get(key);
        if (o != null) {
            return o;
        }
        o = AsyncSubject.create();
        AsyncSubject<V> p = cache.putIfAbsent(key, o);
        if (p != null) {
            return p;
        }
        valueGenerator.call(key).subscribe(o);
        return o;
    }

    public void putIfAbsent(K key, V value) {
        if (!cache.contains(key)) {
            AsyncSubject<V> subj = AsyncSubject.create();
            subj.onNext(value);
            subj.onCompleted();
            cache.putIfAbsent(key, subj);
        }
    }
}
akarnokd
  • 69,132
  • 14
  • 157
  • 192
  • Well, thanks, but it's not right in the logical way: I want to call loadPlayersForTeam() for each team first (main thing) and then merge it with loadCoachesForTeam(team) -> coach.favoritePlayerId (we don't have coach.favPlayer as a FootballPlayer, it's playerId). And if there's no such id (player with such id in loadPlayersForTeam() to be precise) then we'll call loadPlayer(playerId) cause we want to output the stream of players. Hope it makes sense. – Alex Kokorin Jan 11 '18 at 06:06
  • Updated the answer. – akarnokd Jan 11 '18 at 08:34