Binding variable is null.. How to let the observer in UI wait till the queried Livedata is excuted from database? - android

I am trying to get data from database and then bind it in fragment to the XML.
So I have repository getting the data from the DB to the ViewModel and the UI fragment is observing the results and then binding the data to the XML.
But the problem is that the app is crushing saying that data is null Even though I am voiding the null data in the observer.
I've tried the to execute the query on the background thread it seems to be working properly the returning the data (Photo).
I think the problem is that the query is taking time and the Observer in the fragment is not waiting till the query is done.
So the query is okay and I am following exactly Google samples but could not figure out the problem.
Thanks in advance.
_PhotoRepository
class PhotoRepository #Inject constructor(
private val photoDao: PhotoDoa
) {
fun loadPhotoById(photoId: Int): LiveData<Photo> {
// var photo: Photo? = null
// this is working and i am getting the photo object
// appExecutors.diskIO().execute {
photo = photoDao.getObjectPhotoById(photoId)
}
return photoDao.getPhotoById(photoId)
}
}
_PhotoViewModel
class PhotoViewModel #Inject constructor(private val photoRepository:
PhotoRepository) :
ViewModel() {
private var _photoId = MutableLiveData<Int>()
val photoId: LiveData<Int>
get() = _photoId
val photo: LiveData<Photo> = Transformations
.switchMap(_photoId) { id ->
photoRepository.loadPhotoById(id)
}
fun setId(photoId: Int) {
// if (_photoId.value == photoId){
// return
// }
_photoId.value = photoId
}
}
_PhotoFragment
class PhotoFragment : Fragment(), Injectable {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
var binding by autoCleared<FragmentPhotoBinding>()
lateinit var photoViewModel: PhotoViewModel
var photo = Photo()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<FragmentPhotoBinding>(
inflater,
R.layout.fragment_photo,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val params = PhotoFragmentArgs.fromBundle(arguments!!)
photoViewModel = ViewModelProviders.of(
this,
viewModelFactory).get(PhotoViewModel::class.java)
photoViewModel.setId(params.photoId)
// photoViewModel.photo.removeObservers(viewLifecycleOwner)
photoViewModel.photo.observe(viewLifecycleOwner, Observer {
if (it != null) {
binding.photo = it
}
})
}
}
_ The Query in the Doa class
#Query(" SELECT * FROM Photo WHERE id = :id")
abstract fun getPhotoById ( id: Int): LiveData<Photo>
_ fragment_photo.xml
<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>
<import type="com.mustafa.pixabayapp.models.Photo"/>
<variable
name="photo"
type="Photo"/>
<import type="com.mustafa.pixabayapp.utils.StringUtils" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="#+id/photo_fragment_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:imageUrl="#{photo.webFormatURL}"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#color/colorTransparentDark"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="#+id/photo_fragment_tags"
style="#style/PixabayImageTextUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{StringUtils.getTags(photo.tags)}"
tools:text="TEST - TEST - TEST"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/photo_fragment_user_name"
style="#style/PixabayImageTextUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:text="#{StringUtils.byUser(photo.userName)}"
tools:text="By: Mustafa"/>
<TextView
android:id="#+id/photo_fragment_comments"
style="#style/PixabayImageTextUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="4dp"
android:drawableStart="#drawable/ic_comment"
android:text="#{StringUtils.getCommentsAsString(photo.commentsCount)}"
tools:text="2222"/>
<TextView
android:id="#+id/photo_fragment_favorites"
style="#style/PixabayImageTextUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_toStartOf="#id/photo_fragment_comments"
android:drawableStart="#drawable/ic_favorite"
android:text="#{StringUtils.getFavoritesAsString(photo.favoritesCount)}"
tools:text="2222"/>
<TextView
android:id="#+id/photo_fragment_likes"
style="#style/PixabayImageTextUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_toStartOf="#id/photo_fragment_favorites"
android:drawableStart="#drawable/ic_like"
android:text="#{StringUtils.getLikesAsString(photo.likesCount)}"
tools:text="2222"/>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</layout>
_The Error message:
java.lang.IllegalArgumentException:
Parameter specified as non-null is null:
method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter
userName at com.mustafa.pixabayapp.utils.StringUtils.byUser(Unknown
Source:2)at com.mustafa.pixabayapp.databinding.FragmentPhotoBindingImpl.
executeBindings(FragmentPhotoBindingImpl.java:138)

