I am trying to migrate my project to view binding and I get an exception when I start my app.
My main activity contains a NavHostFragment like so:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
...
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
...
</LinearLayout>
And the first fragment loaded by default in the NavHostFragment is implemented like so:
class ToolListFragment : Fragment(R.layout.fragment_tool_list) {
...
private var _binding: FragmentToolListBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentToolListBinding.inflate(layoutInflater, container, false)
return binding.root
}
...
}
And here is the relevant part of the fragment's layout:
<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" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ToolListFragment"
android:orientation="vertical"
android:background="#android:color/white">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed"
app:tabGravity="fill">
<com.google.android.material.tabs.TabItem
android:id="#+id/tab_around_me"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="#string/tool_filter_around_me"/>
<com.google.android.material.tabs.TabItem
android:id="#+id/tab_assigned_to_me"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="#string/tool_filter_assigned_to_me"/>
<com.google.android.material.tabs.TabItem
android:id="#+id/tab_all_tools"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="All"/>
</com.google.android.material.tabs.TabLayout>
...
</LinearLayout>
As you can see, there is a TabItem with id tab_all. And yet here is the exception that crashes my app:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.herontrack.dev, PID: 12477
java.lang.NullPointerException: Missing required view with ID: com.herontrack.dev:id/tab_all_tools
at com.herontrack.databinding.FragmentToolListBinding.bind(FragmentToolListBinding.java:133)
at com.herontrack.databinding.FragmentToolListBinding.inflate(FragmentToolListBinding.java:78)
at com.herontrack.ToolListFragment.onCreateView(ToolListFragment.kt:107)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2950)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:515)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1636)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3112)
at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:3049)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2975)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:543)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1636)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3112)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:3056)
at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:251)
at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:473)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
at com.herontrack.MainActivity.onStart(MainActivity.kt:100)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1435)
at android.app.Activity.performStart(Activity.java:8024)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3475)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
So it crashes on this line in ToolListFragment.onCreateView()
_binding = FragmentToolListBinding.inflate(layoutInflater, container, false)
But I really don't understand why.
The problem that you are facing is that TabItem is just a dummy view. Looking in the source code you can see that
/**
* TabItem is a special 'view' which allows you to declare tab items for a {#link TabLayout} within
* a layout. This view is not actually added to TabLayout, it is just a dummy which allows setting
* of a tab items's text, icon and custom layout. See TabLayout for more information on how to use
* it.
*
* #attr ref com.google.android.material.R.styleable#TabItem_android_icon
* #attr ref com.google.android.material.R.styleable#TabItem_android_text
* #attr ref com.google.android.material.R.styleable#TabItem_android_layout
* #see TabLayout
*/
//TODO(b/76413401): make class final after the widget migration
public class TabItem extends View {
So the ViewBinding fails to find the Tab with the ID that you have specified as this is not added to the view hierarchy.
To fix this you have to remove the id from the TabItem. If you need to access Tab you can use the following code.
// to access first tab
binding.tabs.getTabAt(0)
I found out if your use in fragment:
getLayoutInflater().inflate(R.layout.example_view,
viewgroup, false);
In side of the onCreateView(LayoutInflater, ViewGroup,
Int) method, you can do this and then store var view on it: like:
View variable = getLayoutInflater().inflate(R.layout.example_view,
viewgroup, false);
TabItem item = variable.findViewById(R.id.tabLayoutItem);
/*
Other code
*/
It will work flawlessly. However I hugely agree with #Susan Thapa's answer and currently I am using his solution. Provided this answer just incase's somebody else can't follow that answer, because of xyz reason.
Related
In my fragment, I am displaying a list of items using a Custom View that contains a SwipeRefreshLayout.
When I swipe to refresh, I can see two refreshing indicators and one of them never disappears.
See it here
I obviously have only one SwipeRefreshLayout in my hierarchy and this appeared when I migrated my code to View Binding.
Here are the details of my code and of the behaviour.
I have a fragment that displays a list of items, I'm using a Custom View com.myapp.ItemsListView I built for this purpose.
<?xml version="1.0" encoding="utf-8"?>
<com.myapp.ItemsListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/myItems"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
In order to access this custom view, I am using View Binding. In my fragment, that's what it looks like:
// ItemsFragment.kt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentItemsBinding.inflate(inflater, container, false)
return binding?.root
}
and I then access anything in this custom view via binding?.myItems.
And my custom view uses a SwipeRefreshLayout as its root and contains a RecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/swipeToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/items"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<TextView
android:id="#+id/missingContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text="#string/missing_content_message"
app:drawableTopCompat="#drawable/ic_missing_content_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Following this question I'm using this piece of code to inflate the custom view:
// ItemsListView.kt
class ItemsListView(context: Context, attrs: AttributeSet) : SwipeRefreshLayout(context, attrs) {
private var b: ItemsListBinding? = null
init {
b = ItemsListBinding.inflate(LayoutInflater.from(context), this, true)
}
}
From the code, I customized my SwipeRefreshLayout to look different than the default:
b?.swipeToRefresh?.setSize(LARGE)
b?.swipeToRefresh?.setColorSchemeResources(
R.color.colorPrimary,
R.color.colorPrimaryDark
)
And I access b?.swipeToRefresh to set a listener, display or hide the progress indicator.
fun setListener(listener: OnRefreshListener) {
b?.swipeToRefresh?.setOnRefreshListener(listener)
}
fun showProgress() {
b?.swipeToRefresh?.isRefreshing = true
}
fun hideProgress() {
b?.swipeToRefresh?.isRefreshing = false
}
However, as you can see on the video clip, there is a black progress indicator that sticks and never goes away.
I have tried to access the SwipeRefreshLayout via b?.root and it results in the same behaviour.
In the end, I just do not understand why there are two SwipeRefreshLayout displayed on my screen. I noticed this problem when I introduced View Binding (I previously used Kotlin Synthetics). I'd like to inflate only one SwipeRefreshLayout in the hierarchy.
let's start from the main activity. my main activity has a fragment container with height set to wrap content and is enclosed inside a scroll view. The scroll view fills the screen.
[this is why I can't set it to match parent. because other fragments wont fit then. Also, I can't set the first fragment as a separate activity because this is a part of a training project and most of it was already built with tests in place.]
The first fragment I have has a single button in it and that fragment's height is set to match parent [which is the container with wrap content].
the button inside the fragment is aligned to the centre of the screen for its fragment xml. but, since wrap is enabled for the container, the button is now at the top in the main activity.
now, setting the height for the fragment as some fixed value might solve the problem when the height of the device is fixed. but, in other cases, the button's location might not be shown as wanted.
You will find a screenshot below which will give you a better understanding of my problem.
the top is the fragment layout while the bottom is the main activity layout. all I want is the button to be at the centre of the screen regardless of the screen size.
main acivity -
<?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"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- TODO: Add a FragmentContainerView with navigation/mobile_navigation as the navHost -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/mobile_navigation" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
fragment activity -
<?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:id="#+id/layout_start_order"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.order.StartOrderFragment">
<Button
android:id="#+id/start_order_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/base_margin"
android:layout_marginTop="#dimen/base_margin"
android:layout_marginEnd="#dimen/base_margin"
android:text="#string/start_order"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
I also tried turning the fragment layout into a binding layout and setting the height manually. but, it's giving a val cannot be assigned error. [the same binding variable allows me to change the button text.] the screenshot below is of the same attempt.
the same kotlin code as above -
package com.example.lunchtray.ui.order
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.example.lunchtray.R
import com.example.lunchtray.databinding.FragmentStartOrderBinding
class StartOrderFragment : Fragment() {
// Binding object instance corresponding to the fragment_start_order.xml layout
// This property is non-null between the onCreateView() and onDestroyView() lifecycle callbacks,
// when the view hierarchy is attached to the fragment.
private var _binding: FragmentStartOrderBinding? = 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 = FragmentStartOrderBinding.inflate(inflater, container, false)
val root: View = binding.root
// Navigate to entree menu
binding.startOrderBtn.setOnClickListener {
// TODO: navigate to the EntreeMenuFragment
findNavController().navigate(R.id.action_startOrderFragment_to_entreeMenuFragment)
}
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.layoutStartOrder.height = 1 //error
binding.startOrderBtn.text = "wwmfkkf" //so, binding variable isn't the problem
//todo - remove button text assignment
}
/**
* This fragment lifecycle method is called when the view hierarchy associated with the fragment
* is being removed. As a result, clear out the binding object.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Any form of help is appreciated. Thank You.
I'm making app with custom dialog fragment. I created a separate xml file test_parameters.xml for this dialog fragment, but when I try to refer to views there by their id, Android Studio doesn't see them. Only views from activity_main are available. What's the reason and how can I fix it?
MainActivity.kt:
package com.example.meltflowratecalculator
import...
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
lateinit var methodsSpinner: Spinner
lateinit var timeSpinner: Spinner
lateinit var result: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.settingsButton.setOnClickListener {
var dialog = CustomDialogFragment()
dialog.show(supportFragmentManager, "customDialog")
}
}
}
CustomDialogFragment.kt:
package com.example.meltflowratecalculator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
class CustomDialogFragment : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var rootView: View = inflater.inflate(R.layout.test_parameters, container, false)
return rootView
}
}
activity_main.xml:
<?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">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
test_parameters.xml:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/testing_method_title"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="#+id/methods"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:entries="#array/methods"
android:minHeight="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/result" />
</androidx.constraintlayout.widget.ConstraintLayout>
You're getting that binding object by calling ActivityMainBinding#inflate - ActivityMainBinding is automatically generated from activity_main.xml (hence the name). It only contains bindings for the views in that layout file (and anything it includes but that's more advanced) - right now you only have one view with an id, settings_button. That's why settingsButton is the only property you can see in the binding file.
You can use findViewById to search the view hierarchy for Fragments' views - but that's generally a bad idea, an Activity shouldn't know anything about the Fragment's internal workings or layout. The Fragment should handle all that itself, and either pass data to its host activity directly, or put the data somewhere the Activity can access it (like a ViewModel, or in a Navigation backstack entry)
Basically put all your functionality inside the DialogFragment - you can use TestParametersBinding to inflate the dialog's layout in the same way you did in your activity, and you can access all its views in there, and handle whatever it is the dialog does. The activity should just show it, and do something with the result it sends back
You will have to create a class extending DialogFragment. There you need to set the id.
In my android application I created map fragment with MapsFragment class
class MapsFragment : Fragment() {
val callback = OnMapReadyCallback { googleMap ->
val sydney = LatLng(-34.0, 151.0)
googleMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
private val viewModel: MapViewModel by lazy {
ViewModelProvider(this).get(MapViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMapsBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
mapFragment?.getMapAsync(callback)
}
}
and fragment_maps.xml layout file
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.mobile.viewmodels.MapViewModel" />
</data>
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
</layout>
I use
buildFeatures {
dataBinding true
}
option for creating binding. Then when I try bind my MapViewModel in line
val binding = FragmentMapsBinding.inflate(inflater)
I get an error
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.mobile, PID: 331
java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:1201)
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:719)
at com.mobile.databinding.FragmentMapsBindingImpl.<init>(FragmentMapsBindingImpl.java:25)
at com.mobile.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:58)
at androidx.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:130)
at androidx.databinding.ViewDataBinding.inflateInternal(ViewDataBinding.java:1368)
at com.mobile.databinding.FragmentMapsBinding.inflate(FragmentMapsBinding.java:68)
at com.mobile.databinding.FragmentMapsBinding.inflate(FragmentMapsBinding.java:54)
at com.mobile.ui.MapsFragment.onCreateView(MapsFragment.kt:45)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2600)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:881)
at androidx.fragment.app.FragmentManagerImpl.addAddedFragments(FragmentManagerImpl.java:2100)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1874)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1830)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
I/Process: Sending signal. PID: 331 SIG: 9
How to bind viewModel class properly? Is it possible?
Sorry, I might be late to answer this question but as they say, "Better Late than never."
So for the past two days, I have been trying to do the same thing, i.e., using DataBinding and Maps Fragment together.
I tried using both the methods, i.e., DataBindingUtil and as you did FragmentMapsBinding.
So, there were many errors I can across while trying to implement Data Binding, for starters, there was no DataBinding class generated, which is usually not a problem, except in this case. So, the workaround was to force the compiler to generate DataBinding class by introducing a variable, in your case viewModel in the XML file.
Now the data binding class is finally generated but we get this funny error, saying
Index Out of Bound
Now looking at the last error line which was generated
java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:1201)
and inspecting the code further inside the file ViewDataBinding.java, we see this.
if (isRoot && tag != null && tag.startsWith("layout")) {
final int underscoreIndex = tag.lastIndexOf('_');
if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
final int index = parseTagInt(tag, underscoreIndex + 1);
//ERROR IS GENERATED HERE
if (bindings[index] == null) {
bindings[index] = view;
}
indexInIncludes = includes == null ? -1 : index;
isBound = true;
} else {
indexInIncludes = -1;
}
}
Not what I think the problem was is that the bindings file which we had told the complier to forcefully generate doesn't really have any Views in it and the bindings index.
Data Binding Class -
No. of Views = 0
Index = 0
Now trying to access it gives an error.
BUT WAIT A MINUTE, one might say there is a view present, i.e., fragment. What about this?
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
After going through a few of the android docs, I think Fragment is not a View.
So, the workaround I would suggest is using either of the two methods.
replace fragment with androidx.fragment.app.FragmentContainerView -> This will remove the error and as the documentation says
FragmentContainerView is a customized Layout designed specifically for Fragments. It extends FrameLayout, so it can reliably handle Fragment Transactions, and it also has additional features to coordinate with fragment behavior.
have another view in your XML file, an example would be.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
</androidx.constraintlayout.widget.ConstraintLayout>
Or you can use both, that's up to you.
Hope this answers your question. Have a great day.
UPDATE :
As per Android Developer Docs:
Fragment
kotlin.Any
↳ androidx.fragment.app.Fragment
Fragment Container View
kotlin.Any
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.FrameLayout
↳ androidx.fragment.app.FragmentContainerView
It is evident that fragment does not have View as a SuperClass. Thus, this error is thrown.
The above 2 methods are the best ways to avoid this error since in DataBinding it requires at least 1 View.
Either change fragment tag with androidx.fragment.app.FragmentContainerView
or wrap around fragment with another View or ViewGroup like LinearLayout, FrameLayout, etc.
In my application I'm trying to create 2 fragments with ViewPagers. The fragments have 3 tabs/pages each with RecyclerViews and they are supposed to be backed by a database, so I'm keeping them in a List.
class MainActivity : AppCompatActivity() {
val fragments = listOf(SwipeFragment(), SwipeFragment())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
button1.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
}
button2.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[1]).commit()
}
}
}
The problem is that after navigating away from the SwipeFragment and going back, the view seems empty. Then, if you navigate to e.g. the leftmost page, the rightmost one "appears" (when you go to it).
This results in the middle page staying empty. (the reason for it being that FragmentStatePagerAdapter keeps only the current page and 2 adjacent ones by default. The middle one doesn't get refreshed in a layout with 3 tabs - I tried it also with 5 tabs and I'm able to bring all of them back by going back and forth).
After some debugging I saw that the fragments that represent pages don't get removed from the FragmentManager, but the main SwipeFragment is. I can't get my head around how FragmentManager really works, even after reading almost all of the source code.
If I were able to somehow safely remove the fragments from the FragmentManager, it may work.
I've tried some techniques for saving state, but the framework does that automatically (which might actually be the cause of this problem).
I'm just a beginner in Android, so there's probably a better way to do this anyway. I'll leave the rest of the files here for reference.
activity_main.xml
<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"
android:orientation="vertical"
tools:context="com.example.test.MainActivity">
<FrameLayout
android:id="#+id/placeholder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Button
android:id="#+id/button1"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button1" />
<Button
android:id="#+id/button2"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button2" />
</LinearLayout>
</LinearLayout>
SwipeFragment.kt
class SwipeFragment : Fragment() {
// TODO: set up and keep the database here
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_swipe, container, false)
view.tab_layout.setupWithViewPager(view.pager)
view.pager.adapter = DummyPagerAdapter(activity.supportFragmentManager, index)
return view
}
}
fragment_swipe.xml
<android.support.v4.view.ViewPager
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/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.SwipeFragment">
<android.support.design.widget.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.view.ViewPager>
ItemListFragment.kt
class ItemListFragment() : Fragment() {
// Or keep the database here?
// Probably not the best idea though
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_item_list, container, false)
if (view is RecyclerView) {
view.layoutManager = LinearLayoutManager(view.context)
view.adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
}
return view
}
}
DummyPagerAdapter.kt
class DummyPagerAdapter(manager: FragmentManager, val parentIndex: Int) :
FragmentStatePagerAdapter(manager) {
override fun getItem(position: Int): Fragment = ItemListFragment()
override fun getPageTitle(position: Int): CharSequence = "${ position + 1 }"
override fun getCount(): Int = 3
}
And a basic implementation of RecyclerView.Adapter generated by Android Studio
MyItemRecyclerViewAdapter.kt
class MyItemRecyclerViewAdapter(val values: List<DummyItem>) :
RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
...
}
fragment_item_list.xml
<android.support.v7.widget.RecyclerView 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/list"
android:name="com.example.test.ItemListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:context="com.example.test.ItemListFragment"
tools:listitem="#layout/fragment_item" />
You should use childFragmentManager instead of activity.supportFragmentManager inside SwipeFragment.
view.pager.adapter = DummyPagerAdapter(childFragmentManager, index)
The difference between getSupportFragmentManager() and getChildFragmentManager() is discussed here.
Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.
In my case childFragmentManager didn't help. I think we can use a pattern "Observer" to update existing fragments of ViewPager with new data when return from another fragment.