Android: LiveData postValue() getting null - android

I am trying to transfer a value from one LiveData (Repository.getMovieList(editTextContent.value.toString()).value) to another LiveData (this.movieList.postValue) using postValue().
I am observing the movieList and want to change it's value from the Repo depending on different buttons that were clicked but I when it runs, it only gets the null value and doesn't wait till the Repo's LiveData gets their value.
Fragment xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.example.movieapp.ui.search.SearchMovieFragmentViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.search.SearchMovieFragment">
<EditText
android:id="#+id/search_movie_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={viewmodel.editTextContent}"
android:inputType="text"
android:hint="Movie Name" />
<Button
android:id="#+id/search_fragment_search_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Search"
android:onClick="#{() -> viewmodel.getMovieSearchList()}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_movie_fragment_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>
SearchMovieFragment
class SearchMovieFragment : Fragment(), MovieSearchItemViewModel {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = searchMovieFragmentViewModel
setUpRecyclerView(container!!.context)
return binding.root
}
private fun setUpRecyclerView(context: Context) {
movieRecyclerView = binding.searchMovieFragmentRecyclerView.apply {
this.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
val adapter = MovieListAdapter()
adapter.setCallback(this)
binding.searchMovieFragmentRecyclerView.adapter = adapter
searchMovieFragmentViewModel.getMovieListLiveData().observe(viewLifecycleOwner, Observer {movieList ->
adapter.submitList(movieList)
})
}
}
SearchMovieViewModel
class SearchMovieFragmentViewModel : ViewModel() {
val editTextContent = MutableLiveData<String>()
var movieList: MutableLiveData<List<Movie>> = MutableLiveData()
fun getMovieSearchList(){
this.movieList.postValue(Repository.getMovieList(editTextContent.value.toString()).value)
}
fun getTrendingMovies() {
movieList.postValue(Repository.getTrendingMovies().value)
}
fun getMovieDetail(movieId: String): MutableLiveData<Movie> {
return Repository.getMovieDetail(movieId)
}
fun getMovieListLiveData() : LiveData<List<Movie>> {
return movieList
}
private fun getMovieList(movieSearch: String): MutableLiveData<List<Movie>> = Repository.getMovieList(movieSearch)
}

I think you are implementing it the wrong way, Instead, using the MediatorLiveData will be a good and practical solution as it allows you to observe multiple LiveData objects and select between them based on your preferences (a specific action for example).
This is an example of how to implement it in your case
val editTextContent = MutableLiveData<String>()
val finalList = MediatorLiveData<List<Movie>>()
// Here.. Define all of your LiveData objects
private val movieList = repository.getMovieList(editTextContent.value.toString())
private val trendingMovies = repository.getTrendingMovies()
private val movieDetail = repository.getMovieDetail()
fun setSelection(selection: String) {
finalList.addSource(movieList) { result ->
if (selection == "movieList") {
result?.let { finalList.value = it }
}
}
finalList.addSource(trendingMovies) { result ->
if (selection == "trendingMovies") {
result?.let { finalList.value = it }
}
}
finalList.addSource(movieDetail) { result ->
if (selection == "movieDetail") {
result?.let { finalList.value = it }
}
}
}
So what you have to do is to only observe the MediatorLiveData and then call the setSelection function and send the correspondent selection action to it as a parameter and it will switch the observation to another LiveData

Related

Binding Adapter live data value is always null

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

Changing toolbar title in each fragment using MVVM and Databinding

