Update:
I found out that there is a problem with the async task, while setting the LiveData
I'm using MVVM in my App and I try to bind Livedata to a TextView with Jetpack Databinding.
I get the correct values in the Log.d() of my Viewmodel, however the data is not shown on Screen.
Could someone guess what am I missing?
My XML File
(fragment_lobbyleader.xml)
<data >
<variable
name="viewmodel"
type="com.example.beerrallye.lobby.newLobbyData.LobbyViewModel" />
</data>
<TextView
android:id="#+id/lobbyleader_lobbyCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="#{viewmodel.lobbyID}"
android:textColor="#color/black"
app:layout_constraintBottom_toTopOf="#id/lobbyleader_player_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
My Viewmodel
class LobbyViewModel(private val lobbyRepository: LobbyRepository) : ViewModel() {
private val _lobbyID = MutableLiveData<String?>()
val lobbyID : LiveData<String?> = _lobbyID
fun createLobby(lobby: Lobby) {
viewModelScope.launch {
val result = lobbyRepository.createLobby(lobby)
Log.d("Lobby", "Viewmodel Data: ${result}")
if (result is Result.Success) {
_lobbyID.value = result.data.lobbyID
Log.d("Lobby", "Viewmodel Data: ${_lobbyID.value}")
}
}
}
My Fragment
class LobbyLeaderFragment : Fragment() {
private val lobbyViewmodel: LobbyViewModel by viewModels {
LobbyViewModelFactory((activity?.application as AppApplication).lobbyRepository)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentLobbyleaderBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_lobbyleader,
container,
false
)
binding.viewmodel = lobbyViewmodel
binding.lifecycleOwner = viewLifecycleOwner
lobbyViewmodel.lobbyID.observe(viewLifecycleOwner, {
Log.d("Lobby", "LobbyCode: $it")
})
return binding.root
}
}
Can try switching the order of
binding.viewmodel = lobbyViewmodel
binding.lifecycleOwner = viewLifecycleOwner
And turn it into
binding.lifecycleOwner = viewLifecycleOwner
binding.viewmodel = lobbyViewmodel
It's probable that when it wants to observe livedata, the lifecycleOwner is null and thus it cannot observe it
Also check this Binding Adapter live data value is always null
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()}"
/>
For some reason, the second parameter value for both binding Adapters always returns null and I cannot figure out why. I am selecting a plantIndividual from a RecyclerView in the overview fragment and using it to navigate to a details page - individual fragment. Both Fragments share a viewModel.
Here are my BindingAdapters:
#BindingAdapter("listPhotoData")
fun bindPlantRecyclerView(recyclerView: RecyclerView,
data: List<PlantPhoto>?) {
val adapter = recyclerView.adapter as CollectionIndividualAdapter
adapter.submitList(data)
}
#BindingAdapter("singleImage")
fun loadImage(imgView: ImageView, imgUrl: File) {
imgUrl.let {
Glide.with(imgView.context)
.load(imgUrl)
.apply(
RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(imgView)
}
}
My details fragment layout:
<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.collection.presentation.overview.CollectionOverviewViewModel" />
<variable
name="plantPhoto"
type="com.example.storage.data.PlantPhoto" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="#+id/collection_individual_imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleImage="#={viewModel.plantPhotoDisplay.plantFilePath}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.26999998"
tools:srcCompat="#tools:sample/avatars" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/collection_individual_recyclerview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/collection_individual_imageview"
app:layout_constraintVertical_bias="0.498"
app:listPhotoData="#={viewModel.listPlantPhoto}"
tools:listitem="#layout/image_plant_photo_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModel:
class CollectionOverviewViewModel(application: Application) : AndroidViewModel(application) {
lateinit var mediaPlantList: MutableList<File>
private var newPhotoList = mutableListOf<PlantPhoto>()
private val context = getApplication<Application>().applicationContext
private val _navigateToSelectedPlant = MutableLiveData<PlantIndividual>()
val navigateToSelectedPlant: LiveData<PlantIndividual>
get() = _navigateToSelectedPlant
private val _listPlantPhoto = MutableLiveData<MutableList<PlantPhoto>>()
val listPlantPhoto: LiveData<MutableList<PlantPhoto>>
get() = _listPlantPhoto
private val _plantPhotoDisplay = MutableLiveData<PlantPhoto>()
val plantPhotoDisplay: LiveData<PlantPhoto>
get() = _plantPhotoDisplay
fun displayPlantDetails(plantIndividual: PlantIndividual) {
_navigateToSelectedPlant.value = plantIndividual
}
fun displayPlantDetailsComplete() {
_navigateToSelectedPlant.value = null
}
fun retrievePlantList(plantIndividual: PlantIndividual) {
val dataClassNum = plantIndividual.plantId
viewModelScope.launch {
mediaPlantList = context?.getExternalFilesDir("planio/dataclasses/$dataClassNum")
?.listFiles()?.sortedDescending()?.toMutableList() ?: mutableListOf()
}
}
fun changeToPlantPhotos(plantList: MutableList<File>) {
plantList.map {
val file = FileInputStream(it)
val inStream = ObjectInputStream(file)
val item = inStream.readObject() as PlantPhoto
newPhotoList.add(item)
}
_plantPhotoDisplay.value = newPhotoList.last()
_listPlantPhoto.value = newPhotoList
}
}
OverView Fragment from which I am selecting a plantIndividual from a RecyclerView and navigating to a details page:
viewModel.navigateToSelectedPlant.observe(viewLifecycleOwner, {
if (null != it) {
viewModel.retrievePlantList(it)
viewModel.changeToPlantPhotos(viewModel.mediaPlantList)
this.findNavController().navigate(
CollectionOverviewFragmentDirections.
actionCollectionOverviewFragmentToCollectionIndividualFragment(it))
}
})
Details Fragment:
class CollectionIndividualFragment: Fragment() {
private lateinit var binding: FragmentCollectionIndividualBinding
private val viewModel: CollectionOverviewViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_collection_individual, container,
false)
binding.toCollectionOverview.setOnClickListener {
this.findNavController().navigate(CollectionIndividualFragmentDirections.
actionCollectionIndividualFragmentToCollectionOverviewFragment())
viewModel.displayPlantDetailsComplete()
}
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.collectionIndividualRecyclerview.adapter = CollectionIndividualAdapter()
binding.plantPhoto = viewModel.plantPhotoDisplay.value
return binding.root
}
I guess you're setting the lifecycleOwner after setting the viewModel of your binding and as a result, after viewModel is set in binding, it cannot observe live data because the lifecycleOwner is null at that point. I suggest to set it after setting binding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_collection_individual, container,
false)
binding.lifecycleOwner = this
Edit:
Also don't forget to use by activityViewModels instead of by viewModels to share viewModel among your fragments
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"
)
}
}