Show popup menu for row in recyclerview using databinding - android

I am trying to show a popup menu for the items in my RecyclerView:
All the code samples on how to do this that I found online either use Java or when they in rare cases do use Kotlin, it's done without data binding.
Anyway what I'm trying to achieve is this:
<ImageButton
android:id="#+id/options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="#string/desc_options"
android:onClickListener="#{(v) -> holder.test1.invoke()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_more_vert" />
And in my RowHolder I have the following code:
class AgendaRowHolder(
private val binding: AgendaRowBinding,
val onRowClick: (AgendaModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(model: AgendaModel) {
binding.model = model
binding.holder = this
binding.executePendingBindings()
}
fun test1() {
// do something here
}
}
However this will not compile at all and a workaround to solving this problem is to have a function as a property of the AgendaRowHolder class, but this approach doesn't work since I also have to pass the ImageView for the popup menu to know where to show up. So I used this alternative approach, namely adding an onClickListener in the bind() function:
fun bind(model: AgendaModel) {
binding.model = model
binding.holder = this
binding.executePendingBindings()
binding.options.setOnClickListener { showPopup(it) }
}
private fun showPopup(view : View) {
val popup = PopupMenu(view.context, view)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.delete -> {
AgendaRepository.delete(binding.model!!)
true
} else -> false
}
}
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.actions_agenda, popup.menu)
popup.show()
}
This approach "works" in that it will show a popup menu and allow me to delete items, but I have a new problem here in that when an item/row gets delete it doesn't show up until the view is recreated, right now I'm not observing my data so that might be the reason as to why, but what I'm wondering is what would be the best approach to achieve this? Note: another thing that I tried is adding an android:onClick attribute in the xml and trying to handle this in the MainActivity, but I'm not sure which approach to take here. Can the first one be achieved? Ideally, I'd have:
android:onClickListener="#{(v) -> holder.showPopup.invoke(model, v)}" and then that showPopup function in the ViewHolder.

Related

Exposed Dropdown Menu not showing items

Exposed Dropdown Menu doesn't show items after user selection and fragment transition.
Following is the basic xml declaration:
<com.google.android.material.textfield.TextInputLayout
...
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
<AutoCompleteTextView
....
android:id="#+id/dropdown"
android:dropDownHeight="300dp"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
And, the declared code on the fragment (inside onViewCreated()):
val items = listOf("Material", "Design", "Components", "Android")
val adapter = ArrayAdapter(requireContext(), R.layout.item_menu, items)
dropdown.setAdapter(adapter)
dropdown.setText(items[0], false)
As mentioned here, it was set on AutoCompleteTextView's setText method (dropdown.setText("", false)) the filter parameter as false. However, after navigating to a next fragment and coming back to it only the pre-selected text is shown on the dropdown.
Fragments are changed using navigation component (v. 2.3.2).
The fragment's view gets destroyed when using the navigation component. (maybe not always, but it will certainly happen some of the time as you experienced)
I think you might be able to make it work simply by adding a condition:
if (savedInstanceState == null) {
dropdown.setText(items[0], false)
}
So that the default is only set when not restoring the view state.
Otherwise it's just a matter saving the state as usual. Here's a documentation article about it if you're unsure what I'm talking about. It will essentially amount to adding the following code to your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val item = savedInstanceState?.getInt("selectedPos", 0) ?: 0
dropdown.setText(items[item], false)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("selectedPos", dropdown.getListSelection())
}
If you're using the MVVM architecture, you can save the selected position using SavedStateHandle in your ViewModel, when it gets changed.
I had the same problem. I searched for issues on github page. I found this https://github.com/material-components/material-components-android/issues/2012#issuecomment-808853621 work around for now. It works.
Create an extension like below
fun AutoCompleteTextView.showDropdown(adapter: ArrayAdapter<String>?) {
if(!TextUtils.isEmpty(this.text.toString())){
adapter?.filter?.filter(null)
}
}
Then on click of dropdown
binding.quaters.setOnClickListener {
binding.quaters.showDropdown(arrayAdapter)
}
That's all it should work. This seems to be a bug which should be fixed hopefully.
This is a temprorary solution that is working for me -
https://github.com/material-components/material-components-android/issues/2012#issuecomment-868181589
Write the setup code for ExposedDropdownMenu in onResume() of a fragment,
instead of onCreateView()/onViewCreated()
override fun onResume() {
super.onResume()
val sortingArtist = resources.getStringArray(R.array.sortingArtist)
val arrayAdapterArtist = ArrayAdapter(requireContext(), R.layout.dropdown_items_artist, sortingArtist)
binding?.autoCompleteTextViewArtist?.setAdapter(arrayAdapterArtist)
binding?.autoCompleteTextViewArtist?.setText(sortingArtist[0], false)
}
For reference - https://material.io/components/menus/android#exposed-dropdown-menus