I am looking to change the toolbar title,which is in my main activity, in my fragments page. My project is based on MVVM Architecture, with databinding.
This is my main_activity.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.lalsoft.toolbar_mvvm_databinding.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextColor="#android:color/white"
app:navigationIcon="#drawable/ic_arrow_back"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:navigationOnClickListener="#{()->viewModel.navBackClicked()}"
app:title="#{viewModel.toolbarTitle}"/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
This is my mainActivity.kt
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var dataBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//setSupportActionBar(dataBinding.toolbar)
//dataBinding.toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
viewModel.navClicked.observe(this, navClickObserver)
viewModel.toolbarTitle.observe(this, toolbarTitleObserver)
dataBinding.viewModel = viewModel
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().replace(
R.id.fragment_container,
FirstFragment()
).commit()
}
}
private val navClickObserver = Observer<Boolean> {
supportFragmentManager.popBackStack()
Log.e(TAG, "Nav Back clicked")
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
}
}
And this is my MainViewModel
private const val TAG = "MainViewModel"
open class MainViewModel : ViewModel() {
val toolbarTitle: MutableLiveData<String> = MutableLiveData()
private val _navClicked: MutableLiveData<Boolean> = MutableLiveData()
val navClicked: LiveData<Boolean> = _navClicked
init {
Log.e(TAG, "Inside Init")
//toolbarTitle.value ="Main Activity"
}
fun navBackClicked() {
_navClicked.value = true
}
}
Now i am trying to change the toolbar title in FragmentViewModel by changing the mutable toolbarTitle of my mainActivityViewModel.
private const val TAG = "FirstViewModel"
class FirstViewModel : MainViewModel() {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToFragment: LiveData<Event<String>>
get() = _navigateToDetails
init {
Log.e(TAG, "Inside Init")
toolbarTitle.value="First Fragment"
}
fun onBtnClick() {
_navigateToDetails.value = Event("Second Fragment")
}
}
This is my fragment class
private const val TAG = "FirstFragment"
class FirstFragment : Fragment() {
private lateinit var viewModel: FirstViewModel
private lateinit var dataBinding: FirstFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
return dataBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(FirstViewModel::class.java)
viewModel.toolbarTitle.observe(viewLifecycleOwner, toolbarTitleObserver)
viewModel.navigateToFragment.observe(viewLifecycleOwner, navigateToFragmentObserver)
//(activity as MainActivity?)!!.toolbar.title = "Check"
dataBinding.viewModel = viewModel
}
private val toolbarTitleObserver = Observer<String> {
Log.e(TAG, "Title set : $it")
//(activity as MainActivity?)!!.toolbar.title = "Check"
//Log.e(TAG, "Title set : Check")
}
private val navigateToFragmentObserver = Observer<Event<String>> { it ->
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
Log.i(TAG, "checkIt string $it")
parentFragmentManager.beginTransaction().replace(
R.id.fragment_container,
SecondFragment()
).addToBackStack(null).commit()
}
}
}
Eventhough its observing the toolbarTitle correctly,the Title in my program is not changing..
Hope to get some help to get out of this issue.
This is my sample git project where i am trying to do this : github
I struggle with the same!
I think the problem is the lifecycleowner. But could not find an answer.
Currently I observe the values from the the ViewModel and assign the values inside the observer.
But I think there is a better way!
If your Fragment is using a ViewModel should be scoped to a host Activity, use by activityViewModels() delegate:
#AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
}
I think also this answer will help.
https://stackoverflow.com/a/62560605
When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels

Data binding returning null

I might be doing this all wrong, but I have the same exact implementation in another fragment/viewmodel with no problems. Maybe because it's a dialog? Every time I log message or message.messagebody it returns null. Can anyone maybe point out why? Currently learning mvvm.
xml: (the important bit since it's long)
<?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="user"
type="com.catbellystudio.knodee.models.Users" />
<variable
name="vm"
type="com.catbellystudio.knodee.ui.profile.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_margin="10dp"
android:background="#drawable/custom_background_popup"
android:elevation="10dp"
android:orientation="vertical">
<ScrollView
android:id="#+id/popupTextLayout"
android:layout_width="match_parent"
android:layout_height="277dp"
android:layout_marginTop="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
android:hint="#string/your_message"
android:inputType="textMultiLine"
android:padding="10dp"
android:text="#{vm.message.messageBody}" />
</ScrollView>
</LinearLayout>
</layout>
viewmodel:
class ProfileViewModel(
private val userRepository: UserRepository,
private val messageRepository: MessageRepository
) : ViewModel() {
var message: Message = Message()
var sender: Users? = null
var receiver: Users? = null
var string:String?=null
fun getLoggedInUser() = runBlocking { userRepository.getUser() }
fun onBackPressed(view: View) {
Navigation.findNavController(view).navigateUp()
}
fun postMessage(view:View) {
Coroutines.main {
Log.e("messagevm", message.toString())
}
}
}
fragment:
class MessageFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ProfileViewModel
private lateinit var profileBinding: FragmentProfileBinding
private lateinit var popupBinding: FragmentPopupBinding
override val kodein by kodein()
private val factory: ProfileViewModelFactory by instance()
private lateinit var dialog: Dialog
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(this, factory).get(ProfileViewModel::class.java)
profileBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
popupBinding = FragmentPopupBinding.inflate(LayoutInflater.from(context))
dialog = context?.let { Dialog(it) }!!
dialog.setContentView(popupBinding.root)
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
profileBinding.viewModel = viewModel
popupBinding.vm = viewModel
getSender()
return profileBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsButtonProfile.visibility = View.GONE
messageButtonProfile.setOnClickListener {
showPopUp()
}
val receiver by lazy {
arguments?.let { fromBundle(it).user }
}
viewModel.receiver = receiver
}
private fun showPopUp() {
dialog.show()
val switch = dialog.visibilitySwitchPopup
val visibilityTextView = dialog.visibilityTextViewPopup
dialog.closeButtonPopUp?.setOnClickListener {
dialog.dismiss()
}
switch?.setOnClickListener {
val isIconEnabled = switch.isIconEnabled
if (isIconEnabled) {
visibilityTextView?.text = getString(R.string.anonymous_prompt)
} else {
visibilityTextView?.text = getString(R.string.anonymous_warning)
}
switch.switchState()
}
}
private fun getSender() {
viewModel.getLoggedInUser()?.let { viewModel.sender = it }
}
}
Any help would be appreciated!
move this "popupBinding.vm = viewModel" line to onViewCreated() method and also include this line "popupBinding.lifeCycleOwner=this" in same method
Solved by changing
android:text="#{vm.message.messageBody}
to
android:text="#={vm.message.messageBody}

