Cannot populate spinner with data from database? - android

I'm trying to populate a spinner with data using room, I'm getting no errors but my spinner isn't displaying anything. I think it might have something to do with how I'm calling initFirstUnitSpinnerData() in my onCreateView method? But I'm having no luck. I'm using kotlin.
Thanks in advance.
DAO:
#Query("SELECT firstUnit FROM conversion_table WHERE category LIKE :search")
fun getByCategory(search: String): LiveData<List<String>>
Repository:
fun getByCategory(search: String): LiveData<List<String>>{
return conversionsDAO.getByCategory(search)
}
View Model:
fun getByCategory(search: String): LiveData<List<String>> {
return repository.getByCategory(search)
}
Fragment:
class UnitsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
private lateinit var mConversionsViewModel: ConversionsViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_units, container, false)
mConversionsViewModel = ViewModelProvider(this).get(ConversionsViewModel::class.java)
initFirstUnitSpinnerData()
return view
}
private fun initFirstUnitSpinnerData() {
val spinnerFirstUnit = view?.findViewById<Spinner>(R.id.firstUnitSpinner)
if (spinnerFirstUnit != null) {
val allConversions = context?.let {
ArrayAdapter<Any>(it, R.layout.support_simple_spinner_dropdown_item)
}
mConversionsViewModel.getByCategory("Distance")
.observe(viewLifecycleOwner, { conversions ->
conversions?.forEach {
allConversions?.add(it)
}
})
spinnerFirstUnit.adapter = allConversions
spinnerFirstUnit.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Toast.makeText(requireContext(), "$allConversions", Toast.LENGTH_LONG).show()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
}

This is the kind of thing you should debug really - click on the left gutter for the first line of initFirstUnitSpinnerData (the val spinnerFirstUnit one), click the Debug App button up near the Run one, and it'll pause when it hits that breakpoint you added.
Then you can move through, step by step, looking at the values of stuff and checking if it looks right, and how the code executes. It's a super useful thing to learn and it'll save you a lot of headaches!
Anyway I'm guessing your problem is that you're calling initFirstUnitSpinnerData from inside onCreateView - the latter is called by the Fragment when it needs its layout view inflating, which you do and then return it to the Fragment.
So inside initFirstUnitSpinnerData, when you reference view (i.e. the Fragment's view, which it doesn't have yet, because onCreateView hasn't returned it yet) you're getting a null value. So spinnerFirstUnit ends up null, and when you null check that, it skips setting up the adapter.
Override onViewCreated (which the Fragment calls when it has its layout view) and call your function from there, it'll be able to access view then - see if that helps!

Related

Recycler view not update itself

I have a problem with recycler view. In my previous app, when i get the list for recycler view adapter from database and observe it in my fragment, i used the notifyDataSetChanged() and when i tried to delete a item , view updated successfully. But in this app this does not work and i don't understand why. When i click the delete button the item deleted in database successfully but i can't see it immediatly. When i go to any other fragment and back to this Favourites fragment i see the items deleted.
I tried all the options in stackoverflow but still i can't fix it.
My Adapter:
class FavouritesAdapter(owner: ViewModelStoreOwner, val favouritesList : ArrayList<Vocabulary>) : RecyclerView.Adapter<FavouritesAdapter.FavouritesViewHolder>() {
val viewModel = ViewModelProvider(owner).get(FavouritesViewModel::class.java)
class FavouritesViewHolder(val binding: FavouritesItemRowBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavouritesViewHolder {
return FavouritesViewHolder(FavouritesItemRowBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: FavouritesViewHolder, position: Int) {
holder.binding.englishWordTV.text = favouritesList[position].word
holder.binding.turkishWordTV.text = favouritesList[position].translation
holder.binding.deleteButtonRV.setOnClickListener {
viewModel.deleteVocabulary(favouritesList[position])
notifyDataSetChanged()
}
}
override fun getItemCount(): Int {
return favouritesList.size
}
fun updateList(myList : List<Vocabulary>) {
favouritesList.clear()
favouritesList.addAll(myList)
notifyDataSetChanged()
}
}
My problem is in delete button in my recycler row;
holder.binding.deleteButtonRV.setOnClickListener {
viewModel.deleteVocabulary(favouritesList[position])
notifyDataSetChanged()
}
And here is my fragment ;
class FavouritesFragment : Fragment() {
private var _binding: FragmentFavouritesBinding? = null
private val binding get() = _binding!!
private lateinit var favouritesAdapter : FavouritesAdapter
private lateinit var viewModel : FavouritesViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFavouritesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(FavouritesViewModel::class.java)
favouritesAdapter = FavouritesAdapter(this, arrayListOf())
viewModel.getAllVocabulariesFromDB()
prepareRecyclerView()
observeFavouritesLiveData()
}
fun prepareRecyclerView(){
binding.favouritesRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = favouritesAdapter
}
}
fun observeFavouritesLiveData(){
viewModel.favouritesListLiveData.observe(viewLifecycleOwner, Observer {
it?.let {
favouritesAdapter.updateList(it)
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Try with notifyItemRemoved(position) instead of notifyDataSetChanged().
It all looks fine to me - you observe the favourites LiveData, that passes the new data to an update function in your Adapter, and that modifies the internal data set and calls notifyDataSetChanged() (which works for any kind of update).
So, are you sure your ViewModel is updating favouritesListLiveData properly when you call deleteVocabulary? Check if your observer is actually firing with a new value when you hit delete, and check if its contents are what you expect (the previous list minus the thing you want removed)
You could check it with some logging, but setting some breakpoints and debugging the app might be more helpful if you're not sure where it's going wrong
(also your button doesn't need to call notifyDataSetChanged() - that only needs to happen when the data is updated, which happens through the update function, in there is the right place for it!)
i tried to use the path "button click -> UI sends delete event to VM -> VM updates data -> observer sees new data -> observer calls update with new data" as #cactuctictacs mentioned. I added this lines to my adapter,
lateinit var onDeleteItemClick : ((Vocabulary) -> Unit)
holder.binding.deleteButtonRV.setOnClickListener {
onDeleteItemClick.invoke(favouritesList[position])
notifyItemRemoved(position)
}
and added to my fragment,
fun deleteButtonClicked(){
favouritesAdapter.onDeleteItemClick = {
viewModel.deleteVocabulary(it)
viewModel.getAllVocabulariesFromDB()
observeFavouritesLiveData()
favouritesAdapter.notifyDataSetChanged()
}
}
I hope this is the proper way to do this.

How to update fragmentStateAdapter after a bottomSheet dismiss

I have this fragmentStateAdapter which holds some of my Cards in order to swipe among them:
public class ScreenSlidePagerAdapter(activity: AppCompatActivity, var items: ArrayList<Helpers.Card>, private val saveToDb: Boolean) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = items.count()
var fragment:CardItemFragment?=null
override fun createFragment(position: Int): Fragment{
fragment =CardItemFragment(position, saveToDb ,this) //.newInstance( items[position ].getSaveString())
return fragment as CardItemFragment
}}
and the CardItemFragement is:
class CardItemFragment(val position:Int, val saveToDb: Boolean, val isCardMine: Boolean,val adapter: ScreenSlidePagerAdapter) : Fragment() {
lateinit var cardView1: ConstraintLayout
lateinit var crdNameTB1: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.card_item, container, false)
cardView1 = v.findViewById(R.id.cardView1)
crdNameTB1= v.findViewById(R.id.crdNameTB1)
return v
}
Then I can update the Card inside the fragementStateAdapter like this:
fun updateCard(): Bitmap? {
crdNameTB1.text = .....
}
That's works fine and I can swipe among my Cards, but when I change the value of "name" attribute of my Card via a bottom sheet and when I dismiss the BottomSheet I use this code to refresh the fragmentSatateAdapter:
MainActivity.galleryPagerAdapter?.fragment ?.updateCard()
I set MyActivity as companion object to reach it anywher.
When this code executed, I got the fragment not null object but the updateCard gives me null objects
1what is the problem and how can I overcome this and update the shown Card?
I have found the answer by overriding the following methods in order to make the Adapter run
override fun getItemId(position: Int): Long {return items[position].id}
override fun containsItem(itemId: Long): Boolean = items.any { it.id == itemId }
the answer post is here

Shared element does not return to RecyclerView item

I have a fragment called MainFragment that contains a ViewPager that contains another fragment called LibraryFragment.
LibraryFragment contains a RecyclerView with a list of items that contain an ImageView. The ImageView's contents are loaded with Coil.
When an item is clicked, LibraryFragment navigates to another fragment called ArtistDetailFragment and uses the ImageView from the item.
The problem is that while the enter transition works fine, the ImageView does not return to the list item when navigating back and only fades away. Ive attached an example below:
Ive tried using postponeEnterTransition() and startPostponedEnterTransition() along with adding a SharedElementCallback but neither have worked that well. I've also ruled out Coil being the issue.
Heres LibraryFragment:
class LibraryFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels()
private lateinit var binding: FragmentLibraryBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
return binding.root
}
private fun navToArtist(artist: Artist, itemBinding: ItemArtistBinding) {
// When navigation, pass the artistImage of the item as a shared element to create
// the image popup.
findNavController().navigate(
MainFragmentDirections.actionShowArtist(artist.id),
FragmentNavigatorExtras(
itemBinding.artistImage to itemBinding.artistImage.transitionName
)
)
}
}
Heres ArtistDetailFragment:
class ArtistDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentArtistDetailBinding.inflate(inflater)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
val musicModel: MusicViewModel by activityViewModels()
val artistId = ArtistDetailFragmentArgs.fromBundle(requireArguments()).artistId
// Get the transition name used by the recyclerview ite
binding.artistImage.transitionName = artistId.toString()
binding.artist = musicModel.artists.value?.find { it.id == artistId }
return binding.root
}
}
And heres the RecyclerView Adapter/ViewHolder:
class ArtistAdapter(
private val data: List<Artist>,
private val listener: BindingClickListener<Artist, ItemArtistBinding>
) : RecyclerView.Adapter<ArtistAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
// Generic ViewHolder for an artist
inner class ViewHolder(
private val binding: ItemArtistBinding
) : RecyclerView.ViewHolder(binding.root) {
// Bind the view w/new data
fun bind(artist: Artist) {
binding.artist = artist
binding.root.setOnClickListener { listener.onClick(artist, binding) }
// Update the transition name with the new artist's ID.
binding.artistImage.transitionName = artist.id.toString()
binding.executePendingBindings()
}
}
}
EDIT: I used postponeEnterTransition and startPostponedEnterTransition like this:
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
This only makes the RecyclerView itself refresh however, the shared element still fades away instead of returning to the RecyclerView item.
Chris Banes says;
You may wonder why we set the OnPreDrawListener on the parent rather than the view itself. Well that is because your view may not actually be drawn, therefore the listener would never fire and the transaction would sit there postponed forever. To work around that we set the listener on the parent instead, which will (probably) be drawn.
https://chris.banes.dev/fragmented-transitions/
try this;
change
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
enter code here
to
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
postponeEnterTransition()
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
(view?.parent as? ViewGroup)?.doOnPreDraw {
// Parent has been drawn. Start transitioning!
startPostponedEnterTransition()
}
return binding.root
}

