Should ViewBinding properties in fragments always have a backing non-nullable property? - android

I've been reading https://developer.android.com/topic/libraries/view-binding and was thinking about how they define their view binding property in a fragment.
This is the sample code:
private var _binding: ResultProfileBinding? = 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 = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
So _binding is a nullable because the reference to the view will be destroyed in onDestroyView(). However, they use binding as a backing property, which retrieves _binding and makes it non-nullable (whatever the equivalent to force unwrapping is called in Kotlin).
The question is why should we have _binding and binding and which one should be used? It feels like if you are trying to make _binding nullable, then why make it essentially non-nullable with binding and risk accessing the view when it's destroyed?

_binding is nullable so that it can be set to null in onDestroyView, avoiding any memory leaks. but if you tried to access views using _binding then you will have to write _binding!!.someView every time you access any view, use of non-null assertion(!!) quickly becomes redundant and verbose.
So to remove this redundancy there is non-nullable binding. Now you can access your views as binding.someView, no need to use non-null assertion (!!). This can not cause any issue, since views should only be accessed between onCreateView and onDestroyView.

The reason behind this is to make code easier to read and maintain when you know for sure that you won't try to access the view before onCreateView and after onDestroyView. The non-null property avoids the unnecessary ? or !! null safety operators every time you're trying to reach the binding.
On the other hand if you're uncertain about your call sequences and may try to access the fragment view when the view is destroyed, you wouldn't need to take that extra step and put your binding instance in a backing property. The null safety is your dearest friend in this case.

Related

what is effect of defining fields twice in kotlin?

While exploring some repositories on GitHub I found some people define fields twice for example
private var _binding: FragmentBinding? = null
private val binding: FragmentBinding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBinding.inflate(layoutInflater)
return binding.root
}
why do some developers do this and why not just define the field once
private var binding: FragmentBinding? = null
In this particular context this is done to avoid any memory leaks. Fragments outlive their view,which means any reference to the view must be set to null when view is destroyed. so to take care of the memory leak the reference to the binding object is set to null in onDestroyView. in your case onDestroyView should look as
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
so this takes care of the memory leak issue, but now there is another problem, every time you use the _binding to access some view, you need to use (!!) non-null assertion as _binding!!.myView.setOnClickListener etc.
In order to avoid this redundant use of (!!), there is another property binding, which is not null. so now you can simply write binding.myView.setOnClickListener.
The comments and other answer talking about you missing the concept are talking about a different concept than what you are asking about in your example code.
The concept they're talking about is using a more restrictive publicly exposed version of a property. Like making the public version of a MutableList exposed only as a read-only List. This is a good practice for encapsulation, but it is not what your example code is doing, although both involve using a backing property with an _ prefix.
Your example is with two private properties with the same type. This is done for Android view binding in a Fragment because it needs to be possible to set the property to null when the Fragment is detached, so the bound views are not leaked. But it is inconvenient to have to keep dealing with a nullable property in the Fragment code, since most of your uses of the binding will be in functions that are only called while the Fragment is attached and the binding reference is not null.
This code allows a backing nullable _binding property that stores the actual reference to the binding and can be set to null when the fragment is detached. The binding property has no backing field, so it cannot cause the views to be leaked, but you can use it as a non-null property, which is safe so long as you only call it while the Fragment is attached. It is similar to requireContext() and requireActivity(), which are conveniently non-null, but are only safe to use while the Fragment is attached.
I think you misunderstood the concept here!
It's paradigm in general programming that keep one private member field with the _ prefix while exposing another variable with the same name as public member of the class without _ prefix.
I.e. let's say you have an interface or an abstract class called "Foo" and it's inheritance/child class as "Bar".
interface Foo {}
class Bar : Foo
While using it's functionality into some other class (let's say 'Baz') you found out that creating object of "Bar" is good enough for you on this particular "Baz" class.
class Baz {
var bar: Bar? = null
}
But, at the same time you also want your caller/consumer of "Baz" class not to have access to this var bar: Bar? = null in such a way that it's immutable since it's internal variable used on "Baz".
This is the moment, where this paradigm comes into picture where you expose your bar as read only variable while reassigning/modifying it internally n number of times as well as of the type of it's parent to limit it's scope. something like below:
class Baz {
private var _bar: Bar? = null
val bar: Foo get() = _bar // Here we expose private variable _bar as Foo type only on getter as well
}
Side note: (This happens in Kotlin, because it provides default getters & setters and providing getter only on public one restricts it's usage to read-only outside the class/object)
Source - Kotlin Docs
These are called backing properties in kotlin.
Advantages of using a backing properties.
The variable with prefix _ will be private var.
var - So it is mutable.
private - So it can only be mutated in the same class, but it is not exposed to other files.
The other variable will be public val.
public - So it is accessible from any file.
val - It can only be read. Writing to val is restricted.
If the data has to changes from other classes, we can use a public method to do so. The advantages of using a method rather than the default constructor is we can add any custom validation logic we want before updating the private variable.

Espresso launchFragmentInContainer<> binding issue

I have a simple test function like this:
#Test
fun testfunc() {
val fragScenario = launchFragmentInContainer<MyFragment>()
}
launchFragmentInContainer provides me with this error:
Binary XML file line #10: Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x7f040004 a=-1}
Caused by: java.lang.UnsupportedOperationException
which takes me to something wrong with the binding inflater.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = MyFragmentBinding.inflate(inflater, container, false)
return binding.root
}
I assume view binding doesn't work with isolated fragments like this? Do I need to create an entire new fragment without view binding just for testing?
In the fragment testing documentation there is a note:
Note: Your fragment might require a theme that the test activity doesn't use by default. You can provide your own theme as an additional argument to launch() and launchInContainer().
This error normally occurs when there are style values that the activity isn't aware of. Be sure to pass your app or fragment style via the themeResId parameter.

