I'm building an app that uses a Viewpager with a FragmentStatePagerAdapter.
The FragmentStatePagerAdapter gets an array of objects witch is later used to map each object data to a fragment.
The adapter loads 3 fragments into memory (onCreateView is called 3 times when I'm on a page), it loads the current fragment and the next two.
I have the following issue:
I must change the content of the next fragment based on content change in the current fragment I'm in.
I've tried to modify the array in the FragmentStatePagerAdapter and then call notifydatasetchanged on the adapter, but the adapter doesn't load again the next page.
what is the best way to pull this off?
some code and scenario:
the current fragment I am on contains users data, the logged user can follow him or unfollow.
here is the onClick code:
if(mListener!=null)
mListener.onTweetUserFollowingStatusChanged(tweet.getUser());
the callback from listener in the activity with the viewpage:
#Override
public void onTweetUserFollowingStatusChanged(User user) {
DataManager.getInstance().onTweetUserFollowStatusChanged(user);
List<Tweet> affectedTweets = mPagerAdapter.getTweetsWithUser(user);
for (Tweet affectedTweet: affectedTweets
) {
boolean affectedTweetCurrentUserFollowStats = affectedTweet.isFollowingTweetUser();
affectedTweet.setFollowingTweetUser(!affectedTweetCurrentUserFollowStats);
}
mPagerAdapter.notifyDataSetChanged();
}
The next page containes the same user.
So if I follow him on the curent page I want the follow button from the next page to display "UNFOLLOW"
I have found the solution;
Original problem:
after updating the array in the FragmentStateAdapter I needed a way to update the UI of the already instantiated fragments in the Adapter.
So I after updating the array in the FragmentStateAdapter just call notifyDataSetChange() and add this code to your adapter.
mPagerAdapter.registerDataSetObserver(new DataSetObserver() {
#Override
public void onChanged() {
super.onChanged();
FragmentStatePagerAdapter fragmentPagerAdapter = (FragmentStatePagerAdapter) VP_tweets.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
TweetFragment viewPagerFragment = (TweetFragment) VP_tweets.getAdapter().instantiateItem(VP_tweets, i);
if(viewPagerFragment != null && viewPagerFragment.isAdded()) {
viewPagerFragment.updateUI();
}
}
}
});
Related
I'm trying to save data a user enters in a fragment to a file.
Scenario:
one viewpager and 7 fragments
A user starts in fragment 0 and can enter text into edittexts,
by swiping, using tabhost or pressing floating arrows the user can switch to other fragments.
I want to save alle entered text of the fragment the user leaves with the methods above.
I tried a OnPageChangeListener, but there i can't get the previous tab. I logged the values of the implementation methods onPageScrolled, onPageSelected, onPageScrollStateChanged.
Non of these seem to work for my needs.
onPageScrolled is called several times and shows only the current tab until it is of screen, the offset is different and not always starts by 0.0, so i can't use this reliably.
onPageSelected is the only reliable one but only returns the new current tab
onPageScrollStateChanged has no information i could use to determine the tab
I also looked into onInterceptTouchEvent in the ViewPager but this is also some times invoked several times (for MOVE events) and does not always work for every tab.
Is there a way to get this cost efficent? I want to store the data in an encrypted file and don't want to do this several times over.
Because the suggestions didn't work for my case I came up with another idea I wan't to share with others.
First instead of focusing on the ViewPager to suite my needs I thought wouldn't it be clever to led the fragment know if its changed and handle that instead.
So I created an abstract class extending the android Fragment with a boolean attribute dataChanged which I check every time the OnPageChangeListener calls onPageSelected (iterate over all fragments in the pager).
Naturally all Fragments in the pager should extend the abstract class. Furthermore I added abstract methods save() and load() to the abstract class.
So in onPageSelected(int position), after saving all changes for all fragments, which should only be one at a time, I load the data of the now selected fragment via the position attribute.
There was but one problem. If a fragment was paused and resumed the dataChanged attribute was always true if I set it in onTextChangeListeners, because of the automatic loading of widget values that android does. So I also override onResume to set the dataChanged to false.
Also every MyFragment has to handle the dataChanged attribute in the save() and load() method.
Abstract Fragment
public abstract class MyFragment extends Fragment {
private boolean dataChanged = false;
#Override
public void onResume() {
super.onResume();
setDataChanged(false);
}
public boolean isDataChanged() {
return dataChanged;
}
public void setDataChanged(boolean dataChanged) {
this.dataChanged = dataChanged;
}
public abstract void save();
public abstract void load();
}
OnPageChangeListener of ViewPager
fragmentViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
...
#Override
public void onPageSelected(int position) {
for(Fragment f : fragments) {
if(f instanceof MyFragment && ((MyFragment)f).isDataChanged()) {
((MyFragment) f).save();
}
}
if(fragmentViewPager.getCurrentItem() == position) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.view_pager + ":" + fragmentViewPager.getCurrentItem());
if(fragment instanceof MyFragment) {
((MyFragment) fragment).load();
}
}
}
...
});
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 have a ViewPager, defined in an Activity, and many Fragments sequentially shown in the ViewPager. In these fragments there are dynamically constructed checkboxes and radiobuttons, which the user is supposed to manipulate. On the very moment that the user swipes to the next page I need the user data to be retrieved and stored in the Application object. I can't figure out what the standard way of doing this is. Since there are many Fragments I opted for using the FragmentStatePagerAdapter. Any help would be welcome, thanks in advance!
Update-1:
I do have this:
pageAdapter = new MyPageAdapter(getSupportFragmentManager());
pager = (ViewPager) findViewById(R.id.viewpager);
pager.setAdapter(pageAdapter);
// detects viewpager page change
pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
Log.i("TAG", "onPageSelected");
int index = pager.getCurrentItem();
MyPageAdapter adapter = ((MyPageAdapter) pager.getAdapter());
QuestionFragment fragment = (QuestionFragment) adapter.getItem(position);
if (fragment.rdbtn != null) {
for (int i = 0; i < fragment.rdbtn.length; i++) {
if (fragment.rdbtn[i].isChecked())
Log.i("TAG", "checked");
else
Log.i("TAG", "not checked");
}
}
// fragment.refresh();
}
});
When checking the debugger, after starting up, the ViewPager instantiates Fragments 0 and 1 (standard behavior). When the user manipulates fragment-0 and swipes, the handler is indeed called but with position=1, not 0. And the public elements I want to read are null!
UPDATE-2
I notice in the debugger that the data I need is stored in adapter.mCurrentPrimaryItem.
How to retrieve CurrentPrimaryItem in the code?!
You can implement a pageChangeListener in your activity and set this to the viewPager.
Then you can have a class abstract BaseFragment extends Fragment and declare an abstract method, say, getData() that every fragment in the ViewPager extends and overrides the method.
And in onPageSelected() of the activity you can access those data.
I'm having trouble swapping a list of fragments inside my FragmentStatePagerAdapter, and was wondering if anyone might have an idea. I was hoping to have the following feature;
main activity includes three tabs (restaurants, reviews, favorites), each containing a fragment with a list of restaurants
user can swipe between each tab to view a different list
main activity layout has a button. when user presses the button it will swap all the existing fragments with another list of fragments. The new list of fragments will be three fragments with restaurant location maps (google maps) instead of list of restaurants
I currently have the swipe tabs working with actionbar and view pager. I have some degree of success swapping out the list fragments with the map fragments, but the following problem occurs:
when I hit the button on the main page, the first (restaurants) tab becomes blank.
when I swipe to the next tab (reviews), the tab is populated with a map fragment
when I swipe to the third tab (favorites), the tab is empty again
My setting is the following:
Main activity extending FragmentActivity and implementing ActionBar.ITabListener
One RestaurantListFragment class is used for the restaurant list fragments, populated with different data for Search, Reviews, and Favorites
One RestaurantListMap class is used for the map list fragments, again populated with different data for each tab
One TabPagerAdapter class extending FragmentStatePagerAdapter
I've tried almost every post on stackoverflow about this topic, but so far with no success. Would anyone have any ideas? Code is attached below. Thanks.
//Main activity
public class Main : FragmentActivity, ActionBar.ITabListener
{
//Some generic code here, nothing special
//....//
//The button that triggers the fragments swap
private void BindCommands()
{
ListMapSwitchButton.Click += (sender, e) =>
{
_isList = !_isList;
ListMapSwitchButton.Text = _isList ? RESTAURANTS_MAP_TEXT : RESTAURANTS_LIST_TEXT;
mAdapter.SwapListMapFragments(viewPager);
};
}
}
public class TabsPagerAdapter : FragmentStatePagerAdapter {
private void PopulateFragments()
{
_fragments = new List<RestaurantFragmentBase> ()
{
new RestaurantListFragment (),
new RestaurantListFragment(),
new RestaurantListFragment()
};
}
public void SwapListMapFragments(ViewPager pager)
{
_fragments.Clear ();
_fragments.Add (
new RestaurantMapFragment()
);
_fragments.Add (
new RestaurantMapFragment()
);
_fragments.Add (
new RestaurantMapFragment()
);
NotifyDataSetChanged ();
pager.DestroyDrawingCache ();
}
public override int GetItemPosition (Java.Lang.Object itemObject)
{
return PositionNone;
}
public override Android.App.Fragment GetItem(int index)
{
return _fragments[index];
}
#region implemented abstract members of PagerAdapter
public override int Count {
get {
return 3;
}
}
#endregion
}
Note that this code is actually written in C# with Xamarin, but besides syntax differences it should pretty much be the same as Java. Let me know if any additional info might help.
I am having trouble implementing a feature in my Android app.
Here's the setup:
ItemPagerActivity: An activity that contains a fragment that displays a pager.
ItemPagerFragment: The fragment containing a pager that loads other fragments. A cursor is used to load the fragments.
ItemFragment: The fragment in the pager, which performs an asynchronous task to load its data.
What I want is the following:
as a I swipe pages, the data in the currently displayed ItemFragment is communicated to the ItemPagerActivity (specifically, the name of the item will be used as the activity's title).
I've defined a listener in ItemFragment that notifies when the data is loaded:
public class ItemFragment ... {
public interface OnItemLoadedListener {
public void onItemLoaded(Item item);
}
private Collection<OnItemLoadListener> listeners;
private class LoadItemTask extends AsyncTask<...> {
...
public void onPostExecute(Item item) {
notifyItemLoaded(item);
...
}
}
}
If this fragment was wrapped by an Activity, then I could set the activity's title simply by doing the following:
public class ItemActivity {
public void onCreate(...) {
...
ItemFragment fragment = new ItemFragment();
fragment.registerItemLoadedListener(new ItemLoadedListener() {
public void onItemLoaded(Item item) {
setTitle("Item: " + item.getName());
}
});
...
}
}
So that's easy enough, and works as expected: when the activity starts, it creates the fragment, which loads the item, which notifies the activity, and the title is updated correctly.
But with ItemPagerFragment, the fragments are loaded pre-emptively: swiping to Fragment 3 may mean that Fragment 4 and Fragment 5 are created. Receiving notifications from the ItemFragment class when items are loaded is not correct here because the fragment displayed may not match the fragment that performed the last load.
Now the ViewPager class has a OnPageChangeListener which could be a solution: when I swipe, this listener is invoked with the current page number. From that page number, I need to (somehow) get the fragment representing that page from the adapter, get the Item data out of the fragment, and notify listeners that the Item is now loaded:
public class ItemPagerFragment ... {
private Collection<OnItemLoadedListener> listeners;
public View onCreateView(...) {
...
ViewPager pager = (ViewPager) view.findViewById(R.id.pager):
pager.setOnPageChangeListener(new OnPageChangeListener() {
public void onPageChange(int pageNumber) {
ItemFragment fragment = getItemFragment(pageNumber);
Item item = fragment.getLoadedItem();
notifyItemLoaded(item);
}
});
...
}
}
The ItemPagerActivity class would then register as a listener on the ItemPagerFragment class as follows:
class ItemPagerActivity ... {
public void onCreate(...) {
...
ItemPagerFragment fragment = new ItemPagerFragment();
fragment.registerOnItemLoadedListener(new OnItemLoadedListener() {
public void onItemLoaded(Item item) {
setTitle("Item: " + item.getName());
}
});
...
}
}
This looks good, but there are a number of problems:
The OnPageChangeListener may be invoked before a fragment has loaded its data (i.e., the fragment is swiped into view before the item has asynchronously loaded). So the call to fragment.getLoadedItem() may return null.
The OnPageChangeListener is not invoked for the initial page (only when a page changes, e.g. after a swipe action) so the activity title will be incorrect for the initial page.
The ViewPager class allows for only one OnPageChangeListener. This is a problem because I am also using the ViewPageIndicator library, which wants to assign a listener to the ViewPager.
I'm assuming that this pattern (notifying the activity of the data in a fragment that has been swiped into view) might be common, so I am wondering if there are any good solutions for this pattern, and to the three specific problems that I have identified above.
...so I am wondering if there are any good solutions for this pattern,
and to the three specific problems that I have identified above.
I don't know if I would call it a pattern but the OnPageChangeListener is the way to go.
The OnPageChangeListener may be invoked before a fragment has loaded
its data (i.e., the fragment is swiped into view before the item has
asynchronously loaded). So the call to fragment.getLoadedItem() may
return null.
First, your code should handle the "no data available situation" from the start. Your AsyncTasks will have the job of loading the data and also update the title only if the fragment for which they are working is the visible one(a position field in the ItemFragment tested against the ViewPager's getCurrentItem() method). The OnPageChangeListener will handle the update of the title after the data was loaded, as the user switches between pages and the data is available(it will return null if no data is available). To get the ItemFragment for a certain position you could use the code below:
ItemFragment itf = getSupportFragmentManager()
.findFragmentByTag(
"android:switcher:" + R.id.theIdOfTheViewPager + ":"
+ position);
if (itf != null) {
Item item = fragment.getLoadedItem();
notifyItemLoaded(item);
}
The OnPageChangeListener is not invoked for the initial page (only
when a page changes, e.g. after a swipe action) so the activity title
will be incorrect for the initial page.
See above.
The ViewPager class allows for only one OnPageChangeListener. This is
a problem because I am also using the ViewPageIndicator library, which
wants to assign a listener to the ViewPager
I admit I don't have much knowledge on the ViewPagerIndicator library but at a quick look on its site I saw:
(Optional) If you use an OnPageChangeListener with your view pager you
should set it in the indicator rather than on the pager directly.
titleIndicator.setOnPageChangeListener(mPageChangeListener);
I don't see where is the limitation.
For my purposes, it worked to use ViewPager.OnPageChangeListener.onPageSelected() in conjunction with Fragment.onActivityCreated() to perform an action when the Fragment is visible. Fragment.getUserVisibleHint() helps too.