Yes, your assumption with "it takes time" is correct. The layout wants to draw something as soon its bind and at this time photo is not loaded yet.
You could handle the null value in StringUtils.byUser() or adding a null check in the layout like here: Data binding: set property if it isn't null

Related

Room livedata , databinding issue : UI doesn't update when data changes

I am now developing an application where I use MVVM pattern for UI and repository interaction. In other words, I receive live data object with a list of models from my ROOM data base query via repository, then assign it to my live data variable in viewmodel. After that, this data should be populated to my xml layout recycler view via data binding, but It happens only once fragment is initialised. In other cases recycler view is void
DAO code :
#Query("SELECT * FROM note WHERE content LIKE :query ORDER BY isStarred")
fun searchAllNotes(query : String?) : Flow<List<Note>>
ViewModel code:
#HiltViewModel
class HomeViewModel #Inject constructor (
private val noteRepository : NoteRepository
) : ViewModel() {
private var _notesLiveData : LiveData<List<Note>> = noteRepository.getAllNotes().asLiveData()
val notesLiveData get()= _notesLiveData
fun searchNotes(query : String){
viewModelScope.launch(Dispatchers.IO){
_notesLiveData = noteRepository.searchAllNotes(query).asLiveData()
}
}
fun deleteNote(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.deleteNote(note)
}
}
fun updateNoteChecked(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.updateNoteChecked(note.id, note.isStarred)
}
}
Fragment code
#AndroidEntryPoint
class HomeFragment : Fragment(),
NoteCardAdapter.NoteTouchListener,
SearchView.OnQueryTextListener{
private var _binding : FragmentHomeBinding? = null
val binding get() = _binding!!
private val adapter by lazy {
NoteCardAdapter(this as NoteCardAdapter.NoteTouchListener)
}
private val vm : HomeViewModel by viewModels()
private val noteSharedViewModel : NoteSharedViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.adapter = adapter
binding.vm = vm
binding.apply {
lifecycleOwner = viewLifecycleOwner
homeRecyclerView.isNestedScrollingEnabled = false
homeRecyclerView.layoutManager = LinearLayoutManager(view.context)
homeRecyclerView.itemAnimator= NotesItemAnimator()
}
binding.homeSearchView.isSubmitButtonEnabled = true
binding.homeSearchView.setOnQueryTextListener(this as SearchView.OnQueryTextListener)
binding.addNoteButton.setOnClickListener{
val note = Note(
"","","",false, activity?.getDate(), folderId = -1
)
noteSharedViewModel.selectNote(note)
val action = HomeFragmentDirections.actionHomeFragmentToSingleNoteFragment(
isNew = true
)
findNavController().navigate(action)
}
binding.foldersButton.setOnClickListener{
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToFoldersFragment())
}
}
xml layout code :
<?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="vm"
type="com.example.leonidsnotesapplication.presentation.notes_feature.viewmodels.HomeViewModel" />
<variable
name="adapter"
type="com.example.leonidsnotesapplication.presentation.notes_feature.adapters.NoteCardAdapter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:background="#color/note_background_color_3"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.notes_feature.fragments.HomeFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/homeSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:theme="#style/AppSearchView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="#+id/tvRecentTitle"
android:textStyle="bold"
android:textSize="25sp"
android:textColor="#color/button_color_2"
android:text="#string/recent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
app:layout_constraintTop_toBottomOf="#id/homeSearchView"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:background="#drawable/ic_circle_arrow"
app:layout_constraintStart_toEndOf="#id/tvRecentTitle"
app:layout_constraintTop_toBottomOf="#id/homeSearchView" />
<androidx.recyclerview.widget.RecyclerView
tools:listitem="#layout/note_card_view"
android:scrollbars="vertical"
android:scrollbarStyle="outsideInset"
android:id="#+id/homeRecyclerView"
android:layout_width="match_parent"
android:requiresFadingEdge="vertical"
android:fadingEdge="vertical"
android:fadingEdgeLength="15dp"
android:layout_height="500dp"
android:layout_marginTop="30dp"
app:setNoteAdapter="#{adapter}"
app:submitNoteList="#{vm.notesLiveData}"
app:layout_constraintTop_toBottomOf="#id/tvRecentTitle"
app:layout_constraintStart_toStartOf="parent"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/add_note_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/folders_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_folders_stack"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Data binding adapter :
#BindingAdapter("submitNoteList")
fun submitNoteList(recyclerView: RecyclerView, data : List<Note>?){
val adapter = recyclerView.adapter as NoteCardAdapter
adapter.setData((data as ArrayList<Note>? ?: arrayListOf()))
}
#BindingAdapter("setNoteAdapter")
fun setNoteAdapter(recyclerView: RecyclerView, adapter: NoteCardAdapter){
adapter.let {
recyclerView.adapter = it
}
}
You should observe your live data. Example code is:
vm.notesLiveData.observe(viewLifecycleOwner) { adapter.submitList(it) }

