2

Disclosure up front, this is a school project.

I've been working on an activity in a project that consists of a series of dynamic action bar tabs and list fragments, and have been stuck on the prospect of getting object references to the specific tab fragments in order to set adapters for the list views.

I have it set up at the moment using a ViewPager (the support v4 version) and a FragmentStatePagerAdapter (v13). I've tried methods mentioned in a few SO question threads, namely these three:

Updating fragments/views in viewpager with fragmentStatePagerAdapter

Retrieve a Fragment from a ViewPager

Android ViewPager - can't update dynamically

However, each of these attempts have been to no avail, here is the relevant code from my attempt to try and demonstrate what I'm trying to accomplish.

The code for the FragmentStatePagerAdapter:

    /**
 * The Class ServerPagerAdapter.
 */
private class ServerPagerAdapter extends FragmentStatePagerAdapter
{
    SparseArray<ServerViewFragment> registeredFragments = new SparseArray<ServerViewFragment>();


    /**
     * Instantiates a new server pager adapter.
     *
     * @param fm the fragment manager
     */
    public ServerPagerAdapter(FragmentManager fm)
    {
        super(fm);
    }

    /* (non-Javadoc)
     * @see android.support.v13.app.FragmentPagerAdapter#getItem(int)
     */
    @Override
    public Fragment getItem(int position)
    {
        ServerViewFragment serverFrag = new ServerViewFragment();
        registeredFragments.put(position, serverFrag);

        return serverFrag;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) 
    {
        super.destroyItem(container, position, object);
        registeredFragments.remove(position);
    }


    /* (non-Javadoc)
     * @see android.support.v4.view.PagerAdapter#getCount()
     */
    @Override
    public int getCount()
    {
        // TODO STATIC VALUE.
        return 3;
    }



    /* (non-Javadoc)
     * @see android.support.v4.view.PagerAdapter#getPageTitle(int)
     */
    @Override
    public CharSequence getPageTitle(int position)
    {
        //TODO: HARD CODED
        switch (position)
        {
            case 0:
                return "Status";
            case 1:
                return "#chat";
            case 2:
                return "#help";
        }
        return null;
    }

    public ServerViewFragment getFragment(int position)
    {
        return registeredFragments.get(position);
    }

}

The code in the activity that I'm trying to get to work:

    @Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_server_view);

            /*Code omitted (actionbar navigation)*/

    //connect pager
    servPagerAdapter = new ServerPagerAdapter(getFragmentManager());

    viewPager = (ViewPager)findViewById(R.id.server_view_pager);
    viewPager.setAdapter(servPagerAdapter);
    viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener()
    {
        @Override
        public void onPageSelected(int position)
        {
            actionBar.setSelectedNavigationItem(position);
        }
    });

    //populate tabs
    for (int i = 0; i < servPagerAdapter.getCount(); i++)
    {
        Tab tabToAdd = actionBar.newTab();
        tabToAdd.setText(servPagerAdapter.getPageTitle(i));
        tabToAdd.setTabListener(this);
        actionBar.addTab(tabToAdd);
    }

    svf = servPagerAdapter.getFragment(0);
    csvf = servPagerAdapter.getFragment(1);

    IRCConnection testConn = new IRCConnection();
    testConn.setOnIRCMessageReceivedListener(this);

            //adapters are populated in a separate method
    adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
    cadapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);

    Log.i("ADAPTER", adapter == null ? "Null" : "Not Null");
    Log.i("FRAGMENT", svf == null ? "Null" : "Not Null");


    //svf.setListAdapter(adapter);
    //csvf.setListAdapter(cadapter);

    /*code omitted (network call)*/

}

svf and csvf are variables I'm using to just try and test getting it to work, they are typed of a ListFragment based class that is completely cookie cutter. Eventually I want to be able to add and remove tabs from the adapter and manage the listviews in each of them.

At this stage it's probably a just an issue of me not understanding the topic/way the adapter works, and I'm hoping this is just an ignorance based question, and that there's a simple solution.

