1

I have a problem with RxJava. I have a getAll() method, it's returning with a list. It's serves data from Room database.

@Query("SELECT * from decks ORDER BY id ASC")
fun getAll(): Flowable<List<DeckEntity>>

DeckEntity have an id and a name field.

I created an another class called it PrepareItem, because I want to box it with more parameter. (It will be an Adapter model) Check it:

data class PrepareItem (
    var deckEntity: DeckEntity,
    var countsOfCards: Int
)

So, I want to call the getAll() and I want to map it to PrepareItem. It's working yet.

deckRepository.getAll()
                .map {
                    it.map {
                        PrepareItem(it,0)
                    }
                }

But, there is the countsOfCards is equal 0. I want to make an another repository call, to get the value, and set it. Important! Every value need a single call to repository. So if I have 5 item, than I need to wait until 5 another call finished.

I tried, but I get confused. (CODE UPDATED)

fun mapper(item: DeckEntity) : Single<PrepareItem> {
    return cardRepository.getDueDatedCardsFromDeck(deckId = item.id!! /*TODO !!*/)
            .map {
                PrepareItem(item, it.size)
            }
}

val call = deckRepository.getAll()
                .flatMapIterable { item->item }
                .flatMapSingle {
                    mapper(it)
                }.toList()
                .toObservable()
                ...

The onError or onComplete never called. Why?

Anyone have good idea to How to do it? I want it keep out from repository. Thank you!

UPDATE:

Solution:

Create a new class

class DeckWithCards {

    @Embedded
    lateinit var deckEntity: DeckEntity

    @Relation(
            entity = CardEntity::class,
            entityColumn = "deckId",
            parentColumn = "id")
    lateinit var cards: List<CardEntity>

}

Add new fun to DeckDao

@Query("SELECT * from decks ORDER BY id ASC")
fun getAllWithCards(): Flowable<List<DeckWithCards>>

Thats all its works! Thank you for the answer. It helped me a lot !

4

1 回答 1

2

You should consider throwing the hard stuff on database.

Consider the following entities:

CardEntity that contains informations about particular card. It also ensures that in single deck can exist one card at given name.

@Entity(
  tableName = "cards",
  indices = {
    @Index(value = { "name", "deck_id" }, unique = true)
  }
)
public class CardEntity {

  //region Column Definitions

  @PrimaryKey
  @ColumnInfo(name = "id")
  private Long id;

  @NonNull
  @ColumnInfo(name = "name")
  private String name = "";

  @NonNull
  @ColumnInfo(name = "deck_id")
  private Long deckId = 0L;

  //endregion

  //region Getters and Setters

  (...)

  //endregion
}

DeckEntity that contains informations about particular deck.

@Entity(tableName = "decks")
public class DeckEntity {

  //region Column Definitions

  @PrimaryKey
  @ColumnInfo(name = "id")
  private Long id;

  @NonNull
  @ColumnInfo(name = "name")
  private String name = "";

  //endregion

  //region Getters and Setters

  (...)

  //endregion
}

DeckWithCardsView is a projection that combines together both DeckEntity, collection of CardEntity and some metadata (in this case - a number of cards in this deck).

public class DeckWithCardsView {

  //region Deck

  @Embedded
  private DeckEntity deck;

  public DeckEntity getDeck() {
    return deck;
  }

  public void setDeck(DeckEntity deck) {
    this.deck = deck;
  }

  //endregion

  //region Cards

  @Relation(
    entity = CardEntity.class,
    entityColumn = "deck_id",
    parentColumn = "id")
  private List<CardEntity> cards = new ArrayList<>();

  public List<CardEntity> getCards() {
    return cards;
  }

  public void setCards(List<CardEntity> cards) {
    this.cards = cards;
  }

  //endregion

  //region Cards count

  private Integer count = 0;

  public Integer getCount() {
    return count;
  }

  public void setCount(Integer count) {
    this.count = count;
  }

  //endregion
}

A complete Dao code can looks like:

@Dao
public abstract class DeckDao {

  //region Deck

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  public abstract void insertDeck(DeckEntity entity);

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  public abstract void insertDecks(List<DeckEntity> entities);

  //endregion

  //region Card

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  public abstract void insertCard(CardEntity entity);

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  public abstract void insertCards(List<CardEntity> cards);

  //endregion

  //region Deck with Cards

  @Transaction
  @Query(
    "SELECT D.id as id, D.name as name, count(C.id) as count "
      + "FROM decks D "
      + "INNER JOIN cards C "
      + "WHERE C.deck_id = D.id "
      + "GROUP BY deck_id")
  public abstract Flowable<List<DeckWithCardsView>> getAll();

  //endregion
}

It implements the following features:

  • adding single deck to the database,
  • adding a collection of decks to the database,
  • adding single card to the database,
  • adding a collection of cards to the database,
  • retrieving a list of above mentioned projection from database.

