In my ViewModel class I have a method that fetches data from the internet and sets them on LiveData via setValue() (I've used the approach from these docs):
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
if (mDataList == null) {
mDataList = new MutableLiveData<>();
}
Handler handler = new Handler(Looper.getMainLooper());
// getData() connects to the internet and fetches the online data
mInternetConnection.getData(new InternetConnection.ConnectionCallback<List<?>>() {
#Override
public void onComplete(Result<List<?>> result) {
if (result instanceof Result.Success) {
mDataList.setValue(((Result.Success<List<?>>) result).data);
} else {
Log.e(LOG_TAG, "ViewModel could not obtain data list");
}
}
}, handler);
return mDataList;
}
But mDataList.getValue() is null. The setValue() method is called on the main thread.
onComplete gets called for sure (checked).
When I check whether the value of mDataList is null in the onComplete method right after setValue(), the logs show that it is not null.
Why is it null and how should you get the value of the modified mDataList?
I've spent hours looking for a solution on the web, but couldn't find anything that could help. The related questions on this site don't help to solve the problem either.
EDIT:
From the docs:
In this example, the callback passed into the Repository's makeLoginRequest call is executed on the main thread. That means you can directly modify the UI from the callback or use LiveData.setValue() to communicate with the UI.
So I've followed that, but this part: "or use LiveData.setValue() to communicate with the UI" doesn't work.
The observer is set on it in the Fragment, but LiveData still don't get updated - list in onChanged is null, the screen remains blank, no errors.
Fragment code:
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
View rootView = binding.getRoot();
binding.listRecyclv.setLayoutManager(new LinearLayoutManager(rootView.getContext()));
mAdapter = createAdapter(rootView.getContext(), pageViewModel.getList().getValue());
binding.listRecyclv.setAdapter(mAdapter);
pageViewModel.getList().observe(getViewLifecycleOwner(), new Observer<List<?>>() {
#Override
public void onChanged(#Nullable List<?> list) {
mAdapter.setList((ArrayList<MyObj>) list);
binding.listRecyclv.setAdapter(mAdapter);
}
}
}
});
Getter in ViewModel:
public LiveData<List<?>> getList() {
return getDataList();
}
Code in Adapter:
public class AppAdapter extends RecyclerView.Adapter<AppAdapter.AppViewHolder> {
...
public void setList(ArrayList<MyObj> list) {
mAppList = list;
notifyDataSetChanged();
}
Thanks in advance.
mDataList is null because the call you're making to get data is asynchronous. This means that the function will return before your api call (and its callbacks) does.
Read more here https://www.bmc.com/blogs/asynchronous-programming/
To get the list you need to observe data changes.
So something like this:
From the activity:
Kotlin:
viewModel.getDataList().observe(this) { dataList ->
//TODO: Do something with the list
}
Java:
final Observer<String> listObserver = new Observer<List<>>() {
#Override
public void onChanged(#Nullable final List<> newName) {
//TODO: Use the list
}
};
viewModel.getDataList().observe(this, listObserver);
The logic inside the observer will be called every time the list changes.
I solved it this way:
Changed this line in Fragment's onCreate method
from:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pageViewModel = new ViewModelProvider(this).get(PageViewModel.class);
to
pageViewModel = new ViewModelProvider(requireActivity()).get(PageViewModel.class);
Thanks to this answer
Changed the data type of getDataList() method in the PageViewModel class from LiveData to MutableLiveData (without it the above step doesn't help):
from:
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
to:
private MutableLiveData<List<?> getDataList() {
and now it works! The observer gets triggered now and the data are updated.
Related
I am having the following code:
public class ViewModelDrivers extends ViewModel {
private MutableLiveData<List<DriverDetailsModel>> drivers;
public LiveData<List<DriverDetailsModel>> getDrivers() {
if (drivers == null) {
drivers = new MutableLiveData<>();
loadDrivers();
}
return drivers;
}
private void loadDrivers() {
CloudFactory.getInstance().getAllServerDrivers(PreferenceUtils.getInstance().getDeviceId(), new IServerAllTrucksCallback() {
#Override
public void onAllTrucks(List<DriverDetailsModel> d) {
drivers.setValue(d);
}
});
}
}
And the MainActivity on onCreate method:
ViewModelDrivers model = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(ViewModelDrivers.class);
model.getDrivers().observe(this, items -> {
//here never gets called. Why?
});
I didnt used ViewModelProviders since is deprecated. I never received a result in observe() method. Moreover drivers.setValue(d); gets set
Change drivers.setValue() to drivers.postValue( and if this helps, then you should change your code like Suraj Vaishnav defined, calling loadDrivers after subscribing to observer.
Maybe your drivers.setValue(d); called before return drivers;
So remove loadDrivers(); from getDrivers() method, like this:
public LiveData<List<DriverDetailsModel>> getDrivers() {
if (drivers == null) {
drivers = new MutableLiveData<>();
}
return drivers;
}
And call loadDrivers() from after setting observer, like this:
model.getDrivers().observe(this, items -> {
//here never gets called. Why?
});
loadDrivers(); //here call loadDrivers();
I created the instance of View Model in onCreate method of an activity.
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
Then i have a method, AddTicket, which uses viewModel to hit a service and on response from viewModel i dismiss loading animation.
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, response ->{
dismissLoadingAnimation();
}
Now after adding a ticket, user can repress the Add Ticket button, and the addTicket() method will be called again.
but this time observer defined in ViewModel gets called 2 times, resulting in 2 network calls, and 2 dismissLoadingAnimation execution.
And if i keep pressing addTicket button, the number of executing observer defined inside ViewModel keep increases.
This is my View Model code.
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public MutableLiveData AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
return mObservableResponse;
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
The number of executing observer defined inside ViewModel keep increases becasue with every click You're registering new observers. You're not supposed to register observer with onClick() method.
You should do it in onCreate() method of your Activity or in onViewCreated method of your fragment. If You'll do that, there won't be a need to removeObserver when You'll finish work. Lifecycle mechanism will cover it for you.
But if you really want answer for you question, this is how you can do it
yourViewModel.yourList.removeObservers(this)
Passing this means passing your Activity, or there is a second way:
yourViewModel.yourList.removeObserver(observer)
val observer = object : Observer<YourObject> {
override fun onChanged(t: YourObject?) {
//todo
}
}
The purpose of Viewmodel is to expose observables (Livedata)
The purpose of View(Activity/Fragment) is to get these observables and observe them
Whenever there is a change in these observables(Livedata) the change is automatically posted to the active subscribed owners(Activity/Fragment), so you need not remove them in onPause/onStop as it is not mandatory
I can suggest few changes to your code to solve the problem with the above mentioned pointers
ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public LiveData<String> getResponseLiveData(){
return mObservableResponse;
}
public void AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
View
onCreate(){
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
observeForResponse();
}
private void observeForResponse(){
ticketViewModel.getResponseLiveData().observe(this, response ->{
//do what has to be updated in UI
}
}
public void addTicket(View view){
ticketViewModel.AddTicket(id);
}
Hope this is of help :)
You only need to call the observe once, I prefer to do it in onResume and then call removeObserver in onPause:
Adds the given observer to the observers list
You keep adding listeners to the data so you get multiple callbacks.
Edit:
I took an existing code sample of mine for a Fragment and renamed everything (I hope), there's no example here for setting the data into the ViewModel but it should be ticketViewModel.AddTicket(id); in your case.
public class ListFragment extends Fragment {
private MyViewModel viewModel;
private MyRecyclerViewAdapter recyclerViewAdapter;
private Observer<List<DatabaseObject>> dataObserver;
private RecyclerView recyclerView;
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
initRecyclerView(rootView, getContext());
initObservers();
return rootView;
}
private void initRecyclerView(View rootView, Context context) {
recyclerViewAdapter = new MyRecyclerViewAdapter(context);
recyclerView = rootView.findViewById(R.id.recycler_view);
recyclerView.setAdapter(recyclerViewAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new DividerNoLastItemDecoration());
}
private void initObservers() {
dataObserver = new Observer<List<DatabaseObject>>() {
#Override
public void onChanged(#Nullable final List<DatabaseObject> data) {
recyclerViewAdapter.setData(data);
}
};
}
#Override
public void onResume() {
super.onResume();
initViewModel();
}
private void initViewModel() {
FragmentActivity activity = getActivity();
if (activity != null) {
viewModel = ViewModelProviders.of(activity).get(MyViewModel.class);
viewModel.getData().observe(activity, dataObserver);
}
}
#Override
public void onPause() {
super.onPause();
if (viewModel != null) {
viewModel.getData().removeObserver(dataObserver);
viewModel = null;
}
}
}
I had similar problem. You could try to use SingleLiveEvent
Or, in my, more complicated case, i had to use custom observer. It would looks like this:
public class CustomObserver implements Observer<YourType> {
private MyViewModel mViewModel;
public CustomObserver (){}
public void setViewModel(MyViewModel model) {
mViewModel = model;
}
#Override
public void onChanged(#Nullable YourType object) {
mViewModel.AddTicket(id).removeObserver(this); // removing previous
mmViewModel.refreshTickets(); // refreshing Data/UI
// ... do the job here
// in your case it`s: dismissLoadingAnimation();
}
}
And using it like:
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, myCustomObserver);
}
If you are willing to do some changes, i think we can handle it in much cleaner way
LiveData is meant to be used to contain a property value of a view
In ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
private MutableLiveData<Boolean> showLoadingAnimationLiveData = new MutableLiveData<String>();
public LiveData<Boolean> getShowLoadingAnimationLiveData(){
return showLoadingAnimationLiveData;
}
public void addTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
showLoadingAnimationLiveData.setValue(true);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
showLoadingAnimationLiveData.setValue(false);
}
}
}
In 'onCreate' of your Activity/Fragment
ticketViewModel.getShowLoadingAnimationLiveData().observe(this,showLoadingAnimation->{
if(showLoadingAnimation != null && showLoadingAnimation){
startLoadingAnimation();
}else{
dismissLoadingAnimation();
}
})
The main concept is to divide the responsibilities,
Activity/Fragment doesn't need to know which process is going on, they only need to know what are the current properties/state of there child views.
We need to maintain a LiveData in ViewModels for each changing property/state depending on Views. ViewModel needs to handle the view states depending on whats happening.
Only responsibility the Activity/Fragment has about a process is to trigger it and forget and ViewModel needs handle everything(like informing Repositories to do the work and changing View Properties).
In your Case,
'addTicket' is a process about which Activity/Fragment doesn't need to know about there status.
The only responsibility of Activity/Fragment about that process is to trigger it.
ViewModel is one who needs to analyze the state of process(in-progress/success/failed) and give appropriate values to the LiveDatas to inform the respective Views
So I'm fairly new to MVVM. So I'm fetching my data in my VM and I'm passing in the Activity/fragment as a listener in the method call.
The reason I'm doing this is because I'm going to have a callback if there was to be an error. So I'd handle it in the activity/fragment with a dialog.
I'm not sure if I'm breaking MVVM here? If i'm making any other errors with this pattern, please let me know.
Thanks
In my view, fragment/activity
/*creating and using my VM inside my fragment*/
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
//Create and observe data for any changes throughout the lifecycle
final OverviewViewModel viewModel = ViewModelProviders.of(this).get(OverviewViewModel.class);
//get info
viewModel.getUserInfo(this);
observeViewModel(viewModel);
}
//Listener in the activity/fragment that will handle an error in the request
#Override
public void onTokenExpired() {
ExpiredTokenDialogFragment dialogFragment = new ExpiredTokenDialogFragment();
dialogFragment.show(getFragmentManager(), EXPIRED_DIALOG);
}
My View model where i make request.
public void getUserInfo(AuthenticationListener listener){
mUserInformationObservable = mRepository.getUserInfo(listener);
}
My retrofit request
public LiveData<UserInformation> getUserInfo(final AuthenticationListener authenticationListener){
final MutableLiveData<UserInformation> data = new MutableLiveData<>();
mService.fetchFollowers().enqueue(new Callback<UserInformation>() {
#Override
public void onResponse(Call<UserInformation> call, retrofit2.Response<UserInformation> response) {
//note, the result is in data. Calling response.body.string twice results in an empty string
if(response.body()!=null) data.setValue(response.body());
}
#Override
public void onFailure(Call<UserInformation> call, Throwable t) {
if(t instanceof UnauthorizedException){
data.setValue(null);
mToken.setAccessToken(null);
authenticationListener.onTokenExpired();
}
}
});
return data;
}
Using a listener is not recommended. The android-architecture project uses a SingleLiveEvent class for events like navigation or displaying a Snackbar. You can use the same class for showing a dialog.
In your OverviewViewModel you can add another field:
final SingleLiveEvent<Void> tokenLiveData = SingleLiveEvent<Void>();
in your onFaliure callback you can use:
tokenLiveData.call()
instead of the callback.
In your activity subscribe to tokenLiveData and show a dialog when it emits a value.
I understand there are a lot of information about it out there, but I haven't found one that matches my case yet.
I have a recycleview on a fragment that is always open, so the fragment basically never re-creates itself.
This is my code to load the adapter.
reLoad(); //method shown below
mRecycler.setAdapter(new SolicitationAdapter(myRealm.where(SolicitationDatabase.class).findAllAsync()));
And this is the logic I came up with:
public void reLoad() {
if (!myRealm.where(SolicitationDatabase.class).findAll().isEmpty()) {
mNothingHere.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
} else {
mNothingHere.setVisibility(View.VISIBLE);
mRecycler.setVisibility(View.GONE);
}
}
It works great the first time the user opens the app.
The trouble starts when the user creates a record, since the fragment doesn't re-create itself it never reloads.
The reason I haven't been able to reload after user adds something is because the method to add a new record is on a singleton being called from a different activity. Which means when I try to do it I get a nullpointerexception when declaring the the recycleview and the textview.
Edit - What I tried (reloading views from another place)
I have a class called PostHelper, this class is in charge of posting a new record.
This is the constructor:
public PostHelper(Context context, Activity activity) {
this.mContext = context;
this.mActivity = activity; //I call this in order to use "findViewById"
This is where the post happens:
public String addSolicitation(File _file, boolean fromQueue) {
//loading view
TextView nothingHere = (TextView) mActivity.findViewById(R.id.nothing_here);
RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
...so on until after the post:
SolicitationAdapter n = new SolicitationAdapter(myRealm.where(SolicitationDatabase.class).findAll());
n.notifyDataSetChanged();
nothingHere.setVisibility(View.GONE);
recycler.setVisibility(View.VISIBLE);
And this is the stacktrace:
06-01 21:43:37.511 9122-9122/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.ga.realm3, PID: 9122
io.reactivex.exceptions.OnErrorNotImplementedException: Attempt to invoke virtual method 'void android.widget.TextView.setVisibility(int)' on a null object reference
Edit 2 - I load PostHelper class using the following:
mPostHelper = new PostHelper(this, PostSolicitationActivity.this);
You're supposed to make sure that SolicitationAdapter is a RealmRecyclerViewAdapter, like so:
public class SolicitationAdapter extends RealmRecyclerViewAdapter<SolicitationDatabase, SolicitationViewHolder> {
public SolicitationAdapter(OrderedRealmCollection<SolicitationDatabase> results) {
super(results, true);
}
...
}
And then what you need to do is that you put the RealmResults as a field reference in your Activity:
public class PostSoliticiationActivity extends AppCompatActivity {
RealmResults<Solicitation> results;
Realm realm;
RealmChangeListener<RealmResults<Solicitiation> realmChangeListener = (results) -> {
if(results.isLoaded() && results.isValid()) {
if(results.isEmpty()) {
mNothingHere.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
} else {
mNothingHere.setVisibility(View.VISIBLE);
mRecycler.setVisibility(View.GONE);
}
}
}
SolicitationAdapter adapter;
#Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.soliticiation_activity);
// bind views
realm = Realm.getDefaultInstance();
results = realm.where(SolicitationDatabase.class).findAllSortedAsync("id");
// .sort("id").findAllAsync(); in 4.3.0+
results.addChangeListener(realmChangeListener);
adapter = new SoliticiationAdapter(results);
mRecycler.setAdapter(adapter);
// layout manager as well
}
#Override
public void onDestroy() {
results.removeChangeListener(realmChangeListener);
realm.close();
super.onDestroy();
}
}
So things you don't need:
1.) reLoad() method
2.) onPostAdded callback
3.) PostActionListener
As long as you just add the SoliticiationDatabase to the Realm in a transaction, it'll all work without manually syncing ui.
If I understand correctly, you want to be notified in another view when an action happens elsewhere.
The way to do that is usually interfaces.
public class PostHelper {
// Define these
public interface PostActionListener {
void onPostAdded();
}
private PostActionListener postListener;
public void setPostActionListener(PostActionListener listener) throws ClassCastException {
this.postListener = (PostActionListener) context;
}
public PostHelper(Context context) {
this.mContext = context;
// Cast the passed context as a listener
if (mContext instanceof PostActionListener) {
this.postListener = (PostActionListener) mContext;
}
}
public String addSolicitation(File _file, boolean fromQueue) {
// Do something
// Callback to the UI to update
if (this.postListener != null) {
this.postListener.onPostAdded();
}
}
Then, in your initial Activity
public class YourActivity extends AppCompatActivity
implements PostHandler.PostActionListener {
// ... fields
#Override
public void onPostAdded() {
reLoad();
}
public void reLoad() {
boolean emptyList = myRealm.where(SolicitationDatabase.class).findAll().isEmpty();
mNothingHere.setVisibility(emptyList ? View.VISIBLE : View.GONE);
mRecycler.setVisibility(emptyList ? View.GONE : View.VISIBLE);
}
#Override
public void onCreate(Bundle b) {
...
mPostHelper = new PostHelper(this);
}
However, since you are using Realm, and there really is no data that you need to "return" here, then you can simply let the Android Activity lifecycle refresh the data for you.
I have the following class extended Fragment:
public static class DummySectionFragment extends Fragment {
public static final String ARG_SECTION_NUMBER = "section_number";
private GridView events;
public DummySectionFragment() {}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle args = getArguments();
switch (args.getInt(ARG_SECTION_NUMBER)) {
case 1 : return initializeCalendarScreen(inflater);
case 2 : return inflater.inflate(R.layout.todo_lists_layout, null);
}
return null;
}
public void updateGridView() {
int daysInCurrentMonth=(MainActivity.month==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month];
int daysInPrevMonth=(MainActivity.month-1==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month-1];
int daysInNextMonth=(MainActivity.month+1==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month+1];
Calendar calendar=Calendar.getInstance();
calendar.set(MainActivity.year, MainActivity.month, 1);
int startDayOfWeek=calendar.get(Calendar.DAY_OF_WEEK);
List<Integer> dates=new ArrayList<Integer>();
for (int i=0; i<startDayOfWeek-1; i++) {
dates.add(daysInPrevMonth-i);
}
for (int i=0; i<daysInCurrentMonth; i++) {
dates.add(i+1);
}
ArrayAdapter<Integer> adapter=new ArrayAdapter<Integer>(getActivity(), R.layout.grid_view_cell_layout, R.id.textViewGridCell, dates);
events.setAdapter(adapter);
}
private View initializeCalendarScreen(LayoutInflater inflater) {
View layout=inflater.inflate(R.layout.calendar_layout, null);
events=(GridView)layout.findViewById(R.id.gridViewCalendar);
updateGridView();
return layout;
}
}
It works good (I use it for FragmentPagerAdapter). And I use method updateGridView() from Activity for updating value in GridView. But if I do it I will got NullPointerException, because getActivity() for ArrayAdapter returns null. Please, tell me, how can I update Fragment UI from Activity? Thank you
ONE POSSIBLE SOLUTION IS:
Create static global variable context
create context = inflater.getContext().
Use context to create ArrayAdapter when updating UI.
Change:
...=new ArrayAdapter<Integer>(context,...)
Use an Observer in the Fragment instead of calling updateGridView() outside your Fragment. Have an Observer in your Fragment observe an Observable in your main Activity. When the data needs to be updated, just call the notifyObservers() method of the Observable. The Observer class should be a child class of your Fragment and should look something like this:
class UpdateGridObserver implements Observer {
#Override
public void update(Observable observable, Object data) {
// Do stuff like:
// runOnUiThread is REQUIRED because updating the adapter
// and ListView from a background thread is not allowed
getActivity().runOnUiThread(new Runnable(){
updateGridView();
});
}
}
This allows for the data to be updated without having to worry about scope and all that jazz. As long as the Observable object is accessible to the Observer object you should be good to go.
You can extend the Observable class (as with any class) as well to have a custom Observable object if you need more capability than just notification. It's outside the scope of what I think you are looking for, but I've linked some wikis and howtos below.
Observer-Observable Pattern: http://en.wikipedia.org/wiki/Observer_pattern
More: http://www.javaworld.com/jw-10-1996/jw-10-howto.html