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 ?
Related
I have an app that uses NavigationComponents among with ViewPager 2.
I'd like to use ViewPager for switching between fragments. I've made my ActivityMain as FragmentContainerView, and I'd like to have ViewPager implemented in one of my fragments.
The problem is, the ViewPager doesn't work at all. It doesn't change fragments, don't know why. What should I change in the code?
ActivityMain
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
First fragment
class BlankFragment : Fragment() {
private var mPag: ViewPager2? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_blank, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mPag = view.findViewById(R.id.pager123)
val adapter = PagerAdapter(this)
val list = mutableListOf<Fragment>()
list.add(BlankFragment())
list.add(BlankFragment2())
mPag?.adapter = adapter
}
}
.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment"
android:orientation="vertical">
<TextView
android:id="#+id/test123"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="111111111" />
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager123"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#a1a1"/>
</LinearLayout>
Second fragment
class BlankFragment2 : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank2, container, false)
}
}
.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment2">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="222222222222222" />
</FrameLayout>
Nav graph
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="#+id/nav_graph"
app:startDestination="#id/blankFragment">
<fragment
android:id="#+id/blankFragment"
android:name="com.example.myapplication.BlankFragment"
android:label="fragment_blank"
tools:layout="#layout/fragment_blank" />
<fragment
android:id="#+id/blankFragment2"
android:name="com.example.myapplication.BlankFragment2"
android:label="fragment_blank2"
tools:layout="#layout/fragment_blank2" />
</navigation>
Pager adapter
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
val mFragments = mutableListOf<Fragment>()
override fun getItemCount(): Int {
return mFragments.size
}
override fun createFragment(position: Int): Fragment {
when (position){
0 -> return BlankFragment()
1 -> return BlankFragment2()
}
return mFragments[position]
}
}
What you are essentially missing is what I tried to explain in the previous question.
You need to use Navigation component and ViewPager2 as two separate entities.
There is also issue with your PagerAdapter. Fragment adapters are not similar to other Adapters you might have experience with(mFragments).
They shouldn't hold a reference to those fragments. There are two main Fragment adapters(more here).
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position){
0 -> BlankFragment1()
1 -> BlankFragment2()
else -> throw IllegalArgumentException("Out of fragments, will depend on getItemCount")
}
}
}
You need to have Fragments that are solely in the Navigation component(e.g. FirstFragment & SecondFragment) and Fragments that belong to the ViewPager2(e.g. BlankFragment1 & BlankFragment2)
<?xml version="1.0" encoding="utf-8"?>
<navigation android:id="#+id/nav_graph"
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"
app:startDestination="#id/FirstFragment">
<fragment
android:id="#+id/FirstFragment"
android:name="com.example.myapplication.FirstFragment"
android:label="#string/first_fragment_label"
tools:layout="#layout/fragment_first" >
<action
android:id="#+id/action_FirstFragment_to_SecondFragment"
app:destination="#id/SecondFragment" />
</fragment>
<fragment
android:id="#+id/SecondFragment"
android:name="com.example.myapplication.SecondFragment"
android:label="#string/second_fragment_label"
tools:layout="#layout/fragment_second" />
</navigation>
Fragments for ViewPager2:
class BlankFragment1: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_blank_1, container, false)
}
}
class BlankFragment2: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_blank_2, container, false)
}
}
The FirstFragment is going to host the ViewPager2 that will allow you to swipe between BlankFragment1 and BlankFragment2.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_first, container, false)
view.findViewById<ViewPager2>(R.id.vp2)?.let { viewPager2 ->
val pagerAdapter = PagerAdapter(this)
viewPager2.adapter = pagerAdapter
}
return view
}
Now the only thing you need to do in any of the fragments to use Navigation component is to simply findNavControler and navigate to destination you want.
class BlankFragment1: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_blank_1, container, false)
view.findViewById<MaterialButton>(R.id.blank_1_button).setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
return view
}
}
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.
Here is my code for Fragment class.
class FragmentOne : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_one, container, false)
val binding: FragmentOneBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_one, container, false)
return binding.root
}
fun onClicking(){
Toast.makeText(activity, "You clicked me.", Toast.LENGTH_SHORT).show()
}
}
And here is my code for Fragment 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"
tools:context=".FragmentOne">
<data>
<variable
name="clickable"
type="com.example.fragmentpractise1.FragmentOne" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hola Gola"
android:layout_marginTop="40dp"
android:onClick="#{()-> clickable.onClicking()}"/>
</LinearLayout>
</layout>
Now what I am trying to understand, why android:onClick is not showing any toast result. On pressing the button nothing happens. I can show toast by setting onClickListener on button id in Fragment class but unable to show toast via onClick attribute in XML using databinding.
You're calling clickable.onClicking() in xml which is not set yet. When you instantiate a data binding object, you probably have to set its variables as well (like clickable in your example)
Set that variable after instantiation like this
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_one, container, false)
val binding: FragmentOneBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_one, container, false)
binding.clickable = this // your fragment
return binding.root
}
Also using v instead of () inside onClick is a bit more rational because that's a lambda in Java syntax receiving one view argument. I suggest to change it to below for more readability
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hola Gola"
android:layout_marginTop="40dp"
android:onClick="#{ v -> clickable.onClicking()}"/>
While creating a very simple sample app, I couldn't wrap my head around why my app is closing when I press the hardware back button on my emulator.
I have 1 mainActivity and 2 fragments.
When I am on the NavigationFragment and press back, the app closes instead of going back to IntermediateFragment.
MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
toolbar.setTitle(R.string.app_name)
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.exampleapplication.MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
/>
<fragment
android:id="#+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/main_nav"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
navigation_graph:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/main_nav"
app:startDestination="#+id/intermediateFragment">
<fragment
android:id="#+id/intermediateFragment"
android:name="com.exampleapplication.IntermediateFragment">
<action
android:id="#+id/action_intermediate_to_navigation"
app:destination="#+id/navigationFragment"
/>
</fragment>
<fragment
android:id="#+id/navigationFragment"
android:name="com.exampleapplication.NavigationFragment"
/>
</navigation>
IntermediateFragment:
class IntermediateFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_intermediate, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_next_fragment.setOnClickListener {
findNavController().navigate(R.id.action_intermediate_to_navigation)
}
}
}
NavigationFragment:
class NavigationFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_navigation, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_first_library.setOnClickListener {
findNavController().setGraph(R.navigation.first_library_nav)
}
btn_download_pdf.setOnClickListener {
findNavController().setGraph(R.navigation.download_pdf_nav)
}
}
}
Any ideas?
You're missing one line on your <fragment>:
app:defaultNavHost="true"
As per the Navigation Getting Started guide:
The app:defaultNavHost="true" attribute ensures that your NavHostFragment intercepts the system Back button.
Since you don't set that, Navigtion does not intercept the back button and hence, you only get the default activity behavior (which is closing your activity).
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