Showing fragment after activity fetches data - android

I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.

From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.

Related

Recovering presenters for the ViewPager fragments (MVP)

I'm trying to refactor an existing application to use the MVP architecture. One of the activities has a ViewPager with three fragments. Each fragment is linked with a presenter. To be precise - each presenter, when created, is given a View to work with, i.e. a Fragment. For now, I'm creating these presenters inside the ViewPager's adapter - specifically in the getItem(int position) method.
Fragment fragment = FirstFragment.newInstance();
FirstPresenter presenter = new FirstPresenter(repo, (FirstContract.View) fragment, projectId, userId);
The problem I'm facing is if the process is killed and then restarted, ViewPager has its own lifecycle and therefore getItem is not called again - the fragments are recreated automagically with no presenters.
Is there a known solution to this problem?
As there's still no ideal answer to this question, I thought it might be good to share my interim solution.
As I've mentioned in one of the comments, the goal here is to recover ViewPager from process kill and ideally keep the Presenter initialisation decoupled from the View. For now, my solution is to override restoreState(Parcelable state, ClassLoader loader) inside the FragmentStatePagerAdapter, inspect the state Parcelable similar to the actual implementation of the restoreState method, then for each fragment of a certain class, I can initialise a presenter and assign it a view.
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
if (f instanceof FirstFragment) {
new FirstPresenter(repo, (FirstContract.View) f, projectId, userId);
}
} else {
Log.w(TAG, ".restoreState() - bad fragment at key " + key);
}
}
}
}
super.restoreState(state, loader);
}
As mentioned in comments - Presenter must be attached (and detached) in Activity/Fragment lifecycle methods. Not in external classes because only View can manage to attach-detach Presenter at appropriate time. But it's a good practice to initilize Presenter in separate class (or dependency injection framework) to decouple it from View.
The suggested answer didn't work for me since mFragmentManager is a private member of FragmentStatePagerAdapter. No idea how it worked for vkislicins. Instead, I just called got the parent class to do restoreState then grabbed the fragments with 'instantiateItem'. For example:
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
// this will load all the fragments again
super.restoreState(state, loader);
// since the fragments are now loaded, instantiate can be used because it just returns them
MyFragmentClass tab1 = (MyFragmentClass) instantiateItem(null, 0);
tab1Presenter.setView(tab1);
tab1.setPresenter(tab1Presenter);
// then just do the same for the other fragments
...
}
Feels a bit hacky, but it works.
First of all, my solution includes FragmentManager.FragmentLifecycleCallbacks, which is a
Callback interface for listening to fragment state changes that happen within a given FragmentManager
and sticks with the separation of concerns, in a way that's shown in the Android Architecture Blueprints, I'd say.
Activity creates Presenter, passing along View/ Fragment, so that
Presenter knows its View and furthermore sets itself its Presenter
In Activity's onCreate I register a FragmentLifecycleCallbacks listener by calling this
private void registerFragmentsLifecycleListener() {
// All registered callbacks will be automatically unregistered when
// this FragmentManager is destroyed.
getSupportFragmentManager.registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
// Called after the fragment has returned from its onActivityCreated
#Override
public void onFragmentActivityCreated(FragmentManager fm, Fragment f,
Bundle savedInstanceState) {
createPresenter(f);
}
}, false); // true to register callback for all child FragmentManagers
}
The listener gets notified after the Fragment has returned from its onActivityCreated to make sure, that only for each new Fragment instance added by the ViewPager a new Presenter will be created. The fragment could get attached/detached, its view could be created/destroyed a couple of times, nothing needed to be done, still got its Presenter.
Because in case of recreation (e.g. by rotation) the Fragments' onCreate is called before the Activitys one (where the FragmentLifecycleCallbacks listener is registered!), the listener couldn't implement onFragmentCreated, it has to be onFragmentActivityCreated.
For the given new Fragment instance we can then determine which Presenter is needed:
private void createPresenter(Fragment fragment) {
if (fragment instanceof WhateverContract.View) {
WhateverContract.Presenter whateverPresenter =
new WhateverPresenter((WhateverContract.View) fragment);
} else if (...){}
}
The Presenter connects with its View/Fragment in the constructor
private final WhateverContract.View mView;
public WhateverPresenter(#NonNull WhateverContract.View view) {
mView = checkNotNull(view, "view cannot be null!");
mView.setPresenter(this);
}
and can then be started in the Fragments onResume.
If there's something wrong or to improve, please let me know :)

update listview from fragment within viewpager from main activity