Using data bindings in Fragments

I have a problem using viewmodel in fragment with data binding.
In my programm i'm using only one activity and fragments. I organized transitions between fragments using navigation. And i want to use data binding in my fragment, but when running the application only with binding, everything works well when I pass to the binding viewmodel when switching to this fragment of the application, it crashes.
I would be very grateful for your help!
My fragment
class TimeSettings : Fragment() {
private lateinit var viewModel: TimeSettingsViewModel
private lateinit var binding: FragmentTimeSettingsBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentTimeSettingsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewmodel = viewModel;
return binding.root
}
}
Object for viewmodel
object TimeData {
var inhale: Int = 4
private val _currentTimeInhale = MutableLiveData<Int>()
val currentTimeInhale: LiveData<Int>
get() = _currentTimeInhale
init{
_currentTimeInhale.value = inhale
}
fun changeCurrentTimeInhale(){
_currentTimeInhale.value = 5
}
}
Viewmodel
class TimeSettingsViewModel : ViewModel() {
val currentInhale: LiveData<Int>
get() = TimeData.currentTimeInhale
fun onChange() = TimeData.changeCurrentTimeInhale()
}
xml file for fragment
<?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="ivakin.first.my_first_app.viewmodels.TimeSettingsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="95dp"
android:layout_height="59dp"
android:text="#{viewmodel.currentInhale}"
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.452" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set"
android:onClick="#{()->viewmodel.onChange()"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
U Forgot Initialize ViewModel :
viewmodel = TimeSettingsViewModel()
Also dont use return binding.root

StateFlow value displayed using Data Binding not updating

I've switched from using LiveData to StateFlow in my app and also applied it in Data Binding. But after changing the value of StateFlow, the UI is not updating. From what I understand from the StateFlow docs, I only have to assign a new value to the value property of a StateFlow to update its consumers.
Below, I have a time field which I update the value by using an MaterialTimePicker dialog. But after I change the time and assign a new value to the selectedAlarm property (StateFlow) of my viewmodel, the UI is not updating for the new value. I'm not sure where is my mistake or what is lacking.
Below is my AlarmFormFragment:
#AndroidEntryPoint
class AlarmFormFragment : Fragment() {
private val alarmsViewModel: AlarmsViewModel by activityViewModels()
private var _binding: FragmentAlarmFormBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAlarmFormBinding.inflate(inflater, container, false)
binding.alarmsViewModel = alarmsViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpTimeField()
}
private fun setUpTimeField() {
alarmsViewModel.selectedAlarm.value.let { alarm ->
binding.llTimeField.setOnClickListener {
// Create and then show time picker
val picker =
MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_12H)
.setTitleText("Set Time")
.setHour(alarm.time.get(Calendar.HOUR_OF_DAY))
.setMinute(alarm.time.get(Calendar.MINUTE))
.build()
picker.addOnPositiveButtonClickListener {
alarm.time.set(Calendar.HOUR_OF_DAY, picker.hour)
alarm.time.set(Calendar.MINUTE, picker.minute)
alarmsViewModel.selectedAlarm.value = alarm
}
picker.show(childFragmentManager, "TIME_INPUT_DIALOG")
}
}
}
}
Below is the xml file for AlarmFormFragment:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.example.myclock.utilities.Utility" />
<import type="com.example.myclock.utilities.SnoozeUtils"/>
<variable
name="alarmsViewModel"
type="com.example.myclock.viewmodels.AlarmsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.AlarmFormFragment">
<!-- Form Fields -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- Time Field -->
<LinearLayout
android:id="#+id/llTimeField"
style="#style/FormFieldLayoutStyle">
<TextView
style="#style/FormFieldTextStyle.Label"
android:text="#string/label_time" />
<TextView
android:id="#+id/tvTime"
style="#style/FormFieldTextStyle.Value"
android:text="#{Utility.INSTANCE.timeToString(alarmsViewModel.selectedAlarm.value.time)}" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And below is my AlarmsViewModel:
#HiltViewModel
class AlarmsViewModel #Inject constructor(
private val alarmsRepository: AlarmsRepository
) : ViewModel() {
private val _alarms = MutableStateFlow<List<Alarm>>(emptyList())
val alarms: StateFlow<List<Alarm>> get() = _alarms
var selectedAlarm = MutableStateFlow(Alarm())
...
}
Below is class for the data Alarm:
#Parcelize
#Entity
class Alarm(
#PrimaryKey(autoGenerate = true) val id: Int = 0,
var time: Calendar = Calendar.getInstance(),
...
) : Parcelable {
...
}

