Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OutOfMemoryError using FirebaseRecyclerViewAdapter

I am building an android app for playing audio books. The audio books meta data (title, author, etc.) are stored in a Firebase database using the following structure:

root
  --- audioBooks
    --- <individual hash for each book>
      --- author: "Ray Bradbury"
      --- title: "Fahrenheit 451"
      --- finished: false
      --- key: <individual hash for each book>
      --- cover
        --- b64Cover: <b64-encoded image>
        --- backgroundColor: -14473711
        --- ...
      --- tracks
        --- 0
          --- title: "Track 1"
          --- duration: 361273
          --- finished: false
          --- currentPosition: 12345
          --- ...
        --- 1
          --- ...
      --- currentTrack
        --- title: "Track 1"
        --- duration: 361273
        --- finished: false
        --- currentPosition: 12345
        --- ...

I have three model classes for AudioBook, AudioBookTrack and AudioBookCover (removed setters and getters for better overview).

public class AudioBook {
    private String title;
    private String author;
    private int duration;
    private boolean finished;
    private String key;
    private AudioBookCover cover;
    private ArrayList<AudioBookTrack> tracks;
    private AudioBookTrack currentTrack;

    public AudioBook() {
    }

    public AudioBook(String title, String author, boolean finished, String key, AudioBookCover cover, ArrayList<AudioBookTrack> tracks, AudioBookTrack currentTrack) {
        this.title = title;
        this.author = author;
        this.finished = finished;
        this.key = key;
        this.cover = cover;
        this.tracks = tracks;
        this.currentTrack = currentTrack;
    }
}

public class AudioBookTrack {
    private String title;
    private String album;
    private String author;
    private String filePath;
    private int duration;
    private int currentPosition;
    private int index;
    private boolean finished;
    private String key;

    public AudioBookTrack() {
    }

    public AudioBookTrack(String title, String album, String author, String filePath, int duration, int currentPosition, int index, boolean finished, String key) {
        this.title = title;
        this.album = album;
        this.author = author;
        this.filePath = filePath;
        this.duration = duration;
        this.currentPosition = currentPosition;
        this.index = index;
        this.finished = finished;
        this.key = key;
    }
}

public class AudioBookCover {
    private String b64Cover;
    private int backgroundColor;
    private int primaryColor;
    private int secondaryColor;
    private int detailColor;

    public AudioBookCover() {
    }

    public AudioBookCover(String b64Cover, int backgroundColor, int primaryColor, int secondaryColor, int detailColor) {
        this.b64Cover = b64Cover;
        this.backgroundColor = backgroundColor;
        this.primaryColor = primaryColor;
        this.secondaryColor = secondaryColor;
        this.detailColor = detailColor;
    }
}

Consuming the data in the Firebase database and casting it to the model classes works without problems. However I now want to list all of the audio books in a RecyclerView using FirebaseRecyclerViewAdapter. This is the code, executed in a fragment's onCreateView() method:

mAudioBooksList = (RecyclerView) view.findViewById(R.id.audiobooks_list);
mAudioBooksList.setHasFixedSize(true);

ref = new Firebase(Constants.FIREBASE_URL).child("audioBooks");

mAdapter = new FirebaseRecyclerViewAdapter<AudioBook, AudioBooksViewHolder>(AudioBook.class, R.layout.audiobook_grid_item, AudioBooksViewHolder.class, ref) {
    @Override
    public void populateViewHolder(AudioBooksViewHolder audioBooksViewHolder, AudioBook audioBook) {
        audioBooksViewHolder.author.setText(audioBook.getAuthor());
        audioBooksViewHolder.title.setText(audioBook.getTitle());
        try {
            byte[] byteArray = Base64.decode(audioBook.getCover().getB64Cover());
            Bitmap coverBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
            audioBooksViewHolder.cover.setImageBitmap(coverBitmap);
        }
        catch(IOException e) {
            Log.e(TAG, "Could not decode album cover: " + e.getMessage());
        }
    }
};
mAudioBooksList.setAdapter(mAdapter);

And this is the ViewHolder:

private static class AudioBooksViewHolder extends RecyclerView.ViewHolder {
TextView author;
TextView title;
ImageView cover;

public AudioBooksViewHolder(View itemView) {
    super(itemView);
    author = (TextView)itemView.findViewById(R.id.audiobook_grid_author);
    title = (TextView)itemView.findViewById(R.id.audiobook_grid_title);
    cover = (ImageView) itemView.findViewById(R.id.audiobook_grid_cover);
}

Now this code generally works (I can see the values being displayed at the screen), however after a very short time, the app crashes due to an out-of-memory error:

Uncaught exception in Firebase runloop (2.4.0). Please report to [email protected]
    java.lang.OutOfMemoryError: Failed to allocate a 35116844 byte allocation with 16773184 free bytes and 32MB until OOM
        at java.lang.StringFactory.newStringFromChars(Native Method)
        at java.lang.AbstractStringBuilder.toString(AbstractStringBuilder.java:629)
        at java.lang.StringBuilder.toString(StringBuilder.java:663)
        ...

I test the app on a physical Nexus 5 (no emulator). I guess this happens because of my nested model (AudioBookTracks inside AudioBooks) which leads to several hundreds of Java objects. Unfortunately I do not know how to overcome this problem. For listing only the audio books I would not need their corresponding audio book tracks, however sice the audio book tracks are child elements of an audio book, they are cast to Java objects as well. On the other side I also tried to limit the number of audio books being shown by using ref.limitToFirst(2). The app then still crashes, but it takes more time until it does.

Do you see any problems with my code that could lead to the problem? Or is there any possibility to only select the values I need from the Firebase database, skipping e.g. the audio book tracks?

Thanks!

like image 869
BGoll Avatar asked Jul 21 '26 02:07

BGoll


1 Answers

The problem is indeed likely caused by the fact that you're loading a lot of unnecessary data about each book. Especially on mobile, you should only load data that you are going to display to the user. Since Firebase always loads an entire node, you will have to model your data in such a way that you can load only the data that you need.

The solution is to separate the track from the other information of an audio book. This is called denormalizing the data and is a very common operation in NoSQL databases.

Denormalized you data would be stored as:

root
  --- audioBooks
    --- <individual hash for each book>
      --- author: "Ray Bradbury"
      --- title: "Fahrenheit 451"
      --- finished: false
      --- key: <individual hash for each book>
      --- cover
        --- b64Cover: <b64-encoded image>
        --- backgroundColor: -14473711
        --- ...
  --- audioBookTracks
    --- <individual hash for each book>
      --- 0
        --- title: "Track 1"
        --- duration: 361273
        --- finished: false
        --- currentPosition: 12345
        --- ...
        --- 1
        --- ...
    --- currentTrack
      --- title: "Track 1"
      --- duration: 361273
      --- finished: false
      --- currentPosition: 12345
      --- ...

The information about a book as well as the tracks are stored under the same id (what you call "individual hash for each book"), but under different top-level nodes. The data about the book itself is under audioBooks, while the list of tracks is under authBookTracks.

Now you can separately load either the information for a book or the tracks for that book with:

ref.child("audioBooks").child(bookId).addValueEventListener(...

or

ref.child("audioBookTracks").child(bookId).addChildEventListener(...

You can probably model the AudioBook Java class to keep its tracks member and not serialize it (using Jackson annotations). But you can also treat a book and its list of tracks as separate top-level entities that just happen to have the same IDs (which is how it's modeled in the database).

like image 159
Frank van Puffelen Avatar answered Jul 22 '26 16:07

Frank van Puffelen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!