Avoid execution of AsyncTask when ViewPager is slided fast - android

I'm using a ViewPager to show content fetched from a website with jsoup.
In the onCreateView of each page I call an AsyncTask that fetches the data and updates the View for each page.
The problem is that when the user slides the pages faster than usual the AsyncTask is called several times and, consequently, several useless requisitions are made with jsoup, since the only useful is the last.
I tried using setUserVisibleHint on the Fragment class and adding setOnPageChangeListener in the Activity class but these methods make me lose the ViewPager behaviour of preloading the next page and I don't want that.
Is there a way to know when the user stopped sliding and only call the AsynTask at that moment?
public class ScreenSlidePageFragment extends Fragment {
public static final String PAGE_NUMBER = "page";
private int mProblemNumber;
public static ScreenSlidePageFragment create(int pageNumber) {
ScreenSlidePageFragment fragment = new ScreenSlidePageFragment();
Bundle args = new Bundle();
args.putInt(PAGE_NUMBER, pageNumber);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPageNumber = getArguments().getInt(PAGE_NUMBER);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ViewGroup rootView = (ViewGroup) inflater.inflate(
R.layout.fragment_screen_slide_page, container, false);
new GetPageTask(url).execute();
return rootView;
}
}

I think the best way would be just to check whether the AsyncTask is running or not. Store a reference to your AsyncTask and then if user scrolls back to this page you can check its status using AsyncTask.Status (http://developer.android.com/reference/android/os/AsyncTask.Status.html).
Also, if you want to avoid starting new tasks when user scrolls too fast, you can use handler.postDelayed(yourRunnable, longMs). Each time user selects a page you can do something like this:
handler.removeCallbacks(yourRunnable);
handler.postDelayed(yourRunnable, longMs);
This way you will remove previous pending task and schedule a new one in longMs time. E.g. if you put 1000 ms then your tasks will start only in a second after user selected a page.

You might want to delay the request to fetch the content. For instance, if you are swiping quickly, waiting like half a second to load the content (instead of right away) would give the system a chance to breathe and check if the page is still visible.
Something like this:
handler.postDelayed(new Runnable(){
if (isVisible){
new GetPageTask(url).execute();
}
}, 500);

Related

Android Fragment display a loading while rendering layout

Let's say I've got an Activity with only a FrameLayout (I've also try with the new FragmentContainerView) so I will be loading Fragments on it. Let's assume I've got two Fragments: fragA and fragB, I will first load fragA and with a button, present in that fragment, replace it with fragB.
I've define a function on my Activity so I can load a new Fragment on the container:
public void loadFragment(Fragment fragment) {
FragmentTransaction fragTrans = getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragment)
.addToBackStack(null);
fragTrans.commit();
}
And I will call this from fragA to go to fragB. This far so good, everything works fine. Actually the problem I'm facing is not about functioning, is that I don't like the loading time of fragB, it takes about 2sec, and I'd like to display a loading to the user, but I'm not able to achieve it.
The first thing I've try is adding a gone ProgressBar to the activity layout, where the container is. Then my loadFragment function will first set this progressBar visible and then perform the fragment transaction and each fragment will hide (gone) this View before exiting the onCreateView method. But the problem is that the user never sees this ProgressBar, it's like the main UI is freezed while rendering the layout of the fragB and therefore does not update the UI making this progressBar usesless. I even see some frame skipped in the LogCat about 50 frames.
Both fragments have no logic just the onCreateView implemented where the layout is inflated. I can notice that if I change the layout of fragB to a simpler one it loads instantly, so I'm guessing the issue is related to the time it takes to render the layout. This heavy layout is a really big form with lots of TextInputLayout with TextInputEditText and MaterialSpinner (from GitHub).
I know I maybe can simplify the layout, but I'd like to know how can I display a loading while rendering. For example, I've seen some apps load some kind of dummy-view while loading and then replace it with real data.
One thing I've try is to load a dummy layout with a ProgressBar in the middle and in the onCreateView method post a Handler to inflate a real layout on the same container in the background, like this:
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_dummy, container, false);
Handler mHandler = new Handler();
mHandler.post(new Runnable() {
#Override
public void run() {
container.removeAllViews();
View realView = inflater.inflate(R.layout.fragment_real, container);
}
});
}
This kinda work, as the viewing experience is nice, but when navigating back, the layout of fragB remains visible as background of the other fragments. I have not test it but maybe I can call container.removeAllViews(); before exiting the fragment and it will work, but still seems like a workaround rather than a solution, to me.
And other thing I've not try because maybe is an over-kill is to have an intermediate or loading fragment and load it always before the real fragment, and I will pass an Intent Extra to it so I can tell what's the real fragment I want to display. This intermediate fragment will not be added to the backstack.
How do you solve this kind of problems?
Well it seems that AsyncLayoutInflater is the way to go!
Here's how I use it:
private ViewGroup fragmentContainer;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View dummyView = inflater.inflate(R.layout.fragment_dummy_loading, container, false);
fragmentContainer = container;
AsyncLayoutInflater asyncLayoutInflater = new AsyncLayoutInflater(getContext());
asyncLayoutInflater.inflate(R.layout.fragment_real_layout, container, new AsyncLayoutInflater.OnInflateFinishedListener() {
#Override
public void onInflateFinished(#NonNull View view, int resid, #Nullable ViewGroup parent) {
fragmentContainer.removeAllViews();
fragmentContainer.addView(view);
}
});
return dummyView;
}
#Override
public void onStop() {
super.onStop();
fragmentContainer.removeAllViews();
}
Up there, fragment_dummy_loading is a layout with a ProgressBar and fragment_real_layout is the real heavy layout.
There's just one thing I don't get... and that's how can I bind Objects to the XML widgets (without using Android Data Binding, if possible) when the AsyncLayoutInflater fallsback to infalte in the UI thread.
According to the docs:
If the layout that is trying to be inflated cannot be constructed asynchronously for whatever reason, AsyncLayoutInflater will automatically fall back to inflating on the UI thread.
And I check the source of AsyncLayoutInflater and I can tell that the method onInflateFinished is called no matter if the inflation was in the background or in the main thread
I think you have to use AsycTask when your new fragment loading
1- Load the new fragment
2 -Start Async task
3 - Initialize Progressdialog in constructor of AsyncTask where you want to load the data
4 - Show Progressdialog in onPreExecute() in your AsyncTask with dialog.show()
5 - Dismiss the Progressdialog in onPostExecute() in your AsyncTask with dialog.dismiss()
6- Update the UI / set the new data to your adapter etc
Check here
Show a loading spinner while a fragment is loading

