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

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.

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) }

How to handle config changes in ViewModel

I have simple registration form. When I enter data and change configuration the data is lost. I use ViewModel in my project and official documentation says ViewModel can handle orientation change automatically but it does not happen. How i suppose to store data with SaveState or I made a mistake in ViewModel?
Fragment code
class StartFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: StartFragmentBinding = DataBindingUtil.inflate(
inflater, R.layout.start_fragment, container, false)
val application = requireNotNull(this.activity).application
val dataSource = UsersDatabase.getInstance(application).usersDatabaseDao
val vm: SavedStateHandle by viewModels()
val viewModelFactory = StartFragmentViewModelFactory(dataSource, application)
val startFragmentViewModel =
ViewModelProvider(
this, viewModelFactory).get(StartFragmentViewModel::class.java)
binding.startFragmentViewModel = startFragmentViewModel
binding.lifecycleOwner = this
binding.start.setOnClickListener {
findNavController().navigate(
StartFragmentDirections
.actionStartFragmentToWebViewFragment())
startFragmentViewModel.doneNavigation()
}
return binding.root
}
}
ViewModel
class StartFragmentViewModel(
val database: UsersDatabaseDao,
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var user1 = MutableLiveData<User?>()
private val _navigateToWebView = MutableLiveData<User>()
val navigateToWebView: LiveData<User>
get() = _navigateToWebView
fun doneNavigation() {
_navigateToWebView.value = null
uiScope.launch {
val user = User()
insert(user)
}
}
private suspend fun insert(user: User) {
withContext(Dispatchers.IO) {
database.insert(user)
}
}
}
ViewModelFactory
class StartFragmentViewModelFactory (
private val dataSource: UsersDatabaseDao,
private val application: Application
) : ViewModelProvider.Factory {
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StartFragmentViewModel::class.java)) {
return StartFragmentViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
start_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">
<data>
<variable
name="startFragmentViewModel"
type="com.example.leadsdoittest.StartFragmentViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_start"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_end"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/margin_top"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_name"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toTopOf="#id/margin_top">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/name" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_phone"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#id/input_name">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:hint="#string/phone_number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/input_email"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#id/input_phone">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="#+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="#string/start"
android:textColor="#android:color/white"
app:layout_constraintEnd_toEndOf="#id/margin_end"
app:layout_constraintStart_toStartOf="#id/margin_start"
app:layout_constraintTop_toBottomOf="#+id/input_email" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
View state is handled by the framework so you don't need to implement it yourself. However the saved state depends on having unique IDs on each view, so try adding an ID to each TextInputEditText.
Use instance variable to restore data after rotation. Look:
class StartFragment : Fragment() {
//private lateinit var homeViewModel: HomeViewModel
var name = ""
var email = ""
var phone = ""
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentHomeBinding.inflate(inflater)
binding.etName.doOnTextChanged { text, start, before, count ->
name = text.toString()
}
binding.etPhone.doOnTextChanged { text, start, before, count ->
phone = text.toString()
}
binding.etEmail.doOnTextChanged { text, start, before, count ->
email = text.toString()
}
binding.etName.setText(name)
binding.etEmail.setText(email)
binding.etPhone.setText(phone)
return binding.root
}
}

How to dynamically load images from a URL into ImageView from a ViewModel in Kotlin