Kotlin: spinner onItemSelectedListener from another fragment

i have a fragment with a BottomNavigationView, a Spinner and a FrameLayout, in the FrameLayout appears a a new fragment with the BottomNavigationView.setOnNavigationItemSelectedListener, this is my code:
Fragment ValcuotaEvolFragment
class ValcuotaEvolFragment: Fragment() {
lateinit var fragment : Fragment
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_valcuota_evol, container, false)
val menuBottom: BottomNavigationView = root.findViewById(R.id.nav_view_valcuota_evol)
val spn : Spinner = root.findViewById(R.id.spnAFP)
val db = DataBaseHandler(activity!!.applicationContext)
val afpListName : ArrayList<String> = db.getAFPNames()
fragment= ValcuotaChartFragment()
val bundle = Bundle()
spn.adapter= ArrayAdapter<String>(activity!!.applicationContext,android.R.layout.simple_spinner_dropdown_item,afpListName)
spn.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
bundle.putString("afp",spn.selectedItem.toString())
}
override fun onNothingSelected(parent: AdapterView<*>) { }
}
menuBottom.setOnNavigationItemSelectedListener {
menuItem ->
when(menuItem.itemId){
R.id.nav_evolcuota_chart -> {
fragment = ValcuotaChartFragment()
}
R.id.nav_evolcuota_data -> {
fragment = ValcuotaDataFragment()
}
}
fragment.setArguments(bundle);
val transaction = childFragmentManager.beginTransaction()
transaction.replace(R.id.frame_valcuota_evol, fragment)
transaction.addToBackStack(null)
transaction.commit()
true
}
fragment.setArguments(bundle);
val transaction = childFragmentManager.beginTransaction()
transaction.replace(R.id.frame_valcuota_evol, fragment)
transaction.addToBackStack(null)
transaction.commit()
return root
}
}
I pass to the new fragment the value "afp" through a Bundle, now i need the new fragment to do something different depending on what I select in the spinner of ValcuotaEvolFragment
this is what i need:
class ValcuotaDataFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_valcuota_data, container, false)
val afp = arguments!!.getString("afp")
if(afp == "something"){
doSomething()
else {
doSomethingElse
}
return root
}
}
this actually works, but only when i change the item in the BottomNavigationView i need this works when change the item in the Spinner, thx
EDIT
The EventBus solution works fine , but now i have a new problem in ValcuotaDataFragment i have a RecyclerView, so now i need fill the RecyclerView after change the item in the Spinner, this is how i do it now:
val rcViewValcuota = root. findViewById(R.id.rc_valcuota_data) as RecyclerView
var valcuota : MutableList<ValcuotaModel>
val db = DataBaseHandler(activity!!.applicationContext)
valcuota = db.getCompleteValCuota(spinnerData.selectedItem,"desc")
rcViewValcuota.adapter= ContentValcuotaMonthlyAdapter(valcuota)
i can't access the "root" from the function listenItemChange
Okay, so when you're selecting a different item from the spinner, your fragment is not aware of it unless you pass data to fragment. So for informing the fragment, you can implement the interface if you'd like. Or my favorite you can use EventBus library to pass the data.
I'll show you how you can implement EventBus.
First, add EventBus Dependency is the Gradle file:
implementation 'org.greenrobot:eventbus:3.1.1'
Okay now create a data class for passing data say SpinnerData:
data class SpinnerData(val selectedItem:String)
Then inside your itemSelected listener, pass the data using EventBus:
spn.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
// bundle.putString("afp",spn.selectedItem.toString())
//insted add following line
EventBus.getDefault().post(SpinnerData(spn.selectedItem.toString()))
}
override fun onNothingSelected(parent: AdapterView<*>) { }
}
Then inside your ValcuotaDataFragment add the following:
#Subscribe
fun listenItemChange(spinnerData: SpinnerData){
if (spinnerData.selectedItem == "something") {
doSomething()
} else {
doSomethingElse()
}
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
Now, whenever you change the spinner item Evenbus will be triggered and pass the data to the Subscribed method inside your fragment.
Hope this helps, let me know if you get stuck somewhere.
Edit:
This won't work if your fragment isn't initialized already.
SO keep your line inside your itemSelected listener for first time use:
bundle.putString("afp",spn.selectedItem.toString())

Custom view doesn't update inside a Fragment

I have a project in which I have to build views inside a custom Layout. This layout represents the concept of View in MVP architecture and it lives in a Fragment. The view should be updated by the Presenter whenever an event happens, by calling the View and finally that will update TextViews inside the View. But it seems that after the View is initialized, nothing gets updated anymore.
If my presenter calls the View that contains my TextView - nothing. If I try to update the TextView directly, from the fragment then it works. I can't really understand what is happening and why it doesn't get updated from within the layout that contains that TextView.
MyCustomView:
class MyCustomView(fragment: MyFragment): MyViewInterface, FrameLayout(fragment.context) {
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
override fun getView(): View {
return this
}
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
}
}
MyFragment:
class MyFragment : Fragment() {
#Inject lateinit var view: MyViewInterface
#Inject lateinit var presenter: MyCustomPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... dagger injection ...
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return this.view.getView()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.setData(...some ID to fetch data from API...)
//textview_name.text = "blue" //this works instead
}
}
MyPresenter:
class MyPresenter #Inject constructor(
private val repo: MyRepository,
private val view: MyViewInterface
) {
fun setData(productCode: String) {
.. some code ...
view.setData(it) //call to my view
}
}
MyViewInterface:
interface MyViewInterface {
fun getView(): View
fun setData(uiModel: UiModel)
}
All I can think of is the view's instance is not the same in your UI and presenter. I dont know your Dagger's code so I do not have any suggestions to fix it.
You could move the view away from MyPresenter's constructor and set it in MyFragment.onCreate after injection.
Because of you just inflate when create view
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
add invalidate view in update data function
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
this.invalidate()
this.requestLayout()
}

Categories

Resources