I'm using new arch. components from google.
I have in Activity Login/Registering Fragments that are managed thanks to FragmentTransaction
Activity->RegisterFragment (with ViewPager) -> RegistrationSteps (adapter)
Inside RegisterFragment I have ViewPager.
I want that all pages inside ViewPager will use the same ViewModel.
Those are registration steps (RegistrationStepFragment) that takes parent RegistrationFragment LifecycleOwner that scoped ViewModel to it- I just wanted ViewModel to be scoped to this parent Fragment.
RegistrationFragment.class inhered from
public interface FragmentViewPagerListener<T extends LifecycleFragment> {
void nextPage();
T getLifecycleFragment();
}
RegistartionSteps (pages) inhered from
public abstract class RegisterStepFragment extends LifecycleFragment {
protected FragmentViewPagerListener mListener;
protected RegisterViewModel mViewModel;
public void setListener(FragmentViewPagerListener fragmentViewPagerListener) {
this.mListener = fragmentViewPagerListener;
}
protected abstract void observeViewModel();
#Override
public void onCreated(#Nullable Bundle savedInstanceState) {
super.onCreated(savedInstanceState);
mViewModel = ViewModelProviders.of(mListener.getLifecycleFragment()).get(RegisterViewModel.class);
observeViewModel();
}
protected abstract boolean validateData();
}
All goes well until I reach 3 page, and I want to move back (to second page) Then is thrown exception in mViewPager.setCurrentItem(1) (page 2: index: 1)
*java.lang.RuntimeException: Failed to call observer method
at android.arch.lifecycle.ReflectiveGenericLifecycleObserver.invokeCallback(ReflectiveGenericLifecycleObserver.java:79)
at android.arch.lifecycle.ReflectiveGenericLifecycleObserver.invokeMethodsForEvent(ReflectiveGenericLifecycleObserver.java:53)
at android.arch.lifecycle.ReflectiveGenericLifecycleObserver.invokeCallbacks(ReflectiveGenericLifecycleObserver.java:61)
at android.arch.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:45)
at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.sync(LifecycleRegistry.java:209)
at android.arch.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:102)
at android.arch.lifecycle.Li*fecycleDispatcher.dispatchIfLifecycleOwner(LifecycleDispatcher.java:150)
EDIT
Ok, I've found out that when moving to previous page, Fragment was reCreated calling and mViewModel.observable() received previously SUCCESS message and caused viewpager to move forward, that causes to destroy just created fragment what was causing error.
Solution is to create SingleEventLiveData that emits values only when post is called (don't notify observer if value changed before observer attached)
I mark it as CLOSE
Related
I have an activity that performs network operation, and on success, it needs to send data to view pager fragment.
So this is my structure.
Activity -> Home Fragment -> ViewPager [Fragment#1, Fragment#2]
Now the issue is that i can send data from Activity to Home Fragment, but i am unable to send data to View Pager Fragment, as activity does not have any direct connection with it.
To Fix this, I have taken network call from activity to fragment, and once i get response from service call, i pass data to viewPager fragment from Home Fragment.
This is working fine, and giving me desired result, but i am little confused if this is the right approach.
or there is some other recommended approach available that i can use, and pass data from activity to child fragment, or view Pager fragment, whose reference is not directly available to activity.
I would look at the new Android Architecture Components, specifically ViewModel and LiveData. https://developer.android.com/topic/libraries/architecture/viewmodel
The ViewModel class is designed to store and manage UI-related data in
a lifecycle conscious way.
You can create a shared view model that can be accessed by the activity and any fragment in that activity.
Example from the link:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, item -> {
// Update the UI.
});
}
}
Notice that both fragments use getActivity() when getting the
ViewModelProvider. As a result, both fragments receive the same
SharedViewModel instance, which is scoped to the activity.
For your example, this avoids passing data down the hierarchy from activity to fragment then child fragment, they can all reference SharedViewModel.
I have a Fragment A which has an interface that will return the value of a TextView. The Fragment A is initialized and Attached to FragmentB. Here is the code for Fragment A and B.
Fragment A
public class FragmentA extends Fragment {
...
public interface Listener {
void onValue(int value);
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof Listener) {
listener = (Listener) context;
} else {
throw new RuntimeException(context.toString() + " must implement Listener");
}
}
}
Fragment B
public class FragmentB extends Fragment implements FragmentA.Listener {
...
private void initFragmentA() {
FragmentManager fragmentManager = getChildFragmentManager();
fragmentA = FragmentA.newInstance();
fragmentManager.beginTransaction().add(container, fragmentA, TAG).commit();
}
#Override
public void onValue(int value) {
}
}
When I start the app, a error occurred:
Java.lang.RuntimeException: ####.page.MainActivity#1f7f316c must implement Listener
at ####.widget.FragmentA.onAttach(FragmentA.java:66)
The MainActivity contains the Fragment B, but the Fragment B has already implemented the interface of Fragment A. Why the error occurred? The interface implemented in parent Fragment doesn't work?
From Android Developer website, it says:
Often you will want one Fragment to communicate with another, for example to change the content based on a user event. All Fragment-to-Fragment communication is done through the associated Activity. Two Fragments should never communicate directly.
To allow a Fragment to communicate up to its Activity, you can define an interface in the Fragment class and implement it within the Activity. The Fragment captures the interface implementation during its onAttach() lifecycle method and can then call the Interface methods in order to communicate with the Activity.
Therefore, when you implement that interface in Fragment B, it throws a RuntimeException error.
Summary: you have to implement that interface in your fragment hosting activity.
You can learn more on how to pass data between fragments thru a hosting activity (usually we make use of Bundle) at http://www.vogella.com/tutorials/AndroidFragments/article.html
The error message is pretty clear. When you use onAttach, the context passed through is actually the host Activity - the biggest boss of all, not the parent fragment. So FragmentB which implement the interface but will not receive any update.
You should use getParentFragment to access FragmentB from FragmentA, then cast it to the Listener interface.
I have an external event that changes several fragments state, As I am using Android architecture components I've created several ModelViews for every fragment.
What is the right way to send messages between ModelViews
I am assuming you are using viewModel to preserve state across activity and fragment recreations. You do realize that a viewModel is essentially your model class, right? So why would you want to send messages between viewModels?
If you have an external event that changes a fragment's state, you should propagate it to your activities who will then send those message to your fragments where you can update your view model's state.
You should use one ViewModel in your activity for your event. You can then observe this view model from your activity and other fragments.
For example, below ViewModel class can wrap your event
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
Then you can subscribe to changes to your event in different fragments like below:
public class DetailFragment extends LifecycleFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
Once subscribed, you can now make changes to your event data which will notify the observing fragments or activity:
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
Take a look at sharing data between fragments. Google documentation on android architecture components is limited but good.
I have an activity with 3 fragments, currently I use ViewPager. I want to implement MVP and communicate between activity presenter and fragment presenters i.e:
Passing data from activity presenter to fragment presenters
Sending event from fragment presenters to activity presenter
...
But I don't know how to do it in official way. I can use BusEvent but I don't think it's a good practice.
Communication between fragments and activity or vice-versa can be done by using
nnn's answer or you could use ViewModel and LiveData witch provides a cleaner way and respect the lifecycle from fragments and activities which can save from writing a few lines of code in attempt to prevent a a non-visible fragment from receiving data on the background.
First you extend the ViewModel class, initialize the Livedata and some helper methods.
public class MyViewModel extends ViewModel {
private MutableLiveData<String> toFragmentA, toFragmentB;
private MutableLiveData<List<String>> toAllFragments;
public MyViewModel() {
toFragmentA = new MutableLiveData<>();
toFragmentB = new MutableLiveData<>();
toAllFragments = new MutableLiveData<>();
}
public void changeFragmentAData(String value){
toFragmentA.postValue(value);
}
public void changeFragmentBData(String value){
toFragmentB.postValue(value);
}
public void changeFragmentAllData(List<String> value){
toAllFragments.postValue(value);
}
public LiveData<String> getToFragmentA() {
return toFragmentA;
}
public LiveData<List<String>> getToAllFragments() {
return toAllFragments;
}
public LiveData<String> getToFragmentB() {
return toFragmentB;
}
}
Then you initialize the ViewModel on your activity.
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private TabLayout tabLayout;
MyViewModel mViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewModel = ViewModelProviders.of(this)
.get(MyViewModel.class);
viewPager.setAdapter(new Adapter(getSupportFragmentManager()));
}
}
reading the data in the fragments:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mViewModel = ViewModelProviders.of(getActivity()).get(MyViewModel.class);
mViewModel.getToAllFragments().observe(this, new Observer<List<String>>() {
#Override
public void onChanged(List<String> s) {
myList.addAll(s);
//do something like update a RecyclerView
}
});
mViewModel.getToFragmentA().observe(this, new Observer<String>() {
#Override
public void onChanged(String s) {
mytext = s;
//do something like update a TextView
}
});
}
to change the values of any of the live datas you can use one of the methods in any of the fragments or in the activity:
changeFragmentAData();
changeFragmentBData();
changeFragmentAllData();
Whats happing behind the scenes:
when you use mViewModel = ViewModelProviders.of(this).get(MyViewModel.class) you are creating a n instance of ViewModel and binding it to the lifecycle of the given activity of fragment so the view model is destroid only the the activity or fragement is stopped. if you use mViewModel = ViewModelProviders.of(getActivity()).get(MyViewModel.class)you are bindig it to the lifecycle if the parentactivity`
when you use mViewModel.getToFragmentA().observe() or mViewModel.getToFragmentB().observe() or mViewModel.getToAllFragments().observe() you are connecting the LiveData in MyViewModel class to the given fragment or activity an the value of the onChange() method is updated in all the classes that are observing the method.
I recomend for personal expirience a bit of research about Livedata end ViewModel which ou can on youtube or this link
As per my understanding, for your UseCase, suppose ActivityA have a viewPager having 3 Fragments(FragmentA, FragmentB, FragmentC).
ActivityA have ActivityPresenterA
FragmentA have FragmentPresenterA
As per MVP, FragmentPresenterA should be responsible for all the logical and business flows of FragmentA only and should communicate with FragmentA only. Therefore, FragmentPresenterA can not directly communicate with ActivityPresenterA.
For communication from Fragment to Activity, presenter should not be involved and this should be done as we would communicate in non-MVP architecture, i.e. with the help of interface.
Same applies for Activity to Fragment communication.
For communication between Activity and Fragment read here
You can use one presenter for that case.
Used your Activity Presenter to get all the data that your fragments need.
then create an interface class and implement it to your fragments.
For example:
Create a public interface for your PageAFragment (this interface will the bridge of your data from activity to fragment). and use the method of your interface to handle the result from your presenter to view.
This is the example of interface class that I created for received data. for the parameter you can choose what you want it depends on your need, but for me I choose model.
public interface CallbackReceivedData {
void onDataReceived(YourModel model);
}
In MainActivity Class check the instance of fragment that attached into your activity. put your checking instance after you commit the fragment.
public class MainActivity extends AppCompatActivity{
private CallbackReceivedData callbackReceivedData;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//after commit the fragment
if (fragment instanceof PageAFragment){
callbackReceivedData = (CallbackReceivedData)fragment;
}
}
//this is the example method of MainActivity Presenter,
//Imagine it, as your view method.
public void receivedDataFromPresenter(YourModel model){
callbackReceivedData.onDataReceived(model);
}
}
I assumed that the receivedDataFromPresenter is the received method of our view and get data to presenter.
And now we will pass the data from presenter to callbackReceivedData
In PageAFragment implement the CallbackReceivedData and Override the onDataReceived method. Now you can passed the data from activity to your fragment.
public class PageAFragment extends Fragment implements CallbackReceivedData{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public void onDataReceived(YourModel model) {
}
}
Note: Alternative way, you can use Bundle and pass the data with the use of setArguments.
If you want to send Event from Fragment to Activity you can follow this Idea.
Create an Interface class and implement it to your MainActivity and Override the method from interface to your activity, for My case I do it something like this.
Here's my CallbackSendData Class.
public interface CallbackSendData {
void sendDataEvent(String event);
}
Implement CallbackSendData interface to your MainActivity and Override the sendDataEvent method.
public class MainActivity extends AppCompatActivity implements CallbackSendData{
private CallbackReceivedData callbackReceivedData;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//after commit the fragment
if (fragment instanceof PageAFragment){
callbackReceivedData = (CallbackReceivedData)fragment;
}
}
//this is the example method of MainActivity Presenter,
//Imagine it, as your view method.
public void receivedDataFromPresenter(YourModel model){
callbackReceivedData.onDataReceived(model);
}
#Override
public void sendDataEvent(String event){
//You can now send the data to your presenter here.
}
}
And to your PageAFragment you need to use attach method to cast your interface. The attach method called once the fragment is associated with its activity. If you want to understand the lifecycle of fragment just click this link: https://developer.android.com/reference/android/app/Fragment.html.
public class PageAFragment extends Fragment implements CallbackReceivedData{
private CallbackSendData callbackSendData;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public void onDataReceived(YourModel model) {
//Received the data from Activity to Fragment here.
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup
container, #Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.PagerAFragment, container,
false);
}
#Override
public void onViewCreated(View view, #Nullable Bundle
savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Button Eventbutton;
Eventbutton = view.findViewById(R.id.event_button);
Eventbutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
callbackSendData.sendDataEvent("send Data sample");
}
});
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try{
callbackSendData = (CallbackSendData) context;
}catch (ClassCastException e){
e.printStackTrace();
}
}
}
And now you can use the CallbackSendData to send the data from activity to fragment.
Note: It's much easier if you are using Dependency Injection to your project, you can use Dagger2 library.
Goodluck.
To communicate between a Fragment and an Activity (whether between their presenters or their classes), you need an interface that your activity implements (like ShoppingInteractor).
This way you can call ((ShoppingInteractor)getActivity()).doSomething() in the fragments. If you want your activity's presenter to handle the task, you need to call the presenter in the doSomething inside the activity.
You can do the same with the fragments with another interface and call the fragment's interactor inside the activity.
You can even have a Presenter getPresenter() inside these interfaces to have access to the actual presenter. (((ShoppingInteractor)getActivity()).getPresenter().sendData(data)). Same goes for the fragments.
If you want to use MVP, the first step is to create one presenter for each View, I mean, If you have 3 fragments, then would have 3 presenters. I think that is a bad idea to create one presenter for 4 views (activity and 3 fragments).
Dynamic data:
Here is an example using rxjava2, dagger2 and moxy.
Conditionalities:
Presenters do not depend on the life cycle of the view
One presenter - one view. The views do not share the presenters among themselves and one view has only one presenter.
The solution is similar to the EventBus, but instead uses Subject with a limited lifetime. It is in the component that is created when the activity starts and is destroyed when it exits. Both activity and fragments have an implicit access to it, they can change the value and respond to it in their own way.
Example project: https://github.com/Anrimian/ViewPagerMvpExample
Static data:
Just use arguments in the fragment and that's it.
I have a main fragment with a viewpager inside it. This viewpager has 2 pages (list fragments). When I start the activty, the main fragment is shown and I also show the first paged fragment. This paged fragment displays data from a db using AsyncTask.
In the main fragment I have:
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
onPageSelected(0);
}
#Override
public void onPageSelected(int position) {
Fragment fragment = (Fragment) pagerAdapter.instantiateItem(viewPager, position);
if (fragment instanceof IPagedFragment) {
((IPagedFragment) fragment).onShown(getActivity());
}
}
And the interface is:
public interface IPagedFragment {
void onShown(FragmentActivity activity);
}
The first issue I have is that I have to pass the activity as a parameter because when onShown gets called, the activity is still null.
Furthermore, the paged fragments use progressbar logic similar to the LoginActivity sample. I also get the following exception:
IllegalStateException: Fragment PagedFragment1{4201f758} not attached to Activity
at android.support.v4.app.Fragment.getResources(Fragment.java:620)
So what is the correct stage to start retrieving data from db once the paged fragment is fully available to the UI?
Issues like yours is the reason some developers are starting to question if fragments are really that good or useful.
Also "the correct" is debatable as you can do it in a variety of places and different developers will give you different answers, But let me try to supply you some useful info.
The attach/detach callbacks:
public void onAttach(Activity activity);
public void onDetach();
between those two methods any call to getActivity() will return the non-null activity the fragments is connected to. You can override them and use a private boolean isAttached to keep track of that call.
Also useful is the:
public void onActivityCreated (Bundle savedInstanceState)
this method is called AFTER the Activity.onCreate method. That is very important if you rely on some initialisation that happened there.
Also it's important to remember that on the moment the fragment transaction happens, the Fragment.onCreate happens after the Activity.onCreate and during rotation it happens before it.
As a general rule of thumb I use the Fragment.onStart() / Fragment.onStop() for getting/listening to data. On those calls, all the UI have been created, the fragment is attached to the activity and those callbacks don't get called if there's a dialog/popup (pause/resume does)
From the documentation:
public void onActivityCreated (Bundle savedInstanceState)
[...] tells the fragment when it is fully associated with the new activity instance.
source: http://developer.android.com/reference/android/app/Fragment.html#onActivityCreated(android.os.Bundle)
To get the reference of your activity, create a local object of fragmentActivity and get your activity reference as shown below.
private FragmentActivity fragmentActivity;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
fragmentActivity=activity;
}