Edit: I've made some progress, but calling refresh on the visible tab is throwing a null pointer for some reason. Here's the callback from my connection class, the callback being inside the activity class:

    @Override
public void onNetworkMessageReceived(String message)
{
    final String msg = message;

    this.runOnUiThread(new Runnable()
    {
        @Override
        public void run()
        {
            FragmentData data = fragmentDataMapper.getData("Network");
            ServerViewFragment visFrag = servPagerAdapter.getFragment(getActionBar().getSelectedNavigationIndex());

            if (data == null)
            {

                ArrayList<String> chatStrings = new ArrayList<String>();
                chatStrings.add(msg);
                data = new FragmentData(chatStrings);
                visFrag.refresh(data);
                fragmentDataMapper.updateData("Network", data);
            }
            else
            {
                ArrayList<String> chatStrings = data.getChatStrings();
                chatStrings.add(msg);
                data = new FragmentData(chatStrings);
                visFrag.refresh(data);

                fragmentDataMapper.updateData("Network", data);
            }   
        }
    });

    Log.i("HANDLER", message);
}

The exception is being thrown on either of the visFrag.refresh lines, as visFrag is null. I'm still using the same registeredFragments thing from before.

Community
  • 1
  • 1
Tarkenfire
  • 199
  • 1
  • 4
  • 12
  • when using FragmentStatePagerAdapter you are only certain that the fragment currently visible wont return null. The others can however, specifically if you are on page 1, then page 3 will not get loaded before you view page 2 by default. Maybe that is the issue you are facing. – cYrixmorten Dec 18 '13 at 14:05
  • That is basically the issue I was having, I thought the kerfuffle I implemented with the SparseArray and adding fragments to it when they were created and referencing them from there would be a solution though. – Tarkenfire Dec 18 '13 at 14:08
  • If you only plan to have 3 - 5 fragments then try using FragmentPagerAdapter instead. – cYrixmorten Dec 18 '13 at 14:36
  • There's no upper limit, it could be as small as 1, as big as 50. Also, I had similar results with the non state adapter; I was able to get references to the tabs in that case, but not keep them. – Tarkenfire Dec 18 '13 at 14:46
  • In that case you could increase the number of fragments around the current visible fragment that the viewpager should keep viewPager.setOffscreenPageLimit(10). Though, I cannot help but wonder why you do not simply apply the listadapters directly in your fragments. Seems so much simpler than relying on the activity to get a hold on the fragment but maybe i have misunderstood your goal. Also, if you need some value from the activity then fragments have a getActivity() method, which you can cast to your activity. Like (MyActivity)getActivity().blackMagicValue(). – cYrixmorten Dec 18 '13 at 15:39
  • Well, the data for the adapters would come from a singleton class that would handle multiple instances different server connections, so handling it in the parent activity seems easier to me. – Tarkenfire Dec 18 '13 at 18:32
  • If it is a singleton class then you should have no problem getting an instance of it within the fragment? I just honestly think it would be a better way, as the fragments then are managing their own state and appearance, which in my opinion is the way to go. – cYrixmorten Dec 18 '13 at 19:00
  • I might have phrased it in a weird way. Basically the activity is the host to a server connection, which it gets from the singleton instance, and the activity itself breaks up the responses from the server and sends them to different tabs. – Tarkenfire Dec 18 '13 at 20:17

1 Answers1

2

Given the latest comment, I have a suggestion of how you perhaps could get the behaviour you seek.

I believe that your setup with the activity, the tabs and the fragments makes perfect sense. It is only the communication between activity and fragment which needs to be addressed.

In my own project I make use of the registeredFragments approach but only for the currently visible fragment, as that one will always be non-null. This is neat to update the currently visible information but I think that the update has to be communicated to the other fragments differently in order to be stable.

I will not address how the update is done in your setting specifically, as I do not know how and when you currently handle this, so this will be a bit general.

Ok, so here is my suggestion:

