I have nested fragments with ViewPager2 and Tabs, and I’m loading data into RecyclerView
with MutableLiveData
. Everything works fine till I update something on my Firebase Realtime Database (eg. name of some food item). So if I have 10 category items with each having 5 food items, and I update name of 1 food, my screen flickers and 10 new categories are added with each having 5 food items and now I have total 20 categories..
Desired behaviour would be: Update data, no screen flickers, just updating changed item WITHOUT adding all that categories and food lists all over again
So how could I achieve that my MutableLiveData would update just changed item, not whole list?
ViewModel
public class MenuViewModel extends ViewModel implements ICategoryCallbackListener, IFoodCallbackListener { private MutableLiveData<String> messageError = new MutableLiveData<>(); private MutableLiveData<List<CategoryModel>> categoryListMutable; private ICategoryCallbackListener categoryCallbackListener; private MutableLiveData<List<FoodModel>> foodListMutable; private IFoodCallbackListener foodCallbackListener; public MenuViewModel() { categoryCallbackListener = this; foodCallbackListener = this; } public MutableLiveData<List<CategoryModel>> getCategoryListMutable() { if(categoryListMutable == null) { categoryListMutable = new MutableLiveData<>(); messageError = new MutableLiveData<>(); loadCategories(); } return categoryListMutable; } public MutableLiveData<List<FoodModel>> getFoodListMutable(String key) { if(foodListMutable == null) { foodListMutable = new MutableLiveData<>(); messageError = new MutableLiveData<>(); loadFood(key); } return foodListMutable; } public void loadCategories() { List<CategoryModel> tempList = new ArrayList<>(); DatabaseReference categoryRef = FirebaseDatabase.getInstance() .getReference(Common.RESTAURANT_REF) .child(Common.currentRestaurant.getUid()) .child(Common.CATEGORY_REF); categoryRef.keepSynced(true); categoryRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { for(DataSnapshot itemSnapShot: snapshot.getChildren()) { CategoryModel categoryModel=itemSnapShot.getValue(CategoryModel.class); if(categoryModel != null) categoryModel.setMenu_id(itemSnapShot.getKey()); tempList.add(categoryModel); } categoryCallbackListener.onCategoryLoadSuccess(tempList); } @Override public void onCancelled(@NonNull DatabaseError error) { categoryCallbackListener.onCategoryLoadFailed(error.getMessage()); } }); } public void loadFood(String key) { List<FoodModel> tempList = new ArrayList<>(); DatabaseReference foodRef = FirebaseDatabase.getInstance() .getReference(Common.RESTAURANT_REF) .child(Common.currentRestaurant.getUid()) .child(Common.CATEGORY_REF) .child(key) .child(Common.FOOD_REF); foodRef.keepSynced(true); foodRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { for(DataSnapshot itemSnapShot: snapshot.getChildren()) { FoodModel foodModel = itemSnapShot.getValue(FoodModel.class); tempList.add(foodModel); } foodCallbackListener.onFoodLoadSuccess(tempList); } @Override public void onCancelled(@NonNull DatabaseError error) { foodCallbackListener.onFoodLoadFailed(error.getMessage()); } }); } public MutableLiveData<String> getMessageError() { return messageError; } @Override public void onCategoryLoadSuccess(List<CategoryModel> categoryModels) { categoryListMutable.setValue(categoryModels); } @Override public void onCategoryLoadFailed(String message) { messageError.setValue(message); } @Override public void onFoodLoadSuccess(List<FoodModel> foodModels) { foodListMutable.setValue(foodModels); } @Override public void onFoodLoadFailed(String message) { messageError.setValue(message); }
MenuFragment
public class MenuFragment extends Fragment { public static final String ARG_MENU = "menu"; private MenuViewModel menuViewModel; //Irrelevant code MyFoodListAdapter adapter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { menuViewModel = new ViewModelProvider(this).get(MenuViewModel.class); View root = inflater.inflate(R.layout.fragment_menu, container, false); //Irrelevant code return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { Bundle args = getArguments(); menuViewModel.getFoodListMutable(Objects.requireNonNull(args) .getString(ARG_MENU)) .observe(getViewLifecycleOwner(), foodModels -> { adapter = new MyFoodListAdapter(getContext(), foodModels); recycler_menu.setAdapter(adapter); }); } }
CategoryModel
public class CategoryModel { private String menu_id, name, image, background; private Long numberOfOrders; List<FoodModel> foods;//Setters and Getters}
Advertisement
Answer
If you attach a ValueEventListener
to a location, you get called with a snapshot of all data at that location each time anything is modified under it.
Your onDataChange
adds the items in the snapshot to tempList
whenever that happens. So on the initial load it adds the 10 categories. Then when there’s a change, it adds them again and you end up with 20 categories.
The simplest way to get rid of the duplicate items, is to clear the list before adding the items to it:
categoryRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { tempList.clear(); for(DataSnapshot itemSnapShot: snapshot.getChildren()) { CategoryModel categoryModel=itemSnapShot.getValue(CategoryModel.class); if(categoryModel != null) categoryModel.setMenu_id(itemSnapShot.getKey()); tempList.add(categoryModel); } categoryCallbackListener.onCategoryLoadSuccess(tempList); }
This gets rid of the duplicates, but will probably still result in some flicker as you’re forcing Android to repaint the entire list. If you also want to get rid of that, consider using addChildEventListener
. With that type of listener you get notified of the changes to the individual child node, and can use that information to perform a minimal update to tempList
, which you can then also tell Android to perform by calling notifyItemChanged
and similar methods. This is pretty much what the adapters in FirebaseUI do.