I'm writing an application with 2-way data binding. I can get the edit text field to update, but I'm also trying to update internal logic too. I've tried to do this by calling addOnPropertyChangedCallback(Observable.OnPropertyChangedCallback) on the binding, however despite checking notifyPropertyChanged(int) is being called by the observable, the callback isn't being called
I've tried adding the #Bindable annotation to both the getter and the setter as mentioned in this stackoverflow question but to no avail.
AddItemObservable.kt
class AddItemObservable : BaseObservable(), AddItemImmutableObservable {
private var name = ""
private var quantity: Int? = null
#Bindable
override fun getName() = name
fun setName(name: String) {
if (this.name == name) {
return
}
this.name = name
notifyPropertyChanged(BR.name)
}
#Bindable
override fun getQuantity() = quantity
fun setQuantity(quantity: Int?) {
if (this.quantity?.equals(quantity) == true) {
return
}
this.quantity = quantity
notifyPropertyChanged(BR.quantity)
}
}
AndroidView.kt
class AndroidView : Fragment() {
private lateinit var binding: FragmentAddItemBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentAddItemBinding.inflate(inflater, container, false)
binding.addItem = AddItemObservable()
binding.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
Log.i("VIEW", "updated")
}
})
binding.lifecycleOwner = this
return binding.root
}
}
fragment_add_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="tk.jonathancowling.inventorytracker.additem.AddItemObservable" />
<variable
name="addItem"
type="AddItemObservable" />
</data>
<LinearLayout 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"
android:orientation="vertical">
<EditText
android:id="#+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:hint="#string/add_item_name"
android:inputType="textPersonName"
android:text="#={addItem.name}"
android:importantForAutofill="no"
tools:ignore="UnusedAttribute" />
<EditText
android:id="#+id/editText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:hint="#string/add_item_quantity"
android:inputType="number"
app:add_item_quantity="#={addItem.quantity}"
android:importantForAutofill="no"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</layout>
Adapters omitted for brevity
I expect the OnPropertyChangedCallback to be called and log "updated" when notifyPropertyChanged is called (which happens when text is entered into one of the EditText).
To make it work, you need call addOnPropertyChangedCallback directly on Observable instance. For example,
val observableItem = AddItemObservable()
binding.addItem = observableItem
observableItem.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
Log.i("VIEW", "updated")
}
})
Related
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) }
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
}
}
I write simple app displaying cocktails list. The problem is there's no title text but the pics is loading with picasso working just fine. I suppose the weak point in my Binding Adapter. Cause I can't just pass data value, i need to pass data?.list and I am really confused why I can't. Please help!
The app result
CocktailListFragment.kt
class CocktailListFragment : Fragment() {
private val viewModel: CocktailListViewModel by lazy {
ViewModelProvider(this).get(CocktailListViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = CocktailListFragmentBinding.inflate(inflater)
binding.lifecycleOwner = this
setHasOptionsMenu(true)
binding.cocktail = viewModel
binding.cocktailList.adapter = CocktailListAdapter()
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.filter_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}
}
CocktailListViewModel.kt
class CocktailListViewModel : ViewModel() {
private val _cocktails = MutableLiveData<CocktailsList>()
val cocktails: LiveData<CocktailsList>
get() = _cocktails
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
getCocktails()
}
private fun getCocktails() {
coroutineScope.launch {
val getCocktailsDeferred = CocktailsApi.RETROFIT_SERVICE.getCocktailsAsync()
try {
val result = getCocktailsDeferred.await()
_cocktails.value = result
} catch (e: Exception) {
Log.d("error", "error")
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
CocktailListAdapter.kt
class CocktailListAdapter :
ListAdapter<Cocktail, CocktailListAdapter.CocktailListViewHolder>(DiffCallback) {
class CocktailListViewHolder(private var binding: CocktailItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(cocktail: Cocktail) {
binding.cocktail = cocktail
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailListViewHolder {
return CocktailListViewHolder(CocktailItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: CocktailListViewHolder, position: Int) {
val cocktail = getItem(position)
holder.bind(cocktail)
}
companion object DiffCallback : DiffUtil.ItemCallback<Cocktail>() {
override fun areItemsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem.id == newItem.id
}
}
}
CocktailDataClass
data class CocktailsList(
#SerializedName("drinks")
val list: List<Cocktail>
)
data class Cocktail (
#SerializedName("idDrink")
val id: String,
#SerializedName("strDrinkThumb")
val drinkImg: String,
#SerializedName("strDrink")
val drinkTitle: String
)
BindingAdapters - I think the problem is in binding adapter for list data:
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: CocktailsList?) {
val adapter = recyclerView.adapter as CocktailListAdapter
adapter.submitList(data?.list)
}
#BindingAdapter("strDrinkThumb")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Picasso.get()
.load(imgUri)
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image)
.into(imgView)
}
}
cocktail_list_fragemnt.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="cocktail"
type="com.example.coctaildb.cocktaillist.CocktailListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBarLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
style="#style/Widget.AppCompat.Toolbar"
android:background="#android:color/white"
app:menu="#menu/filter_menu"
app:title="#string/drinks"
app:titleTextColor="#android:color/black" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/cocktail_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/appBarLayout2"
app:listData="#{cocktail.cocktails}"
tools:listitem="#layout/cocktail_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
cocktail_item.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="cocktail"
type="com.example.coctaildb.network.Cocktail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cocktail_img"
android:layout_width="100dp"
android:layout_height="100dp"
android:maxHeight="100dp"
android:scaleType="centerCrop"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strDrinkThumb="#{cocktail.drinkImg}"
tools:srcCompat="#tools:sample/avatars" />
<TextView
android:id="#+id/cocktail_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="21dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="20dp"
android:fontFamily="#font/roboto"
android:text="#{cocktail.drinkTitle}"
android:textColor="#7E7E7E"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="#+id/cocktail_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I have tried many different tutorials and haven't been able to relate any to my application. My application in a gist displays a user's medication that they are taking. Here is my data class...
import java.util.HashMap
class LocalMedication {
var m_medicationName: String? = null
var m_medicationQty: String? = null
var m_medicationType: String? = null
var m_medicationExpDate: String? = null
var m_medicationStatus: Boolean = false
constructor() {}
constructor(medicationName: String, medicationQty: String, medicationType: String, medicationExpDat : String, medicationStatus : Boolean) {
this.m_medicationName = medicationName
this.m_medicationType = medicationType
this.m_medicationQty = medicationQty
this.m_medicationExpDate = medicationExpDat
this.m_medicationStatus = medicationStatus
}
fun toMap(): Map<String, Any> {
val result = HashMap<String, Any>()
result.put("medicationName", m_medicationName!!)
result.put("medicationType", m_medicationType!!)
result.put("medicationQty", m_medicationQty!!)
result.put("medicationExpDate", m_medicationExpDate!!)
result.put("medicationStatus", m_medicationStatus!!)
return result
}
}
Here is my view holder class
package com.example.home_med.viewHolder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.home_med.R
class medicationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var medicationName: TextView
var medicationType: TextView
var medicationQty: TextView
init {
medicationName = view.findViewById(R.id.rv_medicationName)
medicationType = view.findViewById(R.id.rv_medicationType)
medicationQty = view.findViewById(R.id.rv_medicationQty)
}
}
Here is my fragment
class LocalMedication : Fragment() {
private var adapter: FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>? = null
private var firestoreDB: FirebaseFirestore? = null
private var firestoreListener: ListenerRegistration? = null
private var medList = mutableListOf<LocalMedication>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
firestoreDB = FirebaseFirestore.getInstance()
val recyclerView = recyclerview as? RecyclerView
val mLayoutManager = LinearLayoutManager(context)
recyclerView?.layoutManager = mLayoutManager
recyclerView?.itemAnimator = DefaultItemAnimator()
loadMedication()
firestoreListener = firestoreDB!!.collection("notes")
.addSnapshotListener(EventListener { documentSnapshots, e ->
if (e != null) {
Log.e(TAG, "Listen failed!", e)
return#EventListener
}
medList = mutableListOf()
if (documentSnapshots != null) {
for (doc in documentSnapshots) {
val note = doc.toObject(LocalMedication::class.java)
note.m_medicationName = doc.id
medList.add(note)
}
}
adapter!!.notifyDataSetChanged()
recyclerView?.adapter = adapter
})
}
override fun onDestroy() {
super.onDestroy()
firestoreListener!!.remove()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentLocalMedicationBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_local_medication, container, false)
binding.viewMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToViewMedication())
}
binding.addMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToAddMedication())
}
binding.homeButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToHome2())
}
setHasOptionsMenu(true)
return binding.root
}
private fun loadMedication() {
val query = firestoreDB!!.collection("notes")
val response = FirestoreRecyclerOptions.Builder<LocalMedication>()
.setQuery(query, LocalMedication::class.java)
.build()
adapter = object : FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>(response) {
override fun onBindViewHolder(holder: medicationViewHolder, position: Int, model: LocalMedication) {
val note = medList[position]
holder.medicationName.text = note.m_medicationName
holder.medicationType.text = note.m_medicationType
holder.medicationQty.text = note.m_medicationQty
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): medicationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return medicationViewHolder(view)
}
override fun onError(e: FirebaseFirestoreException) {
Log.e("error", e!!.message)
}
}
adapter!!.notifyDataSetChanged()
recyclerview?.adapter = adapter
}
public override fun onStart() {
super.onStart()
adapter!!.startListening()
}
public override fun onStop() {
super.onStop()
adapter!!.stopListening()
}
}
Here is my recyclerViewItem XML file
<LinearLayout 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:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/rv_medicationName"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationQty"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toEndOf="#+id/rv_medicationName"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationType"
style="#style/word_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/rv_medicationQty"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Here is my localMedications XML file
<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=".LocalMedication">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:text="LOCAL MEDICATION"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.478"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="543dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="#android:color/darker_gray"
app:layout_constraintBottom_toTopOf="#+id/homeButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView"
tools:listitem="#layout/recyclerview_item" />
<Button
android:id="#+id/viewMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100dp"
android:layout_marginBottom="100dp"
android:text="View Med"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="#+id/addMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:layout_marginBottom="100dp"
android:text="Add Medication"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/homeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="59dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="103dp"
android:text="Home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/viewMedicationButton"
app:layout_constraintStart_toEndOf="#+id/addMedicationButton"
app:layout_constraintTop_toBottomOf="#+id/addMedicationButton"
app:layout_constraintVertical_bias="0.972" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Here is what my database looks like...
Datatbase
Any help or guidance would be great. It runs the application, but in the Logs, it says "RecyclerView: No adapter attached; skipping layout"
The problem in your code lies in the fact that the names of the fields in your LocalMedication class are different than the name of the properties in your database. You have in your LocalMedication class a field named m_medicationName while in your database I see it as medicationName and this is not correct. The names must match. Behind the scene, Kotlin is creating a Java class with a getter named getM_medicationName() so Firebase is looking in the database for a field named m_medicationName and not medicationName.
There are two ways in which you can solve this problem. The first one would be to remove the data in your database and add it again using field names (m_medicationName, m_medicationQty etc) that exist in your LocalMedication class.
If you are not allowed to use the first solution, then the second approach will be to use annotations in front of your public fields. So you should use the PropertyName annotation in front of every field. So in your LocalMedication class, a field should look like this:
#get:PropertyName("medicationName")
#set:PropertyName("medicationName")
public var m_medicationName: String? = null
As explained in my answer from the following post:
I am trying to get the correct reference to my Firebase Database child and set the values in my RecyclerView but I am having some issues
It's for Firebase realtime database but the same rules apply to Cloud Firestore.
I am trying to develop social networking app which uses Android Jetpack Libraries but while using Navigation Component to use bottom navigation to navigate through fragments inside an activity , This Adapter throws an error at LayoutInflator() which causes app to crash
Can anyone help me through this:
My Adapter Class:
class FeedAdapter : PagedListAdapter<feed,FeedAdapter.ViewHolder>(FeedDiffCallBack()){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val userPost = LayoutInflater.from(parent.context)
.inflate(R.layout.feedrow,parent,false)
return ViewHolder(userPost)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val feedItem = getItem(position)
if(feedItem != null){
holder.bind(feedItem)
}
}
class ViewHolder(itemView:View):RecyclerView.ViewHolder(itemView) {
//Retrieve data
private val username:TextView = itemView.post_name
private val userPic:ImageView = itemView.feedImage1
private val location:TextView = itemView.postLocation
private val time:TextView = itemView.postTime
private val post:ImageView = itemView.postImage
fun bind(feed: feed) = with(itemView){
//TODO:Bind Data with View
showFeedData(feed)
}
private fun showFeedData(feed: feed) {
username.text = feed.username
userPic.setImageURI(null)
userPic.visibility = View.GONE
location.text = feed.location
time.text = feed.timeStamp.toString()
post.setImageURI(Uri.parse(feed.mUrl))
}
}
}
class FeedDiffCallBack : DiffUtil.ItemCallback<feed>() {
override fun areItemsTheSame(oldItem:feed, newItem: feed): Boolean {
return oldItem?.id == newItem?.id
}
override fun areContentsTheSame(oldItem: feed, newItem: feed): Boolean {
return oldItem == newItem
}
}
Fragment Class:
class FeedFragment : Fragment() {
companion object {
fun newInstance() = FeedFragment()
}
private lateinit var viewModel: FeedViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.feed_fragment, container, false)
val context = getContext() ?: return view
val factory = InjectorUtils.provideViewModelFactory(context)
viewModel =
ViewModelProviders.of(this,factory).get(FeedViewModel::class.java)
val adapter = FeedAdapter()
view.findViewById<RecyclerView>(R.id.feedView).adapter = adapter
view.findViewById<RecyclerView>(R.id.feedView).layoutManager =
LinearLayoutManager(MyApplication.getContext())
subscribeUI(adapter)
return view
}
private fun subscribeUI(adapter: FeedAdapter) {
viewModel.showFeed().observe(this, object:Observer<PagedList<feed>>{
override fun onChanged(t: PagedList<feed>?) {
adapter.submitList(t)
adapter.notifyDataSetChanged()
}
})
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}
}
feed_row.xml
Individual item for recycler view -->
<RelativeLayout
android:id="#+id/postContainer"
android:layout_margin="10dp"
android:elevation="2dp"
android:background="#drawable/bg_parent_rounded_corner"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--TODO:Change to Circle Image View-->
<ImageView
android:id="#+id/profileImage"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="5dp"
android:layout_marginLeft="5dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
/>
<LinearLayout
android:id="#+id/postDetail_1"
android:orientation="vertical"
android:layout_marginTop="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="20dp"
android:layout_alignParentRight="true">
<TextView
android:id="#+id/post_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="#style/LabelStyle"
android:textSize="15sp"
android:fontFamily="#font/sf_pro_display_semibold" />
<LinearLayout
android:id="#+id/postDetail_2"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="#+id/postLocation"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/postTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="120dp" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="#+id/postImage"
android:layout_height="200dp"
android:layout_width="match_parent"
android:layout_below="#+id/postDetail_1"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_marginTop="6dp"
/>
</RelativeLayout>
Your issue might be in this method,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val feedItem = getItem(position)
if(feedItem != null){
holder.bind(feedItem)
}
}
Your ViewHolder is using poisition from method parameter which might be inconsistent
See from here,
So, you should change line to this:
val feedItem = getItem(holder.adapterPosition)
instead of
val feedItem = getItem(position)
I hope it resolves the issue.
This might help some one looking for the same Exception to be cleared :
What I did is that my feed_row.xml above is included inside < layout > tags and I changed it in this way , So the exception got cleared:
Before Exception:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
After changing to this Exception cleared:
<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">
I don't know how it works but it did work!!! So Anyone who knows what happens there can explain please!