Shared view transitions with Navigation component not working - android

I've follow the docs to implement shared view transitions with the new Navigation component and it is not working. This is what I have:
Fragment A has this code to call fragment B
val extras = FragmentNavigatorExtras(
taskNameInput to "taskName")
findNavController().navigate(R.id.action_aFragment_to_BFragment,
null), // Bundle of args
null, // NavOptions
extras)
Taking a look to the layout, the id has the transition name set as follows:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/taskNameInput"
android:transitionName="taskName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
Then FragmentB has the following view in the layout:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/taskNameInput"
android:transitionName="taskName"
android:layout_width="0dp"
android:layout_height="wrap_content"
...>
When going from fragmentA to fragmentB, the enter animation is played but not the sharedView transition. Any clue? Thanks

It is missing to setup the sharedTransition to FragmentB, which can be done in onCrateView() as follows:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(R.transition.move)
return inflater.inflate(com.meanwhile.flatmates.R.layout.fragment_b, container, false)
}
Also you need to create the transaction file move.xml:
<transitionSet>
<changeBounds/>
<changeTransform/>
<changeClipBounds/>
<changeImageTransform/>
</transitionSet>
At the time of this post it is not written in the docs for the new Navigation Component, but this is just the old way of doing. Since the navigation component is doing some magic for the enter/exit transition, I was expecting to do also some more for shared view ones. In any case, it is not a big deal to add those lines.

Related

How to manually destroy or recreate a ViewModel

Working on an Android app where each user can have multiple accounts under the same login. There is a single activity with a fragment per tab - based on the Android sample Bottom Navigation.
On one of the tabs is a calendar, which is hooked up to a ViewModel like so:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val calendarViewModel = ViewModelProvider(this).get(CalendarViewModel::class.java)
calendarViewModel.calendarEvents.observe(viewLifecycleOwner) {
// Code that modifies the UI to show the events on a given day
}
val progressBar: ProgressBar = binding.progressBar
calendarViewModel.calendarState.observe(viewLifecycleOwner) {
// Progress bar / error state handling code
}
}
On one of the other fragments (different tab in the bottom tab bar), I allow the user to switch accounts. When I come back to the calendar fragment/tab, the viewModel is still in scope so I have incorrect data for this user.
How can I manually force the ViewModelProvider to recreate the viewModel in onCreateView? Is there some way I can invalidate the ViewModel scope and force a re-pull of the data?

Let DialogFragment in navigation not disappear on poping back stack

