I've done my research on memory leak when using Data Binding. Every posts said that one should assign null to binding object to resolve this issue. However, it just doesn't work.
I've created two identical Fragments with a single Activity. Each Fragment includes a button which can be used to navigate to one another.
To reproduce this issue, click the button repeatedly, LeakCanary will pop up the memory leak warning. (However, the issue does not occur when clicking the buttons "programmatically")
Repro
LeakCanary
Code
The following is the code of my Fragments. (Two fragments are identical)
FirstFragment.kt
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentFirstBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.btnFirst.setOnClickListener {
findNavController().navigate(R.id.action_to_second)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
fragment_first.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/first_fragment" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="To second" />
</FrameLayout>
</layout>
SecondFragment.kt
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentSecondBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.btnSecond.setOnClickListener {
findNavController().navigate(R.id.action_to_first)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
fragment_second.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#string/second_fragment" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="To first" />
</FrameLayout>
</layout>
I've also created a repo in case someone want to try it out.
Let me know if I can provide any further information.
Thanks!
To all whom may have run into this issue, the developer team of leakcanaray has found the source and marked the leak as known issue.
github.com/square/leakcanary/issues/2341
A related issue can be found on Google issue tracker.
Related
I'm trying to make a simple button that changes the fragment, it doesn't give any error, it just doesn't work.
XML code:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.follow.FollowFragment">
<ImageView
android:id="#+id/imageView4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="background"
android:scaleType="fitXY"
android:src="#drawable/background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="#+id/loginButton"
android:layout_width="250dp"
android:layout_height="60dp"
android:layout_marginTop="50dp"
android:backgroundTint="#color/btn_col"
android:text="Log In" />
</LinearLayout>
</FrameLayout>
Fragment code:
class FollowFragment : Fragment() {
companion object {
fun newInstance() = FollowFragment()
}
private lateinit var viewModel: FollowViewModel
private lateinit var loginButton: Button
private lateinit var viewFollow: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_follow, container, false)
viewFollow = root
loginButton = viewFollow.findViewById(R.id.loginButton)
var loginFragment = LoginFragment()
println("Hello :(")
loginButton.setOnClickListener {
println("Hello :)")
//parentFragmentManager.beginTransaction().replace(R.id.nav_host_fragment, loginFragment).commit()
}
return inflater.inflate(R.layout.fragment_follow, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
// TODO: Use the ViewModel
}
}
Only the sad hello is printed, it seems to recognize the button, but the listener don't work. The app has a bottom navigation bar, and the screens are fragments.
By returning inflater.inflate(R.layout.fragment_follow, ...) you're inflating a second layout to be used for your fragment instead of the one that you're using to set the listener.
You should be returning root.
I'm trying to use Data binding to run a function when an onClick event occurs, I'm hoping someone can tell me what I'm doing wrong here.
The log item in myClick doesn't run.
XML
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="myBinding"
type="com.example.deletebindingtest.MyFragment" />
</data>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MyFragment"
android:orientation="vertical">
<Button
android:id="#+id/my_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/button"
android:onClick="#{(view) -> myBinding.myClick()}"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
My Fragment
class MyFragment : Fragment() {
private lateinit var binding: FragmentMyBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_my,
container,
false
)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
fun myClick() {
Log.i("TEST", "Its working")
}
}
When I click on the extension in the XML it takes me to myClick function.
a Quick fix is on onCreateView add binding.myBinding=this
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_my,
container,
false
)
binding.lifecycleOwner = viewLifecycleOwner
binding.myBinding=this // here
return binding.root
}
also I prefer listener binding you can check it here
I went through couple of examples but either they have too many abstractions or out of date tutorials. I would like to implement this feature in the minimum possible way. I need help with the UserForm class here with User passed as arguments in a Navigational Architecture.
Module app build.gradle I've added this
android {
...
dataBinding {
enabled = true
}
}
User class
#Parcelize
data class User(var first: String, var last: String): Parcelable
FormFragment.kt
class UserForm: Fragment() {
private val user by lazy {
arguments?.getParcelable("user") ?: User("John", "Doe")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_form, container, false)
}
}
fragment_form.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="data.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/first"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:text="#{user.first}" />
<EditText
android:id="#+id/last"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:text="#{user.last}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Some snippets
modify your code below:
FormFragment.kt
class UserForm: Fragment() {
private val user by lazy {
arguments?.getParcelable("user") ?: User("John", "Doe")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentFormBinding.inflate(inflater, container, false).apply {
user = this#UserForm.user
}
return binding.root
}
}
Explanation:
Because you used DataBinding in fragment_form.xml, the corresponding FragmentFormBinding class will be automatically generated
We usually use XxxBinding.inflate instead of inflater.inflate (layoutid, container, false)
After FragmentFormBinding.inflate, we bind the user object to fragment_form.xml
I am using a fragment within an activity and using data binding to bind its layout for both the activity and fragment as shown below:
activity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#id/FAB_fromHomeActivity_BottomAppBarAttached"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_add_dark"
android:backgroundTint="#color/colorAccent"
app:layout_anchor="#id/BottomAppBar_fromHomeActivity_Main"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
activity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity)
}
fragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_goals, container, false)
return mBinding.root
}
Is there any way to access the FAB in the activity using the binding in the fragment ? (For example something like that mBinding.parent.FAB)
I can't find any information on this. Can anyone help ?
I have the following code for my layout and my fragment:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ScanFragment">
<SurfaceView
android:id="#+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</layout>
class ScanFragment : Fragment() {
private lateinit var binding: FragmentScanBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
FragmentScanBinding.inflate(inflater, container, false).also {
binding = it
}.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.cameraPreview
}
}
However, I can't seem to access the cameraPreview view directly from the binding. I have already added the pre-requisites for DataBinding below:
dataBinding {
enabled = true
}
and
android.databinding.enableV2=true
inside the gradle.properties
You should use DataBindingUtil
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_scan, container, false)
return binding.root