I am writing an app to display NHL scores, and would like for each team in the RecyclerView to have their logo next to it. There is a URL that I can request with a team's ID that will return a hi-res image of the team's logo. I am trying to make it so that I can load the images in my viewModel and set them in the view, as I'm doing for things like the team name, current score, etc.
I have tried using Picasso for this, but it requires a context, which the viewModel doesn't have, and the viewModel cannot directly access the imageView to be able to change it. So how can I load the images and expose them either with data binding or something else, to allow the view to display them?
Here is my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: GameListViewModel
private var errorSnackbar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.gameList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(GameListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer { errorMessage ->
if (errorMessage != null)
showError(errorMessage)
else
hideError()
})
binding.viewModel = viewModel
}
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()
}
}
ViewModel:
class GameViewModel:BaseViewModel() {
private val awayTeamName = MutableLiveData<String>()
private val homeTeamName = MutableLiveData<String>()
private val awayTeamScore = MutableLiveData<String>()
private val homeTeamScore = MutableLiveData<String>()
private val timeRemaining = MutableLiveData<String>()
fun bind(response: Game) {
awayTeamName.value = response.gameData.teams.away.name
homeTeamName.value = response.gameData.teams.home.name
awayTeamScore.value = response.liveData.linescore.teams["away"]?.goals.toString()
homeTeamScore.value = response.liveData.linescore.teams["home"]?.goals.toString()
if (response.gameData.status.detailedState == "Scheduled") {
val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
parser.timeZone = TimeZone.getTimeZone("UTC")
val formatter = SimpleDateFormat("hh:mm a", Locale.getDefault())
formatter.timeZone = TimeZone.getDefault()
timeRemaining.value = formatter.format(parser.parse(response.gameData.datetime.dateTime))
} else {
timeRemaining.value = response.liveData.linescore.currentPeriodTimeRemaining + " " + response.liveData.linescore.currentPeriodOrdinal
}
}
fun getAwayTeamName(): MutableLiveData<String> {
return awayTeamName
}
fun getHomeTeamName(): MutableLiveData<String> {
return homeTeamName
}
fun getAwayTeamScore(): MutableLiveData<String> {
return awayTeamScore
}
fun getHomeTeamScore(): MutableLiveData<String> {
return homeTeamScore
}
fun getTimeRemaining(): MutableLiveData<String> {
return timeRemaining
}
}
and XML for the recyclerView row:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.example.nhlstats.ui.game.GameViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/awayTeam"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="12dp">
<ImageView
android:id="#+id/awayTeamLogo"
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
tools:src="#drawable/ic_launcher_background"/>
<TextView
android:id="#+id/awayTeamName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:layout_gravity="center_vertical"
android:text="#{viewModel.awayTeamName}"
tools:text="CHI Blackhawks"/>
<TextView
android:id="#+id/awayScore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="#{viewModel.awayTeamScore}"
tools:text="0"/>
<TextView
android:id="#+id/gameTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="#{viewModel.timeRemaining}"
tools:text="14:26 3rd"/>
</LinearLayout>
<LinearLayout
android:id="#+id/homeTeam"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="24dp">
<ImageView
android:id="#+id/homeTeamLogo"
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
tools:src="#drawable/ic_launcher_background"/>
<TextView
android:id="#+id/homeTeamName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:layout_gravity="center_vertical"
android:text="#{viewModel.homeTeamName}"
tools:text="CAR Hurricanes"/>
<TextView
android:id="#+id/homeScore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:text="#{viewModel.homeTeamScore}"
tools:text="4"/>
</LinearLayout>
</LinearLayout>
</layout>
Thanks in advance.
Using data binding you should create a custom binding adapter like below:
#BindingAdapter("app:imageUri")
fun loadImageWithUri(imageView: ImageView, imageUri: String){
Glide.with(imageView.context).load(Uri.parse(imageUri)).into(imageView)
}
And change you imageview like this:
<androidx.appcompat.widget.AppCompatImageView
android:layout_height="36dp"
android:layout_width="0dp"
android:layout_weight="1"
app:imageUri="#{viewmodel.teamLogoUri}"/>
For Android Architecture Components View Model,
It's not a good practice to pass your Activity Context to the Activity's ViewModel as its a memory leak. I don't support to to that.
You can create image url observer in viewmodel and observe it in your View class (Activity or fragment), Like this (as Duy Khanh Nguyen answered):-
viewModel.url.observe(this, Observer {
it?.let { url ->
//So image into itemView using Picasso
}
})
But if you want to go with otherwise you can simply use an Application context which is provided by the AndroidViewModel, you should extend AndroidViewModel which is simply a ViewModel that includes an Application reference. I your case do it into your BaseViewModel. Example:-
class BaseViewModel(application: Application) : AndroidViewModel(application) {
val context = getApplication<Application>().applicationContext
//... ViewModel methods
}
I guest you'll create GameViewModel for each itemView, so when binding the view holder:
Your GameViewModel class
val awayLogoUrl = MutableLiveData<String>()
val homeLogoUrl = MutableLiveData<String>()
fun bind(response: Game) {
awayLogoUrl.value = response... //set away logo url here
homeLogoUrl.value = response... //set home logo url here
}
Your ViewHolder class
viewModel.awayLogoUrl.observe(this, Observer {
it?.let { url ->
//Show image into itemView using Picasso or Glide
Glide.with(itemView.context).load(url).into(binding.awayTeamLogo)
}
})
viewModel.homeLogoUrl.observe(this, Observer {
it?.let { url ->
//Show image into itemView using Picasso or Glide
Glide.with(itemView.context).load(url).into(binding.homeTeamLogo)
}
})

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

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

