I´m trying to clean up my code and I have a more basic question.
In my Activity, I'm just adding a Fragment ->
class OrderActivity : AppCompatActivity(), HasAndroidInjector {
private lateinit var binding: ActivityOrderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_order)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().add(R.id.container, OrderFragment())
.commitNow()
}
}
}
I´m using commitNow() only if the Activity is "new" ( asking for the bundle state ), as I read in Android´s Documentation, commitNow make the commit syncronously -- >
Commits this transaction synchronously. Any added fragments will be initialized and brought completely to the lifecycle state of their host and any removed fragments will be torn down accordingly before this call returns. Committing a transaction in this way allows fragments to be added as dedicated, encapsulated components that monitor the lifecycle state of their host while providing firmer ordering guarantees around when those fragments are fully initialized and ready. Fragments that manage views will have those views created and attached.
Now, I just want to know if it is safe to replace commitNow for ".commit()".
I understand that commit can throw an exception if the Activity State was already saved, but, if I wrote it like this .. >
class OrderActivity : AppCompatActivity(), HasAndroidInjector {
private lateinit var binding: ActivityOrderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_order)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().add(R.id.container, OrderFragment())
.commit()
}
}
}
Is it safe?
This may not cause an error, but you are relying on an unrelated happen stance to manage your Fragment. When you could check if it already exists if not add it.
How to check if the fragment exists
And whether commit is safe or not is up to how you are using the Fragment.
Is the Fragment strongly coupled with the containing Activity i.e. does OrderActivity talk directly to the OrderFragment?
If so then you could talk to the Fragment before it has been added which is generally a no-no if it has anything to do with a view.
If the Fragment is independent and manages itself then you should be fine.
They can still be used together but it needs to be 1 way Fragment -> Activity and NOT Activity -> Fragment as again commit() is async and the containing Activity could be talking to something that isn't "there".
Related
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).
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.
According to the section "Share data between fragments" at https://developer.android.com/topic/libraries/architecture/viewmodel we are told that creating a ViewModel in the activity scope and sharing that amongst the fragments is the way to go.
This is the Fragment which sets the value in the ViewModel
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
This is the detail fragment which uses the property set
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
This is the ViewModel
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a button click, how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
Our DetailFragment will not be seeing the value since we were setting it on the button click in the MasterFragment. To understand the question better, consider we have Fragment A, B, C, and D and they share a ViewModel which has a value Fragment A B and C together computed and placed it in ViewModel for Fragment D to access.
Now when the system kills and recreates our application Fragment D won't have that value available.
OnSaveInstance also won't be able to help out much without resorting to dirty code. For simple situations, yes , but like the one in which FragmentA B and C together are making a value, in that situation, OnSaveInstance would be problematic.
OnSaveInstance should have been inside the ViewModel but alas I don't think that's the case. Any ideas?
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
You can check it here
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a buttonClick , how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
You can't recover the value if the application is killed by the user or system or restarted.
To solve your purpose of accumulating data from Activity A, B and C and display it in Activity D even though the application is killed or restarted, you can choose any 1 method from the following:
1. SharedPreference
2. Local Database Room or SQLite
3. Store data in a file
I recommend you to use SharedPreference for small data and Room for Large and Complex data.
In a nutshell, ViewModel stores data temporary to survive orientation change(no need to write code of onSaveInstanceState and onRestoreInstanceState) and share data between Activities and Fragments. Data will be lost if the activity is destroyed or fragment is detached.
If you still want to get stored value after app reset or killed you need to save data to SharedPreferences or internal SqLite database and restore it after app start.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}
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
}
}
Issue
Summary: Multiple LiveData Observers are being triggered in a Fragment after navigating to a new Fragment, popping the new Fragment, and returning to the original Fragment.
Details: The architecture consists of MainActivity that hosts a HomeFragment as the start destination in the MainActivity's navigation graph. Within HomeFragment is a programmatically inflated PriceGraphFragment. The HomeFragment is using the navigation component to launch a new child Fragment ProfileFragment. On back press the ProfileFragment is popped and the app returns to the HomeFragment hosting the PriceGraphFragment. The PriceGraphFragment is where the Observer is being called multiple times.
I'm logging the hashcode of the HashMap being emitted by the Observer and it is showing 2 unique hashcodes when I go to the profile Fragment, pop the profile Fragment, and return to the price Fragment. This is opposed to the one hashcode seen from the HashMap when I rotate the screen without launching the profile Fragment.
Implementation
Navigation component to launch new ProfileFragment within HomeFragment.
view.setOnClickListener(Navigation.createNavigateOnClickListener(
R.id.action_homeFragment_to_profileFragment, null))
ViewModel creation in Fragment (PriceGraphFragment). The ViewModel has been logged and the data that has multiple Observers only has data initialized in the ViewModel once.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
}
Listen for data from ViewModel in original Fragment (PriceGraphFragment). This is being called multiple times, however it is only expected to have one Observer when the Fragment is loaded.
priceViewModel.graphLiveData.observe(
this, Observer { priceGraphDataMap: HashMap<Exchange, PriceGraphLiveData>? ->
// This is being called multiple times.
})
Attempted Solutions
Creating the Fragment's ViewModel in the onCreate() method.
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
Creating the ViewModel using the Fragment's activity and the child Fragment's parent Fragment.
priceViewModel = ViewModelProviders.of(activity!!).get(PriceDataViewModel::class.java)
priceViewModel = ViewModelProviders.of(parentFragment!!).get(PriceDataViewModel::class.java)
Moving methods that create the Observers to the Fragment's onCreate() and onActivityCreated() methods.
Using viewLifecycleOwner instead of this for the LifecycleOwner in the method observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer).
Storing the HashMap data that is showing as duplicates in the ViewModel opposed to the Fragment.
Launching the child Fragment using the ChildFragmentManager and the SupportFragmentManager (on the Activity level).
Similar Issues and Proposed Solutions
https://github.com/googlesamples/android-architecture-components/issues/47
https://medium.com/#BladeCoder/architecture-components-pitfalls-part-1-9300dd969808
https://plus.google.com/109072532559844610756/posts/Mn9SpcA5cHz
Next Steps
Perhaps the issue is related to creating the nested ChildFragment (PriceGraphFragment) in the ParentFragment's (HomeFragment) onViewCreated()?
ParentFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = viewModel.getCurrentUser()
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.replace(binding.priceDataContainer.id,
PriceGraphFragment.newInstance())
?.commit()
}
Test replacing the LiveData objects with RxJava observables.
This is basically a bug in the architecture. You can read more about it here. You can solve it by using getViewLifecycleOwner instead of this in the observer.
Like this:
mViewModel.methodToObserve().observe(getViewLifecycleOwner(), new Observer<Type>() {
#Override
public void onChanged(#Nullable Type variable) {
And put this code in onActivityCreated() as the use of getViewLifecycleOwner requires a view.
First off, thank you to everyone who posted here. It was a combination of your advice and pointers that helped me solve this bug over the past 5 days as there were multiple issues involved.
Issues Resolved
Creating nested Fragments properly in parent Fragment (HomeFragment).
Before:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.add(binding.priceDataContainer.id, PriceGraphFragment.newInstance())
?.commit()
fragmentManager
?.beginTransaction()
?.add(binding.contentFeedContainer.id, ContentFeedFragment.newInstance())
?.commit()
}
...
}
After:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null
&& childFragmentManager.findFragmentByTag(PRICEGRAPH_FRAGMENT_TAG) == null
&& childFragmentManager.findFragmentByTag(CONTENTFEED_FRAGMENT_TAG) == null) {
childFragmentManager.beginTransaction()
.replace(priceDataContainer.id, PriceGraphFragment.newInstance(),
PRICEGRAPH_FRAGMENT_TAG)
.commit()
childFragmentManager.beginTransaction()
.replace(contentFeedContainer.id, ContentFeedFragment.newInstance(),
CONTENTFEED_FRAGMENT_TAG)
.commit()
}
...
}
Creating ViewModels in onCreate() as opposed to onCreateView() for both the parent and child Fragments.
Initializing request for data (Firebase Firestore query) data of child Fragment (PriceFragment) in onCreate() rather than onViewCreated() but still doing so only when saveInstanceState is null.
Non Factors
A couple items were suggested but turned out to not have an impact in solving this bug.
Creating Observers in onActivityCreated(). I'm keeping mine in onViewCreated() of the child Fragment (PriceFragment).
Using viewLifecycleOwner in the Observer creation. I was using the child Fragment (PriceFragment)'s this before. Even though viewLifecycleOwner does not impact this bug it seems to be best practice overall so I'm keeping this new implementation.
It's better to initialize the view model and observe live data objects in onCreate.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyFragmentViewModel::class.java)
// 'viewLifecycleOwner' is not available here, so use 'this'
viewModel.myLiveData.observe(this) {
// Do something
}
}
However, no matter where you initialize the view model, whether in onCreate or onViewCreated, it will still give you the same view model object as it's created only once for the lifecycle of the Fragment.
The important part is observing the live data in onCreate. Because onCreate is called only on fragment creation, you're calling observe only once.
onViewCreated is called both when the fragment is created and when it is brought back from the back stack (after popping the fragment on top of it). If you observe live data in onViewCreated it will get the existing data that your live data is holding from the previous call immediately on returning from the back stack.
Instead, use onViewCreated only to fetch data from the view model. So whenever the fragment appears, either on the creation or returning from the back stack, it will always fetch the latest data.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchData()
...
}