COMPLETELY EDITED
Ok I will try to be more specific.
I'm developing a small app drawer. Therefore I need a way to let the user choose categories.
This is the main point of the question. If there is another better way than my approach please let me know.
So my try to implement this was to load all apps to an array list and save this to a service to make it available throuout the app. The loading of the apps is done by an asynctask.
MainActivity:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
packageManager = getPackageManager();
if ((AppDrawerService.getApps()) == null) {
apps = new ArrayList<ApplistItem>();
loadAppsToService();
} else {
//if Service already holds the data
manageViews();
}
}
private void loadAppsToService() {
LoadApplications loadApps;
loadApps = new LoadApplications(this);
loadApps.setOnLoadApplicationsFinishedListener(this);
loadApps.execute();
}
private void manageViews() {
FragmentManager fragmentManager = getSupportFragmentManager();
myViewPager = (ViewPager) findViewById(R.id.view_pager);
myViewPager.setAdapter(new ViewPagerAdapter(fragmentManager, this));
}
#Override
public void OnLoadApplicationsComplete(ArrayList<ApplistItem> apps) {
manageViews();
}
LoadApplications:
#Override
protected void onPostExecute(Object o) {
AppDrawerService.setApps(apps);
listener.OnLoadApplicationsComplete(apps);
super.onPostExecute(o);
}
ViewPagerAdapter:
#Override
public Fragment getItem(int position) {
Fragment category = null;
category = new AppListFragment()
return category;
}
#Override
public int getCount() {
pages = 5;
return pages;
}
AppListFragment:
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if ((AppDrawerService.getApps()) == null) {
apps = new ArrayList<ApplistItem>();
} else {
apps = AppDrawerService.getApps();
}
v = getActivity().findViewById(android.R.id.list);
myApplist = (ListView) v;
applistAdapter = new ApplistAdapter(context, 0, apps);
myApplist.setAdapter(applistAdapter);
myApplist.setOnItemClickListener(this);
}
The problem which I have with this code is, that it alway populates only one Fragment with the applist. On first start the first fragment is populated with the apps but if you swipe two fragments to right and then one back to left, this fragment (the second) is populated. The impression which I have is that the
return new ApplistFragment();
from the ViewPager does actually not create an independent instance of the fragment. (At least I think so)
What I want is to show an undefined amount of Fragments which can all show different categories of the same list. So if possible I want to load the list only once and I want to reuse the Fragment code for every fragment since I don't want to restrict the max amount of categories.
My idea was to use the ApplistAdapter to filter the unwanted apps for every category but I really don't know.
Help is really really appreciated since I have no idea how to go along.
Thanks in advance.
I would change how this works all together. A few suggestions of the top of my head:
Download the data in the Service instead of the Activity and
persist it somewhere for example in a database.
You can use an Intent to tell the Service what you want to download and when to do it.
You can also use an IntentService instead of a Service.
IntentServices already handle each Intent in a separate worker
thread so you don't need an AsyncTask or anything like that in the
IntentService to perform the downloading.
Each Fragment should load the data from the database in onResume(). You
can use local broadcasts to inform the Fragments when the data
changed while they are being displayed.
If you don't give us more information it will be difficult to give you very specific advice.

Is it possible to have a fragment without an activity?

This is unlikely but it would potentially save me a lot of time to re-write the same code.
I want to implement a UI using alert-type service (like Chathead) yet I'd still like to use my fragments. Is it possible? I know I can add views to the window but fragments?
Fragments are part of the activity, so they cannot replace activity. Though they behave like activity, they cannot stand themselves. Its like view cannot itself act like activity.
From Android Developers:
A Fragment represents a behavior or a portion of user interface in an
Activity. You can combine multiple fragments in a single activity to
build a multi-pane UI and reuse a fragment in multiple activities. You
can think of a fragment as a modular section of an activity, which has
its own lifecycle, receives its own input events, and which you can
add or remove while the activity is running (sort of like a "sub
activity" that you can reuse in different activities).
I hope it is helpful to you.
Well as people have pointed out you can't, but, you can always create
some sort of fragment wrapper.
For example purposes:
public class ActivityFragmentWrapper extends FragmentActivity {
public static final String KEY_FRAGMENT_CLASS = "keyFragmentClass";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getIntent().getExtras() != null) {
String fragmentClass = (String) getIntent().getExtras().get(KEY_FRAGMENT_CLASS);
try {
Class<?> cls = Class.forName(fragmentClass);
Constructor<?> constructor = cls.getConstructor();
Fragment fragment = (Fragment) constructor.newInstance();
// do some managing or add fragment to activity
getFragmentManager().beginTransaction().add(fragment, "bla").commit();
} catch (Exception LetsHopeWeCanIgnoreThis) {
}
}
}
public static void startActivityWithFragment(Context context, String classPathName) {
Intent intent = new Intent(context, ActivityFragmentWrapper.class);
intent.putExtra(KEY_FRAGMENT_CLASS, classPathName);
context.startActivity(intent);
}
}
You can start it like:
ActivityFragmentWrapper.startActivityWithFragment(context, SomeSpecificFragment.class.getCanonicalName().toString());
Of course if your fragment has another constructor you have to retrieve different
one, but that part gets easier.
No, Fragments can't exist without an Activity. They need an activity for their entry point otherwise they can't initiate their UI components and their lifecycle can't go beyond onAttach and onCreateView