Preload some fragment when the app starts

I have an Android application with a navigation drawer. My problem is that some fragment takes few second to load (parser, Map API). I would like to load all my fragment when the app starts.
I'm not sure if it is possible or a good way to do it, but I was thinking of create an instance of each of my fragments in the onCreate method of the main activity. Then, when the user select a fragment in the navigation drawer, I use the existing instance instead of creating a new one.
The problem is that it does not prevent lag the first time I show a specific fragment. In my opinion, the reason is that the fragment constructor does not do a lot of operation.
After searching the web, I can't find an elegant way to "preload" fragment when the application starts (and not when the user select an item in the drawer).
Some post talks about AsyncTask, but it looks like MapFragment operation can't be executed except in the main thread (I got an exception when I try: java.lang.IllegalStateException: Not on the main thread).
here is what I've tried so far:
mFragments = new Fragment[BasicFragment.FRAGMENT_NUMBER];
mFragments[BasicFragment.HOMEFRAGMENT_ID] = new HomeFragment();
mFragments[BasicFragment.CAFEFRAGMENT_ID] = new CafeFragment();
mFragments[BasicFragment.SERVICEFRAGMENT_ID] = new ServiceFragment();
mFragments[BasicFragment.GOOGLEMAPFRAGMENT_ID] = new GoogleMapFragment();
When an item is selected in the nav drawer:
private void selectItem(int position) {
Fragment fragment = mFragments[position];
// here, I check if the fragment is null and instanciate it if needed
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.commit();
mDrawerList.setItemChecked(position,true);
mDrawerLayout.closeDrawer(mDrawerList);
}
I also tried this solution; it allows to prevent a fragment from being loaded twice (or more), but it does not prevent my app from lag the first time I show it. That's why I try to load all fragments when the application starts (using a splash-screen or something) in order to prevent further lags.
Thanks for your help / suggestion.
You can put your fragments in ViewPager. It preloads 2 pages(fragments) by default. Also you can increase the number of preloaded pages(fragments)
mViewPager.setOffscreenPageLimit(int numberOfPreloadedPages);
However, you will need to rewrite your showFragment method and rewrite back stack logic.
One thing you can do is load the resources in a UI-less fragment by returning null in in Fragment#onCreateView(). You can also call Fragment#setRetainInstance(true) in order to prevent the fragment from being destroyed.
This can be added to the FragmentManager in Activity#onCreate(). From there, Fragments that you add can hook in to this resource fragment to get the resources they need.
So something like this:
public class ResourceFragment extends Fragment {
public static final String TAG = "resourceFragment";
private Bitmap mExtremelyLargeBitmap = null;
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return null;
}
#Override
public void onStart() {
super.onStart();
new BitmapLoader().execute();
}
public Bitmap getExtremelyLargeBitmap() {
return mExtremelyLargeBitmap;
}
private class BitmapLoader extends AsyncTask<Void, Void, Bitmap> {
#Override
protected Bitmap doInBackground(Void... params) {
return decodeBitmapMethod();
}
#Override
protected void onPostExecute(Bitmap result) {
mExtremelyLargeBitmap = result;
}
}
}
Add it to the fragment manager in the Activity first thing. Then, whenever you load your other Fragments, they merely have to get the resource fragment from the fragment manager like so:
public class FakeFragment extends Fragment {
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final ResourceFragment resFragment = getFragmentManager().findFragmentByTag(ResourceFragment.TAG);
Bitmap largeBitmap = resFragment.getBitmap();
if (largeBitmap != null) {
// Do something with it.
}
}
}
You will probably have to make a "register/unregister" listener set up because you will still need to wait until the resources are loaded, but you can start loading resources as soon as possible without creating a bunch of fragments at first.
To preload fragments, attach() can be used. So in OP's case it will be:
ft.attach(fragment).commit();
Make sure to store the fragment somewhere and use that one the next time ft.replace() is called.

