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
Related
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.
I'm working on an android project in kotlin and while trying to add an OnTouchListener to several buttons, I have run into a problem: it cannot be done from XML and I want to keep my backing code clean. After a bit of research, I found out that I could add the XML support by using a method with the #BindingAdapter annotation:
#BindingAdapter("onTouch")
fun Button.setXMLTouchListener(listener : View.OnTouchListener)
{
this.setOnTouchListener(listener);
}
to this method:
class MainActivity : AppCompatActivity()
{
...
...
fun goLeft(v : View, event : MotionEvent) : Boolean
{
// my code
}
}
and in the XML:
<layout
...>
<data>
<variable name="main_activity" type="my.path.to.MainActivity" />
</data>
<androidx.constraintLayout.widget.ConstraintLayout
...>
<Button
...
app:onTouch="#{main_activity.goLeft}" />
...
</androidx.constraintLayout.widget.ConstraintLayout>
</layout>
and enabled data binding in the build.gradle:
apply plugin: 'kotlin.kapt'
and
android {
...
dataBinding {
enabled = true
}
}
This obviously didn't work, these are the solutions I have tried:
move the #Bindingadapter function from companion object to top level, so it's compiled static
try the app:onTouch contents as "main_activity.goLeft" (seen in a tutorial), "main_activity.goLeft()" (original try), and "main_activity::goLeft" (suggested by the compiler as the first is deprecated)
add logging to the click event to ensure the button receives events at all
change the value of the annotation to "app:onTouch" to be absolutely sure it's in the right xml namespace
move the touch listener function to a class that is non-activity and implements View.OnTouchListener (and renamed function accordingly)
After a bit of debugging, I also found out that the binding function doesn't run at all.
What could be the problem, and how can I solve it?
first write your data binding adapter like this.
#BindingAdapter("app:onTouch")
fun setXMLTouchListener(btn : Button , listener : View.OnTouchListener)
{
btn.setOnTouchListener(listener)
}
then chenge the goLeft() fun to it
val goLeftListener = View.OnTouchListener { v, event ->
Log.d("goLeftListener " , "it Worked !")
return#goLeftListener true
}
and don't forget to set activity on your binding object in onCreate fun
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: mainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.main_activity = this
}
and for the last step write onTouch attribute of Button in your xml layout like this
<Button
.
.
app:onTouch="#{main_activity.goLeftListener}"
.
/>
I have a view and viewmodel which has 2 functionalities -
1) Clicking a button in view and getting data.
2) A Spinner where you can select an item and ask the viewmodel to get data for that item as a query parameter.
I already implemented the first point like this -
MyView code -
viewModel.onGetDataClicked.observe(this, Observer {
//....
})
My ViewModel code -
private val viewState = MyViewState()
val onGetDataClicked =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
when (it) {
....
}
}
MyAction code -
sealed class MyAction : Action {
object GetDataAction : MyAction()
}
My question is how do I pass the spinner value from view to the viewmodel? Since in viewmodel I have a val onGetDataClicked and not a function.
First you should get the item value in the view itself, after that pass the item value to the required method in the ViewModel and from the ViewModel to the Repository(where you are querying the data from).
// in view
viewModel.onGetDataClicked(item:DataType).observe(this, Observer {
//....
})
//in viewmodel
private val viewState = MyViewState()
val onGetDataClicked:(item:DataType) =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
//you have item here, pass it where you require
when (it) {
....
}
}
Hi you can use the selectedItemPosition attribute from the view and pass position to viewModel, accordingly you can map the item using the position.
layout.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 class="FeedbackBinding">
<variable
name="vm"
type="com.ec.service.ServiceViewModel" />
</data>
<androidx.appcompat.widget.AppCompatSpinner
android:id="#+id/unitAET"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:selectedItemPosition="#={vm.selectedUnitPosition}"
app:entries="#{vm.unitNames}"
app:layout_constraintEnd_toEndOf="#+id/textView10"
app:layout_constraintStart_toStartOf="#+id/textView10"
app:layout_constraintTop_toBottomOf="#+id/textView10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
the selectedUnitPosition is a MutableLeveData
fragment.kt
In your fragment initialise the vm (viewModel)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
withViewModel<ServiceViewModel>(factory) {
binding.vm = this
}
}
I am trying to use last features from android - Kotlin, mvvm, architecture components, jetpack, databinding, one activity - many fragments approach with new navigation graph, but I am struggling with handling UI events in Fragments
In activity it is simple with kotlin-android-extensions
In XML I create a Button like this:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="clicked"/>
and in Activity I just write
fun clicked(view : View){
}
That's perfect, but unfortunately does not work in Fragment. Yes it is possible to still handle event in Activity and send it to fragment but that's ugly.
Next option is to use an interface,
public interface MyClickCallback{
void onLoginButtonClick();
}
implement it in fragment.
In xml it looks like this:
<variable
name="clickCallback"
type="com.test.MyClickCallback" />
then in fragment's onCreateView I have to set clickCallback to the fragment and finally I can use it
#Override fun onLoginButtonClick() {
}
Problem I have with this is to declare interface and on each new UI event enhance this interface and update fragment which implements it
Next option is RxView.clicks what looks really great with all its features. For example:
RxView.clicks(mSearchBtn)
.throttleFirst(2, TimeUnit.SECONDS)
.map(aVoid -> mSearchEdit.getText().toString().trim())
.filter(s -> !TextUtils.isEmpty(s))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
KeyBoardUtil.closeKeybord(mSearchEdit,
SearchActivity.this);
showSearchAnim();
clearData();
content = s;
getSearchData();
});
Problem here is that I have to bind it to the UI component - mSearchBtn. I do not want this :-). I do not want to have any UI component in fragment unless I really have to. I am always communicating with layout file via variables declared in layout like this
<data>
<variable
name="items"
type="java.util.List" />
</data>
I would love to bind it to variable declared in the XML which is set in Button
android:onClick="myclick"
But I did not find the way how to do it.
Anybody can help me maybe with other simple and nice options ?
In your databinding layout create a variable that is of type View.OnClickListener:
<variable
name="onClickListener"
type="android.view.View.OnClickListener" />
Set it to your View like this:
<View
...
android:onClickListener="#{onClickListener}"
... />
In your Fragment create the onClickListener and set it to the variable:
binding.onClickListener = View.OnClickListener {
/* do things */
/* like getting the id of the clicked view: */
val idOfTheClickedView = it.id
/* or get variables from your databinding layout: */
val bankAccount = binding.bankAccount
}
Or in Java:
binding.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
/* do things */
/* like getting the id of the clicked view: */
Int idOfTheClickedView = view.getId();
/* or get variables from your databinding layout: */
Object bankAccount = binding.getBankAccount()
}
});
it is simple with kotlin-android-extensions
It is indeed simple, but you are currently not using it to its fullest potential.
Setting click listeners in Kotlin is very easy, look:
fun View.onClick(clickListener: (View) -> Unit) {
setOnClickListener(clickListener)
}
And now thanks to synthetic imports in Kotlin-Android-Extensions:
<Button
android:id="#+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="#string/click_me"/>
and
import kotlinx.synthetic.blah.* // something like that
// Activity:
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
setContentView(R.layout.blah)
myButton.onClick {
// handle click event
}
}
// Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?) = inflater.inflate(R.layout.blah, container, false)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
myButton.onClick {
// handle click event
}
}
But if you really want to use databinding and layouts for this, then set the callback lambda and inside the databinding layout file.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="activity" type="com.acme.MainActivity"/>
</data>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/btnOpenSecondView"
android:text="Click me for second view!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:onClick="#{(v) -> activity.startNextActivity(v)}" />
</RelativeLayout>
</layout>
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