So i have a working solution, it seems not to be ideal but just need to confirm - so i have an activity with 3 fragments showing one at a time - i need to block the default back operation for two of those fragments so that when the back button is pressed nothing happens(for now, still developing).
So what i did was that i used an interface
//top of activity snippet
public class StudyKitActivity extends AppCompatActivity implements OnStudyKitBackListener{
private int fragment_id;
.
.
.
.
//interface method
#Override
public void onBackChanged(int fragment_id) {
this.fragment_id = fragment_id;
}
to set a variable on the activity
when i am changing the current fragment in the activity e.g set 1 for fragment A, 2 for fragment B and 3 for fragment C, so this is code snippet within fragment 2 when moving to fragment 3 -
onStudyKitBackListener.onBackChanged(3);
StudyKitResultFragment studyKitResultFragment = StudyKitResultFragment.getInstance(examResult);
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.frame_container, studyKitResultFragment).addToBackStack("exam_result").commit();
and i did an overwrite in my activity's onBackpressed to check the value of that variable and either execute the default OnBackPressed or do nothing like so -
#Override
public void onBackPressed() {
if(this.fragment_id==2||this.fragment_id==3){
}
else{
super.onBackPressed();
}
}
i came up with this cos other solutions like setting a tag and trying to get the fragment's tag or by checking current fragments id did not work for me, this is working like i expected but i have not seen it anywhere just want to know if there is an alternate solution.
Related
Let me explain the whole thing, just in case. I'm using BottomNavigationView with Jetpack's Mobile Navigation graphs and so. I reimplemented the method:
NavigationUI.setupWithNavController(...)
during navigation setup, instead of using this method I made a very similar:
customSetupWithNavController(...)
the only changes I made was to change de default fading transition between fragments, and started to using more natural (in my opinion) side slide animations, just like this in onNavDestinationSelected:
if(origin == 0){
builder.setEnterAnim(R.anim.slide_in_from_right)
.setExitAnim(R.anim.slide_out_to_left)
.setPopEnterAnim(R.anim.slide_in_from_left)
.setPopExitAnim(R.anim.slide_out_to_right);
}else if(origin == 1){
builder.setEnterAnim(R.anim.slide_in_from_left)
.setExitAnim(R.anim.slide_out_to_right)
.setPopEnterAnim(R.anim.slide_in_from_right)
.setPopExitAnim(R.anim.slide_out_to_left);
}
Where origin stands for the direction where the incoming Fragment comes from.
It came with a problem: 2 of my fragments need a FAB to add elements to a recycler, and the side slide transitioning suddenly became ugly. So I added a single FAB in MainActivity's Layout, and a logic shows the FAB only when these 2 fragments are called.
I couldn't find a nice way to pass the click event from Activity to Fragments, because I wasn't able to instantiate the Fragments, since the Navigation handles the whole process.
So, what I did was to create a ViewHolder, since I know it can survive trough lifecycle changes. This ViewHolder holds an int, and a MutableLiveData, in the MainActivity logic I pass the current selected id of the element selected by the BottomNavigationView to the int, and only if the MainActivity's FAB is clicked the live Boolean is set to true. So, in Fragments onViewCreated() I added and observer to this Boolean, when the value is set to true, and the id passed to the ViewHolder matches with the id of the current fragment, the Boolean is set back to false, and something can be done, it's something like this:
eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
eventsNotificationHandler.setClickEvent(false);
//do something here
}
}
});
This notificationHandler is the ViewHolder.
So far so good, at this point I can:
1- Navigate between BottomNavigationView' Fragments freely, and the FAB shows only for needed fragments.
2- Use Log.d(...) inside the observer any time I want, and see that the debug message just fine.
3- Toast a message, any time I want, ONLY if the context parameter was initialized outside the Observer, something like this:
Toast.makeText(previouslyDefinedContext, "SOME TEXT", Toast.LENGTH_SHORT).show();
What I can't:
1- Launch an Activity whenever I want, from inside the observer by using same idea than before, ONLY initializing the context before, and outside the Observer I was able to start the intent, just like this:
eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
eventsNotificationHandler.setClickEvent(false);
Intent newContact = new Intent(previouslyDefinedContext, NewContactActivity.class);
startActivity(newContact);
requireActivity().overridePendingTransition(R.anim.slide_in_from_right,R.anim.slide_out_to_left);
}
}
});
But in this particular case I can launch the new Activity as many times as I want, BUT ONLY if I navigate directly to this particular fragment where the observer is defined after the app opens, if I decide to navigate first trough some other fragments instead, and then I go to this fragment to try to launch the Activity, the app crashes
I've noticed that this exact behavior happens when I call requireContext() from inside the Observer, it works but then stops working.
The app crashes with:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: cu.arrowtech.bpawreckproject, PID: 18019
java.lang.IllegalStateException: Fragment FragmentContacts{883259b} (9f127bdb-127d-4366-b90b-c8900a5a771e)} not attached to Activity
What I want:
1- A right way to launch a new Activity from inside a Fragment, by pressing a FAB in MainActivity, if it's possible.
2- A nice way to switch fragments if a possible solution implies to change the logic I have already.
3- Keep using Jetpack and Navigation Graphs.
I'm able to do what I want by using 2 separate FABs in each Fragment, but the transitioning is not nice and beautiful.
I'm open to suggestions, even if that implies to change the logic. I'm almost certain it must be a better way to do what I'm doing, instead of using ViewHolder for this purpose.
I would like to get something similar to the Google Pay, it seems to be that the Buttons for adding payment method, passes, and transfers is the same button, but it adapts to each situation.
After some reasearch I did found a way to keep fluid transitions between fragments AND Mobile Navigation components AND a single FAB in the MainActivity layout.
What I did was to use an Interface instead of ViewModels (I always knew that approach was wrong):
public interface SharedViewsInterface {
void onFabClicked();
}
So in my MainActivity I defined that interface as null, and created a method to define it according to the situation. My MainActivity looks like:
public class MainActivity extends AppCompatActivity {
//UI elements
private FloatingActionButton main_fab;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Same dode to get the custom animations
//.......
//Main and only Fab configuration
main_fab = findViewById(R.id.main_fab);
main_fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(sharedViewsInterface !=null){
sharedViewsInterface.onFabClicked();
}
}
});
}
//Other auxiliary methods
public void setInterface(SharedViewsInterface sharedViewsInterface){
this.sharedViewsInterface = sharedViewsInterface;
}
}
By doing this, I can, from each fragment to implement the Interface in onCreate by doing this:
((MainActivity) requireActivity()).setInterface(new SharedViewsInterface() {
#Override
public void onFabClicked() {
Intent newContact = new Intent(requireContext(), NewContactActivity.class);
startActivity(newContact);
requireActivity().overridePendingTransition(R.anim.slide_in_from_right_short,R.anim.slide_out_to_left_medium);
}
});
This works well becouse the FAB is shown only when a fragment with Interface implementation is visible, see my example gif
I'm setting a new android app, and I want to exploit the bottom navigation bar in my phone (previous,home) so that i don't have to create a customized one, I want to know if it is possible to set an onClickEvent on these buttons
You have to handle both button separately.
For hardware back button (Previous) you have to override onBackPress method in your Activity class. So, you will get the back press event you can change the behavior of back button if you don't want back event just remove super.onBackPress(). If you want to do this from fragment you just need to add your logic like you can get current fragment instance from FragmentManager like below and call fragment method what ever you want to do on that fragment.
\\ To get current fragment
\\ NOTE: I add back stack name as class name.
public static Fragment getCurrentFragment(FragmentManager fragmentManager) {
int count = fragmentManager.getBackStackEntryCount();
if (count > 0) {
FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount() - 1);
String tag = backStackEntry.getName();
return fragmentManager.findFragmentByTag(tag);
}
return null;
}
#Override
public void onBackPressed() {
// enter code here
super.onBackPressed(); // Remove this line if you don't want to go back.
}
For home button handling you can use this answer or this post.
Please comment me if you have more questions.
I have an Activity whose layout is a fragment which can be filled by different Fragments classes. The thing is that when I am in Fragment X and I press back, I would like to override onBackPressed method in order to execute a method asking the user whether to save input data. However, the problem is that onBackPressed can only be overwritten from the activity, then my question is:
Should I create a public method in Fragment X and call it from the overwritten onBackPressed method or should I use interfaces or whatever else?
I already checked other related posts like how to move back to the previous fragment without loosing data with addToBackStack, but I think this is a different question..
When I wanted to do something similar, I created tags for the fragments and a Fragment object in the parent activity named mCurrentFragment. Every time I would load a fragment, I assigned it to mCurrentFragment. So I would override the onbackPressed() from my activity and then check the fragment instances:
#Override
public void onBackPressed() {
if (mCurrentFragment instanceof FragmentA) {
//Do something
//e.g. mCurrentFragment.save();
} else if (mCurrentFragment instanceof FragmentB) {
//Do something else
} else {
super.onBackPressed()
}
}
If you want to call a method from a fragment, you just use the Fragment object you created (in my case mCurrentFragment) and get access to all of its public methods (as in the example for FragmentA above)
§EDITED to include code from the comments
Problem in short:
I have an MainActivity that holds BottomNavigationView and FrameLayout on top of it. BottomNavigationView has 5 tabs and when tab is clicked, I add some fragment on that FrameLayout. But, from some fragment, I need to open another fragment. From that another fragment, I need to open the other one. Every time when I need to show fragment, I notify MainActivity from fragment, that it needs to add the another one. Every fragment checks does its activity implement interface. And it is annoying. So, if I have 100 fragments, MainActivity implements too many interfaces. It leads to boilerplate code. So, how to properly navigate between fragments if you have a lot?
Problem in detail:
Please, read problem in short section first.
As I've said I have BottomNavigationView that has 5 tabs. Let's call the fragments that responsible for each tab as FragmentA, FragmentB, FragmentC, FragmentD, FragmentE. I really know, how to show these fragments when tab is clicked. I just replace/add these fragments in activity. But, wait, what if you wanna go from FragmentA to FragmentF? After that from FragmentF to FragmentG? This is how I handle this problem: from FragmentF or FragmentG I notify MainActivity that I wanna change the fragment. But how they communicate with MainActivity? For this, I have interfaces inside of each fragment. MainActivity implements those interfaces. And here is problem. MainActivity implements too many interfaces that leads to boilerplate code. So, what is the best way to navigate through Fragments? I don't even touch that I also need to handle back button presses :)
Here is how my code looks like:
MainActivity implementing interfaces to change fragments if necessary:
class MainActivity : AppCompatActivity(), DashboardFragment.OnFragmentInteractionListener,
PaymentFragment.BigCategoryChosenListener, PaymentSubcategoryFragment.ItemClickedListener, PayServiceFragment.OnPayServiceListener, ContactListFragment.ContactTapListener, P2PFragment.P2PNotifier
Here is my PaymentFragment's onAttach method for example:
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof BigCategoryChosenListener) {
listener = (BigCategoryChosenListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement BigCategoryChosenListener");
}
}
And using this listener I notify activity to change fragment. And in EACH fragment I should do so. I don't think that it is best practice. So, is it ok or there is a better way?
Ok What you need is something like this in activity where you would initialized on your BottomNavigationView.
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_1://Handle menu click -
//Call Navigator helper to replace Fragment to Fragment A
break;
case R.id.menu_2:
//Call Navigator helper to replace Fragment to Fragment B
break;
case R.id.menu_3:
//Call Navigator helper to replace Fragment to Fragment C
break;
}
return true;
}
});
I am working on a app where I have only one Login activity and one main activity. I replace fragments down to three level ie like A->B->C or A->B->D and more scenarios like this.I keep them on back stack and on back press retrieve them.Like on backpress of C , B appears. I am having a scenario where I receive push notification then on click of it the app should redirect to C. Now to achieve it I have to go through the launch screen. It means that before clicking on notification if I am on D and I get notification for C then I will land on C , but on backpress I will not come to D but to B because A->B>C . So is there a structure for one activity and all fragments where even if I click on notification,I can get back the flow as it is before the notification arrived ..??? ie directly maintain C to D and then regular flow as it is.
Thank you.
It may depend slightly on what the contents of those fragments are but I think actually the Android docs would suggest the behaviour you have at the moment is actually the correct one. Essentially once you've navigated into the app from a notification you the navigation should be the same as if you'd entered that point in the app normally by navigating through the screens. It should have the previous navigation history cleared.
The docs elaborate on this:
https://developer.android.com/design/patterns/navigation.html#into-your-app
Firstly implements TabLayout.OnTabSelectedListener to your fragment hosted activity,override methods then on onTabSelected get tab position(Let's say tabPosition),from this position try following code in same activity(with required changes):
#Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
// TODO Auto-generated method stub
if ((keyCode == KeyEvent.KEYCODE_BACK))
{
if(tabPosition!=0)
{
pager.setCurrentItem(tabPosition- 1);
return true;
}
else
{
onBackPressed();
}
}
return super.onKeyDown(keyCode, event);
}
You can implement a custom backstack of you own.
So if you have a history that looks like this:
A->B->C->D
If an event then causes the history to add C once more, e.g. the notification you specify above, then what you specify above suggests you want the history to look like this afterward:
A->B->C->D->C
And what you get currently, which you don't want, is this:
A->B->C
To do this you can implement custom fragment history handling in your Activity. Where you would normally add your fragment transition to the backstack, instead add the fragment to an ArrayList or similar entity. I have thrown together some code below:
private ArrayList<Fragment> mFragmentHistory = new ArrayList<>();
private void loadFragment(Fragment fragment, boolean addToHistory) {
// your fragment transition code here without adding to backstack
// add the fragment to your custom history
if (addToHistory) {
mFragmentHistory.add(fragment);
}
}
You will then need to implement handling code in the onBackPressed method of the same Activity:
#Override
public void onBackPressed() {
// if the history is not at the "top", i.e. only one (or 0) fragments in history
if (mFragmentHistory.size() > 1) {
// remove the last (==current) fragment from history
mFragmentHistory.remove(mFragmentHistory.size() - 1);
// get new last fragment from history - this is now the one to load
Fragment loadFragment = mFragmentHistory.get(mFragmentHistory.size() - 1);
// load this fragment, don't add it to the history
loadFragment(loadFragment, false);
} else {
// perform default system back or implement your custom handler here
super.onBackPressed();
}
}
You will have to consider maintaining state over Activity restarts (screen rotation etc.).