Trouble with NavController inside OnBackPressedCallback - android

I am getting a does not have a NavController set error inside a OnBackPressedCallback. Here is the code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.patient_info_fragment, container, false)
if(Utils.connectedToInternet()){
fragmentView.pastScreeningsButton.visibility = View.GONE
}
requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Navigation.findNavController(fragmentView).navigate(R.id.action_patientInfoFragment_to_patientsList)
}
})
setHasOptionsMenu(true)
return fragmentView
}
I am only getting this error when I come back into the fragment where this is implemented.
I have android:name="androidx.navigation.fragment.NavHostFragment" in the home fragment.
To clarify, I am using Navigation controller to do all my Navigation around my app and it works just fine. I only get this error inside this OnBackPressedCallback and only when the user navigates back into the fragment where this is implemented.
Let me know if you need to see any more of my code.

You might run into an issue with leaks of old instances of your fragment. Also it's not a good practice to store the created view within another variable like fragmentView. All in all your onCreateView implementation is doing too many things unrelated to its purpose.
I'd suggest to split up the functionality into relevant life-cycle methods and use the fragment.view directly within your callback. To not run into an issue with unattached views, you then bind and unbind the callback with the life-cycle.
class PatientInfoFragment: Fragment() {
private val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Navigation.findNavController(view).navigate(R.id.action_patientInfoFragment_to_patientsList)
}
}
override fun onCreate(
savedInstanceState: Bundle?
) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.patient_info_fragment, container, false)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
if(Utils.connectedToInternet()){
view.pastScreeningsButton.visibility = View.GONE
}
}
override fun onStart() {
super.onStart()
requireActivity().onBackPressedDispatcher.addCallback(callback)
}
override fun onStop() {
callback.remove()
super.onStop()
}
}
The callback life-cycle binding can be bound with the fragment as lifecycle owner calling the other addCallback(LifecycleOwner, OnBackPressedCallback).
Additionally you could have a look into Android KTX and Kotlin Android Extensions to simplify your implementation.

Move your code in the onViewCreated and pass the view that is provided by the overriden method:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Navigation.findNavController(view).navigate(R.id.action_patientInfoFragment_to_patientsList)
}
})
}
If that also doesn't work, replace the .navigate() with popBastack(R.id.framgment_where_you_want to go, false), in case your previous fragment stays on the backStack

Related

Setting setOnClickListener using bindings android kotlin

I have been doing a tutorial that is a bit out of date and uses synthetics rather than bindings. I am trying to use bindins. I am trying to set up a listener in a fragment (AddEditFragment.kt). It's using a callback to MainActivity.onSaveClicked.
In AddEditFragment I use an import for the binding
import com.funkytwig.tasktimer.databinding.FragmentAddEditBinding
I have a lateinit on the first line of the class defenition
class AddEditFragment : Fragment() {
private lateinit var binding: FragmentAddEditBinding
I am initializing the bunding in onActivityCreated and setting up the listner. I can use findViewById to get the ID
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val addEditSave = view?.findViewById(R.id.addEditSave) as Button
addEditSave.setOnClickListener { listener?.onSaveClicked() }
}
And this works fine but if I try to use the binding
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.addEditSave.setOnClickListener { listener?.onSaveClicked() }
}
The code does not show any errrors but it does not seem to create the listner. I have a Log.d in the onSaveClicked callback function and when I use the first (findViewById) version of the function it works (it calles onSaveClicked) but with the second version (using bindings) onSaveClicked does not get called when I click the Button.
I Cant figre out why the second version does not work, I thought the two versions of onActivityCreated should do the same thing.
The interface in AddEditFragment.kt is
interface OnSaveClicked {
fun onSaveClicked()
}
In fragment you should add your view in onCreateView or in OnViewCreated not in onActivityCreated
Please refer link for more details.
private var _binding: FragmentAddEditBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAddEditBinding.inflate(inflater, container, false)
val view = binding.root
binding.addEditSave.setOnClickListener { listener?.onSaveClicked() }
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
OK, thanks for all the help. turned out I was doing the inflate wrong.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG, "onCreateView")
binding = FragmentAddEditBinding.inflate(layoutInflater, container, false)
return binding.root
}
I was doing
binding = FragmentAddEditBinding.inflate(layoutInflater)
I missed out on the last 2 args as I was taking the code from the inflate when I am in an Activity, not a Fragment. I think it is to do with the layout effecticly being in the parent.

