0

I'm following ViewModel implementation guidelines as per Android development team provided. However, I'm getting unexpected results. I have been digging the internet but no luck.

BookMarketPojo.java

class BookMarketPojo {
private String bookTitle;
private String bookAuthor;
private String bookDescription;
private String bookPictureUrl;
private String ownerId;
private boolean sold;

public BookMarketPojo() {
}

public BookMarketPojo(String bookTitle, String bookAuthor, String bookDescription, String bookPictureUrl, String ownerId, boolean sold) {
    this.bookTitle = bookTitle;
    this.bookAuthor = bookAuthor;
    this.bookDescription = bookDescription;
    this.bookPictureUrl = bookPictureUrl;
    this.ownerId = ownerId;
    this.sold = sold;
}

// getters and setters
}

BookViewModel.java

public class BookViewModel extends ViewModel {
private List<BookMarketPojo> booksList = new ArrayList<>();

private final String NODE_NAME = "_books_for_sale_";

private FirebaseDatabase db = FirebaseDatabase.getInstance();

public MutableLiveData<List<BookMarketPojo>> getData() {
    fetchData();
    MutableLiveData<List<BookMarketPojo>> data = new MutableLiveData<>();
    data.setValue(booksList);
    return data;
}

private void fetchData() {
    db.getReference(NODE_NAME).addChildEventListener(new ChildEventListener() {
        @Override
        public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
            booksList.add(dataSnapshot.getValue(BookMarketPojo.class));
        }

        @Override
        public void onChildChanged(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        }

        @Override
        public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {

        }

        @Override
        public void onChildMoved(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        }

        @Override
        public void onCancelled(@NonNull DatabaseError databaseError) {

        }
    });
}
}

BookMarketAdapter.java

public class BookMarketAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private Context context;
private List<BookMarketPojo> bookMarketPojos;

public BookMarketAdapter(Context context, List<BookMarketPojo> bookMarketPojos) {
    this.context = context;
    this.bookMarketPojos = bookMarketPojos;
}

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View itemView = LayoutInflater.from(context).inflate(R.layout.book_market_row, parent, false);
    return new ViewHolder(itemView);
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
    final BookMarketPojo book = bookMarketPojos.get(position);

    ((ViewHolder) viewHolder).title.setText(book.getBookTitle());
    ((ViewHolder) viewHolder).author.setText(book.getBookAuthor());
    ((ViewHolder) viewHolder).description.setText(book.getBookDescription());
    Picasso.get().load(book.getBookPictureUrl()).into(((ViewHolder) viewHolder).bookImg);
}

@Override
public int getItemCount() {
    return bookMarketPojos.size();
}

private class ViewHolder extends RecyclerView.ViewHolder {
    TextView title, author, description;
    ImageView bookImg;

    public ViewHolder(View convertView) {
        super(convertView);
        title = convertView.findViewById(R.id.tv_title);
        author = convertView.findViewById(R.id.tv_author);
        description = convertView.findViewById(R.id.tv_description);
        bookImg = convertView.findViewById(R.id.iv_bookImg);
    }
}
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MarketActivity";

private BookMarketAdapter myAdapter;
private RecyclerView recyclerView;

private BookViewModel bookViewModel;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    recyclerView = findViewById(R.id.rv_bookMarket);

    bookViewModel = new ViewModelProvider(this).get(BookViewModel.class);

    bookViewModel.getData().observe(this, books -> {
        myAdapter.notifyDataSetChanged();
    });

    initRecyclerView();

}

public void initRecyclerView(){
    myAdapter = new BookMarketAdapter(this, bookViewModel.getData().getValue());
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(myAdapter);
}
}

Expected result: App on launch opens MainActivity and displays the books inside a recycler view.

Actual Result: App on launch opens MainActivity displays no content. When clicked return app closes. When clicked on app icon to open it launches into displaying MainActivity showing duplicated dataset of books.

Link to gif showing the outcome.

Can anyone spot where is the problem? Why onCreate does not initialize the adapter on the first go? And why I get a duplicated list of books?

Link to Git repository

Martin
  • 411
  • 8
  • 21

1 Answers1

1

Let's breakdown what you've written and how LiveData works from your questions shall we?

First, Your adapter looks fine (mostly) and it isn't the problem here.

Let's look at the ViewModel and what's wrong here, read my comments inline for the solutions:

public class BookViewModel extends ViewModel {

    private List<BookMarketPojo> booksList = new ArrayList<>();

    private final String NODE_NAME = "_books_for_sale_";

    private FirebaseDatabase db = FirebaseDatabase.getInstance();

    /** 
    * The purpose of livedata is to simply emit events. 
    * Therefore this can be simply written as [note the general convention used in naming]
    */
    public MutableLiveData<List<BookMarketPojo>> onBookListUpdated = MutableLiveData<List<BookMarketPojo>>();

    /** 
    * The problem with this code, is that when you set the value initially, you only set an empty arraylist. 
    * Remember, your LiveData is only a way to emit changes to a data (the data here being booksList) with the safetynet of LifeCycle Awareness
    * therefore, to fetch the data, we only need to do this. 
    */
    public void getData() {
        fetchData()
    }

    private void fetchData() {
        db.getReference(NODE_NAME).addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

                //To remove duplicates, make sure the book isn't already present in the list. Exercise is left to you
                booksList.add(dataSnapshot.getValue(BookMarketPojo.class));

                //This will now trigger the livedata, with the list of the books.
                onBookListUpdated.setValue(booksList)

            }

            @Override
            public void onChildChanged(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

            }

            @Override
            public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {

            }
        });
    }
}

and finally in your Activity's OnCreate()

 bookViewModel.onBookListUpdated.observe(this, books -> {
        myAdapter.notifyDataSetChanged();
   });

Additionally, have a look at the Firebase Realtime DB Documentation for Reading Data Once since I believe that fits your usecase better of just fetching books once!

Rajiv Rajan
  • 151
  • 4
  • This produces a run time error as onBookListUpdated.getValue().size() produces a null pointer exception. – Martin Mar 16 '20 at 14:19
  • @martin you would have to add a Null check. Java will throw NullPointerExceptions when a value can be null and you are trying to access it. – Rajiv Rajan Mar 18 '20 at 04:53
  • Programming never stops me to amaze. After couple of days of frustration and desperation, this has finally been sorted. And as always... the issue was always in an open field. Thank you for point this out to me. I believe I have not taken enough time to understand what LiveData objects are. – Martin Mar 18 '20 at 16:01
  • Happy to help @Martin! I highly recommend looking at the google codelabs to understand more. Also, perhaps, a good time to look at Kotlin for Android! – Rajiv Rajan Mar 19 '20 at 18:43
  • Why Kotlin? Isn't it just another language to achieve the same thing? Also not looking forward to the Android as this is just an assignment work. – Martin Mar 19 '20 at 18:54
  • @Martin there are a lot of reasons why Kotlin over Java, first and foremost, is that Android's official language is Kotlin. It has other useful features as well. There are loads of articles online on this, which I will leave to you for reading. – Rajiv Rajan Mar 21 '20 at 05:47