Simply have a static class which holds a mapping between fragments and the information you want to show. This mapping could be between a unique name or a pagenumber given to each fragment to a model of the information it should display. The idea is outlined here here:

public class FragmentDataMapping {
    // FragmentId could be anything like String or Integer
    private Map<FragmentId, FragmentData> fragmentData = new HashMap<>();

    public static void updateData(FragmentId fragmentId, FragmentData fragmentData) {
        fragmentData.put(fragmentId, fragmentData);
    }

    public static FragmentData getData(FragmentId fragmentId) {
        return fragmentData.get(fragmentId);
    }
}

// FragmentData could be any kind of model object for your fragments
public class FragmentData {
    private String someData;
    private Integer someOtherData;
    private... and so on

    // constructor, setter and getter methods
}

Using some sort of Mapping and Model data like this, you can now in your activity, as soon as you get data from the server, create a FragmentData object with the information and associate it with a fragment.

public void serverResponse(String someData, Integer someOtherData) {
    FragmentData newData = new FragmentData(someData, someOtherData);
    FragmentDataMapping.updateData("DetailsFragment", newData);
}

And in onResume of your fragments, you would inspect the FragmentData to get the most recent data.

// This is inside DetailsFragment
private String fragmentId = "DetailsFragment"; // <- some id assigned at creation

public void onResume() {
    FragmentData data = FragmentDataMapping.getData("DetailsFragment");
    Log.d("DetailsFragment", "My data: " + data.getSomeData() + " and " + data.getSomeOtherData());
    refresh(fragmentData)
    ....
}

public void refresh(FragmentData fragmentData) {
   // set adapters, update view or do whatever with the data.
   // the reason for creating a seperate method to handle the fragmentData follows below
}

With this (or something like this), the fragments will update the data as soon as a new tab gets selected.

Now, the currently visible fragment would not update itself with this method as onResume() is not called. Therefore, to update the current fragment you can use the registeredFragments of your viewpager:

public void serverResponse(String someData, Integer someOtherData) {
    FragmentData newData = new FragmentData(someData, someOtherData);
    FragmentDataMapping.updateData("DetailsFragment", newData);
    registeredFragments(getActionBar().getSelectedNavigationIndex()).refresh(newData);
}

I know this description of an implementation is vague but I hope it has some ideas and some points that you can benefit from investigating.

cYrixmorten
  • 7,110
  • 3
  • 25
  • 33
  • A problem I'm having with this solution is the inability to create any new instances of the FragmentData class. The error I get is along the lines of "No enclosing instance of type FragmentDataMapper is accessible. Must qualify the allocation with an enclosing instance of type FragmentDataMapper". This is on a line IN the FragmentDataMapper class of "= new FragmentData(arg)". – Tarkenfire Dec 19 '13 at 16:55
  • The brainstorm I had was to convert the data mapping class into a singleton. So, hopefully that will work. Will comment as to if it does. – Tarkenfire Dec 19 '13 at 16:58
  • Just place FragmentData in its own class. That is, outside FragmentDataMapper. Was just to save space that I placed them in the same area above. – cYrixmorten Dec 19 '13 at 17:01
  • I was going to just comment that that was what I did and it resolved the error. – Tarkenfire Dec 19 '13 at 17:02
  • I've made some progress, but calling the fragment's refresh method to update the visible fragment is now throwing a null pointer exception. I edited my original post to show the code that is throwing it. – Tarkenfire Dec 19 '13 at 17:28
  • Something else I've noticed is that I no longer have any fragments added to the adapter. So that might be a cause of problems. – Tarkenfire Dec 19 '13 at 18:10
  • I resolved the issue. It was a bad implementation of the getCount() in the adapter. – Tarkenfire Dec 19 '13 at 18:19
  • And the res of your solution works, so I'm going to mark it now. – Tarkenfire Dec 19 '13 at 18:22
  • Cool :) was just at the top of my head, so was a bit in doubt how well it would fit your application. Happy coding. – cYrixmorten Dec 19 '13 at 18:39