How to prevent the reload of a webView in a fragment when another fragment in the top was closed

I'm making an app with webviews.
I just have a single activity containing
a FrameLayout to dynamically add fragments.
Every fragment contains a webView.
Everything works fine, but when I remove a fragment
from the stack, the webview of the fragment in the
top of the stack is reloaded so the content inside the
script of the html is called again.
The restoreState() method is not working.
This is the onCreateView of the fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.wv_container, container, false);
webView = (WebView)view.findViewById(R.id.webviewcontainer);
if(webViewBundle == null)
webView.loadUrl("file:///android_asset/www/main.html");
else
webView.restoreState(webViewBundle);
return view;
}
This is the onPause() method:
#Override
public void onPause() {
super.onPause();
webViewBundle = new Bundle();
webViewContent.saveState(webViewBundle);
}
How should I prevent the page reloading for my case?
I don't want to hack the html with flags or something like that.
Thanks in advance!
The saveState method in your onPause returns a WebBackForwardList, which contains a list of WebHistory items. These are basically the titles, urls and favicons from recently visited pages. It does not cache the contents of the downloaded url.
Try looking at the method saveWebArchive instead.

How do I only load one tab into memory at a time in Android?

I'm creating an application with a CustomPagerAdapter that can be controlled by ActionBar tabs or horizontal swipe. When you select a tab, a fragment corresponding to that tab is displayed on the screen. When the app is created and when any tab is selected, the adjacent tabs, fragments are loaded into memory. I do not want this to happen. I would like it so that when a tab is selected only that selected tab's fragment is loaded into memory. Is there a way to do this?
Edit: The code I'm currently having trouble with is as follows:
public class fragA extende Fragment
{
private VideoView videoViewA;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_A, container, false);
videoViewA = (VideoView) rootView.findViewById(R.id.videoViewA);
return rootView;
}
#Override
public void setUserVisibleHint(final boolean isVisibleToUser)
{
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser)
{
videoViewA.setVideoURI(LINK);
videoViewA.start();
}
else
{
videoViewA.stopPlayback();
}
}
}
The error I'm receiving is at the videoViewA.setVideoURI(LINK); line. Mind you, the link is actually there, but for privacy reasons I cannot post it.
Edit 2: It's ajava.lang.NullPointerException.
Edit 3: Sorry, but I'm doing this all the hard way. The code now reflects what I have actually written.
Try loading your videos within setUserVisbleHint(), which gets fired by the FragmentPageAdapter upon showing the fragment.
http://developer.android.com/reference/android/support/v4/app/Fragment.html#setUserVisibleHint(boolean)
If that doesn't work for you, you can also try to do check onHiddenChanged(boolean hidden).
http://developer.android.com/reference/android/app/Fragment.html#onHiddenChanged(boolean)

