How to bind viewmodel to SupportMapFragment - android

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.

Related

can't find how to align an element in a fragment to center of the screen without changing the container's height to match parent [xml question]

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.

Android - Activity and Fragment initialization

I have an application composed of one activity and several fragments, as recommanded by Google. Other details here. I would like to keep a menu still and to switch my fragments in the container in the center.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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.appbar.AppBarLayout
android:someProperties="propertiesValues">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:someProperties="propertiesValues" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/navigation_map"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:someProperties="propertiesValues" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("Activity creation")
val binding = ActivityMainBinding.inflate(layoutInflater)
println("Activity creation part 2")
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
User.initSharedPref(this)
}
Fragment
private lateinit var mylist: MutableList<String>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("Fragment creation")
mylist = User.loadScenarioList()
}
User
object User
{
private lateinit var sharedPref : SharedPreferences
fun initSharedPref(context: Context){
sharedPref = context.getSharedPreferences("JeuDePisteKtPreferenceFileKey",Context.MODE_PRIVATE)
}
fun loadList(): MutableList<String>> {
val json = sharedPref.getString(KEY_LIST, "") ?: ""
if (json == "") return mutableListOf()
return Json.decodeFromString(json)
}
}
Problem encountered
When i start the activity, it initialize a variable sharedPref as shown in code.
But when in fragment onCreate i use this variable (mylist = User.loadScenarioList()), the binding line in activity fail with Binary XML file line #31: Error inflating class androidx.fragment.app.FragmentContainerView as shown in logcat below
Logcat & error
Here is the full logcat, we can see the the second sout is missing, but with no error thrown at this point.
The problem here came from the order of creation call
We can see it in the corrected code logcat
The activity onCreate is called first, but the first inflate call the fragment OnCreate, before resuming the activity OnCreate.
So every variable used in the fragment onCreate should be initialized before inflate call

How can I keep data when fragment is replaced?