onClick not triggering associated function - kotlin

EDIT - I tried to set android:focusable and android:clickable attributes to true in the XML, but it didn't changed anything. Still looking!
I can't understand why my onClick handler isn't working on this particular case.
I want to make a CardView clickable, using databinding. I've seen a lot of code for onClickListener, but i decided to use this pattern, as described in the Android doc.
I have a class, named Recipe(), which contains 3 elements : an ID, a Title and an URL. The ultimate goal of this click handler is to navigate to a new fragment, passing the ID as a parameter to display new elements. I'll later get the ID in the Fragment using observer.
In this example, I already use data provided by the ViewModel in the XML (recipe.image and recipe.title), and it works just fine: data is correctly binded and displayed.
However, clicking on the CardView don't result in anything: as the Log isn't display, I suppose clicking don't trigger the onClick event.
You'll find element from the ViewModel and XML below. Thanks in advance!
class DefaultRecipeListViewModel : ViewModel() {
private val _recipeList = MutableLiveData<List<Recipe>>()
val recipeList: LiveData<List<Recipe>>
get() = _recipeList
//Defining coroutine
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main )
//Livedata observed in the Fragment
private var _navigateToRecipe = MutableLiveData<Int>()
val navigateToRecipe: LiveData<Int>
get() = _navigateToRecipe
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
init {
getRecipesForNewbie()
}
fun getRecipesForNewbie() {
coroutineScope.launch {
var getRecipes = service.getRecipe().await()
try {
_recipeList.value = getRecipes.results
} catch (e: Exception) {
Log.i("ViewModel","Error: $e")
}
}
}
fun onRecipeClicked(id: Int) {
_showSnackbarEvent.value = true
_navigateToRecipe.value = id
Log.i("ViewModel", "Item clicked, id: $id")
}
fun doneNavigating(){
Log.i("ViewModel", "done navigating, navigateToRecipe set to -1")
_navigateToRecipe.value = -1
}
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here's the XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="recipe"
type="com.example.recipesfornewbies.recipes.Recipe" />
<variable
name="viewModel"
type="com.example.recipesfornewbies.defaultrecipelist.DefaultRecipeListViewModel" />
</data>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
app:cardCornerRadius="10dp"
app:cardElevation="6dp"
android:onClick="#{()-> viewModel.onRecipeClicked(recipe.id)}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/recipe_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:contentDescription="#{recipe.title}"
app:imageFromUrl="#{recipe.image}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="#+id/recipe_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="#{recipe.title}"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="#id/recipe_image"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
Fragment code:
class DefaultRecipeListFragment: Fragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentDefaultRecipeListBinding.inflate(inflater)
val viewModel: DefaultRecipeListViewModel by lazy {
ViewModelProviders.of(this).get(DefaultRecipeListViewModel::class.java)
}
binding.viewModel = viewModel
// Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
binding.setLifecycleOwner(this)
//Creating the RecyclerView
val manager = LinearLayoutManager(activity)
binding.recyclerRecipeList.layoutManager = manager
binding.recyclerRecipeList.adapter = RecipeListAdapter()
viewModel.navigateToRecipe.observe(this, Observer{id ->
if (id != -1){
Log.i("Fragment","Navigate to ${id}")
}
viewModel.doneNavigating()
})
viewModel.showSnackBarEvent.observe(this, Observer {
if (it == true) {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
"Clicked!",
Snackbar.LENGTH_SHORT
).show()
viewModel.doneShowingSnackbar()
}
})
return binding.root
}
}
ViewHolder class, used in the RecyclerView Adapter
class RecipeViewHolder(private var binding: RecipeViewBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(Recipe: Recipe) {
val imageURI = "https://spoonacular.com/recipeImages/"
Recipe.image = imageURI + Recipe.image
binding.recipe = Recipe
// Forces the data binding to execute immediately,to correctly size RecyclerVieW
binding.executePendingBindings()
}
I finally decided to use this solution on my code, which works pretty well.
I stopped trying to access the function in my ViewModel directly from the XML: I actually listen for event on the Fragment, even if I find the solution to be less pretty that the one I wanted.
In the Fragment, this is how I handle the click:
binding.recyclerRecipeList.addOnItemTouchListener(RecyclerItemClickListener(this.context!!,
binding.recyclerRecipeList, object : RecyclerItemClickListener.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
viewModel.recipeList.value?.let {
val ident = it[position].id
findNavController().navigate(
DefaultRecipeListFragmentDirections.actionDefaultRecipeListFragmentToDetailedRecipeFragment(ident))
Log.i("Fragment", "id: $ident")
}
}
override fun onItemLongClick(view: View?, position: Int) {
TODO("do nothing")
}
}))
Still haven't understood why my first solution didn't work.