Saving fragments on rotation when using an Async Task

The question pretty much sums it up, I am trying to save my fragments so when I rotate the screen the application does not crash but I am not sure where or how to save my fragments, I have tried using a fragment manager and setting retainstate to true as well as checking the saved instance state.
This is my code :
EventsActivty - Hosts the fragments
#Override
public void onCreate(Bundle savedInstanceState) {
new AsyncLogin().execute(username, password);
super.onCreate(savedInstanceState);
username = getIntent().getStringExtra("username");
password = getIntent().getStringExtra("password");
}
private List<Fragment> getFragments(){
List<Fragment> fList = new ArrayList<Fragment>();
EventListFragment eventListFragment = (EventListFragment)
EventListFragment.instantiate(this, EventListFragment.class.getName());
EventGridFragment eventGridFragment = (EventGridFragment)
EventGridFragment.instantiate(this, EventGridFragment.class.getName());
fList.add(eventListFragment);
fList.add(eventGridFragment);
return fList;
}
The getFragments is called here, on the OnPostExecute of the AsyncTask
protected void onPostExecute(JSONObject jsonObject) {
try {
getEvents(jsonObject);
setContentView(R.layout.eventlist);
List<Fragment> fragments = getFragments();
pageAdapter = new MyPageAdapter(getSupportFragmentManager(), fragments);
ViewPager pager = (ViewPager)findViewById(R.id.viewpager);
pager.setAdapter(pageAdapter);
}
Fragment 1 : OnCreateView
#Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
eventObjects = ((EventsActivity)getActivity()).getEventObjects();
setRetainInstance(true);
View view = inflater.inflate(R.layout.eventlist ,container,false);
final ListView listView = (ListView) view.findViewById(R.id.listView);
listView.setAdapter(new MyCustomBaseAdapter(getActivity(), eventObjects));
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
Object o = listView.getItemAtPosition(position);
EventObject fullObject = (EventObject)o;
System.out.println("asd");
}
});
return view;
}
}
Fragment 2:
#Override
public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setRetainInstance(true);
View view = inflater.inflate(R.layout.eventgrid ,container,false);
GridView gridView = (GridView) view.findViewById(R.id.eventgrid);
gridView.setAdapter(new ImageAdapter(view.getContext())); // uses the view to get the context instead of getActivity().
return view;
}
Actually, a FragmentActivity will automatically restore your fragments in onCreate().
Weird thing is, you call AsyncTask first, prior to calling through to super.onCreate() and retrieving username and password from the Intent.
Even having set that aside, that approach will make your activity spawn a new login task every time it's rotated.
Better way is to check savedInstanceState for null:
if (savedInstanceState == null) {
// first launch
new AsyncLogin().execute(username, password);
}
...
That way it's only going to run when Activity is created for the first time.
Second, you need to completely unbind login info from your activity. Make the AsyncTask return whatever login result you get to the Application and store it there. And your activity and fragments to retrieve that info from the Application - that way you get full control over you login procedure: you check whether there's an active session, if there isn't you check whether the AsyncTask is already running, if it isn't - you need to launch it. If it is - you need to wait for it to finish (show progress bar or something).
You should understand that retaining the fragments won't prevent the activity from being distroyed.
Now take a look here:
#Override
public void onCreate(Bundle savedInstanceState) {
new AsyncLogin().execute(username, password);
// .....
}
When a screen orientation change occurs, your activity is still destroyed. As a result a new AsyncTask is started. But when the old AsyncTask completes the job, it tries to deliver the result to the old activity, that was destroyed already. As a result this will cause a crash.
The solution would be to put the AsyncTask in the fragment itself. So that the instance to not be destroyed on orientation change.
See if this article can help you.

Categories

Resources