androidx.navigation multiple destinations for menu item

I have a single activity app using the androidx navigation library. For one of the menu destinations I effectively have a fragment as destination with no view whatsoever that depending on the state of the user provided configuration either redirects to the real destination that should be there or to one of currently two different views that tell the user that either he needs to setup a configuration first or that there currently is no active configuration (deleted?) and he needs to select one of the available configurations.
Now, functionally this approach works perfectly fine. However, since androidx navigation ties menu items to destinations by id the menu item that gets you to that view is never selected as it matches the fragment destination with no view in it.
I tried to add a NavController.OnDestinationChangedListener to my Activity and added it to the navController navController.addOnDestinationChangedListener(this). But it seems to get overwritten by the navigation afterwards.
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
if(destinations.contains(destination.id)) {
nav_view.menu.getItem(0).isChecked = true
}
}
It is deffinitely the right menu item. As when I change isChecked = true to isEnabled = false I can no longer click on it.
Also when I do this odd hack it works
GlobalScope.launch(Dispatchers.Main) {
delay(1000)
nav_view.menu.getItem(0).isChecked = true
}
Needless to say this is not a very good solution.
Anyone here knows how to overwride the default behaviour of androidx navigation in this regard?
I´ll come back to this later and report back if I find a proper solution to this.
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
Instead of using setupWithNavController(), as mentioned in the documentation, setup it up yourself.
As mentioned here, onNavDestinationSelected() helper method in NavigationUI is called when the menu item is clicked when you set it up using setupWithNavController(). So you could try something like this:
yourNavigationView.setNavigationItemSelectedListener { item: MenuItem ->
if(item.itemId == R.id.noViewFragmentId) {
val isConfigurationProvided = ...
if(!isConfigurationProvided) {
//Perform your actions (navigate to either of the two alternate views)
return#setNavigationItemSelectedListener true
}
}
val success = NavigationUI.onNavDestinationSelected(item, navController)
if(success) {
drawerLayout.closeDrawer(GravityCompat.START)
item.isChecked = true
}
success
}
I´ll add this as a possible solution and stick with it for the time being. I still feel like there should be a better way to do this, so I will not accept it as an awnswer.
It´s essentially the idea I got at the end of writing the question
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
class SetActiveMenuDrawerListener(
private val navController: NavController,
navigationView: NavigationView) : DrawerLayout.DrawerListener {
private var checked = false
private val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
private val menu = navigationView.menu.getItem(0)
init {
navController.addOnDestinationChangedListener { _, _, _ -> checked = false }
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerStateChanged(newState: Int) {
if(checked) return
val currentDestination = navController.currentDestination ?: return
if(destinations.contains(currentDestination.id)) {
menu.isChecked = true
}
checked = true
}
}
Then add this to the DrawerLayout
drawer_layout.addDrawerListener(SetActiveMenuDrawerListener(navController, nav_view))
I did add the code into the onDrawerStateChanged instead onDrawerOpened, because onDrawerOpened gets called a bit late if clicking the drawer and not at all while dragging it.
It´s not the pretties thing to look at, but it gets the job done.

Custom click event with android data binding

i want to set a certain action (like preventing multiple click) on every click event in data binding , in other phrase when a user click on each view, first do a specific action and after that do action relevant to clicked view(different for each view). How can I do this?
description: i implement MVVM and use databinding
This is what I do in this situation.
First: add onclick in your xml that call method on view model and pass it view
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="model"
type="....ViewModel" />
</data>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="#{(v)-> model.onClick(v)}"/>
</layout>
Second: adding prevent double click with kotlin extensions
Kotlin:
fun View.preventDoubleClick() {
isClickable = false
Handler().postDelayed({ isClickable = true },500L)
}
Third:
Kotlin:
fun onClick(view: View?){
view?.preventDoubleClick()
}
now you have access to your view that clicked in view model.
remember make your view nullable. this help you when for example you want add unit test for your method you can just send view null.
First: create a mutableLiveData of type boolean in your SomeViewModel class with initial value to true
val data = MutableLiveData<Boolean>(true)
next in your xml
<data>
<variable
name="viewModel"
type="..SomeViewModel" />
</data>
<View
android:enabled = "#{viewModel.data}" // if working with button
android:clickable = "#{viewModel.data}" // for views which dont have enable tag
android:onClick="#{() -> viewModel.disableButtonAndPerformRequiredAction()}"/>
// In viewmodel
fun disableButtonAndPerformRequiredAction() {
data.value = false // it will disable the click for the view
// Perform other tasks
// post executing required task set
data.value = true // it will again enable the click for the view
}
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78

Android expandable cardView for existing recyclerView adapter

Alrighty, So I have an existing custom recycler view adapter that populates a recycler view using provided items, and sets attributes in a layout i have as follows, ill try to remove irrelevant items from code
class TransactionAdapter(val context: Context, var transactions: List<Transaction>) :
androidx.recyclerview.widget.RecyclerView.Adapter<TransactionAdapter.CustomViewHolder>() {
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): CustomViewHolder {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.transaction_list_inner_view, p0, false)
return CustomViewHolder(view)
}
override fun onBindViewHolder(p0: CustomViewHolder, p1: Int) {
p0.transactionNameTextView?.text = transactions[p1].title
p0.transactionAmountTextView?.text = transactions[p1].amount
if (!transactions[p1].location.isNullOrEmpty()) {
p0.transactionLocationTextView?.text = transactions[p1].location
} else {
p0.transactionLocationTextView?.text = "N/A"
}
p0.transactionTimeTextView?.text = transactions[p1].createdAt
p0.transactionDeleteButton?.setOnClickListener { println("working delete") }
}
class CustomViewHolder(v: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(v) {
val transactionNameTextView: TextView? = v.findViewById(R.id.transactionNameTextView) as TextView?
val transactionAmountTextView: TextView? = v.findViewById(R.id.transactionAmountTextView) as TextView?
val transactionLocationTextView: TextView? = v.findViewById(R.id.transactionLocationTextView) as TextView?
val transactionTimeTextView: TextView? = v.findViewById(R.id.transactionTimeTextView) as TextView?
val transactionDeleteButton: AppCompatButton? = v.findViewById(R.id.transactionDeleteButton) as AppCompatButton?
}
}
Now, I want to implement this library to use instead expandable card views so that I can only show for example the name and amount of transactions, and have the rest be expanded, this library can be found here https://github.com/AleSpero/ExpandableCardView , the library asks me to make a new layout with an inner_view attribute, I have done so, my layout is called transaction_list_expandable_view.xml and it looks like this
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="ExtraText">
<com.alespero.expandablecardview.ExpandableCardView
android:id="#+id/transaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:title="testt"
app:inner_view="#layout/transaction_list_inner_view"
app:expandOnClick="true"
app:animationDuration="300"
app:startExpanded="false"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Now comes the problem, my custom TransactionAdapter only handles one layout, which is the one I called transaction_list_inner_view, how can I have this adapter instead handle both inner and expandable views and get the desired result? (a list of cards with relevant titles that expands to reveal the rest of the details belonging to them)
Sorry for the long question and code, thanks in advance for any help.
After checking the code of the library you're using, i think that you shouldn't be inflating the inner view manually (in your adapter) as that's the responsibility of the ExpandableCardView you should inflate the transaction_list_expandable_view.xml in your adapter.
It would look like :
val view = inflater.inflate(R.layout.transaction_list_expandable_view, p0, false)
For populating your inner view, i'm not sure whether the inner view is already inflated in the expandable one at the time of instantiating the CustomViewHolder.
if it's the case just the switching of inner => expandable views above should do the trick.