I have FragmentA, FragmentB and DialogFragment(BottomDialogFragment). I I abbreviated them as A,B and D
D will be shown after the button in A is clicked. It means A -> D
B will be shown after the button in D is clicked. It means D -> B
I config them in navigation.xml
<fragment
android:id="#+id/A"
android:name="com.example.A">
<action
android:id="#+id/A_D"
app:destination="#id/D" />
</fragment>
<dialog
android:id="#+id/D"
android:name="com.example.D">
<action
android:id="#+id/D_B"
app:destination="#id/B" />
</dialog>
<fragment
android:id="#+id/B"
android:name="com.example.B">
</fragment>
Now when I click the button in A, the fragment will jump to D.
Then I click the button in D, the fragment will jump to B.
But when I pop the navigation stack in B, it will back to A, and the D doesn't show.
What should I do? I want the D still exists on the surface of A.
What should I do? I want the D still exists on the surface of A.
So far, this is not possible, because dialogs are handled in a separate window than the activities/fragments; and therefore their back stack is handled differently And this is because Dialog implements the FloatingWindow interface.
Check this answer for more clarification.
But to answer your question, there are two approaches in mind:
Approach 1: Change fragment B to a DailogFragment, and in this case both B & D are dialogs and therefore when you popup the stack to back from B to D, you'll still see D showing.
Approach 2: To have a flag that is set if you return from B to D, and when if so, you re-show D.
Actually, approach 2 isn't that good, because it doesn't keep D in the back stack while you go from D to B; it's just a workaround; also the user would see the dialog transitioning/fade animation while it returns from B to D; so it's not natural at all. So, here only approach 1 will be discussed.
Approach 1 in detail:
Pros:
It's very natural and will keep the back stack the same as you would like to.
Cons:
DialogFragment B has a limited window than the normal fragment/activity.
B is no longer a normal fragment, but a DialogFragment, so you might encounter some other limitations.
To solve the limited window of B, you can use the below theme:
<style name="DialogTheme" parent="Theme.MyApp">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
</style>
Where Theme.MyApp is your app's theme.
And apply it to B using getTheme():
class FragmentB : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return layoutInflater.inflate(R.layout.fragment_b, container, false)
}
override fun getTheme(): Int = R.style.DialogTheme
}
Also you need to change B in the navigation graph to a dialog:
<dialog
android:id="#+id/B"
android:name="com.example.B">
</dialog>
Preview:
Refer to this link for a full working example.
You need to leverage the NavigationUI global action (as explained here) in order to be able to navigate "back" to a destination. Put this code into the main_graph xml:
<action android:id="#+id/action_global_fragmentD" app:destination="#id/fragmentD"/>
Next, in your activity add these to catch back press:
class MainActivity: AppCompatActivity {
...
var backPressedListener: OnBackPressedListener? = null
override fun onBackPressed() {
super.onBackPressed()
backPressedListener?.backHaveBeenPressed()
}
}
interface OnBackPressedListener {
fun backHaveBeenPressed()
}
this is like delegates in swift
Next in FragmentB, add these:
class FragmentB: Fragment(), OnBackPressedListener {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
(activity as MainActivity).backPressedListener = this
return inflater.inflate(R.layout.fragment_b, container, false)
}
override fun backHaveBeenPressed() {
// show Dialog
findNavController().navigate(R.id.action_global_fragmentD)
}
}
You can then navigate back to the DialogFragment as you require. This approach does not use the popBackStack because your use case is a custom behavior not handled by the NavigationUI framework (you need to implement it).
There is no need to pop the stack with controller.popBack() as the Stack is managed by the Navigation Library. Note that the stack operates on a LIFO basis so that is why the Fragment disappeared.
You need to add more navigation graph actions:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/navigation"
app:startDestination="#id/A">
<fragment
android:id="#+id/A"
android:name="com.example.A"
android:label="A" >
<action
android:id="#+id/action_A_to_D"
app:destination="#id/D" />
</fragment>
<dialog
android:id="#+id/D"
android:name="com.example.D"
android:label="D" >
<action
android:id="#+id/action_D_to_B"
app:destination="#id/B" />
<action
android:id="#+id/action_D_to_A"
app:destination="#id/A" />
</dialog>
<fragment
android:id="#+id/B"
android:name="com.example.B"
android:label="B" >
<action
android:id="#+id/action_B_to_D"
app:destination="#id/D" />
</fragment>
</navigation>
Then in your dialog Fragment add the following:
For OK/Yes:--
private fun doNav() {
NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentB)
}
For CANCEL/No:--
private fun doBackNav() {
NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentA)
}
Finally in the B Fragment, override the back press button and execute this:
Navigation.findNavController(requireView()).navigate(R.id.action_fragmentB_to_fragmentD)

"Views added to a FragmentContainerView must be associated with a Fragment" with android Nav Component

When nav component switches to a fragment, I get this "Views added to a FragmentContainerView must be associated with a Fragment" crash. What causes this?
I didn't see this mentioned anywhere and it took a while to figure out but in this case, I was trying to set up a old legacy fragment while migrating to the nav arch component.
The reason was in the frag's onCreateView, the inflate looked like:
layoutView = inflater.inflate( R.layout.home, container, false );
The last argument automatically attaches the view to the container. This works fine in old style fragments and activities. It does not work with the nav arch component because the root container is a FragmentContainerView which only allows fragments to be attached to it.
Setting the last argument to false makes it work properly.
Just replace your onViewCreated method.
class MyFragment : Fragment() {
override fun onCreateView( inflater: LayoutInflater,container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_post,container,false)
}
}
If you are working with lifeCycleScope, make sure you launch and run the block at least in Lifecycle.State.STARTED state.

