My project use MVP architecture and RxJava to get data from a remote JSON Api.
I have a MainActivity, it has 2 roles. The first one is to be a fragment container, the second one is to get data from the JSON api and transmit it to my fragment (I only have one fragment for now but will have another one later using the same data).
For now, I'm getting the data in my MainActivity. I'm trying to get the data from my fragment by calling a method in my MainActivity (using an interface for decoupling).
The problem is the data in my fragment is always empty, I suppose it's because my activity inflate my fragment so fast that when my fragment calls my activity method to get the data this data is still empty since the request didn't receive the answer yet and this request is called asynchronously using RxJava.
So I want to wait for the data being loaded to open my fragment,or open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user). The problem is not really how to do this but when and where. Thank you for your help.
I moved my loadData() method and the transaction to open my fragment several times in different positions in the lifecycle, nothing worked. For now everything is in in MainActivity.onStart() :
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
// Load data from JSON API
presenter.loadData(city, authToken);
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
The data is retrieve in the loadData() method of my presenter :
public class MainPresenter implements MainActivityMVP.Presenter {
final static String TAG = MainPresenter.class.getCanonicalName();
private MainActivityMVP.View view;
private MainActivityMVP.Model model;
private Subscription subscription = null;
public MainPresenter(MainActivityMVP.Model model) { this.model = model; }
#Override
public void loadData(String city, String authToken) {
subscription = model.result(city, authToken)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Aqicn>() {
#Override
public void onCompleted() {
Log.i(TAG,"completed");
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
}
#Override
public void onNext(Aqicn aqicn) {
Data data = aqicn.getData();
Iaqi iaqi = data.getIaqi();
ViewModel viewModel = new ViewModel(data.getAqi(),
data.getDominentpol(),
iaqi.getCo().getV(),
iaqi.getH().getV(),
iaqi.getNo2().getV(),
iaqi.getO3().getV(),
iaqi.getP().getV(),
iaqi.getPm10().getV(),
iaqi.getPm25().getV(),
iaqi.getR().getV(),
iaqi.getSo2().getV(),
iaqi.getT().getV(),
iaqi.getW().getV());
Log.d(TAG,data.getCity().getName());
if (view != null) {
view.updateData(viewModel);
}
}
});
}
#Override
public void rxUnsubscribe() {
if (subscription != null) {
if (!subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
}
#Override
public void setView(MainActivityMVP.View view) {
this.view = view;
}
}
When the response to the request is received the presenter call the updateData() method in MainActivity (see in my presenter code above). This is where I initialize the ArrayList pollutionLevels that is supposed to contain the data I try to get from my fragment :
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
This is the method in my MainActivity called from my fragment to get data :
#Override
public ArrayList<PollutionLevel> getPollutionLevels() {
return pollutionLevels;
}
In my fragment I try to get the data in onAttach() but it's always empty :
public interface PollutionLevelsListener{
ArrayList<PollutionLevel> getPollutionLevels();
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
pollutionLevelsListener = (PollutionLevelsListener) context;
ArrayList<PollutionLevel> levels = pollutionLevelsListener.getPollutionLevels();
for(PollutionLevel l:levels) {
Log.d(TAG,l.getName());
}
} catch (ClassCastException castException){
castException.printStackTrace();
}
}
EDIT : add ViewModel.getAllPolluants() method
This is the method in my ViewModel that returns the ArrayList :
public ArrayList<PollutionLevel> getAllPolluants() {
ArrayList<PollutionLevel> allLevels = new ArrayList();
allLevels.add(new PollutionLevel("Co",Double.toString(co)));
allLevels.add(new PollutionLevel("H",Double.toString(h)));
allLevels.add(new PollutionLevel("No2",Double.toString(no2)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("p",Double.toString(p)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("pm10",Integer.toString(pm10)));
allLevels.add(new PollutionLevel("pm25",Integer.toString(pm25)));
allLevels.add(new PollutionLevel("r",Double.toString(r)));
allLevels.add(new PollutionLevel("so2",Double.toString(so2)));
allLevels.add(new PollutionLevel("t",Double.toString(t)));
allLevels.add(new PollutionLevel("w",Double.toString(w)));
return allLevels;
}
EDIT : Add new modified MainActivity class and PollutionLevelListener interface, trying to apply #cricket_007 answer
public class MainActivity extends AppCompatActivity implements MainActivityMVP.View, PollutionLevelsListener {
final static String TAG = MainActivity.class.getCanonicalName();
#BindString(R.string.city)
String city;
#BindString(R.string.aqicn_token)
String authToken;
#Inject
MainActivityMVP.Presenter presenter;
ArrayList<PollutionLevel> pollutionLevels;
PollutionLevelsListener pollutionListener;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
((App) getApplication()).getComponent().injectPollutionLevels(this);
}
#Override
public void updateData(ViewModel viewModel) {
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
//===== NullPointerException
pollutionListener.onPollutionLevelsLoaded(pollutionLevels);
}
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
presenter.loadData(city, authToken);
}
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
presenter.rxUnsubscribe();
}
}
Interface
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
#################### EDIT ########################
After a lot of doubt with what solution to adopt I follow the answer and recommendations of #yosriz. This is the code I ended with. Be aware that I still need to implement a cache management feature as for now the JSON resquest is made for both fragment.
As a result I have a common repository used by my both fragment. The MainActivity became only a fragment container, it doesn't get any data. it doesn't even have a MVP structure since I think It's now useless.
My both fragment (so my both features) get their data from PollutionLevelRepository :
public interface Repository {
Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken);
Observable<Aqicn> getPollutionLevels(String city, String authToken);
}
public class PollutionLevelsRepository implements Repository {
private PollutionApiService pollutionApiService;
private static Observable<Aqicn> pollutionData = null;
public PollutionLevelsRepository(PollutionApiService pollutionApiService) {
this.pollutionApiService = pollutionApiService;
}
#Override
public Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken) {
pollutionData = pollutionApiService.getPollutionObservable(city, authToken);
return pollutionData;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
return getPollutionLevelsFromNetwork(city, authToken);
}
}
The Model of my first fragment (Donut feature) :
public class DonutModel implements DonutFragmentMVP.Model {
final static String TAG = DonutModel.class.getSimpleName();
private Repository repository;
public DonutModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
The Model of my second fragment (Pollution level feature) :
public class PollutionLevelsModel implements PollutionLevelsFragmentMVP.Model {
private Repository repository;
public PollutionLevelsModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> result(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
Well, you probably have timing issue, the model.result is async IO operation that will update data on activity in async fashion when it will finish, while your fragment call to get the data is happening as soon as the fragment attached the activity (which is still async as you call fragment commit() and not commitNow()) but if you compare it to the probably network call of model.result it will be probably always faster.
Actually I think your approach is wrong, when you're using reactive fashion with Rx you should push the data, here at the end, you're pulling it at the fragment side from the Activity, while you don't know if this data is already available.
The data that is loaded from the presenter should update immediately the fragment, meaning either your Activity.updateData() will update the fragment, or more correct approach to my opinion is that the presenter will be tied to the fragment itself as this is the actual View it's updating, so the view.UpdateData() at the presenter will notify the fragment directly.
Did you tried to make an method inside the fragment and you can hit it once updateData(ViewModel viewModel) called ?
for example (try to add this method in you fragment):
public class YourFragmentName extends Fragment {
public YourFragmentName(StepsHandler stepsHandler){
this.stepsHandler = stepsHandler;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_supplier_registrtion_first, container, false);
return rootView;
}
public void dataLoaded() {
// Do what you need after data finish loading..
}
}
From your Activity :
public class MainActivity extends Activity implements StepsHandler {
YourFragmentName fragmentName;
//onCreate ()
fragmentName = new YourFragmentName(this);
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
fragmentName.dataLoaded();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
}
I'm trying to get the data from my fragment by calling a method in my MainActivity
It seems your interface is only returning the field, which could very possibly be before the request has finished. Which you seem to understand...
didn't receive the answer yet and this request is called asynchronously using RxJava
I wouldn't suggest you wait, and instead do
open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user).
However you want to implement that, you can try a new ProgressDialog() and show / hide that.
Your issue is that onAttach gets immediately called and the request is still going on indefinitely.
You need to "subscribe" for that data from the Fragment.
A "listener" is not typically written to implement a "getter", so let's rewrite that
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
Then, you can use that instead to start your Fragment rather than immediately when the Activity starts
// The Activity
class ... implements PollutionLevelsListener {
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Moved this section here
// Load fragments
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
// If your object is Parcelable
/*
* Bundle args = new Bundle();
* args.putParcelableArrayList(levels);
* fragment.setArguments(args);
*/
ft.add(R.id.ll_container, fragment).commit();
}
}
And now that you have that method,
the presenter call the updateData() method in MainActivity
Well, there's where the list comes from, so just pass it to that new method where the Fragment is then loaded
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
if (pollutionLevels == null) {
pollutionsLevels = new ArrayList<>();
}
pollutionLevels.clear();
pollutionLevels.addAll(viewModel.getAllPolluants());
this.onPollutionLevelsLoaded(pollutionsLevels);
}
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);
}
I'm new with the ViewModel and I understand that it's a powerful and easy way to communicate with fragments.
My problem is the following : How to load the data retrieved in the SplashActivity in the ViewModel of the mainActivity ?
My app achitecture is the following :
SplashActivity : retrieve data with retrofit and store it into a List
Main Activity : contains two fragments displaying the data in different ways
Here is a piece of code showing my implementation.
SplashActivity
public class SplashActivity extends AppCompatActivity {
private final String TAG = "TAG.SplashActivity";
public static List<Toilet> toiletList = new ArrayList<>(); // HERE IS THE DATA I WANT TO
RETRIEVE IN THE MAIN ACTIVITY
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*Create handle for the RetrofitInstance interface*/
GetDataService service = ...;
// MY STUFF RETROFIT including
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra("toiletList", (Serializable) toiletList);
startActivity(intent);
finish();
}
}
MainActivity
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
// SETTING UP FRAGMENTS
}
}
FragmentExample
public class MainFragment extends Fragment {
public static List<Toilet> toiletArrayList = new ArrayList<>();
private final String TAG = this.getClass().getName();
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
// SETTING UP UI
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
model.getToiletList().observe(this, new Observer<List<Toilet>>() {
#Override
public void onChanged(#Nullable List<Toilet> toilets) {
// update UI
}
});
}
}
ToiletListViewModel
public class ToiletListViewModel extends ViewModel {
private final String TAG = getClass().getName();
private MutableLiveData<List<Toilet>> toiletList;
public LiveData<List<Toilet>> getToiletList() {
if (toiletList == null) {
toiletList = new MutableLiveData<>();
loadToilets();
}
return toiletList;
}
private void loadToilets() {
// asynchronously fetch toilets
// HERE IS MY PROBLEM : How to access the toiletList retrieved
in the SplashActivity ?
toiletList.setValue(SplashActivity.toiletList);
}
#Override
protected void onCleared() {
super.onCleared();
Log.d(TAG, "onCleared() called");
}
}
I hope that's clear. If you want any further info, fell free to ask !
Best
You can share your ToiletListViewModel between the MainActivity and its Fragments.
So what you need is to provide your ViewModel with MainActivity scope (It means you bound the lifecycle of your ViewModel to your Activity) and call initToilets then child fragments can easily retrieve this ViewModel and observe on its LiveData.
ToiletListViewModel:
public class ToiletListViewModel extends ViewModel {
private MutableLiveData<List<Toilet>> toiletList = new MutableLiveData();
public LiveData<List<Toilet>> getToiletList() {
return toiletList;
}
private void initToilets(List<Toilet> toilets) {
toiletList.setValue(toilets);
}
}
MainActivity:
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
ToiletListViewModel vm = ViewModelProviders.of(this).get(ToiletListViewModel.class);
vm.initToilets(toiletList);
// SETTING UP FRAGMENTS
}
}
So, when setValue is called, Fragments that listen to the toiletList live data will be notified.
Note:
You can create a shared ViewModel without providing it on MainActivity, instead of calling
ViewModelProviders.of(this).get(ToiletListViewModel.class);
in your Fragment do
ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
In order to get use out of the a view model, you need to store a reference to it's instance in your activities and then interface with them to modify data.
I would first of all suggest to you that you read the developer guide on View Model.
When you are set-up and storing a reference to the model in your activities and fragments, you could add a method to the model, like setToilets(List<Toilet>), which updates the toilets in the View Model, calls loadToilets() or stores the raw toilets so loadToilets() can later access it and now what toilets to load.
Then you can access all the data that you want to expose from other classes by writing the respective methods, just like you did with the getToiletList(LiveData<Toilet>) -method.
There are two suggestions:
You can add data to list directly (Off Topic):
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
use this instead of:
if (s instanceof List<?>) {
toiletList.addAll((List<Toilet>)s);
}
Back to main topic:
You can take ViewModel instance of Activity instead of this in Fragment. How?
Take ViewModel in activity as below,
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
& for Fragment share it like this,
ToiletListViewModel model = ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
This will share your ViewModel between fragments inside of activity & observe your livedata.
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();
}
});
}
}
I am new to android architecture components and I am little confused with viewmodel. I am building an app which get a list of items from the server and display as a list in the layout. I have implemented the network call in the Repository class.
Repository.java:
//Get list of top rated movies
public LiveData<NetworkResponse> getTopRatedMovies() {
final MutableLiveData<NetworkResponse> result = new MutableLiveData<>();
ApiService api = retrofit.create(ApiService.class);
Call<MovieData> call = api.getTopRateMovies("api_key");
call.enqueue(new Callback<MovieData>() {
#Override
public void onResponse(Call<MovieData> call, Response<MovieData> response) {
result.postValue(new NetworkResponse(response.body()));
}
#Override
public void onFailure(Call<MovieData> call, Throwable t) {
Log.e(TAG, t.getLocalizedMessage());
result.postValue(new NetworkResponse(t));
}
});
return result;
}
Now in the ViewModel class I am doing this:
public class MovieListViewModel extends ViewModel {
public LiveData<NetworkResponse> result, topRatedMovies;
public LiveData<List<MovieEntity>> favoriteMovies;
private Repository repository;
public MovieListViewModel() {
repository = new Repository(MyApplication.getInstance());
}
public void getTopRatedMovieList() {
topRatedMovies = repository.getTopRatedMovies();
}
}
Now in the MainActivity.java:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
((MyApplication) getApplication()).getComponent().inject(this);
movieListViewModel = ViewModelProviders.of(this).get(MovieListViewModel.class);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
adapter = new MovieListAdapter(this);
movieListViewModel.getTopRatedMovieList();
observeTopRatedMovies();
}
private void observeTopRatedMovies() {
movieListViewModel.topRatedMovies.observe(this, new Observer<NetworkResponse>() {
#Override
public void onChanged(#Nullable NetworkResponse networkResponse) {
if (networkResponse.getPostData() != null) {
Log.e(TAG, "Successful");
topRatedData = networkResponse.getPostData();
adapter.addData(networkResponse.getPostData().getResults());
recyclerView.setAdapter(adapter);
} else {
Log.e(TAG, "failure");
}
}
});
}
Now everything works fine and I am able to see the list. But if I rotate the phone the viewmodel makes the network call again. How can I avoid the network call again on screen orientation change?
You can initialize live data only once. That should be enough:
public class MovieListViewModel extends ViewModel {
public LiveData<NetworkResponse> result, topRatedMovies;
public LiveData<List<MovieEntity>> favoriteMovies;
private Repository repository;
public MovieListViewModel() {
repository = new Repository(MyApplication.getInstance());
topRatedMovies = repository.getTopRatedMovies();
}
}
I suggest you to use headless-fragment design pattern. A headless fragment is a fragment that retain his configuration and it doesn't inflate any xml. If you rotate your app the fragment continue with his logic and configuration and is very useful when you have to do async task or async call (like you in retrofit)
define your fragment:
public class YourFragment extends Fragment {
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true); // <--------- the fragment retain his configuration
}
public void yourLogic(){
// do your logic
}
}
in your MainActivity class create the fragment or get the istance fragment if it already exists:
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_yourHeadLessFragment= (YourFragment) getSupportFragmentManager().findFragmentByTag(HEADLESS_FRAGMENT);
if (_yourHeadLessFragment== null) {
_yourHeadLessFragment= new YourFragment();
_yourHeadLessFragment.setListener(this); // if you want a callback
getSupportFragmentManager().beginTransaction().add(_yourHeadLessFragment, HEADLESS_FRAGMENT).commit();
}
else{
_yourHeadLessFragment.setListener(this); // refresh the callbacks if a rotation happened
}
}
}
you can put network call in the init block of ViewModel
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();
}
}
});
}