I'm fairly new to android studio, any help would be appreciated.
I have set up a bottom navigation bar programatically in my MainActivity - what's the best way to set this up with other fragments. I have three fragments, one for each tab in the navigation bar and other fragments which can be opened when buttons are pressed from the navigation bar fragments.
Where do I set up these other fragments? in the same activity that connects the fragments that are connected to the navigation bar or in a different activity.
How do I save the current state of the displayed fragment so that when I move to a different tab and then move back it will be in the same state as when I left it?
My question is, where do i set up these other fragments? in the same activity that connects the fragments that are connected to the navigation bar or in a different activity.
It's really up to you and how you want to display the fragments. You can display them in the same activity or open another activity. However bear in mind that if you open another activity, you will lose the navigation bar of the previous activity (an activity always uses the whole screen)
What does FragmentManager and FragmentTransaction exactly do?
How do I save the current state of the displayed fragment so that when
I move to a different tab and then move back it will be in the same
state as when i left it?
Read about the fragment lifecycle at https://developer.android.com/guide/components/fragments.html#Lifecycle
Specifically, you want to save your state in onSaveInstanceState, and the stuff you save will be sent back to you when the fragment is recreated in onCreate
I'd like to expand on what #rupps said, because I feel like the part about what do FragmentManager/Transaction do is not approached from where you are expecting.
I assume you're using a BottomNavigationView.
Regardless of the (important) lifecycle of Fragments, you have to understand that a Fragment is always attached to an activity (note: this is not true, but let's not talk about headless fragments for now).
The approach you can take is that the Activity layout looks like this: (in pseudo code)
<RelativeLayout width=match_parent height=match_parent>
<FrameLayout
id="#+id/your_fragment_container"
width=match_parent
height=match_parent
layout_above="#+id/navbar" />
<BottomNavigationView
id="#id/navbar"
width=match_parent
height=wrap_content
align_parent_bottom=true />
</RelativeLayout>
This way the BottomNavBar will always be present at the bottom of your layouts.
Now you have to deal with putting the fragments there… Let's say that you need to attach a Listener to that bar, and when you receive a callback that a new menu item has been selected… you can proceed to change the fragment (you will always get one event upon startup or you can force it during onCreate I suppose).
You will literally add a switch/if statement to the onNavigationItemSelected(MenuItem item) method.
and call addFragment(TAG); depending which case it is.
Pseudo-Code for you to get the idea:
private void addFragment(final String tag) {
final Fragment existing = getSupportFragmentManager().findFragmentByTag(tag);
if (existing == null) {
final Fragment newInstance = getNewFragmentInstanceWithTag(tag);
getSupportFragmentManager()
.beginTransaction()
.replace(getFragmentContainerLayout(), newInstance, tag)
.commit();
}
}
You'll also need to provide:
private int getFragmentContainerLayout() {
return R.id.your_fragment_container;
}
and…
public static final String TAB1_TAG = "TAB1_TAG";
public static final String TAB2_TAG = "TAB2_TAG";
public static final String TAB3_TAG = "TAB3_TAG";
protected Fragment getNewFragmentInstanceWithTag(String tag) {
switch (tag) {
case TAB1_TAG:
return Tab1Fragment.newInstance();
case TAB2_TAG:
return Tab2Fragment.newInstance();
case TAB3_TAG:
return Tab3Fragment.newInstance();
default:
return null;
}
}
So what the frog is the FragmentManager/Transaction?
Think of the Manager as a singleton object (one per app) that keeps a reference to your Fragments and can retrieve them for you (if they existed before). It handles Transactions (add/remove/hide/show, etc.) so you can later roll back them (say you add a fragment in a transaction, if you also addToBackStack() then you can simply tell the Manager: pop the last transaction, effectively rolling it back.
It's a monster. It had bugs for over 9000 years and it's not very intuitive; but once you get used to it, you just "use it".
Related
When working with java in Android Studio, how do I get back from a fragment to a previous fragment?
To use the bottom tab, I am using fragments divided into three in MainActivity. I opened another fragment on that fragment to input information, but when I press Back on the phone, the application quits. How should I do it?
This is the code I added to MainActivity.
**public void onBackPressed() {
List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
for(Fragment fragment : fragmentList){
if(fragment instanceof onBackPressedListener){
((onBackPressedListener)fragment).onBackPressed();
return;
}
}
if(System.currentTimeMillis() - lastTimeBackPressed < 1500){
finish();
return;
}
lastTimeBackPressed = System.currentTimeMillis();
Toast.makeText(this,"Press the 'Back' button again to finish.",Toast.LENGTH_SHORT).show();
}**
And can the page that receives information from being entered in a different way than a fragment?
Is it possible to delete the fragment while storing the input information after it is input from the fragment? When I go back, the stack is still piled up, so I keep seeing the previously entered information.
For the fragment you add that allows the user to input information, make sure to call:
fragmentTransaction.addToBackStack(null);
during the fragment transaction where you add the fragment.
Then get rid of the onBackPressed override, it's not necessary.
addToBackStack() will enable automatic handling of the BACK button press, removing the fragment transaction.
I understand how to navigate between fragments using Jetpack's Navigation Component, but I haven't been able to find is how to navigate from one child fragment to another child fragment.
The following is what I've done so far:
https://pastebin.com/dNQ0Ep4S the code example is pretty big so sum it up, I'm trying to do is" A>B and Ba>Bb". Ba is a fragment inside of fragment B. I'm not sure how to set up the nav graph for something like that
The navigation works until the home fragment. The bot nav just doesn't seem to work. It always displays the pending layout. Another thing I tried was setting the nav graph of the home to the same one used for the login and adding the pending and history fragments to the nav file without any actions. But the home loads the login, the tabs work, but they're replacing the home fragment instead of being placed in the home nav host.
Update
I managed to find the problem, but I'm stuck trying to find a solution.
So B seems to be getting the navhost for A when I setup the botnavview to the navcontroller. So calling NavController navController = NavHostFragment.findNavController(this); from B returns the navhost of A. I'm at a lost here. If I do this NavController navController2 = Navigation.findNavController(currentActivity, R.id.homeNavHostFragmentContainer); (homeNavHostFragmentContainer being the navhostfor B) still returns the navhost for A. For some reason, I can't get a reference to B's navhost.
Usually we use: NavigationUI with jetpack, i recommend this, https://developer.android.com/guide/navigation with nav_graph.xml with NavigationDirections such as:
val direction = FirstFragmentDirections.actionSecondFragment()
Navigation.findNavController(requireView()).navigate(direction)
Or you can use non-jetpack way, i do not recommend, but depending on your UI you may need it:
fun displayChildFragment(frameId: Int, fragment: Fragment) {
requireActivity().supportFragmentManager?.let {
val transaction = it.beginTransaction()
//transaction.setTransition(TRANSIT_FRAGMENT_OPEN)
transaction.replace(frameId, fragment).commit()
}
}
My problem was that I misunderstood how NavHostFragment.findNavController() functions. Fragment B is not a navhost is just a regular Fragment. The navhost for B is a child of B. The argument for findNavController() has to be a NavHostFragment not a regular Fragment. So by passing, this (being B), it wasn't getting a NavFragment, just a regular Fragment. I thought the method would have extracted the navhost from whatever fragment was passed. Guess the documentation caused a bit of confusion. To get the correct navhost, I had to get the navhost from the fragment manager like so getChildFragmentManager().findFragmentById(navHostResId) (in my case, navHostResId was the id of the fragment container in B). Then everything worked beautifully.
This is a scaled down version of the actual problem. To recreate the difficulty I am facing.
I have taken the example from the official website developer.android.com to cite my problem.
Building a Flexible UI
MainActivity has 2 layouts. One is for the default(portrait in small screen devices) layout in the layout folder. The other layout for both large-screen and landscape mode, kept in layout-large and layout-land folder.
Default layout for activity_main.xml contains only one FrameLayout (R.id.fragment_container) in which I add and replace 2 fragments that I create, dynamically.
The other layout is same for both the layout-land and layout-large folders. It has 2 static fragments [R.id.headlines_fragment - to display a list of headlines] and [R.id.article_fragment - to display the details when headlines are selected]. Horizontally placed. One on the left to show the lists and the one on the right to show details.
This is the code for MainActivity.java which controls all the fragments :
public class MainActivity extends Activity implements OnHeadLineSelectedListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_article);
if(findViewById(R.id.fragment_container) != null) {
if(savedInstanceState != null) {
return;
}
HeadlinesFragment firstFragment = new HeadlinesFragment();
firstFragment.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(R.id.fragment_container, firstFragment).commit();
}
}
#Override
public void onArticleSelected(int position) {
ArticleFragment articleFrag = (ArticleFragment) getFragmentManager().findFragmentById(R.id.article_fragment);
if(articleFrag != null && articleFrag.isVisible()) {
articleFrag.updateArticleView(position);
} else {
articleFrag = new ArticleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction().replace(R.id.fragment_container, articleFrag);
transaction.addToBackStack(null);
transaction.commit();
}
}
}
As soon as the activity is started, I check if fragment_container that is the FrameLayout is present or not. If it is not present, then the layout with the 2 fragments has been loaded. Hence no need to add the fragments dynamicaly as they are already present.
Otherwise, I check if the savedInstanceState is null or not. If null, then I create a new HeadlinesFragment and add it to the frame. If it is not null, then it means that the activity has already been created previously, and hence the HeadlinesFragment must have been added already. No need to add it again. So, return.
And onArticleSelected() method replaces the existing fragment in the frame with the ArticleFragment, or if in the other layout, it simply updates the fragment as it is already present. It is called from the HeadlinesFragment when an item is selected.
Now, this all works perfectly well if we enter the activity in portrait mode and then change the orientation. No problem. Flawless.
But if we enter the activity in the landscape mode, as soon as I change the orientation to the portrait mode, a blank screen is shown.
The reason being, the onCreate() is called, and the savedInstanceState returns as not null. Hence, the HeadlinesFragment is not created and added to the frame.
And yes, if I remove that check, then the app works fine, but that will mean that a new HeadlinesFragment is created and added to the frame each time and gets stacked on top of eachother. Which is not at all desirable.
I cannot implement this by just finding out the orientation and applying the appropriate layout. Because, in large-screen devices, even if it is in portrait mode, it is supposed to show both the fragments at once.
I have tried many convoluted logic. But nothing seems to work. Any help is appreciated.
Entering activity in portrait mode
1> List Items are shown.
2> Clicking items replaces the fragment with the ArticleFragment (details).
3> Changing the orientation, shows both side by side. Everything works.
--->
Entering activity in landscape mode
1> Both the list and details are shown. Everything works.
2> But as soon as the orientation changes, you get the blank screen. As Headlines fragment is not created and added.
-->
It would be really helpful if someone could guide me as to how I can solve this problem. And as the actual project is huge, and this logic has already been implemented, a drastic change in logic is not an option anymore as that will mean re writting thousands of lines of code. Thank you. :)
Ok. I got the problem. The problem is we are using Fragments in xml layouts in large devices.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.example.android.fragments.HeadlinesFragment"
android:id="#+id/headlines_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<fragment android:name="com.example.android.fragments.ArticleFragment"
android:id="#+id/article_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
So android is trying to catch the Fragments in savedInstanceState in MainActivity. When screen rotates, system tries to restore the above Fragments even though different layout loads in potrait mode. And so system considers that the article_fragment is also available on the right side and it tries to update it on click on the Headline.
So, What's the solution ?
I have changed a little code in MainActivity and nothing else :-)
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
setContentView(R.layout.news_articles);
// Check whether the activity is using the layout version with
// the fragment_container FrameLayout. If so, we must add the first fragment
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
/*if (savedInstanceState != null) {
return;
}*/
// Create an instance of ExampleFragment
HeadlinesFragment firstFragment = new HeadlinesFragment();
// In case this activity was started with special instructions from an Intent,
// pass the Intent's extras to the fragment as arguments
firstFragment.setArguments(getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}
So what I have done just I told the system that I don't want any thing restored by using super.onCreate(null); so its restoring nothing now.
About the blank screen you are getting
Whenever you start activity in landscape mode. It loads large display by default. Without entering into if satement. Because it can't get fragment_container in landscape mode. And then you rotate screen to load portrait layout and system gets savedInstanceState != null and it returns without loading HeadlinesFragment. So you get bank screen.
So I have commented If statement as you can notice.
/*if (savedInstanceState != null) {
return;
}*/
So now It load everything correctly.
No issue
Download the code sample from this Developer site and modify it according to your needs.
Build Dynamic UI with fragments
Its super and reliable code.
I'm sure there are reasons why the Fragment was rendered correctly in Portrait -> Landscape and not in the reverse but one thing is very clear about the Activity lifecycle
Called when the activity is starting. This is where most
initialization should go: calling setContentView(int) to inflate the
activity's UI, using findViewById(int) to programmatically interact
with widgets in the UI, calling managedQuery(android.net.Uri,
String[], String, String[], String) to retrieve cursors for data
being displayed, etc.
Note the part that says This is where most initialization should go. By exiting after checking that savedInstanceState is null, you're leaving it up to the super class to restore your Fragment which is not a good idea considering the previous view was destroyed.
My advice, inspect the content of savedInstanceState instead of just checking if it is null. Ensure that it contains enough data to restore your previous state, if not initialize your Fragment.
The best practices with Fragments involve implementing all necessary methods to monitor the state.
I am in a peculiar situtation in my app.
When i app first loads there is a custom listview which is populated with data from the server.I am also using a class which contains different fields for the string data from the server.
When i click an item on the custom listview,the object of the corresponding class is passed onto the next fragment.
That is the current fragment is replaced with a new fragment and the object is passed with bundle.
Now a new listview loads with different tasks.On clicking a task a new fragment with a camera is loaded.
After taking the image and uploading to server, the status in the JSON changes to "COMPLETED".But now when i press back the old listview is shown.
Is there a way to populate the listview on back pressed with new data?
The issue is that I am passing an object right from the first fragment.
Now i need a new object on back pressed,how to pass the new object on back pressed?
When Fragment 2 gets the data, it should pass it along at some point before Fragment 1 is woken.
There are almost a half dozen ways to pass data, and the best way depends on a number of factors like who should own the lifecycle of the data, data pull vs push, dependency between fragments, do multiple components need updating, etc.
I'm just going to advise to simply cache the data on the activity until you learn more about the different methods.
//Fragment 2 puts data to activity
((MyActivity) getActivity).mListViewData = listViewData;
Then the next part of the question is how does fragment 1 get the data. Fragment 1 is hibernating on the backstack. When it wakes up it will call the onViewCreated() method (because it's previous view was destroyed before being placed on the backstack).
In that method, we check if there's new data waiting for Fragment 1.
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
MyDataType listViewData = ((MyActivity) getActivity).mListViewData;
if(listViewData != null){
//setData is your own function for replacing the adapters
//data backing
listView.getAdapter().setData(listViewData);
}else{
listView.getAdapter().setData(...defaultData);
}
listView.getAdapter.notifyDataChanged();
}
Override the onBackPressed in the Activity that manages the Fragments. In it you can check if the fragment is visible or not (the one from which an action should be performed if the back is pressed) and then execute your action.
#Override
public void onBackPressed(){
Fragment myFragment = getFragmentManager().findFragmentByTag("MY_FRAGMENT");
if (myFragment.isVisible()) {
String json = myFragment.getJsonData(); //update it locally
if(isUpdated){
Fragment listFragment = getFragmentManager().findFragmentByTag("MY_LIST_FRAGMENT");
listFragment.updateListView(json); //Add this method on your fragment
}
}
super.onBackPressed();
}
Obs.: To use the .findFragmentByTag() you should add tags once you're making the transaction like so:
fragTrans.replace(android.R.id.content, myFragment, "MY_FRAGMENT");
If, for any reason the listFragment has been cleaned from memory, you would have to reload the data anyway so just download the new data again.
To update the ListView please see: How to refresh Android listview? . Note thought that you will need to will need to send a new data set to the list view (which you can do inside the updateListView() method)
In the main activity of my app there is a container that hosts fragments.
When a user clicks a button in the "default" fragment (the first fragment that is displayed), the fragment changes, and so do the actionbar buttons.
One of the buttons in the actionbar of this new fragment open another activity.
In that activity, when a user clicks the back button, the activity closes, and the fragment that was shown in the MainActivity (the fragment that opened the new activity) is still there (which is fine).
However, if a user clicks the back button again, it does not return to the previous fragment. While it does return when the activity does not open.
It turns out that opening the activity clears the backstack (verified by Logging the count from the FragmentManager class), while I'm not quite sure whether this is supposed to behave like this or not, it kinda makes sense. Unfortunately, it is not the behavior I desire.
MainActivity: Fragment A (default) ---> Fragment B ---> Acivity B
Therefore, my question is how can I keep the backstack after the activity resumes, if at all?
I tried searching for similar questions, but all questions I found actually asked how to clear the backstack.
Try that:
#Override
public void onBackPressed() {
Intent intent = new Intent(A_Acticity.this, B_Activity.class);
startActivity(intent);
}
Hope it helped! :)
Reading the documentation, there is a way to pop the back stack based on either the transaction name or the id provided by commit. Using the name may be easier since it shouldn't require keeping track of a number that may change and reinforces the "unique back stack entry" logic.
Since you want only one back stack entry per Fragment, make the back state name the Fragment's class name (via getClass().getName()). Then when replacing a Fragment, use the popBackStackImmediate() method. If it returns true, it means there is an instance of the Fragment in the back stack. If not, actually execute the Fragment replacement logic.
private void replaceFragment (Fragment fragment){
String backStateName = fragment.getClass().getName();
FragmentManager manager = getSupportFragmentManager();
boolean fragmentPopped = manager.popBackStackImmediate (backStateName, 0);
if (!fragmentPopped){ //fragment not in back stack, create it.
FragmentTransaction ft = manager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.addToBackStack(backStateName);
ft.commit();
}
}