I am getting a random crash "lateinit property binding has not been initialized". Most of the time it's working fine but a few time randomly we are getting this crash on crashlytics.
Please let me know what's wrong here
I have a BaseActivity with following code
abstract class BaseActivity<D : ViewDataBinding> : AppComptActivity() {
abstract val layoutId: Int
lateinit val binding: D
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
binding = DataBindingUtil.setContentView(this, layoutId)
....
}
}
I have a HomeActivity which override BaseActivity with following code
class HomeActivity : BaseActivity<ActivityHomeBinding>() {
override val layoutId: Int get() = R.layout.activity_home
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
....
}
}
I am using bottomNavigation menu and one of the fragment is HomeFragment
class HomeFragment : BaseFragment<FragmenntHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState:Bundle)
(activity as HomeActivity).binding.appBarHome.visible(false)
//HERE I AM GETTING lateinit property binding has not been initialized crash
}
}
I don't want to use isInitialized property of lateinit as this will not solve my issue
As mentioned in the comment, I'd suggest instead of calling parent container (Activity) objects directly, register a listener to a navigation change like this in HomeActivity:
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if(destination.id = R.id.homeFragment) {
// TODO hide/show your view here
}
}
In that case, you are sure that the view gets hidden/shown when it should be without relying on the HomeFragment being only in HomeActivity as this can change in the future and your app will start crashing
If you have an orientation change or other config change, or the OS process is killed while in the background and the user returns to the app, Android will recreate the Activity and the Fragments.
Unfortunately, it creates the Fragments first, before creating the Activity. So you cannot rely on the existence of the Activity until the Fragment has been attached to the Activity. You should move code that relies on the existence of the Activity to
onActivityCreated().
Note: I also agree with the comment about not doing it this way. Your Fragment should not make assumptions like this (that it is hosted by HomeActivity), but instead should make some callback to the hosting Activity and let the hosting Activity set the visibility of the app bar (or whatever else it wants to do).
Related
I have implemented a TabLayout (which uses fragments) in my bottom sheet toolbar that has buttons which should affect the Main Activity. How do I pass the button clicks from the fragments in my TabLayout to the Main Activity?
I'm stuck and I don't know where to start.
There are multiple methods to do communication between fragments and its activity . I'll explain the ones which are used widely.
Using an interface.
Using a SharedViewModel for all your fragments and its activity . ( this can be used if you are implementing MVVM architecture )
check this out link
EDIT :
This is a simple step by step implementation on how to pass data from a fragment to activity . I am just using dummy class names and method parameters .
Create a folder called 'listeners' inside your app module , this is where you should have all your interface classes. ( This is just for a clean approach , if that is not your priority then you can save the interface class anywhere ). for Eg I am making TabLayoutFragmentClickListner.
interface TabLayoutFragmentClickListener {
}
Add a method to this interface . This is the method which would be called when we click a button inside the fragment. add the required parameters which needs to be passed from fragment to the activity. In this case I am just using a String.
interface TabLayoutFragmentClickListener {
fun onLayoutFragmentClick(value : String)
}
Implement this interface in the activity in which you want the data to be received.
class MainActivity : AppCompatActivity() , TabLayoutFragmentClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
This would make you override the interface method inside that activity.
class MainActivity : AppCompatActivity() , TabLayoutFragmentClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onLayoutFragmentClick(value: String) {
Log.d("testing" , value)
}
}
This overiden method is the definition for your interface method in this activity. Hence when you access the interface method from your fragment, the overidden method inside the activity would be called. Try understanding how interface works in java or kotlin.
Now initialise the instance of the listener in your fragment's onAttach method. like this
class TestingFragment : Fragment() {
lateinit var listener: TabLayoutFragmentClickListener
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as TabLayoutFragmentClickListener
}
}
now call the interface method from your fragment with the required parameter. This would hence trigger the interface method definition in your activity hence passing data from the fragment to the activity.
class TestingFragment : Fragment() {
lateinit var listener: TabLayoutFragmentClickListener
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as TabLayoutFragmentClickListener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
test_btn.setOnClickListener {
listener.onLayoutFragmentClick("testing string")
}
}
Hope this helps.
I think You need to create a function in MainActivity And call that function in the fragment. You can easily access that function bcoz it is your parent activity so you can use it on the button.
References:
I know this is in java but I think it's helpful for you link
I have an activity with three fragments. It will navigate between fragments using navigation controller. But everytime i move to other fragment, the previous fragment destroyed.
When I back (using back key or app bar back button), it will called onCreateView again.
The problem is, I have a method called fetchProducts() that should run once when view created on fragment. Because the fragment alwasy get destroyed, so my fetchProducts always get called again and I dont wanna do that.
Im using viewBinding btw.
Here some of my code:
#AndroidEntryPoint
class HomeMainFragment : Fragment(R.layout.fragment_main_home) {
private var _binding: FragmentMainHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeMainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentMainHomeBinding.bind(view)
setupRecyclerView()
observe()
goToCreateProductPage()
fetchProducts()
}
//...
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
How to keep the fragment so it will not destroyed? Especially using viewBinding
As this issues:
Support multiple back stacks for Bottom tab navigation
You can use navigation library version 2.4.0-alpha04 and fragment version 1.4.0-alpha04 for back stacks support.
And yes you should consider using ViewModel to get data that you fetch.
I have an app that has a main activity and fragments depend on it, so this is normal.
Now, two of my 10 fragments need to communicate, which I use the example given here
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ? Because now I dont need anymore that viewmodel instance, but as per documentation says, this instance will be retained from the Activity that contains these fragments
This is not what I'm looking for to communicate between fragments since now a new instance of that viewmodel will be the same as the past one I have created, I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ?
Correct. While it so happens that only two of your fragments use it, that ViewModel is scoped to the activity.
I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Then perhaps you should not be using activityViewModels(). For example, you could isolate these two fragments into a nested navigation graph and set up a viewmodel scoped to that graph.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
The ViewModel system does not know about what is or is not "depending on it". It is all based on the ViewModelStore and the ViewModelStoreOwner that supplies it. activityViewModels() uses the activity as the ViewModelStoreOwner, so viewmodels in that ViewModelStore are tied to the activity.
The Objective: I'm trying to make a notepad application. What my app does is, a button is pressed to create a new note. This pops up a fragment in which the user types his note. Within the same fragment, I have another button that signifies when the user is done typing.
Question 1: Is there a way by which pressing the other button in the Fragment could trigger a method in my Activity?
Question 2: Would this cause the app to become too bloated? Should I keep the button within my activity itself?
Thank you for your help.
Question 1: Is there a way by which pressing the other button in the Fragment could trigger a method in my Activity?
Sure, the simplest way to do it is:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = MyFragmentBinding.bind(view) // viewBinding enabled
binding.myButton.setOnClickListener {
(requireActivity() as MyActivity).doSomething() // <--
}
}
However, if this Fragment can be used in different Activity instances, then it should expose a Listener with which it exposes its potential events, and doesn't need to know the actual Activity instance it is talking to.
interface ActionHandler {
fun onMyButtonClicked()
}
lateinit var actionHandler: ActionHandler
override fun onAttach(context: Context) {
super.onAttach(context)
actionHandler = context as ActionHandler
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = MyFragmentBinding.bind(view) // viewBinding enabled
binding.myButton.setOnClickListener {
actionHandler.onMyButtonClicked()
}
}
This way, your Fragment will always have a listener to talk to even after config changes / process death, which seems to not be the case for most other answers here.
Question 2: Would this cause the app to become too bloated? Should I keep the button within my activity itself?
This depends on whether the button actually belongs in the Activity, though it probably doesn't. Most modern apps are written as single-Activity anyway, and unless the view is shared among all screens, it's put inside a Fragment, possibly maybe even using <include tags from a common layout resource.
There is an easy way of doing this as your fragments have access to activity (Kotlin) | getActivity() (Java) and by casting it you can use it.
But this is not the proper way of doing this because it affects the modularity of fragments.
The proper way of doing this:
Your activity wants to listen to Fragments events without any overhead:
In Fragment
class MyFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is MyFragment.Listener) {
listener = context
} else {
throw ClassCastException(context.toString() + " You need to implement MyFragment.Listener")
}
}
interface Listener {
fun onSomethingHappened()
}
private var listener: MyFragment.Listener? = null
fun aMethodInsideFragmentThatHandlesButtonEvents() {
listener?.onSomethingHappened()
}
}
And in your activity:
class MyActivity : AppCompatActivity(), MyFragment.Listener {
override void onSomethingHappened() {
// do your work here
}
...
}
For triggering a method on click of a button in fragment, there are number of ways to achieve this. Try this.
If (getActivity() instanceof MainActivity){
//Getting instance of your activity
MainActivity instance = ((MainActivity)getActivity());
//Using the instance calling the method in your activity
instance.methodName();
}
Use the above code in your fragment on button click.
Another way is using Interface, calling its abstract methods in fragment and overriding it MainActivity; on button click those methods will be called.
Or you can also try using RxEventBus. You can publish it in the fragment and listen in the MainActivity.
Hope this resolves your issue.
Just make your activity implement View.OnClickListener and on your fragment set your activity as onClickListener of your button.
your fragment:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
myButton.setOnclickListener((MyActivity)getActivity));
}
I'm trying to pass a bundle of object instances down from my main activity to the first fragment in a chain of other fragments using the NavHostFragment. I've tried all sorts but the bundle always seems to be null once it reaches the first fragment.
Here's how I'm initiating the NavHostFragment (frameContainer is a Frame Container element in my layout xml)
NavHostFragment navHost = NavHostFragment.create(R.navigation.claim_nav_graph);
getSupportFragmentManager().beginTransaction()
.replace(R.id.frameContainer, navHost)
.setPrimaryNavigationFragment(navHost)
.commit();
The documentation says there are 2 different .create functions, one of them you can pass a second arguments to as a bundle, but Android Studio doesn't allow me to use this version.
Does anyone have any ideas?
Thanks in advance!
It does seem to be a flaw with the NavHostFragment, passing data down to the first fragment does not seem to be possible, as the Bundle you can set as a second argument on the create function is overwritten along the way.
In the end I resolved this by building the bundle in the first fragment of the activity instead. I was able to access the activities intent properties using the below.
// Kotlin
activity.intent?.extras?.getBundle(KEY_BUNDLE_ID)
// Java
getActivity().getIntent().getBundleExtra(KEY_BUNDLE_ID)
This was enough of a workaround for me in this situation, but it would be great if it was possible
If you're using viewModels, you can do this:
your viewmodel:
class NiceViewModel: ViewModel() {
var dataYouNeedToPass = "initialValue"
}
your activity:
class MainActivity : AppCompatActivity() {
val niceViewModel: NiceViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
niceViewModel.dataYouNeedToPass = "data You Need To Pass"
}
}
your fragment:
class YourFragment : Fragment() {
private lateinit var niceViewModel: NiceViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
niceViewModel = (activity as MainActivity).niceViewModel
niceViewModel.dataYouNeedToPass //do whatever you need to do with this
}
}