The most important part is in the Sqlite query code. Lines by line:

SELECT D.id as id, D.name as name, count(C.id) as count

It defines from which exact columns we expect the query will return values from database. Especially:

  • D.id as id - it will return id from column aliased as D (decks),
  • D.name as name - it will return name from column aliased as D (decks),
  • count(C.id) as count - it will return a number of ids retrieved from column aliased as C (cards).

FROM decks D INNER JOIN cards C WHERE C.deck_id = D.id

It defines we would like to retrieve values from tables decks aliased as D and cards aliased as C and defines the relation on CardEntity's deck_id to DeckEntity's id columns.

GROUP BY deck_id

Thanks to grouping we can easily do count(C.id) to retrieve a number of cards returned for every single deck.

Since we also used a @Embedded annotation in DeckWithCardsView - corresponding (aliased) columns will be matched to correct fields in embedded DeckEntity deck field.

Database Manager

Having prepared such entities, projection and dao - the database manager can be implemented as simple as:

public Completable saveAllDecks(@Nullable List<DeckEntity> decks) {
  return Completable.fromAction(
    () -> database
      .deckDao()
      .insertDecks(decks)
  ).subscribeOn(Schedulers.io());
}

public Completable saveAllCards(@Nullable List<CardEntity> cards) {
  return Completable.fromAction(
    () -> database
      .deckDao()
      .insertCards(cards)
  ).subscribeOn(Schedulers.io());
}

public Flowable<List<DeckWithCardsView>> getDecksWithCards() {
  return database
    .deckDao()
    .getAll()
    .subscribeOn(Schedulers.io());
}

Example data:

I've also prepared a sample code that creates five complete decks (all suits and ranks in every deck).

private static final Long[] DECK_IDS = {
  1L, 2L, 3L, 4L, 5L
};

private static final String[] DECK_NAMES = {
  "Deck 1", "Deck 2", "Deck 3", "Deck 4", "Deck 5"
};

Completable prepareDecks() {
  final Observable<Long> ids = Observable.fromArray(DECK_IDS);
  final Observable<String> names = Observable.fromArray(DECK_NAMES);

  final List<DeckEntity> decks = ids.flatMap(id -> names.map(name -> {
    final DeckEntity entity = new DeckEntity();
    entity.setId(id);
    entity.setName(name);
    return entity;
  })).toList().blockingGet();

  return cinemaManager.saveDecks(decks);
}

It creates a list of ids and deck names and creates a product of such tables. Flat mapping such product results in a complete list of five deck entities.

private static final String[] CARD_SUITS = {
  "diamonds", "hearts", "spades", "clubs"
};

private static final String[] CARD_RANKS = {
  "Two of ",
  "Three of ",
  "Four of ",
  "Five of ",
  "Six of ",
  "Seven of",
  "Eight of ",
  "Nine of ",
  "Ten of ",
  "Jack of ",
  "Queen of ",
  "King of ",
  "Ace of "
};

Completable prepareCards() {
  final Observable<Long> decks = Observable.fromArray(DECK_IDS);
  final Observable<String> suits = Observable.fromArray(CARD_SUITS);
  final Observable<String> ranks = Observable.fromArray(CARD_RANKS);

  final List<CardEntity> cards = decks.flatMap(deck -> suits.flatMap(suit -> ranks.map(rank -> {
    final CardEntity entity = new CardEntity();
    entity.setName(String.format("%s %s", rank, suit));
    entity.setDeckId(deck);
    return entity;
  }))).toList().blockingGet();

  return cinemaManager.saveCards(cards);
}

Using similar approach I've created a complete collection of cards for previously prepared decks.

Final touches:

Hard job has been done. The easy part left:

private Completable prepareData() {
  return prepareDecks().andThen(prepareCards());
}

void observeDecksWithCards() {
  disposables.add(
    prepareData()
      .andThen(deckManager.getDecksWithCards())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(
        this::handleObserveDecksWithCardsSuccess,
        this::handleObserveDecksWithCardsError));
}

private void handleObserveDecksWithCardsSuccess(@NonNull List<DeckWithCardsView> decks) {
  Timber.v("Received number of decks: %d", decks.size());
}

private void handleObserveDecksWithCardsError(@NonNull Throwable throwable) {
  Timber.e(throwable.getLocalizedMessage());
}

Result:

As a result we have two tables. For decks and cards. Subscribing to getDecksWithCards gives access to a collection of decks. With complete collection of cards and number of cards (calculated by database).

enter image description here

Remember - since the relation to table with cards has been made, you do not need to use the count sqlite method. You can just use getCards().getSize() in DeckWithCardsView projection. This will simplify the code significantly.

于 2018-07-24T15:31:18.537 回答