How can a ViewModel function access the Place instance returned by PlaceSelectionListener of AutocompleteSupportFragment?

I am building an Android app using Kotlin.
I have a layout that contains a create_button. When the user clicks the button, the onClick should call onCreateJourney() in my ViewModel. This function requires the selectedPlaceId parameter to update a Journey entity with its value.
In my Fragment, I use the AutocompleteSupportFragment for the user to search and choose the desired place. This works well, as onPlaceSelected() returns the correct place in response to the user's selection. But I can't figure out how to use the returned place id value (p0.id) in my ViewModel onCreateJourney().
At this stage, the compiler throws this error:
cannot find method onCreateJourney() in class com.example.traveljournal.journey.NewJourneyViewModel
Please, can you shed some light on this issue for me?
My UI (.xml fragment 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"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".journey.NewJourneyFragment">
<data>
<variable
name="newJourneyViewModel"
type="com.example.traveljournal.journey.NewJourneyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/whereQuestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:text="#string/whereQuestion"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.027"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/journeyBckgImageView" />
<androidx.cardview.widget.CardView
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.cardview.widget.CardView>
<fragment
android:id="#+id/autocomplete_fragment"
android:name="com.google.android.libraries.places.widget.AutocompleteSupportFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/whereQuestion" />
<Button
android:id="#+id/create_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:onClick="#{() -> newJourneyViewModel.onCreateJourney()}"
android:text="#string/createJourneyButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My ViewModel
class NewJourneyViewModel (
private val journeyKey: Long = 0L,
val database: TravelDatabaseDao) : ViewModel() {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val _navigateToJourneys = MutableLiveData<Boolean?>()
val navigateToJourneys: LiveData<Boolean?>
get() = _navigateToJourneys
fun doneNavigating() {
_navigateToJourneys.value = null
}
fun onCreateJourney(selectedPlaceId: String) {
uiScope.launch {
withContext(Dispatchers.IO) {
val journey = database.getJourney(journeyKey) ?: return#withContext
journey.placeId = selectedPlaceId
database.updateJourney(journey)
}
_navigateToJourneys.value = true
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
My Fragment
class NewJourneyFragment : Fragment(), PlaceSelectionListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.createJourney)
val binding: FragmentNewJourneyBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_new_journey, container, false
)
val application = requireNotNull(this.activity).application
val arguments = NewJourneyFragmentArgs.fromBundle(arguments!!)
val dataSource = TravelDatabase.getInstance(application).travelDatabaseDao
val viewModelFactory = NewJourneyViewModelFactory(arguments.journeyKey, dataSource)
val newJourneyViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(NewJourneyViewModel::class.java)
binding.newJourneyViewModel = newJourneyViewModel
newJourneyViewModel.navigateToJourneys.observe(this, Observer {
if(it == true) {
this.findNavController().navigate(
NewJourneyFragmentDirections.actionNewJourneyDestinationToJourneysDestination())
newJourneyViewModel.doneNavigating()
}
})
if (!Places.isInitialized()) {
this.context?.let { Places.initialize(it, getString(R.string.apiKey), Locale.US) }
}
val autocompleteFragment = childFragmentManager.findFragmentById(R.id.autocomplete_fragment)
as? AutocompleteSupportFragment
autocompleteFragment?.setOnPlaceSelectedListener(this)
autocompleteFragment!!.setHint(getString(R.string.destinationExample))
autocompleteFragment!!.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME))
return binding.root
}
#ExperimentalStdlibApi
override fun onPlaceSelected(p0: Place) {
Log.i("PLACE", "Place: " + p0.name + ", " + p0.id)
}
override fun onError(status: Status) {
Toast.makeText(this.context,""+status.toString(),Toast.LENGTH_LONG).show()
Log.i("ERROR", "An error occurred: " + status)
}
}
You have to pass the field selectedPlaceId as an argument in the xml :
android:onClick="#{() -> newJourneyViewModel.onCreateJourney(newJourneyViewModel.selectedPlaceId)}"
How will you get the selectedPlaceId in view model depends on your programming logic.
For testing purposes, you can hardcode the value.
In NewJourneyViewModel, I set up selectedPlaceId as MutableLiveData.
val selectedPlaceId = MutableLiveData<String>()
In NewJourneyFragment, I used lateinit to create a field for NewJourneyViewModel called newJourneyViewModel and initialized it in onCreateView().
private lateinit var newJourneyViewModel : NewJourneyViewModel
Now having access to my ViewModel outside of onCreateView(), in my Fragment onPlaceSelected() method I can set the value of selectedPlaceId to p0.id.
newJourneyViewModel.selectedPlaceId.value = p0.id
If anybody can come up with a better and safer approach on this, it will be highly appreciated.

