im trying to make a base bottom sheet dialog fragment class that supports data binding. here is my class:
abstract class RoundedBottomSheetDialogFragment<VM : BaseViewModel, DB : ViewDataBinding> :
BottomSheetDialogFragment() {
abstract val viewModel: VM
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, true)
}
abstract fun getLayoutRes(): Int
abstract fun configEvents()
abstract fun bindObservables()
/**
*
* You need override this method.
* And you need to set viewModel to binding: binding.viewModel = viewModel
*
*/
abstract fun initBinding()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val parentLayout = inflater.inflate(R.layout.rounded_bottom_sheet, container, false)
init(inflater, parentLayout.container)
showDialogAsExpanded()
return parentLayout
}
private fun showDialogAsExpanded() {
dialog?.setOnShowListener {
val bottomSheetInternal =
(dialog as BottomSheetDialog).findViewById<View>(R.id.design_bottom_sheet) ?: return#setOnShowListener
val behavior = BottomSheetBehavior.from(bottomSheetInternal)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configEvents()
bindObservables()
}
}
if you see i'm inflating a layout inside this dialog fragment class and i want to use data binding inside that layout xml file.
this is an example of my xml file:
<layout>
<data>
<variable
name="vm"
type="com.mobtakerteam.walleto.ui.login.searchcountry.SearchCountryViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_list"
android:layout_width="match_parent"
android:layout_height="400dp"
app:data="#{vm.countries}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="20"
tools:listitem="#layout/fragment_search_country_row" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
but the problem is that it is not working in the xml layout and i have to manually observe the live data objects inside kotlin class like this:
viewModel.countries.observe(this, Observer {
adapter.submitList(it)
})
so what is the problem?
Using LiveData with binding you have to set lifecycle owner like this
binding.lifecycleOwner = this
Related
I am trying to call a function in my fragment via expression binding from my XML file in "android:onclick...", but it will not work. The error is that the fragment is not attached to a context.
It is the
MaterialAlertDialogBuilder(requireContext())
which gives me headache.
How do I give the context to the fragment?
I have seen similar questions regarding that topic, but none that helped me.
Any help is much appreciated.
ItemDetailFragment.kt:
class ItemDetailFragment : Fragment() {
private lateinit var item: Item
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
private val navigationArgs: ItemDetailFragmentArgs by navArgs()
private var _binding: FragmentItemDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentItemDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.fragment = ItemDetailFragment()
}
/**
* Displays an alert dialog to get the user's confirmation before deleting the item.
*/
fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(android.R.string.dialog_alert_title))
.setMessage(getString(R.string.delete_question))
.setCancelable(false)
.setNegativeButton(getString(R.string.no)) { _, _ -> }
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
/**
* Called when fragment is destroyed.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
fragment_item_detail.kt:
<?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.example.inventory.InventoryViewModel" />
<variable
name="fragment"
type="com.example.inventory.ItemDetailFragment" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="#dimen/margin"
tools:context=".ItemDetailFragment">
<Button
android:id="#+id/delete_item"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin"
android:onClick="#{()->fragment.showConfirmationDialog()}"
android:text="#string/delete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/sell_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
That is the error i am getting:
java.lang.IllegalStateException: Fragment ItemDetailFragment{e562873} (c6ab2144-3bdc-410b-91eb-e5668e8b617a) not attached to a context.
You should not pass your fragment instance as a data binding variable.
You could define a Boolean mutable live data variable in your InventoryViewModel and show the dialog when it changes:
private val _showConfirmation = MutableLiveData(false)
val showConfirmation
get() = _showConfirmation
fun onShowConfirmation() {
_showConfirmation.value = true
}
fun onConfirmationShown() {
_showConfirmation.value = false
}
Then, define an observer for this property in your ItemDetailFragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.executePendingBindings()
viewModel.showConfirmation.observe(viewLifecycleOwner) {
if (it) {
showConfirmationDialog()
viewModel.onConfirmationShown()
}
}
}
Finally, remove the fragment variable from the XML and change your Button's onClick as:
<Button
...
android:onClick="#{() -> viewModel.onShowConfirmation()}"
/>
I have been working through the Android Basics in Kotlin Course available on developer.android.com and have ran into a problem with Data Binding. The project I am working on doesn't have solution code provided, but I have been modeling my approach off of a previous similar Codelab.
I am attempting to initialize data binding variables declared in layout xml files in the fragments corresponding to each layout but when I attempt to initialize the fragment variable I receive an error: "Classifier 'EntreeMenuFragment' does not have a companion object, and thus must be initialized here". build.Gradle has both dataBinding and viewBinding set to true.
<layout
...
<data>
<variable
name="viewModel"
type="com.example.lunchtray.model.OrderViewModel" />
<variable
name="EntreeMenuFragment"
type="com.example.lunchtray.ui.order.EntreeMenuFragment" />
</data>
...
</layout>
class EntreeMenuFragment : Fragment() {
private var _binding: FragmentEntreeMenuBinding? = null
private val binding get() = _binding!!
private val sharedViewModel: OrderViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentEntreeMenuBinding.inflate(inflater, container, false)
val root: View = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
EntreeMenuFragment = this#EntreeMenuFragment // ERROR
}
}
....
It was sufficient to remove capitalization on the binding variable in order to avoid the error:
<variable
name="entreeMenuFragment"
type="com.example.lunchtray.ui.order.EntreeMenuFragment" />
entreeMenuFragment = this#EntreeMenuFragment // fixed
I am stuck here, help.
I've got the following code:
ProfileFragment:
#AndroidEntryPoint
class ProfileFragment : Fragment() {
private val profileViewModel: ProfileViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentProfileBinding.inflate(inflater, container, false)
binding.viewModel = profileViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
}
ProfileViewModel:
class ProfileViewModel #ViewModelInject constructor(
#ApplicationContext private val context: Context,
private val profileRepository: ProfileRepository
) : ViewModel() {
fun getUser() {
....
}
}
fragment_profile.xml:
<data>
<variable
name="viewModel"
type="my.app.viewmodel.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="#{()->viewModel.getUser()}" />
</LinearLayout>
The problem is that the onClick is never triggered no matter what I do and how I try.
However, as soon as I do it like this in ProfileFragment it works just fine:
binding.myButton.setOnClickListener {
profileViewModel.getUser()
}
Any ideas? Cause I am stuck here
I am not sure if this issue still exist or not
but if you are using data binding and hilt for dependency injection please add below lines in your fragment onViewCreated
binding.lifecycleOwner = this
binding.viewModel = profileViewModel
What fixed this problem for me was adding the fragment reference to itself in onCreateView:
binding.<fragment_name> = this
Like so:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.v(TAG, "onCreateView")
binding = FragmentSettingsBinding.inflate(inflater, container, false)
binding.settingsFragment = this <---
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
I've created one dialog fragemnt with view model (mvvm). Dialog consist of one button (custom view). when using view model with data binding, button click is not working when livedata change.I'm using boolean value to check if button is clicked or not. What is causing issue? Also suggest any other approach if needed.
profile_dialog_fragment.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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.test.ui.ProfileDialogViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ProfileDialog">
<com.google.android.material.button.MaterialButton
android:id="#+id/login"
style="#style/TextAppearance.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:onClick="#{() -> viewmodel.onLoginButtonClick()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ProfileDialog.kt
class ProfileDialog : DialogFragment() {
companion object {
fun newInstance() = ProfileDialog()
}
private val viewModel: ProfileDialogViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = ProfileDialogFragmentBinding.inflate(inflater, container, false)
.apply {
this.lifecycleOwner = this#ProfileDialog
this.viewmodel = viewmodel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.startLogin.observe(viewLifecycleOwner, Observer {
Log.d("insta", "This is working")
if (it == null) return#Observer
if(it) {
Log.d("insta", "This is not working")
val loginIntent = Intent(this.context, LoginActivity::class.java)
this.context?.startActivity(loginIntent)
}
})
}
}
ProfileDialogViewModel.kt
class ProfileDialogViewModel : ViewModel() {
private val _startLogin = MutableLiveData<Boolean>(false)
val startLogin: LiveData<Boolean>
get() = _startLogin
fun onLoginButtonClick() {
Log.d("insta", "This ain't working")
_startLogin.postValue(true)
}
}
Your viewmodel is defined in
private val viewModel: ProfileDialogViewModel by viewModel()
So, pay attention to viewModel. The problem located in
this.viewmodel = viewmodel
where this points to ProfileDialogFragmentBinding. Here you assinging ProfileDialogFragmentBinding.viewmodel = ProfileDialogFragmentBinding.viewmodel - that's why it's not working.
To solve problem, properly assign it like that:
this.viewmodel = viewModel
In my app, i have a fragment that call a remote service for get user profile information and show it, and I've used DataBinding for show data.
This is my layout:
<?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="viewModel"
type="com.myapp.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.profile.firstName+ ' '+ viewModel.profile.lastName}" />
<!-- Other textviews -->
</LinearLayout>
</layout>
this is ProfileViewModel class
class ProfileViewModel : ViewModel() {
#Inject
lateinit var profileRepository: ProfileRepository
private var _profile = MutableLiveData<Profile>()
val profile: LiveData<Profile>
get() = _profile
fun getProfile(token: String) {
profileRepository.profile(
token,
{
// success
_profile.value = it.value
},
{
//error
}
)
}
}
data class Profile(
firstName : String,
lastName : String,
// other fields
)
And this is fragment where profile should be showed:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}
Now it happen that first time i open fragment, repository call service and get data correctly, but "null" is showed inside textviews. If i close fragment and i reopen it, edittext are populate correctly. What's wrong with my code?
Set binding.lifecycleOwner in your fragment class in order for updates in LiveData objects to be reflected in corresponding views. Your fragment class should look like this:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
//add lifecycleOwner
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}