Kotlin databinding with extension methods

I'm trying to use Kotlin extension methods inside Android's databinding. For example; calling an onclick handler. So I've made this code:
posttest_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
<import type="android.view.View"/>
<import type="com.example.test.post.posttest.PostTestItemViewModelExtensionKt" />
<variable
name="viewModel"
type="com.example.test.post.posttest.PostTestItemViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:clickable="true"
android:onClick="#{(view) -> viewModel.clicked(view)}"
>
[...]
PostTestItemViewModel.kt
open class PostTestItemViewModel : ViewModel() {
val postTitle = MutableLiveData<String>()
val postBody = MutableLiveData<String>()
/**
* Binds the required properties/entities to this ViewModel
*/
fun bind(post: Post) {
postTitle.value = post.title
postBody.value = post.body
}
}
PostTestItemViewModelExtension.kt
fun PostTestItemViewModel.clicked(v: View) {
this.postTitle.value = "clicked"
}
So when I place the clicked method inside the viewmodel, it works perfectly the way it should be. However, when I create it as an extension method, I get the following error on compilation:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
cannot find method clicked(android.view.View) in class ...PostItemViewModel
I've tried different things already, such as changing the android:onclick tag to PostTestItemViewModelExtensionKt instead of viewModel. Unfortunately all the things don't seem to work. So it looks like the extension method is getting generated after the databinding takes place. Is there a way around this or am I still doing something wrong? Or is it just not possible to bind extension methods?
I'm using Kotlin version 1.2.71, gradle 3.2.0 and have the databinding { enabled = true } and kapt { generateStubs = true } added to my .gradle, and have the plugings kotlin-android, kotlin-android-extensions and kotlin-kapt defined.
Unfortunately you can't use extension methods as onClick callbacks.
Extension methods in Kotlin are created as Java static methods while the Android framework is expecting an instance method.
Note that in Android Studio you can decompile the Kotlin classes as Java to see the generated Java code.
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78

Categories

Resources