Android kotlin data-binding with fragments error:cannot find symbol import com.example.***.databinding.FragmentUserBindingImpl;

I am relatively new to kotlin and trying to build a project with data binding in some fragments.I have a fragment named UserFragment with a Recyclerview in it like this:
class UserFragment : Fragment() {
private lateinit var binding: FragmentUserBinding
private lateinit var viewModel: UserListViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding=DataBindingUtil.inflate(inflater,R.layout.fragment_user, container, false)
binding.userRecycler.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(UserListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer {
errorMessage -> if(errorMessage != null) showError(errorMessage) else hideError()
})
binding.mViewModel=viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//start add activity
val i= Intent(activity,AddUserActivity::class.java)
userFab.setOnClickListener(View.OnClickListener {
startActivity(i)
})
}
private fun showError(#StringRes errorMessage:Int){
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
errorSnackbar?.dismiss()
}
}
and the xml layout file fragment_user.xml looks like this:
<?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="mViewModel"
type="com.example.***.ui.User.UserListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/userDateEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/pick_date"
android:background="#drawable/roundededittext"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/userRecycler"
android:layout_width="293dp"
android:layout_height="475dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/userDateEditText"
app:adapter="#{viewModel.getUserListAdapter()}"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/userFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_marginBottom="48dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"/>
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mutableVisibility="#{viewModel.getLoadingVisibility()}"
android:id="#+id/userProgressBar" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="140dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.804"
app:layout_constraintVertical_bias="0.499"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
there is also a similar adapter class and item_user.xml:
class UserListAdapter : RecyclerView.Adapter<UserListAdapter.ViewHolder>() {
private lateinit var userModelList:List<UserModel>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListAdapter.ViewHolder {
val binding: ItemUserBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_user, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: UserListAdapter.ViewHolder, position: Int) {
holder.bind(userModelList[position])
}
override fun getItemCount(): Int {
return if(::userModelList.isInitialized) userModelList.size else 0
}
fun updateUserList(userModelList:List<UserModel>){
this.userModelList = userModelList
notifyDataSetChanged()
}
class ViewHolder(private val binding: ItemUserBinding):RecyclerView.ViewHolder(binding.root){
private val viewModel = UserViewModel()
fun bind(userModel: UserModel){
viewModel.bind(userModel)
binding.viewModel =viewModel
}
}
}
the item-user.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">
<data>
<variable
name="viewModel"
type="com.example.***.ui.MyUser.UserViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="#+id/user_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textStyle="bold"
app:mutableText="#{viewModel.getUserTitle()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/user_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:mutableText="#{viewModel.getUserDesc()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/user_title" />
</android.support.constraint.ConstraintLayout>
</layout>
note that databinding has been enabled in the gradle
and the very important issue here is that in both my fragment and the adapter
this line:
binding.viewModel =viewModel
reports a type mismatch like this:
Type mismatch.
Required:MyUser.UserListViewModel?
Found:com.example.***.ui.MyUser.UserListViewModel
and when i build my project the error is as follows:
error: cannot find symbol
import com.example.***.databinding.FragmentUserBindingImpl;
The first error is pretty explicit: binding.viewModel expects a nullable UserListViewModel? and get a non-nullable UserListViewModel(see kotlin null safety doc).
You can try something like this to get rid of it:
Declare your viewModel as
private var viewModel: UserListViewModel? = null
and set your binding this way:
viewModel?.let{binding.viewModel = it}
Concerning the second error, your declarations seems fine, but sometimes the Android Studio's cache get corrupted, try Invalidate Caches/Restart, it may help.
Type mismatch.
Required:MyUser.UserListViewModel?
Found:com.example.***.ui.MyUser.UserListViewModel
Basically the error is telling that your
binding.viewModel //is a nullable type and there for it expects a nullable
//type to be assigned as well
So just turn your view model into a nullable type by addind the ? simbol after its delacarion (note late init types do not allow nullable types). try it like this
private var viewModel: UserListViewModel? = null
About the second error data binding library need to compile in order to autogenerate the binding class, just rebuild the project and this error will be gone.
Please feel free to use this template as a base to avoid all that boilerplate code
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.CLASS)
annotation class ContentView(#LayoutRes val id: Int)
fun ViewGroup.inflate(#LayoutRes layoutId: Int,
addContainer: Boolean = false): View {
return LayoutInflater.from(context).inflate(layoutId,this,addContainer)
}
#Suppress("UNCHECKED_CAST")
abstract class BaseFragment<Model : ViewModel, Binding : ViewDataBinding> : Fragment() {
/**
* Data binding class variable all view elements declared on the
* Xml file will be available within this instance if a view model
* Is required for the xml to work we will need to bind it on #onBindViewModel
*/
protected lateinit var binding: WeakReference<Binding?>
/**
* Fragments view model according to MVVM android architecture
* Each fragment class should have one , in order to facilitate
* Live Data and Binding library features, tho we can skip it
*/
protected lateinit var viewModel: WeakReference<Model?>
/**
* Here is where most likely you will get not null data , both binding and
* view model references can be destroyed by garbage collector
* If this application reaches low memory levels
*
* This optional method is used to bind the required view model inside the
* Xml file, this is optional to use tho is recommended
* Bind them by calling the view model binding.customViewModel = viewModel
*/
protected open fun onBindViewModel(viewModel: Model?, binding: Binding?) {}
/**
* There will be the occasion where custom params will be needed on view model's
* Constructor in this case will want to override the default creation #see ViewModelFactory
*/
protected open fun onCreateViewModel(modelType: Class<Model>): Model? = if (modelType != ViewModel::class.java)
ViewModelProviders.of(requireActivity()).get(modelType) else null
/**
* Here we will inherit view model and binding values based on the class
* Parameters and store them in global variables so any class extending
* From Base activity has access to binding and view model instances by default
*/
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val layout = this.javaClass.annotations.find { it.annotationClass == ContentView::class } as ContentView?
?: throw RuntimeException("You annotate this class with #ContentView and provide a layout resource")
container?.let { binding = WeakReference(DataBindingUtil.bind(it.inflate(layout.id))!!) }
?: run { binding = WeakReference(DataBindingUtil.bind(inflater.inflate(layout.id, null))) }
viewModel = WeakReference(
onCreateViewModel(
(this.javaClass.genericSuperclass
as ParameterizedType).actualTypeArguments[0] as Class<Model>
)
)
setHasOptionsMenu(true)
onBindViewModel(viewModel.get(), binding.get())
return binding.get()?.root
}
}
And just use it like this (see how much boiler plate code is gone)
#ContentView(R.layout.fragment_user)
class UserFragment: BaseFragment<UserListViewModel, FragmentUserBinding> {
override fun onBindViewModel(viewModel: UserListViewModel?, binding: FragmentUserBinding?) {
binding.viewModel = viewModel
}
}

Categories

Resources