Destroy instance of a Fragment/trigger onDestroy with Navigation library

I am using Navigation library and my use case is preserve Fragment state on back press which I achieve by returning already inflated binding in onViewCreated as when changing fragments Navigations seems not to destroy already existing instance of this fragment the actual view variable exists when you navigate there back or up.
But I also have a use case when I need to recreate this Fragment instance so I expect to have a way to call onDestroy() for that fragment. But I don't see any api for removing/obtaining existing in the backstack instances.
So my question is how to get an existing instance of a Fragment from nav back stack and destroy it or just remove it by calling nav controller api.
some code:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(requireActivity(), mViewModelFactory)
.get(MainViewModel::class.java)
parseNavigationExtra()
return if (::mBinding.isInitialized) {
mBinding.root
} else {
//create new binding
}
so when I call this action I still get the old binding root as the variable is still present.
<action
android:id="#+id/clearBackStack"
app:destination="#+id/mainFragment"
app:launchSingleTop="true"
app:popUpTo="#+id/mobile_navigation"
app:popUpToInclusive="true" />
List<Fragment> fragments = getActivity().getSupportFragmentManager().getFragments();
Fragment lastFragment = fragments.get(fragments.size() - 1);
getActivity().getSupportFragmentManager().beginTransaction().remove(emptyDialog);
changes in the navigation library since 2.1.0
NavBackStackEntry: You can now call NavController.getBackStackEntry(), passing in the ID of a destination or navigation graph on the back stack. The returned NavBackStackEntry provides a Navigation-driven LifecycleOwner, ViewModelStoreOwner (the same returned by NavController.getViewModelStoreOwner()), and SavedStateRegistryOwner, in addition to providing the arguments used to start that destination.
So the plan is to use the new api to see what is available for NavBackStackEntry.

Navigate between different graphs with Navigation components

I have two activities, one holds all the fragments for the Login process, and the other one holds all the fragments for the main app.
Let's say I want to navigate from Activity1 (that holds all navigation graph of Login) to Activity2 (That holds all the navigation graph for the main app)
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
fun goToMainActivity(){
startActivity(Intent(this,MainActivity::class.java))
finish()
}
}
Here I call the method goToMainActivity()
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_login,container,false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_go.setOnClickListener {
// call the method goToMainActivity() to kill all fragments contained by that Activity and move foward to MainActivity with another nav_graph
}
}
}
Since LoginActivity holds a nav_graph and is the navigation host for all the Login Fragments, now I want to kill all the fragments contained to LoginActivity and move towards a new Activity (MainActivity) that holds a different nav graph
Is this the good way to do it? Or I should navigate differently ?
You don't need to define a second activity, simply add a second navigation graph to your nav_graph.xml file. Something like:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:id="#+id/nav_graph"
app:startDestination="#id/loginFragment">
<fragment
android:id="#+id/loginFragment"
android:name="com.mycompany.loginFragment"
tools:layout="#layout/fragment_login"
android:label="Login" >
<action
android:id="#+id/action_loginFragment_to_new_graph"
app:destination="#id/new_graph" />
</fragment>
<include app:graph="#navigation/new_graph" />
</navigation>
Then, with your navController, navigate the action:
navController.navigate(R.id.action_loginFragment_to_new_graph)
You can migrate to a single Activity Navigation. In your Nav Graph add an Action to navigate between the last LoginFragemnt and MainFragment and select:
Pop Behaviour:
Pop To - Self
Inclusive - YES
This should automatically clear the stack for you and pressing back will close the App.
EDIT:
Or just manually add these two lines to your nav xml under the action that moves from the LoginFragment to the MainFragment:
app:popUpTo="#id/loginFragment"
app:popUpToInclusive="true"

Categories

Resources