How to run code just one time in Fragment on Android

In my application I have some fragments and for show this fragments I want use NavigationComponent.
I have one problem. When click on BottomNavigationItems and change fragments, run again fragment code!
I want just run codes just for first time!
My codes (for one of fragments) :
#AndroidEntryPoint
class HomeNewFragment : Fragment(), HomeContracts.View {
//Binding
private lateinit var binding: FragmentHomeNewBinding
#Inject
lateinit var presenter: HomePresenter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHomeNewBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//InitViews
binding.apply {
//Call apis
presenter.callApi()
}
}
}
For example when click on items, every time call this code : presenter.callApi()
Or when go to DetailFragment and when click on back, again call presenter.callApi()
How can I fix it?
You should replace your presenter.callApi() with fun onFragmentCreated triggered in your Presenter(ViewModel).
All you need is a boolean var to check its first time or not cause your Presenter(ViewModel) attatched to the activity, not the fragment so it can store your state.
Presenter {
fun onFragmentCreated() {
if(firstTime) callApi()

BottomSheetDialog dims background being used in ViewPager2

I have a fragment that I want to display as an embedded fragment in a ViewPager and as a Bottom Sheet. I followed this https://developer.android.com/reference/android/app/DialogFragment#DialogOrEmbed and created a DialogFragment
private val mViewModel: CardPricesViewModel by viewModels()
private val binding by viewBinding(FragmentCardDetailPricesBinding::inflate)
companion object {
// This is the same value as the navArg name so that the SavedStateHandle can acess from either
const val ARG_SKU_IDS = "skuIds"
fun newInstance(skus: List<Long>?) =
CardDetailPricesFragment().apply {
arguments = Bundle().apply {
putLongArray(ARG_SKU_IDS, skus?.toLongArray())
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
However, when it gets inflated in a ViewPager the background dims as though it is a BottomSheetDialogFragment
However, when I manually do it with
supportFragmentManager
.beginTransaction()
.replace(binding.cardPricesFragmentContainer.id, cardDetailPricesFragment)
.commit()
It works fine. I see that the FragmentStateAdapter uses FragmentViewHolders instead of the using transactions directly (?), so I am not sure how to resolve this issue. I see that onCreateDialog() is being called, so if I call dismiss() after onViewCreated(), it works properly, but I am not sure if this a workaround
After some digging, I found the DialogFragment.setShowsDialog(boolean) method that you can use to disable the dialog being created.

How to implement databinding with Reflection in BaseFragment()

I'm trying to implement a BaseFragment in which I will pass the layout resource on it and it should outputs the binding to work in the fragment itself instead of need to do it everytime the fragment is extended.
For example I have this BaseFragment
open class BaseFragment(#LayoutRes contentLayoutId : Int = 0) : Fragment(contentLayoutId) {
private lateinit var onInteractionListener: OnFragmentInteractionListener
val toolbar : Toolbar?
get() {
return if(activity is BaseActivity)
(activity as BaseActivity).toolbar
else
null
}
override fun onAttach(context: Context) {
super.onAttach(context)
setOnInteractionListener(context)
}
...
In which I use like this
class A(): BaseFragment(R.layout.myFragment) { ... }
Now, if I use this I will need to do the definition of the binding class again in my onCreateView
class A(): BaseFragment(R.layout.myFragment) {
private lateinit var binding: MyFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.myFragment, container, false)
return binding.root
}
override fun onDestroy(){
binding = null
}
}
What I want to implement is that since I'm passwing the layout to my BaseFragment, I want my BaseFragment to handle the creation of the binding and just return me the binding in the fragment which I use to extend BaseFragment
What I want to have is something like this
class A(): BaseFragment(R.layout.myFragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.myTextView = ""
}
}
So my question is, how I can implement inside BaseFragment the onDestroy() and the onCreateView to always create a binding for me from the layout I'm passing in ?
I heard that I should use reflection but I'm not that sure on how to accomplish it
I didn't hear about the possibility to get the databinding just from a layout, but even if it's possible, I don't think that is the recommended way, because of two reasons:
Reflection is slow
It makes things more complicated than they are.
Instead of making magic with Reflection, you could do something like this:
abstract class BaseFragment<out VB: ViewDataBinding>(
private val layout: Int,
// Other Dependencies if wanted
) : Fragment() {
abstract val viewModel: ViewModel
// other variables that all fragments need
// This does not cause any memory leak, because you are not storing the binding property.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = DataBindingUtil.inflate<VB>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
}.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Do some basic work here that all fragments need
// like a progressbar that all fragments share, or a button, toolbar etc.
}
And then, when you still need the bindingProperty, I would suggest the following library (it handles all the onDestoryView stuff etc):
implementation 'com.kirich1409.viewbindingpropertydelegate:viewbindingpropertydelegate:1.2.2'
You can then use this like:
class YourFragment(yourLayout: Int) : BaseFragment<YourBindingClass>() {
private val yourBinding: YourBindingClass by viewBinding()
override val viewModel: YourViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// do binding stuff
}
}
Let me know if this worked for you.
Cheers

Android Navigation Component saveState and restoreState

Guys I need your help.
I use android navigation component and want to save backstack after user press button and restore it after. I found 2 methods
navController.saveState(): Bundle and navController.restoreState(bundle: Bundle).
But i have problem in use it. Seems like saveState work greate (i see bundle, and backstack inside), but i dont understand how to use restoreState, because the documentation says:
Restores all navigation controller state from a bundle. This should be called before any call to setGraph.
https://developer.android.com/reference/kotlin/androidx/navigation/NavController#restorestate
Okay, i did it, seems like backstack restored, but on screen i see first fragment (instead of the one I had when I saved it). What i do wrong?
Code:
FirstFragment
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_forward.setOnClickListener { findNavController().navigate(R.id.action_firstFragment_to_secondFragment) }
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
SecondFragment
class SecondFragment : Fragment() {
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_forward.setOnClickListener { findNavController().navigate(R.id.action_secondFragment_to_thirdFragment) }
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
ThirdFragment
class ThirdFragment : Fragment() {
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_third, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_finish.setOnClickListener {
(requireActivity() as MainActivity).saveState() //here save bundle
requireActivity().finishAfterTransition()
}
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
MainActivity
class MainActivity : AppCompatActivity() {
private val TAG = "MySuperActivity"
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "onCreate($savedInstanceState) called")
initNavController()
}
private fun initNavController() {
val navHostFragment = nav_host_fragment as NavHostFragment
val graphInflater = navHostFragment.navController.navInflater
val graph = graphInflater.inflate(R.navigation.main_graph)
navController = navHostFragment.navController
navHostFragment.childFragmentManager
if (App.instance.savedBundle != null) {
Log.d(TAG, "bundle: ${App.instance.savedBundle}")
navController.restoreState(App.instance.savedBundle)
graph.startDestination = R.id.thirdFragment
}
navController.graph = graph
Log.d(TAG, "navController.currentDestination: ${navController.currentDestination}")
Log.d(TAG, "navController.graph.startDestination: ${navController.graph.startDestination}")
}
fun saveState(){
App.instance.savedBundle = navController.saveState()
Log.d(TAG, "saveState() : ${App.instance.savedBundle}")
}
}
here some logs: logs
full code:github
I am not sure if my answer helps you, but I had many issues trying to save the navigation state from handling rotations. The issue that I had comes from an old version of the navigation component, I update to the most recent, and it fixes the issue:
def android_navigation = '2.3.4'
implementation "android.arch.navigation:navigation-fragment-ktx:$android_navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$android_navigation"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$android_navigation"

Categories

Resources