Pleas can someone tell me why onSelect is not working when i click in something?
i passed many hours trying to solve this but cant find a good solution
RecyclerView fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerviewCriptomoedasMenu.adapter = CryptoCardListAdapter(
cryptoCards(),
object : CryptoCardListAdapter.OnSelectOnClickListener {
override fun onSelect(position: Int) {
when (cryptoCards()[position].coinTitle) {
"Bitcoin" -> {
val direction =
InvestimentosFragmentDirections.actionInvestimentosFragmentToAddFragment(
cryptoCards()[position]
)
findNavController().navigate(direction)
}
else -> {
val direction =
InvestimentosFragmentDirections.actionInvestimentosFragmentToAddFragment(
cryptoCards()[position]
)
findNavController().navigate(direction)
}
}
}
})
}
It is hard to detect your problem while lacking of your adapter implementation for the onSelect callback inside the listener. But there are some samples for handling on item click for Recyclerview that you can reference.
From Udacity
From official Android
Related
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.
I am using Navigation component to navigate between two Fragments. The landing fragment has a recycler view and the detail fragment has a view pager. I am using a call back listener to trigger navigation action from the recycler view adapter.
The action to be trigger is a zoom event, with the library ZoomHelper ZoomHelper
When the event happens the app crashes with the error above.
However it works well with onclick event listener.
View Holder
class CampaignListViewHolder<T : ViewBinding>(private val binding: T) : RecyclerView.ViewHolder(binding.root) {
var campaignDetails: CampaignDetails? = null
#SuppressLint("ClickableViewAccessibility")
fun bindTo(campaignDetails: CampaignDetails?, position: Int, listener: ItemZoomListener) {
this.campaignDetails = campaignDetails
binding as CampaignItemLayoutBinding
binding.campaignNameTv.text = campaignDetails?.name
binding.campaignImageIv.load(campaignDetails?.image?.url) {
crossfade(true)
placeholder(R.drawable.westwing_placeholder)
}
ViewCompat.setTransitionName(binding.campaignImageIv, campaignDetails?.name)
binding.root.setOnClickListener {
if (campaignDetails != null) {
listener.navigate(position)
}
}
ZoomHelper.addZoomableView(binding.campaignImageIv)
ZoomHelper.getInstance().addOnZoomScaleChangedListener(object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
listener.navigate(position)
}
}
})
}
}
Landing Fragment
class LandingFragment : Fragment(R.layout.fragment_landing), ItemZoomListener, FragmentUiStateListener {
private val TAG by lazy { getName() }
private val binding by viewBinding(FragmentLandingBinding::bind)
private val campaignListViewModel: CampaignListViewModel by activityViewModels()
lateinit var campaignViewAdapter: CampaignListViewAdapter
lateinit var activityUiState: ActivityUiStateListener
lateinit var fragmentUiUpdate: FragmentUiStateListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
campaignViewAdapter = CampaignListViewAdapter(this)
fragmentUiUpdate = this
}
override fun onResume() {
super.onResume()
setupView()
setUpData()
}
private fun setUpData() {
setUpUiState(campaignListViewModel.campaignUiState, fragmentUiUpdate)
}
private fun setupView() {
val orientation = checkOrientation()
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.campaignLandscapeRcv?.apply {
layoutManager = GridLayoutManager(requireContext(), 2)
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 2))
}
} else {
binding.campaignListRcv?.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 1))
}
}
}
override fun navigate(position: Int) {
val direction = LandingFragmentDirections.actionLandingFragmentToCampaignDetailsFragment(position)
goto(direction)
}
I understand that one of the reason for the error is probably the zoom event calling the navigation controller multiple times but I can not figure how debug that and what could be a way around this.
As you guessed, the issue is most likely caused by the navController being fired multiple times. You can handle this by "navigating safely". Here's a sample implementation below:
fun NavController.safelyNavigate(#IdRes resId: Int, args: Bundle? = null) =
try { navigate(resId, args) }
catch (e: Exception) { Timber.e(e) }
}
You can then make your navigation call like this:
findNavController().safelyNavigate(your_id)
This way, any extra call to NavController.navigate() gets absorbed in the try and catch. Crash prevented :D
If someone had the same issues due to multiple clicks on the screen. It can be resolved by checking the current destination first before navigating
For example
Fragments A, B, and C
navigating from A to B while clicking on a button in fragment A that navigates to C might lead to crashes in some cases
for that you should check the current destination first as follows:
if(findNavController().currentDestination?.id==R.id.AFragment)
findNavController().navigate(
AFragmentDirections.actionAFragmentToCFragment()
)
If it is being called twice, then you can remove the 'ZoomHelper.OnZoomScaleChangedListener' before the navigation occurs.
I haven't tested the code below, but you can explore the library's code here https://github.com/Aghajari/ZoomHelper/blob/master/ZoomHelper/src/main/java/com/aghajari/zoomhelper/ZoomHelper.kt. You will find a method called 'removeOnZoomScaleChangedListener' which from what I understand, will remove a listener of type 'ZoomHelper.OnZoomScaleChangedListener'.
Ex.
val onZoomScaleChangedListener = object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
ZoomHelper.getInstance().removeOnZoomScaleChangedListener(onZoomScaleChangedListener) // Remove this event listener to avoid multiple triggers as you only need 1.
listener.navigate(position)
}
}
}
ZoomHelper.getInstance().addOnZoomScaleChangedListener(onZoomScaleChangedListener) // You can add the listener above like this.
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!
I am working with the MVVM architecture.
The code
When I click a button, the method orderAction is triggered. It just posts an enum (further logic will be added).
ViewModel
class DashboardUserViewModel(application: Application) : SessionViewModel(application) {
enum class Action {
QRCODE,
ORDER,
TOILETTE
}
val action: LiveData<Action>
get() = mutableAction
private val mutableAction = MutableLiveData<Action>()
init {
}
fun orderAction() {
viewModelScope.launch(Dispatchers.IO) {
// Some queries before the postValue
mutableAction.postValue(Action.QRCODE)
}
}
}
The fragment observes the LiveData obj and calls a method that opens a new fragment. I'm using the navigator here, but I don't think that the details about it are useful in this context. Notice that I'm using viewLifecycleOwner
Fragment
class DashboardFragment : Fragment() {
lateinit var binding: FragmentDashboardBinding
private val viewModel: DashboardUserViewModel by lazy {
ViewModelProvider(this).get(DashboardUserViewModel::class.java)
}
private val observer = Observer<DashboardUserViewModel.Action> {
// Tried but I would like to have a more elegant solution
//if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED)
it?.let {
when (it) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.action.observe(viewLifecycleOwner, observer)
// Tried but still having the issue
//viewModel.action.reObserve(viewLifecycleOwner, observer)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
// Tried but still having the issue
//viewModel.action.removeObserver(observer)
}
private fun navigateToQRScanner() {
log("START QR SCANNER")
findNavController().navigate(LoginFragmentDirections.actionLoginToPrivacy())
}
}
The problem
When I close the opened fragment (using findNavController().navigateUp()), the Observe.onChanged of DashboardFragment is immediately called and the fragment is opened again.
I have already checked this question and tried all the proposed solutions in the mentioned link (as you can see in the commented code). Only this solution worked, but it's not very elegant and forces me to do that check every time.
I would like to try a more solid and optimal solution.
Keep in mind that in that thread there was no Lifecycle implementation.
The issue happens because LiveData always post the available data to the observer if any data is readily available. Afterwords it will post the updates. I think it is the expected working since this behaviour has not been fixed even-though bug raised in issue tracker.
However there are many solutions suggested by developers in SO, i found this one easy to adapt and actually working just fine.
Solution
viewModel.messagesLiveData.observe(viewLifecycleOwner, {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
//Do your stuff
}
})
That's how LiveData works, it's a value holder, it holds the last value.
If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this
class ConsumableValue<T>(private val data: T) {
private val consumed = AtomicBoolean(false)
fun consume(block: ConsumableValue<T>.(T) -> Unit) {
if (!consumed.getAndSet(true)) {
block(data)
}
}
}
then you define you LiveData as
val action: LiveData<ConsumableValue<Action>>
get() = mutableAction
private val mutableAction = MutableLiveData<ConsumableValue<Action>>()
then in your observer, you'd do
private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
it?.consume { action ->
when (action) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
UPDATE
Found a different and still useful implementation of what Frances answered here. Take a look
I'm trying to use the new paging library for Android coding in Kotlin but am really stuck at the moment. My backend uses post method for connecting with the api calls and I'm trying to adapt the tutorials I've found using get but not being successful so far. Any help much appreciated indeed.
That's how my adapter is being called from my Fragment class but it's being always null.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setVerticalRecyclerView(rv_resources)
val itemViewModel = ViewModelProviders.of(this).get(ItemViewModel::class.java)
val adapter = ResourcesAdapter(activity as MainActivity)
itemViewModel.itemPagedList.observe(this, object : Observer<PagedList<Resource>> {
override fun onChanged(items: PagedList<Resource>?) {
adapter.submitList(items)
}
})
rv_resources.adapter = adapter
}
I feel the problem is probably coming from here:
class ItemDataSource : PageKeyedDataSource<Int, Resource>() {
override fun loadInitial(params: PageKeyedDataSource.LoadInitialParams<Int>, callback: PageKeyedDataSource.LoadInitialCallback<Int, Resource>) {
getResources()
}
override fun loadBefore(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, Resource>) {
getResources()
}
override fun loadAfter(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, Resource>) {
getResources()
}
private fun getResources() {
val jo = JsonObject()
jo.addProperty("page", 0)
jo.addProperty("page_size", 10)
GetAllResourceListAPI.postData(jo, object : GetAllResourceListAPI.ThisCallback {
override fun onSuccess(getResourceList: GetResourceList) {
Toast.makeText(App.getContext(), "onSuccess ${getResourceList.count}", Toast.LENGTH_SHORT).show()
}
override fun onFailure(failureMessage: String) {
Toast.makeText(App.getContext(), "onFailure", Toast.LENGTH_SHORT).show()
}
override fun onError(errorMessage: String) {
Toast.makeText(App.getContext(), "onError", Toast.LENGTH_SHORT).show()
}
})
}
Initially I'm trying to show any page within my adapter that's why pasted the same codes for loadInitial, loadBefore and loadAfter trying to tackle a problem at time if possible as currently my adapter shows empty even though I get a success from my api call. I may be missing something pretty obvious here but just can't see it as it's my first time using pagination and not very familiar with observers either.
I have a gist with a bit more of my code created here
Thanks very much for your help.