I use the navigation component to do various screen transitions.
Pass the title data from A fragment to B fragment at the same time as the screen is switched. (using safe args)
In fragment B, set the data received from A.
And to keep the title data even when the screen is switched, I set it in LiveData in the ViewModel.
But if you go back from fragment B to fragment C,
B's title is missing.
Some say that because this is a replace() method, a new fragment is created every time the screen is switched.
How can I keep the data even when I switch screens in the Navigation Component?
Note: All screen transitions used findNavController.navigate()!
fragment A
startBtn?.setOnClickListener { v ->
title = BodyPartCustomView.getTitle()
action = BodyPartDialogFragmentDirections.actionBodyPartDialogToWrite(title)
findNavController()?.navigate(action)
}
fragment B
class WriteRoutineFragment : Fragment() {
private var _binding: FragmentWritingRoutineBinding? = null
private val binding get() = _binding!!
private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
private val args : WriteRoutineFragmentArgs by navArgs() // When the screen changes, it is changed to the default value set in <argument> of nav_graph
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.setValue(args) // set Data to LiveData
viewModel.title.observe(viewLifecycleOwner) { titleData ->
// UI UPDATE
binding.title.text = titleData
}
}
UPDATED Navigation Graph.xml
<?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/calendar">
<!-- fragment A -->
<dialog
android:id="#+id/bodyPartDialog"
android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
android:label="BodyPartDialogFragment"
tools:layout="#layout/fragment_body_part_dialog">
<action
android:id="#+id/action_bodyPartDialog_to_write"
app:destination="#id/write"/>
</dialog>
<!-- fragment B -->
<fragment
android:id="#+id/write"
android:name="com.example.writeweight.fragment.WriteRoutineFragment"
android:label="WritingRoutineFragment"
tools:layout="#layout/fragment_writing_routine">
<action
android:id="#+id/action_write_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="title"
app:argType="string"
android:defaultValue="Unknown Title" />
</fragment>
<!-- fragment C -->
<fragment
android:id="#+id/workoutListTabFragment"
android:name="com.example.writeweight.fragment.WorkoutListTabFragment"
android:label="fragment_workout_list_tab"
tools:layout="#layout/fragment_workout_list_tab" >
<action
android:id="#+id/action_workoutListTabFragment_to_write"
app:destination="#id/write"
app:popUpTo="#id/write"
app:popUpToInclusive="true"/>
</fragment>
</navigation>
UPDATED ViewModel(
This is the view model for the B fragment.)
class WriteRoutineViewModel : ViewModel() {
private var _title: MutableLiveData<String> = MutableLiveData()
val title: LiveData<String> = _title
fun setValue(_data: WritingRoutineFragmentArgs) {
_title.value = _data.title
}
}
Error
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.writeweight, PID: 25505
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:52)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
at com.example.writeweight.fragment.WriteRoutineFragment.getArgs(Unknown Source:4)
at com.example.writeweight.fragment.WriteRoutineFragment.onViewCreated(WriteRoutineFragment.kt:58)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8512)
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: java.lang.IllegalArgumentException: Required argument "title" is missing and does not have an android:defaultValue
at com.example.writeweight.fragment.WriteRoutineFragmentArgs$Companion.fromBundle(WriteRoutineFragmentArgs.kt:26)
at com.example.writeweight.fragment.WriteRoutineFragmentArgs.fromBundle(Unknown Source:2)
A single ViewModel can also be used for multiple fragments. Fragments are obviously showing inside an activity. The ViewModel in the activity can be passed to each fragment, that has the reference from the title. It is the solution if you want to solve using ViewModel.
Otherwise you can try the savedInstance method for solving this issue. Here is a thread about it.
Following from my comment:
I would make the title argument nullable and pass null as the default value. Then in viewModel.setValue(), ignore it if it's null instead of passing it to the LiveData.
The ViewModel's setValue() function should look like:
fun setValue(_data: WritingRoutineFragmentArgs) {
_data.title?.let { _title.value = it }
}
so the value is only passed along to the LiveData if it is not the default (null).
Your xml for the value should mark the default as #null and needs to have nullable="true". Your stack trace looks like there was a problem with how you specified the default or making it nullable.
<argument
android:name="title"
app:argType="string"
app:nullable="true"
android:defaultValue="#null" />
IMO, for proper separation of concerns, the ViewModel should not have any awareness of navigation arguments. The setValue function should take a String parameter, and you should decide in the fragment whether to update the ViewModel. Like this:
// In ViewModel
fun setNewTitle(title: String) {
_title.value = title
}
// in Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
args.title?.let { viewModel.setNewTitle(it) } // set Data to LiveData
viewModel.title.observe(viewLifecycleOwner) { titleData ->
// UI UPDATE
binding.title.text = titleData
}
}

Missing required view with ID with view binding and navigation component

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.

Android : Kotlin : MVVM : Why viewModel.onButtonClicked() causes the app crash?

Logcat Message :
java.lang.IllegalStateException: Could not find method #={() -> viewModel.onBtnClicked()(View) in a parent or ancestor Context for android:onClick attribute defined on view class androidx.appcompat.widget.AppCompatButton with id 'button'
File1 : activity_main.xml
<data>
<variable
name="viewModel"
type="com.wingsquare.databindingdemo.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick = "#={() -> viewModel.onBtnClicked()"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
File 2 : MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
// val binding = ActivityMainBinding.inflate(layoutInflater)
val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = mainViewModel
binding.lifecycleOwner = this
}
}
File 3 : MainViewModel.kt
class MainViewModel : ViewModel() {
fun onBtnClicked() {
Log.d("MainViewModel", "MainViewModel")
}
}
The Logcat Message is some how misleading.
I was facing the same error for couple of hours checking everything else I thought it be the root cause. But for this particular error, all you have to do is to keep an eye on "{ }". This is one of the drawbacks of data binding in android. lots of the times you don't get any error on compile times, And if you do! that's not really helpful !
and another thing to consider is that '=' in
android:onClick = "#={() -> viewModel.onBtnClicked()}"
is used for a two way binding.
you don't need it in this case.
you can read about it here on android official docs
android:onClick="#{() -> viewModel.onBtnClicked()}"
Add a parenthesis at the end.
also you may want to remove the equal sign after the #

Categories

Resources