Switching between ActionBar.Tabs issue

I have implemented application which downloads data from web and then shows it in two ActionBar.Tabs. I have one issue with it. If I switch from one tab to other one, application starts another download and while download is not finished, the app freezes. If internet connection is slow it becomes very annoying. I've decide to add ProgressDialog to app to show user that application is downloading data from web. I've added a piece of code which implements ProgressDialog to AsyncTask which performs the download, but that didn't help. I understand why that happens but can't find the way how to fix it :(
Data which will be fitted into tab is represented as instance of Fragment class. This instance after creation will be added to transaction, and only after adding mFragment object to transaction, app switches to another tab.
This is the part of tabListener code:
// ...
#Override
public void onTabSelected(Tab tab, FragmentTransaction transaction) {
if (mFragment == null) {
/*
* Creation of ParkFragment is the reason why app locks!! Because in
* ParkFragment data is being downloaded from web
*/
mFragment = new ParkFragment(mUrl, mActivity);
/*
* mFragment can't be added to transaction until downloading is
* finished, that's why app doesn't switch fast
*/
transaction.add(android.R.id.content, mFragment, mTag);
} else {
transaction.attach(mFragment);
}
}
Please, if anybody has any ideas how to implement ProgressDialog to avoid the delay in switching between tabs, share with it.
Thank you for reading.
Updated: I've read the answer to this question but didn't understand how that implementation will help me managing delays: Changing Tabs is Slow/Laggy - Using Fragments.
UPD:
public class ParkFragment extends ListFragment {
private ArrayList<Cinemas> cinema;
private CinemasAdapter cinemaAdapter;
private String url;
private Activity activity;
public ParkFragment (String cinema,Activity activ){
url = cinema;
activity = activ;
}
public void onCreate(Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
cinema = new Handler().handle(url,activity);
cinemaAdapter = new CinemasAdapter(activity, R.layout.movie_data_row, cinema);
setListAdapter(cinemaAdapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
Cinemas movie = cinemaAdapter.getItem(position);
Intent intent = new Intent (activity, More.class);
intent.putExtra("Cinemas", movie);
intent.putExtra("data", movie.getBitmap());
Bundle translateBundle =
ActivityOptions.makeCustomAnimation(activity,
R.anim.slide_in_left, R.anim.slide_out_left).toBundle();
startActivity (intent, translateBundle);
}
}
In your ParkFragment you can use content providers combining with loaders. Downloading data should be handled in a service, when done the service inserts data into DB via content providers. And in your fragment, the loader will load the data.
There is one app named API Demos in any Android emulators, which has several examples related to content providers/ loaders/ services… The source code of its is available in Android SDK, at: [Android SDK]/samples/android-x/ApiDemos, in which x is API level.
I just think so, but if you could share your code of ParkFragment, perhaps there would be another problem?
Edited
In your ParkFragment, you can create a ResultReceiver (available in API 3+), put it into an Intent and start a service to download/ handle the url. The service keeps the instance of the ResultReceiver, when done it sends back the downloaded data to your fragment via send(int, Bundle). A Bundle can hold primitive data types such as String, int, byte[]… For more complex data, you create a class to hold it which implements Parcelable or Serializable.
Or with Bound Services, you can call the service's methods directly from within the fragment. Note that services run on main UI thread, so to avoid of NetworkOnMainThreadException, you need something like Thread in your service.

Handling onNewIntent in Fragment

I am writing an application that uses NFC to read some data stored on it. My application uses Fragments and Fragment don't come with onNewIntent() method. Since, the data I am reading is done with my separate class which handles NFC related operation, the only thing I need to do is update the TextView inside the Fragment. However this implementation can also be used to pass new Intent to the Fragment.
Here is my current implementation which makes use of an interface. I am calling the listener after new Intent is received and NFC related checks succeeds. This is the FragmentActivity which hosts Fragment.
public class Main extends FragmentActivity implements
ActionBar.OnNavigationListener {
private Bundle myBalanceBundle;
private NFC nfcObj;
private NewBalanceListener newBlanceListener;
#Override
public void onNewIntent(Intent intent) {
setIntent(intent);
}
#Override
protected void onResume() {
getNFCState();
super.onResume();
}
private void getNFCState() {
//Other NFC related codes
else if (nfc_state == NFC.NFC_STATE_ENABLED){
readNFCTag();
}
}
private void readNFCTag() {
//Other NFC related codes
if (getIntent().getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) {
nfcObj.setTag((Tag) getIntent().getParcelableExtra(
NfcAdapter.EXTRA_TAG));
nfcObj.readQuickBalance();
transitQuickReadFragment(nfcObj.getCurrentBalance());
}
}
private void transitQuickReadFragment(String balance) {
// Creates a balance bundle and calls to select MyBalance Fragment if it
// is not visible. Calls listener is it is already visible.
if (actionBar.getSelectedNavigationIndex() != 1) {
if (myBalanceBundle == null)
myBalanceBundle = new Bundle();
myBalanceBundle.putString(Keys.BALANCE.toString(), balance);
actionBar.setSelectedNavigationItem(1);
} else {
newBlanceListener.onNewBalanceRead(balance);
}
}
#Override
public boolean onNavigationItemSelected(int position, long id) {
// Other fragment related codes
fragment = new MyBalance();
fragment.setArguments(myBalanceBundle);
newBlanceListener = (NewBalanceListener) fragment;
// Other fragment related codes
}
// Interface callbacks. You can pass new Intent here if your application
// requires it.
public interface NewBalanceListener {
public void onNewBalanceRead(String newBalance);
}
}
This is MyBalance Fragment which has TextView that needs to be updated whenever NFC is read:
public class MyBalance extends Fragment implements NewBalanceListener {
private TextView mybalance_value;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//Other onCreateView related code
Bundle bundle = this.getArguments();
if (bundle != null)
mybalance_value.setText(bundle.getString(Keys.BALANCE.toString(),
"0.00"));
else
mybalance_value.setText("0.00");
//Other onCreateView related code
}
#Override
public void onNewBalanceRead(String newBalance) {
mybalance_value.setText(newBalance);
}
}
This code works perfectly like expected for my application but, I want to know if there is better way to handle new Intent from Fragments?
This is an old question, but let me answer it in case anybody bumps into it.
First of all you have a bug in your code:
You can't register Fragments as listeners inside Activity the way you do it. The reason is that Activity and Fragments can be destroyed by the system and re-created later from saved state (see documentation on Recreating an Activity). When this happens, new instances of both the Activity and the Fragment will be created, but the code that sets the Fragment as a listener will not run, therefore onNewBalanceRead() will never be called. This is very common bug in Android applications.
In order to communicate events from Activity to Fragment I see at least two possible approaches:
Interface based:
There is an officially recommended approach for communication between Fragments. This approach is similar to what you do now in that it uses callback interfaces implemented by either Fragment or Activity, but its drawback is a tight coupling and lots of ugly code.
Event bus based:
The better approach (IMHO) is to make use of event bus - "master component" (Activity in your case) posts "update" events to event bus, whereas "slave component" (Fragment in your case) registers itself to event bus in onStart() (unregisters in onStop()) in order to receive these events. This is a cleaner approach which doesn't add any coupling between communicating components.
All my projects use Green Robot's EventBus, and I can't recommend it highly enough.
There is at least one alternative: From Activity.onNewIntent documentation:
An activity will always be paused before receiving a new intent, so you can count on onResume() being called after this method.
Note that getIntent() still returns the original Intent. You can use setIntent(Intent) to update it to this new Intent.
FragmentActivity.onNewIntent documentation is different but I don't think it contradicts the above statements. I also make the assumption that Fragment.onResume will be called after FragmentActivity.onResume, even though the documentation seems a little fussy to me, though my tests confirm this assumption. Based on this I updated the Intent in the activity like so (examples in Kotlin)
override fun onNewIntent(intent: Intent?) {
setIntent(intent)
super.onNewIntent(intent)
}
And in Fragment.onResume I could handle the new intent like so
override fun onResume() {
super.onResume()
doStuff(activity.intent)
}
This way the activity don't need to know about what fragments it holds.
No, there is no better way. Fragments can live longer than Activities and are not necessarily tied to them at all so providing new intents would not make sense.
Btw, you have a few bugs in your code :)
if (actionBar.getSelectedNavigationIndex() != 1) {
Magic numbers are bad! use a constant.
if (myBalanceBundle == null)
myBalanceBundle = new Bundle();
myBalanceBundle.putString(Keys.BALANCE.toString(), balance);
actionBar.setSelectedNavigationItem(1);
we already know that the navigationitem is set to 1
} else {
newBlanceListener.onNewBalanceRead(balance);
Add a null check. The user might have never selected a navigation item.

Categories

Resources