Hell community,
I am new to Android and I could not find a good answer to that problem:
I want to use a bottom-navigation and some of the elements should also use tabs.
Now my question is what is the most common (best) way of implementing that?
I am confused in if I should use activities or Fragments for this navigation.
Is it an okay way just using fragments for both, the bottom-navigation and the tabs or is the best way using activities for the bottom-navigation and only fragments for the tabs?
Best regards
The best approach is to use Fragments for both. If you use an Activity for each screen reachable from the bottom navigation, you will have to put a BottomNavigationView in each and duplicate a lot of code, as well as tweaking transitions between those activities.
Activities should be considered entry-points of your application: when clicking on the app icon in the launcher, the main activity defined in the Manifest is started.
For anything else (portions of the UI, parts of a navigation flow...) you may use Fragments.
Here is a sample Activity and Fragments organization to solve your case:
MainActivity
|--- TopLevelFragment1
|--- TopLevelFragment2
| |--- TabFragment1
| |--- TabFragment2
| |--- TabFragment3
|
|--- TopLevelFragment3
|--- TopLevelFragment4
As you can see in the above schema, an Activity can host child fragments, and fragments can also have nested fragments (also called "child fragments").
TopLevelFragmentN are fragments that are displayed when selecting an item in the BottomNavigationView. TabFragmentN are fragments that are displayed when selecting a Tab from TopLevelFragment2.
Let's dive into the implementation. I have not tested the following pieces of code, those are just provided for guidance.
Here is sample code for the activity:
main_activity.xml
<CoordinatorLayout android:id="#+id/coordinator_layout">
<FrameLayout android:id="#+id/fragment_host"><!-- TopLevelFragments will be displayed here --></FrameLayout>
<BottomNavigationView android:id="#+id/bottom_nav"/>
</CoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
// Retrieve a reference to the BottomNavigationView and listen for click events.
BottomNavigationView bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav);
bottomNav.setOnNavigationItemSelectedListener(item -> {
// Depending on the clicked item, change the displayed TopLevelFragment.
switch(item.getItemId()) {
case R.id.top_level_1:
showTopLevelFragment(new TopLevelFragment1());
return true;
case R.id.top_level_2:
// Do the same with other TopLevelFragments
return true;
default:
return false;
}
}
// Show the first TopLevelFragment by default.
showTopLevelFragment(new TopLevelFragment1());
}
private void showTopLevelFragment(Fragment fragment) {
// Use the fragment manager to dynamically change the fragment displayed in the FrameLayout.
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_host, fragment)
.commit();
}
}
Please refer to the documentation to understand how to define items in the BottomNavigationView.
Now, let's focus on the TopLevelFragment2 that contains tabs. It is a common pattern to use TabLayout with a ViewPager, so that you can move from one tab to another by swiping left or right.
top_level_fragment_2.xml
<ConstraintLayout android:id="#+id/constraint_layout">
<AppBarLayout android:id="#+id/appbar_layout">
<Toolbar android:id="#+id/toolbar"/>
<TabLayout android:id="#+id/tab_layout"/>
</AppBarLayout>
<ViewPager android:id="#+id/tab_pager"/>
</ConstraintLayout>
TopLevelFragment2.java
public class TopLevelFragment2 extends Fragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.top_level_fragment_2, container, false);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
MyTabPagerAdapter tabPager = new MyTabPagerAdapter(getFragmentManager());
ViewPager viewPager = getView().findViewById(R.id.tab_pager);
viewPager.setAdapter(tabPager);
// Display a tab for each Fragment displayed in ViewPager.
TabLayout tabLayout = getView.findViewById(R.id.tab_layout);
tabLayout.setupWithViewPager(viewPager);
}
static class MyTabPagerAdapter extends FragmentPagerAdapter {
MyTabPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return 3; // One for each tab, 3 in our example.
}
#Override
public Fragment getItem(int position) {
switch(position) {
case 0:
return new TabFragment1();
case 1:
// Return a new instance of the fragment associated with the tab at position 1
default:
throw new IllegalArgumentException();
}
}
}
}
After those steps, all you have to do is to write the code for the other fragments.
Related
I am new to programming. I have 4 working activities with 4 xml layouts. I also copied this simple example fragment (I think I need 4 of those for my 4 activites) from a tutorial:
public class SelectFriends extends Fragment {
public SelectFriends () {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_SelectFriends , container, false);
}
}
This is the relevant part from MainActivity:
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.navigation_item1:
getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.fade_in, R.anim.fade_out).replace(R.id.container, SelectFriends).commit();
return true;
case R.id.navigation_item2:
getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.fade_in, R.anim.fade_out).replace(R.id.container, StartFood).commit();
return true;
[...]
}
return false;
}
I can start my activitie´s selectFriends.xml when I replace fragment_SelectFriends in #Override public View onCreateView with ID of selectFriends.xml . The problem is I don´t know where to put the corresponding Activity, so the selectFriends.xml shows up correctly with BottomNavigation but there is no interaction possible, of course. What is best practice? Internet is confusing me: am I understanding the use of fragments false? I don´t even understand why I should make fragments with BottomNavigation when the things I want to show in the different BottomNavigation displays are completely different from each other. Thank you
Let's start from your question:
How to correctly put my activities in Tutorial´s Bottom Navigation?
You will not put your activities in the Bottom Navigation.
You will have one Activity which will draw the layout of your screen. Secondly you will have a number of Fragment's which will represent some parts of your screen.
How many Fragment's? As many as the Bottom Navigation's options.
Take the Youtube app as an example.
The whole screen is an Activity, Youtube's MainActivity. As you can see the user pressed Subscriptions in the Bottom Navigation so the MainActivity called a SubscriptionsFragment to draw the subscriptions part of the screen (all the layout except the ActionBar at the top and the Bottom Navigation at the bottom.
If a user selects Home at the Bottom Navigation then the MainActivity will replace the part of the screen where the SubscriptionsFragment draw its layout with the HomeFragment's layout. And the same thing will happen with the other options of Youtube's Bottom Navigation bar.
So to clarify. There is only one activity here. The MainActivity. Not four. This activity commands 4 fragments to draw the 4 main parts of its screen (the home part with HomeFragment, the trending part with TrendingFragment, the subscription part with SubscriptionFragment and finally the library part with LibraryFragment.
From your last comment:
I have to put the two pieces of code that I posted above into one big
class?
No you don't have to. It is not necessary to create one file, such as MainActivity.java (where your MainActivity is defined), and then define the Fragment's classes inside the same file.
You want to display 4 screens using a bottom navigation, right? Create a file for your activity and four separate files, one per Fragment.
Example:
Your first file, SelectFriends.java, where SelectFriends fragment is defined, as you've posted above...
public class SelectFriends extends Fragment {
public SelectFriends () {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_SelectFriends , container, false);
}
}
Three more separate files like this one, one per fragment.
And lastly your activity, again, as you've posted above.
public class MainActivity extends AppCompatActivity{
#Override
public void onCreate(Bundle savedInstanceState){
// ... some code here
}
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.navigation_item1:
..
// Here the activity figured out that the first item of the bottom navigation
// was clicked, so it calls the support fragment manager to display a fragment
// inside the container view
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
.replace(R.id.container, SelectFriends).commit();
return true;
case R.id.navigation_item2:
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
.replace(R.id.container, StartFood).commit();
return true;
}
return false;
}
}
Edit: Read the Ultimate Guide to Bottom Navigation on Android
I'm not quite sure if there's a "best way" to tackle the following design issue.
I have a Tablayout with 2 Tabs in my MainActivity. Each Tab is a different Fragment. I go to Tab1 and see Fragment1. I need to launch a new Fragment (1A) from Fragment 1 and am not sure the best way to do it? I was thinking about one of these.
A) Take the Tabs out of my MainActivity and place them in a separate MainFragment, which gets launched with the app. That way when the user launches Fragment 1A, it replaces just the 1 MainFragment with the Tabs.
or
B) Keep the Tabs in the MainActivity and find a way to replace Fragment 1 with Fragment 1A when under Tab1.
Any suggestions would be appreciated. Thank you.
I think you shouldn't do both of points... Frag1 visible under Tab1 should contain all the layout (including initialy hidden) and logic for this view. If you need to show smth new it may be smaller (then popup, dialog etc.) or expand some layout, maybe with some animation (you may still use ViewPager inside Fragment inside ViewPager, disable touch events and manipulate scrolling programmatically...).
When Action picked by user is intended to show smth so-much-improtant that previous screen is not needed at all then you should probably open new Activity
PS. If you insist to replace current "screen" (in fact Activitys content) note that title of Tab1 may not representing what contains Frag1A. It very depends what kind of content you have there. You may consider move TabLayout/RecyclerView to e.g. FrameLayout container and add to it you Frag1A covering whole previous view including Tabs. In current design guidelines you can even find suggested solution for way of showing new fragment - with circular reveal animation
I do not understand very well what you want, but possibly this.
Use FragmentPagerAdapter..
public class TabsPagerAdapter extends FragmentPagerAdapter {
private static final int NUM_TABS = 3;
public TabsPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch(position){
case 0:
return Tab1Fragment.newInstance();
case 1:
return Tab2Fragment.newInstance();
default:
return Tab3Fragment.newInstance();
}
}
#Override
public int getCount() {
return NUM_TABS;
}
#Override
public CharSequence getPageTitle(int position) {
if (position == 0){
return "Tab 1";
}
if (position == 1){
return "Tab 2";
}
return "Tab 3";
}
In your activity...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Tabs
TabLayout tabs = (TabLayout) findViewById(R.id.tabs);
ViewPager pager = (ViewPager) findViewById(R.id.pager);
TabsPagerAdapter adapter = new TabsPagerAdapter(getSupportFragmentManager());
pager.setAdapter(adapter);
tabs.setupWithViewPager(pager);
}
Ok so i'm building an android app that uses this library for a bottom navigation and i'm using a base Activity to hold it along with a Framelayout to manage my fragments for loading/replacing etc.
What works:
tapping on a bottom bar icon loads the fragment it corresponds to and that works perfectly.
My problem:
If i tap on the first tab and then the second tab and then the first tab AGAIN, the entire fragment reloads from scratch.
I don't want this behavior. Anyone have any good tips on how to retain the state of a fragment while ALSO using the bottom bar library.
I achieved something similar with a pagerview in a previous app (the previous app did not use a bottom bar for navigation) but I'm not sure how to use a pager view with ONE base activity that holds the Framelayout for replacing the fragments or if that is even the best solution.
I like the solution i have so far except that the fragments reload from scratch each time they replace the previous. If anyone has any help or suggestions that can help me out it would be greatly appreciated.
Alright i seemed to figure out a work around for the time being. It keeps the fragment state after switching tabs so I'm satisfied.
In the base activity class that hosts the fragment container i have the following
public class BaseActivity extends AppCompatActivity
{
AFragment AFragment = new AFragment();
BFragment BFragment = new BFragment();
Fragment currentFragment;
Boolean aIsActive = false;
Boolean bIsActive = false;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base);
BottomBar bottomBar = BottomBar.attach(this, savedInstanceState);
bottomBar.setItems(
new BottomBarTab(null,"A"),
new BottomBarTab(null,"B")
);
bottomBar.setDefaultTabPosition(0);
bottomBar.setOnTabClickListener(new OnTabClickListener()
{
#Override
public void onTabSelected(int position)
{
if (position == 0)
{
if(!aIsActive)
{
getSupportFragmentManager().beginTransaction().add(R.id.fragmentContainer,AFragment).commit();
aIsActive = true;
}
else
{
getSupportFragmentManager().beginTransaction().hide(currentFragment).show(AFragment).commit();
}
currentFragment = AFragment;
}
else if(position == 1)
{
if(!bIsActive)
{
getSupportFragmentManager().beginTransaction().add(R.id.fragmentContainer,BFragment).commit();
bIsActive = true;
}
else
{
getSupportFragmentManager().beginTransaction().hide(currentFragment).show(BFragment).commit();
}
currentFragment = BFragment;
}
}
#Override
public void onTabReSelected(int position) {
}
});
}
}
And loe and behold it works as expected without refreshing the fragments :)
any suggestions or feedback please let me know and feel free to comment.
Could anybody give me a clue in the following situation :
I have an Android 3+ app, which consists of a sofisticated set of interchangably operating activities derived from ActionBarActivity. One of them includes a set of fragments derived from Fragment, that can be selected by user by means of
getSupportFragmentManager().beginTransaction()
.replace(R.id.containerBase, FragmentX.getInstance(0))
.commit()
where
FragmentX extends Fragment {
public static FragmentX getInstance() {
FragmentX fragment = new FragmentX();
return fragment;
}
public FragmentX() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_x, container, false);
. . . . . . . .
return rootView;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
. . . . . . . .
}
}
All fragments (X, Y, Z etc) appear properly and I was happy until it became necessary to modify some fragments so that they can have several subpages, which should be swipable horizontally by user. Other fragments should stay as they are.
Which Android classes could be the optimal choice and how could they be intergrated in my app?
Any help would be highly appreciated.
The Fragment that needs to have "horizontally swipable subpages" should probably use a ViewPager of some sort. Your use case isn't completely clear but in my experience a ViewPager with another set of fragments provides a nice and flexible solution. Also, if your "child" fragments (those in the ViewPager) are essentially the same but display different data (e.g. a set of event details fragments), this fragment can also be reused. If not, you can use a switch or something similar in the ViewPager's public Fragment getItem(int position) method.
You should take a look at the FragmentStatePagerAdapter which should fit your needs, the linked page also has a nice and detailed example of how to use it. One thing to note here, since you are using fragments within fragments, the fragment which contains the ViewPager will need to use getSupportChildFragmentManager instead of the regular getSupportFragmentManager to display those fragments.
Certainly I must give some more details about my app.
public class BasePage extends ActionBarActivity implements NavigationBase.NavigationDrawerCallbacks {
private int mPage;
private NavigationBase mNavigationFragment;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
. . . . . . . .
setContentView(R.layout.activity_base);
mNavigationFragment = (NavigationBase) fragmentManager.findFragmentById(R.id.navigation_drawer);
mNavigationFragment.setUp(
R.id.navigation_drawer,
(DrawerLayout) findViewById(R.id.drawer_layout));
}
private void setPage() {
switch(mPage) {
case 1:
fragmentManager.beginTransaction()
.replace(R.id.containerBase, FragmentX.getInstance())
.commit();
break;
case 2:
fragmentManager.beginTransaction()
.replace(R.id.containerBase, FragmentY.getInstance())
.commit();
break;
}
}
}
The activity_base layout looks like this:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="app.BasePage">
<RelativeLayout
android:id="#+id/containerBase"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<fragment
android:id="#+id/navigation_drawer"
android:layout_width="#dimen/navigation_drawer_width"
android:layout_height="match_parent"
android:layout_gravity="start"
android:name="app.NavigationBase" />
</android.support.v4.widget.DrawerLayout>
User navigates through current fragments by setting mPage and calling setPage().
Now I need to modify the fragment X thus it has several subpages, whereas the fragment Y must stay as it is. Programmatically the classes FragmentX and FragmentY are identical, they display solely different user contents.
I comprehend that ViewPager must be a right solution in this scenario. My problem is I do not see where to build the ViewPager in my current code...
Firstly, I know these subjects have been created a lot of time on stackoverflow, but I don't have found the solution to my problems. Secondly, I'm french, so my english is not perfect, sorry per advance and tell me if you don't understand something. And to finish this introduction, it's the first time that I'm dealing with fragments, so, sorry if there is something that I don't have well understand !
I have three buttons, that allow to switch between three fragments.
Inside one of these fragments, I have a view pager with two fragments. For the moment, each fragments (there are 5), only contains a TextView.
I'm using the latest version of android-support-v4 (I have read a lot of subject in stackoverflow that say that the latest version of support solve the "Recursive entry to executePendingTransactions" error that I have).
My two problems :
When I click two times in one button, I have an IllegaleStateException "can't change tag of fragment". I was able to fix that by creating a new fragment on onButtonSelected method, but I don't want to recreate fragment each time, for memory reasons and for functional reasons : fragment have to keep her state. This problem is not my main problem, indeed, i know that to disable the button when user is already on fragment is possible, but it's strange to have an exception when this management is not done, no ?.
When I go out from the fragment with the view pager, and I go back to this fragment, I have an IllegalStateException "Recursive entry to executePendingTransactions". I can fix this by setting my adapter on an handler, or use FragmentStatePagerAdapter instead of FragmentPageAdapter (see Display fragment viewpager within a fragment), but even if my application don't crash, when I go back to my fragment with the view pager, the view pager has disapear !
Can you help me ?
Java source code is bellow, layout source code is, I think, useless.
MainActivity :
public class MainActivity extends SherlockFragmentActivity{
private SherlockFragment fragmentOne, fragmentTwo, fragmentThree;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// this.fragmentOne = new fragmentOne();
// this.fragmentTwo = new fragmentTwo();
// this.fragmentThree = new fragmentThree();
// Call new or instanciate ? What's the correct way ?
this.fragmentOne = (SherlockFragment) SherlockFragment.instantiate(this, FragmentOne.class.getName());
this.fragmentTwo = (SherlockFragment) SherlockFragment.instantiate(this, FragmentTwo.class.getName());
this.fragmentThree = (SherlockFragment) SherlockFragment.instantiate(this, FragmentThree.class.getName());
// Add fragment
FragmentTransaction transaction = (
this.getSupportFragmentManager().beginTransaction()
);
transaction.add(
R.id.tab_fragment,
this.fragmentOne,
this.fragmentOne.toString()
);
transaction.commit();
}
public void onButtonSelected(View v){
switch (v.getId()){
case R.id.button_one_tab:{
showFragment(this.fragmentThree);
break;
}
case R.id.button_two_tab:{
showFragment(this.fragmentOne);
break;
}
case R.id.button_three_tab:{
showFragment(this.fragmentTwo);
break;
}
default:{
break;
}
}
}
public void showFragment(SherlockFragment fragmentToShow){
FragmentTransaction transaction = (
this.getSupportFragmentManager().beginTransaction()
);
transaction.replace(R.id.tab_fragment, fragmentToShow, fragmentToShow.toString());
transaction.commit();
}
}
Fragment two and three only inflate a layout that only contains a TextView.
Fragment one (note that i'm using a DirectionalViewPager - a lib - instead of a ViewPager):
public class FragmentOne extends SherlockFragment{
private FragmentOneAdapter fragmentOneAdapter;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Set up the pager
final DirectionalViewPager pager = (DirectionalViewPager)getActivity().findViewById(R.id.pager);
if (this.fragmentOneAdapter== null){
this.fragmentOneAdapter= new FragmentOneAdapter (getActivity().getSupportFragmentManager());
}
pager.setAdapter(fragmentOneAdapter);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_landing_page, container, false);
}
FragmentOneAdapter :
public class FragmentOneAdapter extends FragmentPagerAdapter{
private ArrayList<SherlockFragment> fragmentsList;
public FragmentOneAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
SherlockFragment fragmentFour = new FragmentFour();
SherlockFragment fragmentFive = new FragmentFive();
this.fragmentsList = new ArrayList<SherlockFragment>();
this.fragmentsList.add(fragmentFour);
this.fragmentsList.add(fragmentFive);
}
#Override
public Fragment getItem(int position) {
return this.fragmentsList.get(position);
}
#Override
public int getCount() {
return this.fragmentsList.size();
}
}
Thanks per advance for your help !
I have solved my problem !
I have simply replace getSupportFragmentManager() on FragmentOne to getChildFragmentManager(). Then, I have edited my onButtonSelected's method by creating a new instance of fragment each time instead of using my three different instances (if I don't do that, I have an exception : java.lang.IllegalStateException: Activity has been destroyed).
I have still a problem with this solution : I lose the state of each fragment each time I'm switching between fragmentOne, fragmentTwo and fragmentThree. Do you have a solution for that ?