I am working on an Android App where I want to implement an Activity Group for each tab. But since Activity Group is deprecated I have to use Fragments. I googled the last days and did some research on that topic but I still don't get it. The graphic below describes what I want to do. Also I'm coming straight from iOS and I need some feedback about my theories.
As you can see every Fragment consists of a WebView Fragment. When the user clicks on a link in that WebView Fragment - the request gets caught and a new Fragment is replaced which again holds a WebView and loads the link which was clicked in the previous Fragment. At a point the user decides to go back to the first Fragment and presses the back button.
The Fragments should pop off the stack in the reverse order until he again sees the first one. Ideally each Fragment should save his instancestate, so that when the user goes back the WebView doesn't need to load the site again.
I've come across ActionBar Sherlock which provides an example about Fragments Tabs. There is also an Example of an Fragment Stack. It would be ideal to combine these two examples, so that each tab consist of a Fragment Stack.
My code structs look like the following:
My TabHost Fragment
public class MyActivity extends SherlockFragmentActivity {
static TabHost mTabHost;
static TabManager mTabManager;
public static int THEME = R.style.Theme_Sherlock_Light;
#Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(THEME); //Used for theme switching in samples
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_tabs);
mTabHost = (TabHost)findViewById(android.R.id.tabhost);
mTabHost.setup();
mTabManager = new TabManager(this, mTabHost, R.id.myRealTabContent);
// adding some Fragment Tabs
mTabManager.addTab(mTabHost.newTabSpec(tabNames[i]).setIndicator(tabNames[i]),
FragmentStackSupport.CountingFragment.class, bundle);
if (savedInstanceState != null) {
mTabHost.setCurrentTabByTag(savedInstanceState.getString("tab"));
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("tab", mTabHost.getCurrentTabTag());
}
// defining the Tab Manager
How would the Fragment Stack for each Tab should look like?
Currently I have a Fragment Activity with a Fragment inside of each tab. But I don't achieve the logic I need.
Here is my code: My Fragment with WebView inside
I would be glad for some feedback and hints (or are there any examples on the web?). I also have some concerns about the memory - when the user clicks and clicks and clicks and the memory has to hold each Fragment in the stack.
Cheers.
I can't think of many good reasons to create a new fragment each time the user clicks a link. Is there a reason that you're trying to do that? If you simply return false from shouldOverrideUrlLoading() instead of creating a new fragment, the WebView will load the new page, and if you want to go back, you can use the canGoBack() and goBack() methods. To handle back button presses, you can override onKeyDown in your activity, and do something like:
if ((keyCode == KeyEvent.KEYCODE_BACK) && myWebView.canGoBack() {
myWebView.goBack();
return true;
}
Related
I am trying to build a delivery app. I have a list of products to choose from. After the user selects a product then he is lead to a series of stages to define extras and options of a certain product.
List of products:
Then, Lets say someone clicks on one of the products, we go to the controller FragmentActivity:
The subtotal at top and the button at the bottom of the page belongs to the fragment activity. Then I place a group of radio buttons at the center layout. Everything is fine till now. Click on the button leads for the replacement of the fragment:
Everything is beautiful until now. I can access the buttons and the subtotal through the fragments. However, if I press the back button on the device it leads me back to the list of products and not to the previous fragment. Even if I manage to go back to the previous fragment it would lose the radiobutton selection as well.
Then the next fragment is a calculation of the products and its extras:
When I press the button, I just use finish() in the fragment and it leads me back to the list of products which is my desired result. However, I need to know that I am coming from there in the list of products so I can add that product to the shopping cart that is being built for the delivery order.
I am really new in using fragments, but I can pass arguments just fine. What I am struggling is to control the navigation of the fragments through the FragmentActivity that controls the fragments. Also, I am struggling to keep the states of the fragments (remembering user input). At last, I need to go back to the list of products with a result of that item that was being constructed so I can add it to the shopping cart.
Am I going to the right direction? How can I implement these features(navigation, fragment state, returning to previous activity with some data since I just use finish()), Many thanks guys!
You can navigate between the fragments just by adding them into the back stack like following:
// Works with either the framework FragmentManager or the
// support package FragmentManager (getSupportFragmentManager).
getSupportFragmentManager().beginTransaction()
.add(detailFragment, "detail")
// Add this transaction to the back stack
.addToBackStack()
.commit();
That way, when you will click the back button, it wont load the previous activity from the stack but the previous fragment that has been added in the backstack. You can find more details here: http://developer.android.com/training/implementing-navigation/temporal.html
Use the onSaveInstanceState(Bundle savedInstanceState) and the onActivityCreated(Bundle savedInstanceState) on every fragment that you want to save and retrieve data. And then do the following:
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
//Restore the fragment's state here
String yourString = savedInstanceState.getString("key");
}
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
//save whatever you want into the bundle
savedInstanceState.putString("key", "your_value");
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
You can save whatever you want into the Bundle. From Strings to Parcelables and Serializables. More info here: http://developer.android.com/reference/android/os/Bundle.html
I'm currently dealing with an issue with Android & It's Re-Creation Cycle on screen rotation:
I have one single Activity and lots of Fragments (Support-V4) within.
For example, the Login it's on a Single Activity with a Fragment, when the logs-in then the App changes it's navigation behavior and uses multiple fragments, I did this, because passing data between Fragment A to Fragment B it's way much easier than passing data Between an Activity A to an Activity B.
So My issue it's presented when I rotate the device, on my first approach, the initial fragment was loaded, but what would happen, if the user it's on Page 15 and it rotates it's device, it would return to Fragment 1 and give a very bad user-experience. I set all my fragments to retain their instance and added this on the MainActivity on Create:
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
initBackStackManager();
initControllers();
mayDownloadData();
setTitle();
if(savedInstanceState == null){
addAreaFragment();
}
}
Now, the first fragment is not loaded after screen orientation change, but If I try to make a fragment transaction, it says Can not perform FragmentTransaction.commit() after onSaveInstanceState(), is there a way to handle this? Or Do I really really need to use multiple Activities with a Fragment embedded within?
Thank you very much!
EDITED
I forgot to add that this happens only on a specific Fragment... For example I have the following fragment flow:
AreaFragment -> WaiterSelectionFragment -> WaiterOptionsFragment.
If I'm in the AreaFragment and I rotate the device I can still add/replace fragments and nothing happens, no error it's being thrown. If I'm on the WaiterSelectionFragment no error happens too. BUT, If I'm on the WaiterOptionsFragment the error it's being thrown. The WaiterSelectionFragment has the following structure:
LinearLayout
FragmentTabHost
Inside the FragmentTabHost there are some fragments, and that's where the error it's happening. You might wonder Why FragmentTabHost? easy, the Customer wants that App to show the TabBar, If I use Native Android Tabs the Tabs get rearranged to the ActionBar when on
Landscape position.
EDIT 2
I've used the method provided by #AJ Macdonald, but no luck so far.
I have my Current Fragment being saved at onSaveInstanceState(Bundle) method and restore my fragment on onRestoreInstanceState(Bundle) method on the Android Activity, I recover my back button and the current Fragment but when I get to the third Fragment the error still occurs. I'm using a ViewPager that holds 4 Fragments, Will this be causing the Issue? Only on this section of the App Happens. I've 4 (main workflow) fragments, on the First, Second and Third Fragment no error it's being presented, only on the ViewPager part.
Give each of your fragments a unique tag.
In your activity's onSaveInstanceState, store the current fragment. (This will probably be easiest to do if you keep a variable that automatically updates every time the fragment changes.)
In your activity's onCreate or onRestoreInstanceState, pull the tag out of the saved bundle and start a new fragment of that type.
public static final int FRAGMENT_A = 0;
public static final int FRAGMENT_B = 1;
private int currentFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//other stuff
if(savedInstanceState == null){
addAreaFragment();
currentFragment = FRAGMENT_A;
}else{
currentFragment = savedInstanceState.getInt("currentFragment");
switch(currentFragment){
case FRAGMENT_A:
addAreaFragment();
break;
case FRAGMENT_B:
addFragmentB();
}
}
}
// when you switch fragment A for fragment B:
currentFragment = FRAGMENT_B;
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt("currentFragment", currentFragment);
super.onSaveInstanceState(savedInstanceState);
}
A suggestion to try is to use FragmentTransaction.commitAllowingStateLoss() in place of FragmentTransaction.commit(). That should stop the Exception from being thrown, but the downside is if you rotate the device again the most recent state of the UI may not return. That is a suggestion given that I am not sure of the effect of using FragmentTabHost, if it has any effect at all.
The main activity opens the main_fragment with this transaction:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.fragment_main_container, new MainFragment())
.commit();
}
Then I replace that fragment with another one like this:
// method to handle Conversions button click
public void addConversionsFragment (View v) {
// replace the main fragment with the conversion fragment
UnitConversionFragment newFragment = new UnitConversionFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
transaction.replace(R.id.fragment_main_container, newFragment);
// and add the transaction to the back stack so the user can navigate back
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
Originally, I was using support.v4.app.fragment, and everything was working as expected (back button in the second fragment would pop that one out and return to the MainFragment. But then I decided to implement a PreferenceFragment elsewhere, which the support library didn't seem to like. So I converted the whole project to the regular app.fragment by deleting all the support imports, replacing with the regular imports, then editing all the getSupportFragmentManager() with getFragmentManager(), etc.
Good news is thePreferenceFragment works well, however any time I hit the back button in a fragment, it closes the hosting activity rather than reversing the transaction.
I did many searches and it seems that I am implementing the code correctly, but it is just not responding as I am expecting. Is there more involved in converting away from the support library? Or am I missing something else obvious? I saw a lot of answers out there overriding the onBackPressed(), but I really don't want to do that.
Is there some fundamental difference between the v4 support library and the regular library that requires me to handle the fragment transactions differently?
Preference Fragment has a bit of extra logic to handle hierarchical preferences. You can configure it to launch sub fragment screens, and navigate back, as demonstrated here.
<PreferenceScreen android:title="Sub Preferences"
android:fragment="com.example.SettingsDemo.SubPrefFragment"/>
For normal fragments , fragment back-stack pops first before reaching Activity back stack. This is what is documented.
The code in both android.app.Activity and android.support.v4.app.FragmentActivity is exactly same:
/**
* Take care of popping the fragment back stack or finishing the activity
* as appropriate.
*/
public void onBackPressed() {
if (!mFragments.popBackStackImmediate()) {
finish();
}
}
The only reason it is not happening as expected is that something else consumes "back press", this may happen when there are 3 levels of components:
The Activity is inside an ActivityGroup.
Fragment is inside another Fragment. (for this, there are some bugs with v4 fragments).
I'm building an app that the interface is based on this http://code.google.com/p/android-playground/, and I need to have a fragment inside of each tab (that are fragments). The tabs are all inflated from the same xml, and in that xml I have a fragment tag.
The problem is that when the activity is created, as the id of each fragment in the tabs are equal, the contents that should go to the second tab, go in the first.
I'm using this code to replace the fragment in the tab
FragmentTransaction ft = x.getSupportFragmentManager().beginTransaction();
ft.replace(R.id.details, fragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
I don't know if is possible to correct this.
do you want all the tabs to have the exactly the same content?
Usually when putting fragments in tabs each tab would be showing differnt content, so a xml file per tab would not be uncommon. You can have a seperate xml layout for each tab that just declares your fragment with a different id each time. Without declaring a seperate (read unique) id for each fragment there is no efficent / simple operation i know to get a handle to a specific fragment (as the id is the unique handle).
You also may be able to use a FragmentPagerAdapter depending on your needs. You could then fade your current tab fragment out, then call public void notifyDataSetChanged () and provide a new fragment. This is not really the standard way of doing it though and will not be preserved on the back stack.
Optionally you could create each tab programatically in the PagerAdapter and set a tag for each fragment when calling FragmentTransaction.add(..) and then use this tab-unique tag in future fragment transactions Ignore this, it does not look like you can switch fragements with a tag, id only im afraid. Go with my first suggestion I would!
I've struggled with ths as well and the way I reference a Fragment from its Activity later on is by accessing the Adapter for the tabs.
Assuming you use an Adapter, you could do something like this
private mTabsAdapter;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// etc
this.mTabsAdapter = new TabsAdapter(this, this.mViewPager);
// Tab1
Tab tab = actionBar.newTab();
this.mTabsAdapter.addTab(tab, MyFragment1.class, null);
// Tab2
tab = actionBar.newTab();
this.mTabsAdapter.addTab(tab, MyFragment2.class, null);
}
public MyFragment1 getTab1() {
return (MyFragment1)this.mTabsAdapter.getItem(0);
}
public MyFragment2 getTab2() {
return (MyFragment2)this.mTabsAdapter.getItem(1);
}
Then inside your fragment, you would have a method to access
public class MyFragment1 {
// ...
public void reloadData() {
// reload data here
}
}
Now you can access the Fragment such as
this.getTab1.reloadData();
This feels like a sad way to access a fragment, but you can't rely on a Tag in every scenario. Also, you must take care to only reference this where the reference to the Fragment exists.
There may be times that this will be null. For example if you nave a bunch of tabs, they may become garbage collected at some point. you should communicate between Fragments via a callback. This example is for only a few scenarios.
The callback method is described at Communicating with the Activity and I'd suggest this method if it fits your application. This will prevent the need for directly accessing tabs in most cases.
I've put a little app together that has three tabs to show three different web pages. It does work however I am bit worried I haven't got enough control over how this whole thing works. When I click a tab, I get a web page loaded (see code sample below), now when I click another tab another page loads in another view. When I go back to the first tab, the whole thing get initilized again and the page loads. Is there a way how I can control this and keep the underneeth tab's activity in its current state as long as I want (and say only "refresh" the page when it changes).
do I need to handle onPause()/onResume() methods for that or instead implement my tabs as views of a single activity (is this possible at all?)? How do I store the state of my activity to avoid re-initializing it every time?
this how activities are hooked to tabs:
intent = new Intent().setClass(this, tab_schedule.class);
spec = tabHost.newTabSpec("Schedule").setIndicator("Schedule",
res.getDrawable(R.drawable.tab_icon_schedule)).setContent(
intent);
tabHost.addTab(spec);
the tab_schedule.class does a simple web page load:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tab_people);
try {
WebView currentView = (WebView) findViewById(R.id.tab_people_WebView);
currentView.getSettings().setJavaScriptEnabled(true);
currentView.loadUrl("http://pda.lenta.ru");
} catch (Exception e) {
Log.v("webClientInit", e.getMessage());
}
}
If you don't want to create a new activity for each tab, you can use a TabWidget with a FrameLayout to toggle between views.
As to switching activities, see this question for a way to not recreate the activity each time.
Regardless, you should always implement onPause and onResume to restore the state of your app. You might want to read up on the Activity Lifecycle, but basically you cannot prevent your activity from being killed if goes into the background. Thus, you should store your state in onPause. The link above has some info on how to do so as well.
To bring the previous activity to the top of the stack use intent.addFlag(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);