I have this structure:
Activity A has a ViewPager. The pages of a ViewPager are as known Fragments.
Activity A has a list of complex objects, which I want to display in the pages.
When the user clicks on an item inside the ViewPager activity B must be started. Activity B also needs the list with complex objects.
So I have to pass this list
A -> Fragment(s) -> B
First I found out that it's not possible to use the fragment's constructor to pass data to it. I have to use parcelable or serializable. In the fragment I then have to parcel or serialize again to pass to B.
But I don't want to serialize or parcel whole list of complex data 2 times only to get it through to B.
I would like to avoid using static fields.
Then I came to the idea to pass a listener to the fragment, which listens to item click, and notifies my activity A and activity A then starts activity B with the data. This looks cleanest for me because A is actually the one the data belongs to, so A starts fragment with data, fragment comes back to A and then A starts B with data. I don't have to pass the data everywhere (serializing!).
But this doesn't seem to work, because how do I pass a listener to the fragment using serialization? What is wrong about using a listener in a fragment to come back to the activity?
How do I solve this?
Edit
I found this solution in a thread about dialog fragments: Get data back from a fragment dialog - best practices?
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
So I add this to my fragment and make my activity implement certain interface.
This would solve the problem of passing data from fragments to B. But I still need to pass the list from A to the fragments. Probably I'll have to use a Parcelable...
Edit: I tried Parcelable, it works. But the objects in my list have a lot of fields between them also maps, for example. Writing the Parcelable code (write / read) is a pain. Maybe I just stick to static data...
The listener solution you found is excellent and actually recommended by Google (http://developer.android.com/training/basics/fragments/communicating.html).
As for your remaining problem of passing data to fragments, are you sure you need the entire list passed, and not just an element of the list for each fragment in the ViewPager? If you just need an element, you can do something like this:
/*
* An adapter for swiping through the fragments we have.
*/
private class CustomPagerAdapter extends FragmentStatePagerAdapter {
List<Thing> things;
public CustomPagerAdapter(FragmentManager fragmentManager, List<Thing> things) {
super(fragmentManager);
this.things = things;
}
#Override
public int getCount() {
return things.size();
}
#Override
public Fragment getItem(int position) {
return ThingFragment.newInstance(things.get(position));
}
}
/*
* Loads a view based on the thing.
*/
private static class ThingFragment extends Fragment {
private String name;
static ThingFragment newInstance(Thing thing) {
ThingFragment f = new ThingFragment();
// add some arguments to our fragment for onCreateView
Bundle args = new Bundle();
args.putString("name", thing.getName());
f.setArguments(args);
return f;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
name = getArguments().getString("name");
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.thing_fragment_layout, container, false);
TextView t = (TextView)v.findViewById(R.id.thingText);
t.setText(name);
return v;
}
}
And pass your list to the adapter. Of course, this requires that your dynamics be created dynamically, but inflating xml fragments is really easy.
If you actually DO need to pass the entire list, yeah, you might have to make the individual complex object Parcelable, and then add it to your Fragment args like with Bundle.putParcelableArray().
Related
I have two fragments, A and B let's say, where B contains a list. I would like to add a listener on Fragment B that notifies Fragment A of the chosen list item. I couldn't figure out how to initialize the listener in Fragment B since it is bad practice to pass arguments in fragment's constructors.
NOTE: Fragment B is contained inside Fragment A. i.e. I have a FrameLayout in Fragment A; and Fragment B covers that FrameLayout.
Any idea how I could do that?
If you're saying that Fragment B is a child fragment of Fragment A (that is, you've added it to Fragment A using Fragment A's getChildFragmentManager()), then you can use the same approach that you use for Activity interfaces, but using getParentFragment() instead of getActivity().
For example:
Fragment B:
#Override
public void onAttach(Context context) {
MyInterface myInterface = (MyInterface) getParentFragment();
}
Assuming that Fragment A implements MyInterface.
One convenience method we've used to avoid having to know whether a Fragment is hosted by another Fragment or an Activity is something like:
public static <T> getInterface(Class<T> interfaceClass, Fragment thisFragment) {
final Fragment parent = thisFragment.getParentFragment();
if (parent != null && interfaceClass.isAssignableFrom(parent)) {
return interfaceClass.cast(parent);
}
final Activity activity = thisFragment.getActivity();
if (activity != null && interfaceClass.isAssignableFrom(activity)) {
return interfaceClass.cast(activity);
}
return null;
}
Then you can just use:
MyInterface myInterface = getInterface(MyInterface.class, this);
and it doesn't matter whether Fragment B is hosted as a child Fragment or in an Activity directly.
A better approach for this situation, since what you want to do is communication between fragments, is to use an interface. You want to notify A when B has changed. This should be done through the parent activity. Here is the android documentation on the topic: https://developer.android.com/training/basics/fragments/communicating.html.
The gist of it is that you want to define an interface with a method called OnItemSelected (you can name it whatever you want). In B, you want a reference to this interface. When an item is selected, call your new OnItemSelected method. Implement this interface in the parent activity of the two fragments. In the implementation, you can put whatever code you want to modify A.
An example
CommunicationInterface
public interface CommunicationInterface {
public void onItemSelected(int position);
}
FragmentB
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
CommunicationInterface myInterface = (CommunicationInterface) getActivity();
// What ever else you want here
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
// Send the event to the host activity
myInterface.onItemSelected(position);
}
MainActivity
public class MainActivity extends FragmentActivity implements CommunicationInterface {
// What ever other code you have
#Override
public void onItemSelected(int position) {
FragmentA fragA = (FragmentA)
getSupportFragmentManager().findFragmentById(R.id.fragment_a);
// Code to interact with Fragment A
}
Checkout the contract pattern https://gist.github.com/JakeWharton/2621173
If you are using multiple fragment, you dont have do it for every fragment, just add it to your BaseActivity if you have one.
This example shows the communication between activity and fragment. But for nested fragment you can replace the acitivy with getParentFragment();
I have a ViewPager using a FragmentPagerAdapter for displaying three tabs, each represented by its ow fragment. One of these fragments contains a list, that should be updated on switching / swiping to that tab. But I don't find any way to make it happen. I tried using the onResume method, but the fragments seem not to be paused and resumed on tab change. I also tried using ViewPager.OnPageChangeListener in my MainActivity:
#Override
public void onPageSelected(int position)
{
FragmentRefreshInterface currentFragment = (FragmentRefreshInterface) mSectionsPagerAdapter.getItem(position);
currentFragment.onRefreshed();
}
And in the fragment I use the following:
#Override
public void onRefreshed()
{
List<Record> records = mRecordingService.getRecords();
mRecordAdapter.clear();
mRecordAdapter.add(record);
}
But using this code I can't access my RecordingService class that is used to provide the database functions (because mRecordingService seems to be null). I initialize it in the fragment like this:
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mRecordingService = new RecordingService(getContext());
}
Using the onPageChangeListener is the correct way to do it. I believe the reason why your code is not working, is because you are calling getItem on your pager adapter: getItem() actually returns a new instance of the fragment. In order to get the current instance, you use instantiateItem() (which returns a reference to the fragment actually being used).
Change your code to look something like this:
#Override
public void onPageSelected(int position)
{
FragmentRefreshInterface currentFragment = (FragmentRefreshInterface) mSectionsPagerAdapter.instantiateItem(viewPager,position);
currentFragment.onRefreshed();
}
And it should work.
I suggest that the code you have in onRefreshed() go in onResume() instead. Fragment doesn't have an onRefreshed() method. You must be implementing another interface that declares this method.
Since you are storing data in a database, you should be use a CursorAdapter or subclass such as SimpleCursorAdapter. If you do this correctly, the ListView will automatically update when you add a record to the database. Then the service can add records without needing to access the service from the fragment.
In your MainActivity:
private FirstFragment firstFragment;
private WantedFragment wantedFragment;
private ThirdFragment thirdfragment;
In getItem
switch(postition){
//return first, wanted, third fragments depending on position
}
onPageSelected:
if(position == 1) // position of the wanted fragment
wantedfragment.onRefreshed()
I'm using TabLayout with ViewPager to show tabs in my app. I have only two tabs. These two tabs uses the same fragment (different instances) and I have one recycler in each fragment. Every recycler shows different data.
In my FragmentPagerAdapter, I do this:
#Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
if (position == 0) {
bundle.putString(TreinamentosFragment.EXTRA_TIPO, TreinamentosFragment.TIPO_PENDENTE);
} else {
bundle.putString(TreinamentosFragment.EXTRA_TIPO, TreinamentosFragment.TIPO_CONCLUIDO);
}
Fragment fragment = new TreinamentosFragment();
fragment.setArguments(bundle);
return fragment;
}
With that approach I'm able to know at runtime what type is every fragment
and Get the correct informations in web service to show.
But, the problem is, when user is in Fragment A and interact in some way with one of the recycler items, I remove this item from the adapter and I need to put this item in the adapter of Fragment B. How can I do this? How can I recover the specific instance of Fragment B and change its adapter?
I wanna to do this without use of no kind of event bus.
My Fragment A and B are already inside another fragment because I'm using Navigation Drawer too.
class TreinamentosFragment extends Fragment{
public onItemClick(Item item){
List fragments = getParentFragment().getChildFragmentManager().getFragments();
for(Fragment fragment : fragments ){
if(fragment instanceof TreinamentosFragment &&TreinamentosFragment.TIPO_CONCLUIDO .equalIgnoreCase(fragment.getArgments().getString(TreinamentosFragment.EXTRA_TIPO))){
(TreinamentosFragment )fragment.addItem(item);
}
}
//or
List fragmentA = getParentFragment().getChildFragmentManager().FindFragmentByTag("android:switcher:" + R.id.viewpager + ":1");
if(fragmentA instanceof TreinamentosFragment){
(TreinamentosFragment )fragmentA.addItem(item);
}
}
public addItem(Item item){
}
}
You need to use the Singleton data classes.
https://en.wikipedia.org/wiki/Singleton_pattern
You have 2 adapters then you need two Singleton Data classes.. When you delete item from Fragment-A then add that deleted item in Frag-Bs Singleton Data instance. As Fragment-B resume/visible the just reload data from Frag-Bs Singleton Data instance.
No event Bus solutions.. :)
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() { }
public static synchronized SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
}
I'm using a master/detail pattern with one activity managing the 2-pane view and the selector list and the other activity managing the detail fragments. I'm using an interface to handle fragment callbacks to the Activities.
There seems to be a lot of code duplication though with the details activity copying many of the callback methods from the 2-pane activity. Where appropriate, I've used static methods where context isn't required, but where context is required I'm not sure how to remove the code duplication neatly.
Inheritance with an Abstract parent Activity is an option but seems like a lot of overhead.
Is there a better way of doing this?
I asked a similar question here: How many Activities vs Fragments?
I too was worried about the duplication of logic, and the answers I got caused quite a healthy debate.
In the end I chose to follow Stephen's answer, of putting as much of the logic into the Fragments themselves.
However other contributors seemed very keen on duplicating the logic as per the examples.
So lets say u have Activity AB that controls Frag A and Fragment B.
MY ANSWER:
If the variable is used by Frag A and Frag B, put it in Activity AB. Then pass it to Frag A or Frag B everything they need it. Or have Frag A or Frag B retrieve it from Activity AB.
If the variable is used by Frag A only or Frag B only, put it in Frag A or Frag B respectively.
For methods that are used by both Frag A and Frag B, put those methods in another class and create instances of that class inside Frag A and Frag B for each of the 2 fragments to use.
The following is an answer I gave to another person. However, it seems relevant to your question so I am re-posting it here.
Inside Fragment A u need an interface that Activity AB can implement.
In the sample android code, they have:
private Callbacks mCallbacks = sDummyCallbacks;
/*A callback interface that all activities containing this fragment must implement. This mechanism allows activities to be notified of item selections.
*/
public interface Callbacks {
/*Callback for when an item has been selected. */
public void onItemSelected(String id);
}
/*A dummy implementation of the {#link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity. */
private static Callbacks sDummyCallbacks = new Callbacks() {
#Override
public void onItemSelected(String id) {
}
};
The Callback interface is put inside one of your Fragments (let’s say Fragment A). I think the purpose of this Callbacks interface is like a nested class inside Frag A which any Activity can implement. So if Fragment A was a TV, the CallBacks is the TV Remote (interface) that allows Fragment A to be used by Activity AB. I may be wrong about the details because I'm a noob but I did get my program to work perfectly on all screen sizes and this is what I used.
So inside Fragment A, we have:
(I took this from Android’s Sample programs)
#Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
//mCallbacks.onItemSelected( PUT YOUR SHIT HERE. int, String, etc.);
//mCallbacks.onItemSelected (Object);
}
And inside Activity AB we override the onItemSelected method:
public class AB extends FragmentActivity implements ItemListFragment.Callbacks {
//...
#Override
//public void onItemSelected (CATCH YOUR SHIT HERE) {
//public void onItemSelected (Object obj) {
public void onItemSelected(String id) {
//Pass Data to Fragment B. For example:
Bundle arguments = new Bundle();
arguments.putString(“FragmentB_package”, id);
FragmentB fragment = new FragmentB();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction().replace(R.id.item_detail_container, fragment).commit();
}
So inside Activity AB, you basically throwing everything into a Bundle and passing it to B. If u are not sure how to use a Bundle, look the class up.
I am basically going by the sample code that Android provided. The one with the DummyContent stuff. When u make a new Android Application Package, it's the one titled MasterDetailFlow.
I have a ViewPager (extends FragmentPagerAdapter) which holds two Fragments. What I need is just refresh a ListView for each Fragment when I swipe among them. For this I have implemented ViewPager.OnPageChangeListener interface (namely onPageScrollStateChanged). In order to hold references to Fragments I use a HashTable. I store references to Fragments in HashTable in getItem() method:
#Override
public Fragment getItem(int num) {
if (num == 0) {
Fragment itemsListFragment = new ItemsListFragment();
mPageReferenceMap.put(num, itemsListFragment);
return itemsListFragment;
} else {
Fragment favsListFragment = new ItemsFavsListFragment();
mPageReferenceMap.put(num, favsListFragment);
return favsListFragment;
}
}
So when I swipe from one Fragment to another the onPageScrollStateChanged triggers where I use the HashTable to call required method in both Fragments (refresh):
public void refreshList() {
((ItemsListFragment) mPageReferenceMap.get(0)).refresh();
((ItemsFavsListFragment) mPageReferenceMap.get(1)).refresh();
}
Everything goes fine until orientation change event happens. After it the code in refresh() method, which is:
public void refresh() {
mAdapter.changeCursor(mDbHelper.getAll());
getListView().setItemChecked(-1, true); // The last row from a exception trace finishes here (my class).
}
results in IllegalStateException:
java.lang.IllegalStateException: Content view not yet created
at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
at android.support.v4.app.ListFragment.getListView(ListFragment.java:222)
at ebeletskiy.gmail.com.passwords.ui.ItemsFavsListFragment.refresh(ItemsFavsListFragment.java:17)
Assuming the Content view is not created indeed I set the boolean variable in onActivityCreated() method to true and used if/else condition to call getListView() or not, which shown the activity and content view successfully created.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments. This is just my assumption.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments? May be some other solution? Thank you very much.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments.
The fragments should be automatically recreated, just like any fragment is on an configuration change. The exception would be if you used setRetainInstance(true), in which case they should be the same fragment objects as before.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments?
What is wrong with the fragments that are there?
I've spent some days searching for a solution for this problem, and many points was figured out:
use FragmentPagerAdapter instead of FragmentStatePagerAdapter
use FragmentStatePagerAdapter instead of FragmentPagerAdapter
return POSITION_NONE on getItemPosition override of FragmentPagerAdapter
don't use FragmentPagerAdapter if you need dynamic changes of Fragments
and many many many others...
In my app, like Eugene, I managed myself the instances of created fragments. I keep that in one HashMap<String,Fragment> inside some specialized class, so the fragments are never released, speeding up my app (but consuming more resources).
The problem was when I rotate my tablet (and phone). The getItem(int) wasn't called anymore for that fragment, and I couldn't change it.
I really spent many time until really found a solution, so I need share it with StackOverflow community, who helps me so many many times...
The solution for this problem, although the hard work to find it, is quite simple:
Just keep the reference to FragmentManager in the constructor of FragmentPagerAdapter extends:
public class Manager_Pager extends FragmentPagerAdapter {
private final FragmentManager mFragmentManager;
private final FragmentActivity mContext;
public Manager_Pager(FragmentActivity context) {
super( context.getSupportFragmentManager() );
this.mContext = context;
this.mFragmentManager = context.getSupportFragmentManager();
}
#Override
public int getItemPosition( Object object ) {
// here, check if this fragment is an instance of the
// ***FragmentClass_of_you_want_be_dynamic***
if (object instanceof FragmentClass_of_you_want_be_dynamic) {
// if true, remove from ***FragmentManager*** and return ***POSITION_NONE***
// to force a call to ***getItem***
mFragmentManager.beginTransaction().remove((Fragment) object).commit();
return POSITION_NONE;
}
//don't return POSITION_NONE, avoid fragment recreation.
return super.getItemPosition(object);
}
#Override
public Fragment getItem( int position ) {
if ( position == MY_DYNAMIC_FRAGMENT_INDEX){
Bundle args = new Bundle();
args.putString( "anything", position );
args.putString( "created_at", ALITEC.Utils.timeToStr() );
return Fragment.instantiate( mContext, FragmentClass_of_you_want_be_dynamic.class.getName(), args );
}else
if ( position == OTHER ){
//...
}else
return Fragment.instantiate( mContext, FragmentDefault.class.getName(), null );
}
}
Thats all. And it will work like a charm...
You can clear the saved instance state
protected void onCreate(Bundle savedInstanceState) {
clearBundle(savedInstanceState);
super.onCreate(savedInstanceState, R.layout.activity_car);
}
private void clearBundle(Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.remove("android:fragments");
savedInstanceState.remove("android:support:fragments");
savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
savedInstanceState.remove("android:lastAutofillId");
}
}