Before android architecture components were released I started to work on a project where I had my own ViewModels that had the same life cycle as a Fragment and the ViewModels state was saved in a StateObject inside a Loader so that the state could survive orientation change. The ViewModel talked to the Fragment through an Interface. This worked fine because the ViewModel and Fragment had the same life cycle.
My ViewModel contained all kinds of state. It had a isLoading boolean, isEmptyStateVisible boolean etc. And every time a state changed, I called something like view.notifyIsLoadingChanged(true/false) and in this case the Fragment would show or hide a spinner.
Now I am about to change my implementation to use the new ViewModels together with LiveData. The fastest way to implement LiveData is to change my implementation of the UI interface that the ViewModel is using. So I could keep my current implementation and just add this UI interface implementation:
public class LiveDataProductReviewSheetUI extends LiveDataUI implements ProductReviewSheetUI {
public final MutableLiveData<ReviewViewModelState> ratingDescChanged = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> ratingChanged = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> reviewChanged = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> reviewValid = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> expandReview = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> reviewQuestion = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> reviewCreated = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> showMsg = new MutableLiveData<>();
public final MutableLiveData<ReviewViewModelState> dismiss = new MutableLiveData<>();
public void observe(LifecycleOwner owner, final ProductReviewSheetUI observer) {
ratingDescChanged.observe(owner, state -> observer.onRatingDescriptionChanged(state));
ratingChanged.observe(owner, state -> observer.onRatingChanged(state));
reviewChanged.observe(owner, state -> observer.onReviewChanged(state));
reviewValid.observe(owner, state -> observer.onHasValidReviewDataChanged(state));
expandReview.observe(owner, state -> observer.onExpandReviewFieldHasChanged(state));
reviewQuestion.observe(owner, state -> observer.onProductReviewQuestionChanged(state));
reviewCreated.observe(owner, state -> observer.onReviewCreated(state));
showMsg.observe(owner, state -> observer.onShowMessage(state));
dismiss.observe(owner, state -> observer.onCloseView());
}
#Override
public void onRatingDescriptionChanged(ReviewViewModelState state) {
ratingDescChanged.setValue(state);
}
#Override
public void onRatingChanged(ReviewViewModelState state) {
ratingChanged.setValue(state);
}
#Override
public void onReviewChanged(ReviewViewModelState state) {
reviewChanged.setValue(state);
}
#Override
public void onHasValidReviewDataChanged(ReviewViewModelState state) {
reviewValid.setValue(state);
}
#Override
public void onExpandReviewFieldHasChanged(ReviewViewModelState state) {
expandReview.setValue(state);
}
#Override
public void onProductReviewQuestionChanged(ReviewViewModelState state) {
reviewQuestion.setValue(state);
}
#Override
public void onReviewCreated(ReviewViewModelState state) {
reviewCreated.setValue(state);
}
#Override
public void onShowMessage(ReviewViewModelState state) {
showMsg.setValue(state);
}
#Override
public void onCloseView() {
dismiss.setValue(dismiss.getValue());
}
}
The LiveDataUI class that this class extends has even more methods like:
public final MutableLiveData<Boolean> showLoading = new MutableLiveData<>();
public final MutableLiveData<Boolean> showEmptyState = new MutableLiveData<>();
With this implementation I will end up with a lot of MutableLiveData objects and it does not feel right. Am I putting to much state into my ViewModel? My idea was to have all logic inside the ViewModel so that I could write tests where I could verify that isLoading is true when data is being loaded, and if there is no data returned from Api, the isEmptyState is true etc.
I have also noticed that if i call setValue(state); multiple times within a very short time on the same MutableLiveData object, the onChanged method is only invoked once. Is that correct?
Related
The structure of my application is as follows:
MainActivity(Activity) containing Bottom Navigation View with three fragments nested below
HomeFragment(Fragment) containing TabLayout with ViewPager with following two tabs
Journal(Fragment)
Bookmarks(Fragment)
Fragment B(Fragment)
Fragment C(Fragment)
I am using Room to maintain all the records of journals. I'm observing one LiveData object each in Journal and Bookmarks fragment. These LiveData objects are returned by my JournalViewModel class.
JournalDatabase.java
public abstract class JournalDatabase extends RoomDatabase {
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService dbWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
private static JournalDatabase INSTANCE;
static synchronized JournalDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), JournalDatabase.class, "main_database")
.fallbackToDestructiveMigration()
.build();
}
return INSTANCE;
}
public abstract JournalDao journalDao();
}
JournalRepository.java
public class JournalRepository {
private JournalDao journalDao;
private LiveData<List<Journal>> allJournals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalRepository(Application application) {
JournalDatabase journalDatabase = JournalDatabase.getInstance(application);
journalDao = journalDatabase.journalDao();
allJournals = journalDao.getJournalsByDate();
bookmarkedJournals = journalDao.getBookmarkedJournals();
}
public void insert(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.insert(journal);
});
}
public void update(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.update(journal);
});
}
public void delete(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.delete(journal);
});
}
public void deleteAll() {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.deleteAll();
});
}
public LiveData<List<Journal>> getAllJournals() {
return allJournals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
JournalViewModel.java
public class JournalViewModel extends AndroidViewModel {
private JournalRepository repository;
private LiveData<List<Journal>> journals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalViewModel(#NonNull Application application) {
super(application);
repository = new JournalRepository(application);
journals = repository.getAllJournals();
bookmarkedJournals = repository.getBookmarkedJournals();
}
public void insert(Journal journal) {
repository.insert(journal);
}
public void update(Journal journal) {
repository.update(journal);
}
public void delete(Journal journal) {
repository.delete(journal);
}
public void deleteAll() {
repository.deleteAll();
}
public LiveData<List<Journal>> getAllJournals() {
return journals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
I'm instantiating this ViewModel inside onActivityCreated() method of both Fragments.
JournalFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
journalAdapter.submitList(list);
}
});
}
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getBookmarkedJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
adapter.submitList(list);
}
});
}
However, the problem when I use this approach is as I delete make some changes in any of the Fragment like delete or update some Journal some other Journal's date field changes randomly.
I was able to solve this issue by using single LiveData object and observe it in both fragments. The changes I had to make in BookmarkFragment is as follows:
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
List<Journal> bookmarkedJournals = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getBookmark() == 1)
bookmarkedJournals.add(list.get(i));
}
adapter.submitList(bookmarkedJournals);
}
});
}
It works properly now.
However, I want to know why it didn't work using my first approach which was to use two different LiveData objects and observe them in different fragments.
Are multiple LiveData objects not meant to be used in single ViewModel?
OR
Are two instances of same ViewModel not allowed to exist together while making changes and fetching different LiveData objects from the same table simultaneously?
I found out the reason causing this problem.
As I was using LiveData with getViewLifecycleOwner() as the LifecycleOwner, the observer I passed as parameter was never getting removed. So, after switching to a different tab, there were two active observers observing different LiveData objects of same ViewModel.
The way this issue can be solved is by storing the LiveData object in a variable then removing the observer as you switch to different fragment.
In my scenario, I solved this issue by doing the following:
//store LiveData object in a variable
LiveData<List<Journal>> currentLiveData = journalViewModel.getAllJournals();
//observe this livedata object
currentLiveData.observer(observer);
Then remove this observer in a suitable Lifecycle method or anywhere that suits your needs like
#Override
public void onDestroyView() {
super.onDestroyView();
//if you want to remove all observers
currentLiveData.removeObservers(getViewLifecycleOwner());
//if you want to remove particular observers
currentLiveData.removeObserver(observer);
}
This is my first time using MVVM architecture.I am also using LiveData. I simply retrieve data from server using Retrofit.So upon clicking a button in the View(MainActivity.class) I invoke the ViewModel class's method(handleRetrofitcall()) to take up the duty of Api calling from the Model class(Retrofit Handler.class).The Model class upon retrieving the data informs the ViewModel of the data(which is actually the size of items).I set the size to LiveData and try to listen for it.Unfortunately I couldn't.For detailed analysis please go through the code.
Model...
RetrofitHandler.class:
public class RetrofitHandler {
private ApiInterface apiInterface;
private SimpleViewModel viewModel;
public void getData(){
apiInterface= ApiClient.getClient().create(ApiInterface.class);
Call<Unknownapi> call=apiInterface.doGetListResources();
call.enqueue(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
List<Unknownapi.Data> list;
Unknownapi unknownapi=response.body();
list=unknownapi.getData();
viewModel=new SimpleViewModel();
viewModel.postValue(list.size());
Log.e("Size",Integer.toString(list.size()));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
}
});
}
}
ViewModel....
SimpleViewModel.class:
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler;
private int size;
private MutableLiveData<Integer> mutablesize=new MutableLiveData<>();
public SimpleViewModel() {
super();
}
#Override
protected void onCleared() {
super.onCleared();
}
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData();
}
public void postValue(int size){
this.size=size;
mutablesize.postValue(this.size);
Log.e("lk","f");
}
public MutableLiveData<Integer> getObject() {
return mutablesize;
}
}
View.....
MainActivity.class:
public class MainActivity extends AppCompatActivity {
private TextView status;
private SimpleViewModel viewModel;
private Observer<Integer> observer;
private MutableLiveData<Integer> mutableLiveData;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status=findViewById(R.id.status);
viewModel=ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
observer=new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
};
viewModel.getObject().observe(MainActivity.this,observer);
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.handleRetrofitcall();
}
});
}
#Override
protected void onDestroy() {
if (observer!=null){
viewModel.getObject().removeObserver(observer);
}
super.onDestroy();
}
}
You're creating a new ViewModel in the RetrofitHandler, so nothing is observing that viewmodel. Instead of having the RetrofitHandler rely on a ViewModel internally, it's probably safer to handle the Retrofit callback inself, and post data there.
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData(new Callback<List<Unknownapi.Data>> {
// add actual callback implementation here
); // add a callback here, so that the data is available in the view model. Then post the results from here.
}
Edit: More clarification.
In the Activity, you're correctly creating a ViewModel and observing it (we'll call that ViewModel A). ViewModel A is then creating a RetrofitHandler and calling getData on that Retrofithandler. The issue is that RetrofitHandler is creating a new ViewModel in getData (which I'm going to call ViewModel B).
The issue is that the results are being posted to ViewModel B, which nothing is observing, so it seems like nothing is working.
Easy way to avoid this issue is to make sure that only an Activity/Fragment is relying on (and creating) ViewModels. Nothing else should know about the ViewModel.
Edit 2: Here's a simple implementation. I haven't tested it, but it should be more or less correct.
// shouldn't know anything about the view model or the view
public class RetrofitHandler {
private ApiInterface apiInterface;
// this should probably pass in a different type of callback that doesn't require retrofit
public void getData(Callback<Unknownapi> callback) {
// only create the apiInterface once
if (apiInterface == null) {
apiInterface = ApiClient.getClient().create(ApiInterface.class);
}
// allow the calling function to handle the result
apiInterface.doGetListResources().enqueue(callback);
}
}
// shouldn't know how retrofit handler parses the data
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler = new RetrofitHandler();
// store data in mutableSize, not with a backing field.
private MutableLiveData<Integer> mutableSize = new MutableLiveData<>();
public void handleRetrofitCall() {
// handle the data parsing here
retrofitHandler.getData(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
Unknownapi unknownapi = response.body();
int listSize = unknownapi.getData().size;
// set the value of the LiveData. Observers will be notified
mutableSize.setValue(listSize); // Note that we're using setValue because retrofit callbacks come back on the main thread.
Log.e("Size", Integer.toString(listSize));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
// error handling should be added here
}
});
}
// this should probably return an immutable copy of the object
public MutableLiveData<Integer> getObject() {
return mutableSize;
}
}
public class MainActivity extends AppCompatActivity {
private TextView status;
// initialize the view model only once
private SimpleViewModel viewModel = ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status = findViewById(R.id.status);
// observe the view model's changes
viewModel.getObject().observe(this, new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
// you should handle possibility of interger being null
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
});
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// call the view model's function
viewModel.handleRetrofitCall();
}
});
}
}
So according to android developers: "Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI. ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance."
In the code below there is an asynchronous class that gets called in deleteItem function. My question is this: Does ViewModel also handles the asynchronous calls made inside it or will cause memory leaks?
Thank you
public class BorrowedListViewModel extends AndroidViewModel {
private final LiveData<List<BorrowModel>> itemAndPersonList;
private AppDatabase appDatabase;
public BorrowedListViewModel(Application application) {
super(application);
appDatabase = AppDatabase.getDatabase(this.getApplication());
itemAndPersonList = appDatabase.itemAndPersonModel().getAllBorrowedItems();
}
public LiveData<List<BorrowModel>> getItemAndPersonList() {
return itemAndPersonList;
}
public void deleteItem(BorrowModel borrowModel) {
new deleteAsyncTask(appDatabase).execute(borrowModel);
}
private static class deleteAsyncTask extends AsyncTask<BorrowModel, Void, Void> {
private AppDatabase db;
deleteAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
#Override
protected Void doInBackground(final BorrowModel... params) {
db.itemAndPersonModel().deleteBorrow(params[0]);
return null;
}
}
}
I would provide an example, probably you need to modify the code.
First you need a live data change and subscribe to that in your view. Then in the controller you post the value telling the subscriber that something appends. This way asynchronously the view would get alerted.
private MutableLiveData<String> databaseLiveData = new MutableLiveData<>();
...
And in the deleteAsyncTask class you can add:
protected void onPostExecute(Void result) {
databaseLiveData.postValue("some data deleted");
}
And in the BorrowedListViewModel class this method to access from the view add this method:
public LiveData<String> getChanger() {
return databaseLiveData;
}
In the view e.g.Activity add this:
private BorrowedListViewModel mBorrowedListViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
BorrowedListViewModel = ViewModelProviders.of(this).get(BorrowedListViewModel.class);
subscribe();
}
private void subscribe() {
final Observer<String> liveDataChange = new Observer<String>() {
#Override
public void onChanged(#Nullable final String message) {
Log.d("Activity", message);
}
};
liveDataChange.getChanger().observe(this, liveDataChange);
}
Hope this help.
I discover the new android architecture component and I want to test the couple ViewModel / LiveData through a small test application. The latter has two fragments (in a ViewPager), the first creates/updates a list of cards (via an EditText) and the second displays all the cards.
My ViewModel:
public class CardsScanListViewModel extends AndroidViewModel {
private MutableLiveData> cardsLiveData = new MutableLiveData();
private HashMap cardsMap = new HashMap();
public CardsScanListViewModel(#NonNull Application application) {
super(application);
}
public MutableLiveData> getCardsLiveData() {
return this.cardsLiveData;
}
public void saveOrUpdateCard(String id) {
if(!cardsMap.containsKey(id)) {
cardsMap.put(id, new Card(id, new AtomicInteger(0)));
}
cardsMap.get(id).getCount().incrementAndGet();
this.cardsLiveData.postValue(cardsMap);
}
}
My second fragment:
public class CardsListFragment extends Fragment {
CardsAdapter cardsAdapter;
RecyclerView recyclerCardsList;
public CardsListFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
observeViewModel(viewModel);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_cards_list, container, false);
recyclerCardsList = v.findViewById(R.id.recyclerCardsList);
recyclerCardsList.setLayoutManager(new LinearLayoutManager(getActivity()));
cardsAdapter = new CardsAdapter(getActivity());
recyclerCardsList.setAdapter(cardsAdapter);
return v;
}
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
}
}
});
}
}
TheHashMap, like my MutableLiveData, update well but my second fragment doesn't receive the information via observer.
You are observing the new instance of ViewModel instead of observing the same ViewModel used by your First Fragment.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
Above code initialize new instance of CardsScanListViewModel for your second fragment CardsListFragment, because you passed this as context.
If you update any data from this fragment it will update in this instance of ViewModel.
It works in your first Fragment because it updates data and observes data from same instance of ViewModel
To keep data common among ViewModels initiate view model by passing activity context instead of fragment context in both the fragments.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(getActivity()).get(CardsScanListViewModel.class);
This will create single instance of CardsScanListViewModel and data will be shared between fragments as they are observing LiveData from single instance of ViewModel.
For confirmation, you need to add notifyDataSetChanged() after updating the list if you haven't done that in adapter itself
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
cardsAdapter.notifyDataSetChanged();
}
}
});
}
I'm a bit confused about how data binding should work when using the new Architecture Components.
let's say I have a simple Activity with a list, a ProgressBar and a TextView. the Activity should be responsible for controlling the state of all the views, but the ViewModel should hold the data and the logic.
For example, my Activity now looks like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
listViewModel = ViewModelProviders.of(this).get(ListViewModel.class);
binding.setViewModel(listViewModel);
list = findViewById(R.id.games_list);
listViewModel.getList().observeForever(new Observer<List<Game>>() {
#Override
public void onChanged(#Nullable List<Game> items) {
setUpList(items);
}
});
listViewModel.loadGames();
}
private void setUpList(List<Game> items){
list.setLayoutManager(new LinearLayoutManager(this));
GameAdapter adapter = new GameAdapter();
adapter.setList(items);
list.setAdapter(adapter);
}
and the ViewModel it's only responsible for loading the data and notify the Activity when the list is ready so it can prepare the Adapter and show the data:
public int progressVisibility = View.VISIBLE;
private MutableLiveData<List<Game>> list;
public void loadGames(){
Retrofit retrofit = GamesAPI.create();
GameService service = retrofit.create(GameService.class);
Call<GamesResponse> call = service.fetchGames();
call.enqueue(this);
}
#Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
setList(response.body().data);
}
}
#Override
public void onFailure(Call<GamesResponse> call, Throwable t) {
}
public MutableLiveData<List<Game>> getList() {
if(list == null)
list = new MutableLiveData<>();
if(list.getValue() == null)
list.setValue(new ArrayList<Game>());
return list;
}
public void setList(List<Game> list) {
this.list.postValue(list);
}
My question is: which is the correct way to show/hide the list, progressbar and error text?
should I add an Integer for each View in the ViewModel making it control the views and using it like:
<TextView
android:id="#+id/main_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{viewModel.error}"
android:visibility="#{viewModel.errorVisibility}" />
or should the ViewModel instantiate a LiveData object for each property:
private MutableLiveData<Integer> progressVisibility = new MutableLiveData<>();
private MutableLiveData<Integer> listVisibility = new MutableLiveData<>();
private MutableLiveData<Integer> errorVisibility = new MutableLiveData<>();
update their value when needed and make the Activity observe their value?
viewModel.getProgressVisibility().observeForever(new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer visibility) {
progress.setVisibility(visibility);
}
});
viewModel.getListVisibility().observeForever(new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer visibility) {
list.setVisibility(visibility);
}
});
viewModel.getErrorVisibility().observeForever(new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer visibility) {
error.setVisibility(visibility);
}
});
I'm really struggling to understand that. If someone can clarify that, it would be great.
Thanks
Here are simple steps:
public class MainViewModel extends ViewModel {
MutableLiveData<ArrayList<Game>> gamesLiveData = new MutableLiveData<>();
// ObservableBoolean or ObservableField are classes from
// databinding library (android.databinding.ObservableBoolean)
public ObservableBoolean progressVisibile = new ObservableBoolean();
public ObservableBoolean listVisibile = new ObservableBoolean();
public ObservableBoolean errorVisibile = new ObservableBoolean();
public ObservableField<String> error = new ObservableField<String>();
// ...
// For example we want to change list and progress visibility
// We should just change ObservableBoolean property
// databinding knows how to bind view to changed of field
public void loadGames(){
GamesAPI.create().create(GameService.class)
.fetchGames().enqueue(this);
listVisibile.set(false);
progressVisibile.set(true);
}
#Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
gamesLiveData.setValue(response.body().data);
listVisibile.set(true);
progressVisibile.set(false);
}
}
}
And then
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="MainViewModel"/>
</data>
...
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="#{viewModel.progressVisibile ? View.VISIBLE : View.GONE}"/>
<ListView
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="#{viewModel.listVisibile ? View.VISIBLE : View.GONE}"/>
<TextView
android:id="#+id/main_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{viewModel.error}"
android:visibility="#{viewModel.errorVisibile ? View.VISIBLE : View.GONE}"/>
Also notice that it's your choice to make view observe
ObservableBoolean : false / true
// or
ObservableInt : View.VISIBLE / View.INVISIBLE / View.GONE
but ObservableBoolean is better for ViewModel testing.
Also you should observe LiveData considering lifecycle:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
listViewModel.getList().observe((LifecycleOwner) this, new Observer<List<Game>>() {
#Override
public void onChanged(#Nullable List<Game> items) {
setUpList(items);
}
});
}
Here are simple steps to achieve your point.
First, have your ViewModel expose a LiveData object, and you can start the LiveData with an empty value.
private MutableLiveData<List<Game>> list = new MutableLiveData<>();
public MutableLiveData<List<Game>> getList() {
return list;
}
Second, have your view (activity/fragment) observe that LiveData and change UI accordingly.
listViewModel = ViewModelProviders.of(this).get(ListViewModel.class);
listViewModel.data.observe(this, new Observer<List<Game>>() {
#Override
public void onChanged(#Nullable final List<Game> games) {
setUpList(games);
}
});
Here it is important that you use the observe(LifecycleOwner, Observer) variant so that your observer do NOT receive events after that LifecycleOwner is no longer active, basically, that means that when your activity of fragment is no longer active, you won't leak that listener.
Third, as a result of data becoming available you need to update your LiveData object.
#Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
List<Game> newGames = response.body().data; // Assuming this is a list
list.setValue(newGames); // Update your LiveData object by calling setValue
}
}
By calling setValue() on your LiveData, this will cause onChanged on your view's listener to be called and your UI should be updated automatically.