Observer's inside view model don't trigger

I have a problem that happened to me during creating internal live data observer in My view model and connecting it with data binding. Right now part of my code look's like this:
Fragment:
class SearchFragment : Fragment(), Injectable {
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
#Inject lateinit var searchViewModel: SearchViewModel
private lateinit var disposable: Disposable
private var searchView: SearchView? = null
private var adapter: SearchCityListAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: SearchFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.search_fragment, container, false)
binding.apply { viewModel = searchViewModel
setLifecycleOwner(this#SearchFragment)}
setHasOptionsMenu(true)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
searchViewModel = ViewModelProviders.of(this, viewModelFactory).get(SearchViewModel::class.java)
submitViewModel()
}
private fun submitViewModel(){
viewModel.searchCityList.observe(this, Observer {it ->
if (adapter == null){
it?.let { adapter = SearchCityListAdapter(it)}.also {createAdapter()}
} else {
it?.let { adapter!!.updateList(it) }.also { adapter!!.notifyDataSetChanged() }
}
})
viewModel.responseStatus.observe(this, Observer {
it?.let { it ->
when(it.status){
Status.SUCCESS -> progressBar.visibility = View.INVISIBLE
Status.FAILURE -> progressBar.visibility = View.INVISIBLE
Status.ERROR -> progressBar.visibility = View.INVISIBLE
Status.LOADING ->progressBar.visibility = View.VISIBLE
} }
})
}
ViewModel:
class SearchViewModel #Inject constructor(val haloApplication: HaloApplication): ViewModel() {
val progressBarVisibility: MediatorLiveData<Boolean> = MediatorLiveData()
private val responseStatus: MutableLiveData<Response> = MutableLiveData()
val searchCityList: MutableLiveData<SearchCityList> = MutableLiveData()
fun searchCity(cityName: String){
haloApplication.networkRepository.fetchCityList(cityName, responseStatus, searchCityList)
}
init {
progressBarVisibility.value = false
progressBarVisibility.addSource(responseStatus){
it?.let { when(it.status) {
Status.FAILURE -> progressBarVisibility.value = false
Status.ERROR -> progressBarVisibility.value = false
Status.SUCCESS -> progressBarVisibility.value = false
Status.LOADING -> progressBarVisibility.value = true
} }
}
}
}
XML layout file:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.mbojec.halo.viewmodel.SearchViewModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.SearchFragment">
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/progressBar"
android:layout_gravity="center"
android:visibility="#{viewModel.progressBarVisibility, default=invisible}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/searchCityListRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</layout>
DataBinder file:
import android.view.View
import androidx.databinding.BindingAdapter
#BindingAdapter("android:visibility")
fun setVisibility(view: View, value: Boolean) {
view.visibility = if (value) View.VISIBLE else View.INVISIBLE
}
For some unknown reason's the Fragment part works fine but the viewmodel part never triggers when there are changes in the responseStatus and the connection via data binding is useless. This isn't my first time with creating mediator's in view model or tranformation's but it's the first time I have such problem. I tried dozen of scenario's and each time the result is the same. First I ws blaming the new AndroidX library but their Sunflower Sample from github works also fine so I have no clue on this moment.
MediatorLiveData only starts observing sources added via addSource when it has something observing the MediatorLiveData itself. Since nothing is observing progressBarVisibility, you won't get any callbacks.
You have to observe the progressBarVisibility to receive onChanged callbacks. (Every observable needs an observe to work with)
Let`s make some changes:
Your progressBarVisibility does not need to be a MediatorLiveData;
You dont need to make the responseStatuspublic and observe it;
Let your progressBarVisibility add responseStatus as source.
internal val progressBarVisibility: LiveData<Boolean> = Transformations.map(responseStatus) {
when (it.status) {
Status.LOADING -> true
Status.ERROR, Status.SUCCESS, Status.FAILURE -> false
}
}
Now, you can observe the progressBarVisibilityand receive callbacks.

Categories

Resources