onClick not working in mvvm in android databinding

I am trying to implement mvvm and databinding in kotlin first time. I followed some tutorial and able to implement same. But now button click is not working which I have written in mvvm.
Here is activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.abc.abc.presentation.user.viewmodels.LoginViewModel"/>
</data>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.user.views.LoginActivity"
android:layout_margin="24dip">
<Button
android:layout_width="match_parent"
android:layout_height="40dip"
android:background="#drawable/btn_user_create_account"
android:layout_above="#+id/or_layout"
android:layout_marginBottom="24dp"
android:text="#string/create_account"
android:id="#+id/btn_create_account"
android:layout_marginEnd="12dip"
android:layout_marginStart="12dip"
android:gravity="center_horizontal|center_vertical"
android:textColor="#color/text_color"
android:textAllCaps="false"
android:onClick="#{() -> viewmodel.redirectToRegisterActivity()}"
android:fontFamily="#font/vollkron"
android:textStyle="bold"
/>
<Button
android:layout_width="match_parent"
android:layout_height="40dip"
android:background="#drawable/btn_user_login_reg"
android:layout_above="#+id/btn_create_account"
android:layout_marginBottom="12dp"
android:text="#string/login"
android:id="#+id/btn_login"
android:layout_marginEnd="12dip"
android:layout_marginStart="12dip"
android:gravity="center_horizontal|center_vertical"
android:textColor="#color/white"
android:textAllCaps="false"
android:onClick="#{() -> viewmodel.executeEmailLogin(context)}"
android:fontFamily="#font/vollkron"
android:textStyle="bold"/>
</RelativeLayout>
Here is my ViewModel code :
class LoginViewModel(application: Application) : AndroidViewModel(application) {
var email: ObservableField<String>? = null
var password: ObservableField<String>? = null
var progressDialog: SingleLiveEvent<Boolean>? = null
var launchRegisterActivity: SingleLiveEvent<Boolean>? = null
var userLogin: LiveData<User>? = null
init {
progressDialog = SingleLiveEvent<Boolean>()
launchRegisterActivity = SingleLiveEvent<Boolean>()
email = ObservableField("")
password = ObservableField("")
userLogin = MutableLiveData()
}
fun redirectToRegisterActivity() {
launchRegisterActivity?.value = true
}
fun executeEmailLogin(context: Context) {
progressDialog?.value = true
val user = User(
email = email.toString(),
password = password.toString(),
)
userLogin = UserRepository.getInstance().registerUserUsingEmail(context, user)
}
}
Here is my Login Activity
class LoginActivity : AppCompatActivity() {
var binding: ActivityLoginBinding? = null
var viewModel: LoginViewModel? = null
var customProgressDialog: CustomProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FacebookSdk.sdkInitialize(applicationContext);
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
customProgressDialog = CustomProgressDialog(this)
viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
observeViewModel(viewModel!!)
}
private fun observeViewModel(viewModel: LoginViewModel) {
viewModel.progressDialog?.observe(this, Observer {
if (it!!) customProgressDialog?.show() else customProgressDialog?.dismiss()
})
viewModel.launchRegisterActivity?.observe(this, Observer {
if (it!!) startActivity(Intent(this, RegisterActivity::class.java))
})
viewModel.userLogin?.observe(this, Observer { user ->
// redirect user to home scree
Toast.makeText(this, "welcome, ${user?.user_name}", Toast.LENGTH_LONG).show()
})
}
}
Can you please help me to understand what things I am doing wrong here. Do I implemented mvvm correctly?
Any button click is not working.
I think, You forgot to bind viewmodel to databinding.
Add this in onCreate of LoginActivity :
binding.lifecycleOwner = this
binding.viewmodel = viewModel
Try adding
binding.lifecycleOwner = this
after inflating your layout.

Categories

Resources