Android Dev in Kotlin, confused by tutorial

Im struggling with one of the android basics in kotlin code labs. Im a beginner who took up programming in lockdown for fun.
This is the tutorial in question
At the begining of the tutorial it says to get a nullable reference to binding named_binding, then use
private val binding get() = _binding!!
to get a version of it we can refer to without using the ? null safety thing. All good so far.
However in step four it shows the following code:
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentLetterListBinding.inflate(inflater, container, false)
val view = binding.rootreturn view}
Why do we refer to _binding to inflate the view then binding in the next line when assigning the view?
Why do we refer to _binding to inflate the view then binding in the next line when assigning the view?
Two concepts to understand here.
_binding is considered a backing property - that is, the actual variable reference that holds a value. In this case, the variable is a nullable type.
binding is a standard property - that is, a thing that provides access to an underlying backing field. In this case, it's using _binding as a backing property as a minor convenience to expose _binding as a non-null.
So - since binding just exposes _binding as a non-null value, _binding must be set first. So that's why it's assigned the value of the inflate call. Also note that _binding is a var which means it can be re-assigned while binding is a val which means it can't. So trying to use binding when inflating the view would not compile.
Finally, why they use binding to get the view is unclear. Probably just for the sake of the convenience / consistency of using binding as the single property to reference the class binding. Using _binding?.root would work fine too.
This is best practice to protect someone modify _binding like _binding = null and you got error when access to _binding,
You should use binding.root instead _binding.root

How I can prevent reloading data second time after configuration change using ViewModel?

Reloading data after every rotation
I fetch data in onCreate and observe in onCreateView().
I want to know after rotating the phone(or after configuration changes data is reloaded again as a result I have these logs before rotation
fetchConfig ->observe
and after rotating
I have
observe ->fetchConfig ->observe
How I can prevent reloading data second time?
I have added in fetchConfig()
if(customerConfigData.value==null) {}
but I am not sure is it the best solution
private val viewModel: HomeViewModel by lazyViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.fetchConfig()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel.customerConfigData.observe(viewLifecycleOwner, Observer {
Log.i("test","observe")
})
return inflater.inflate(R.layout.fragment_home,container,false)
}
fun fetchConfig() {
Log.i("test","fetchConfig")
uiScope.launch {
val configEndpoint = EnigmaRiverContext.getExposureBaseUrl().append("v1/customer").append(AppConstants.CUSTOMER_UNIT)
.append("businessunit").append(AppConstants.BUSINESS_UNIT).append("defaultConfig").append("?preview=true")
val parsedData = homeRepository.fetchConfig(configEndpoint, GetConfigCall())
customerConfigMutableData.postValue(parsedData)
}
}
One solution I think would be to move call to fetchConfig() in to the init block of your ViewModel
As you can see, your method has a parameter called savedInstanceState: Bundle?. This bundle is able to save the state of the app when the configuration changes. So, you can put here any flag you want.
Now, remember that ViewModels are designed to be implemented with a good code base. So, you need to separate the Ui layer from the business layer. The fetch configuration method should be in another class which doesn't depend on the Android lifecycle. I strongly recommend reading these articles.
https://medium.com/androiddevelopers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090
https://developer.android.com/jetpack/docs/guide
In conclusion. Your solution is not the best. The best approach is to implement a correct layer for fetching the info in a way that it doesn't depend on the Android lifecycle.
I too had similar issue. I was suggested to try Event wrapper for LiveData, it had solved my problem:)
Here's the link:
How to stop LiveData event being triggered more than Once
Hope this helps!

Kotlin Android extensions and retained fragment

I am using Kotlin Android extensions in my project and I came across some behavior that I am unable to understand. I use this code to retain my fragment in the activity:
val fragment = fragmentManager.findFragmentByTag("hello") ?: HelloFragment()
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, "hello")
.commit()
This is the retained Fragment:
import kotlinx.android.synthetic.hello.*
public class HelloFragment : Fragment() {
val text = "Hello world!"
override fun onCreate(savedInstanceState: Bundle?) {
super<Fragment>.onCreate(savedInstanceState)
setRetainInstance(true)
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.hello, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super<Fragment>.onViewCreated(view, savedInstanceState)
text_view.setText(text) // <- does not work when retained
}
}
and its XML layout hello.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center" />
Everything works as expected – the text_view.setText() displays Hello world! on screen on the first launch. But when you rotate the screen the text_view.setText() does not work. This is weird because text_view is not nullable and has to reffer to some view. If you remove setRetainInstance(true) and leave the fragment recreate every time this problem disappears. Any thoughts what might cause this problem?
UPD: The issue is fixed now. You don't have to call clearFindViewByIdCache() manually anymore.
View cache is not cleared after calling onDestroyView(). There is an open issue.
For now, you can explicitly invoke clearFindViewByIdCache() in onDestroyView() to clear the cache. This method is part of the synthetic package so you have to import it
import kotlinx.android.synthetic.*
I have found the answer myself. The Fragment class does not inflate the layout directly – it has property view: View? which holds it. This should be pretty obvious since it is created with onCreateView. In order to access the properties within the view you have to set the import
import kotlinx.android.synthetic.hello.view.*
and then access the properties as follows
view?.text_view?.setText(text)
Note that these properties are nullable.
Just to clarify. The issue is fixed now. You don't have to pass clearFindViewByIdCache() anylonger. Please see issue tracker: https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-8073

Categories

Resources