Simple way how to handle UI events inside fragment (onClick, ...) - android

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>

Related

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

How can an Android activity access the instance if its current View?

How can an activity access the instance of the view, after it's been set using SetContentView? I need this because the activity uses a custom view which includes logic and I need this view to sent events to the activity, through a custom event listener that the activity needs to set in the view.
I'm programming with android studio in kotlin.
I previously had all the UI control logic in the activity so I was fine, but I am factoring some UI code in a Custom View to re-use it in several activities.
Here is the initialization of the activity
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.custom_view)
// Here need to access the view instance
*xxxxxxx*.setCustomViewListener(new CustomView.MyCustomViewListener() {
#Override
public void onCancelled() {
// Code to handle cancellation from the view controls
}
});)
}
}
Here is the view layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:id="#+id/button_do"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="Do" />
<com.kotlin.app.views.CustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/view_custom" />
</FrameLayout>
Here is the custom view class CustomView.kt
class CustomView : FrameLayout, View.OnClickListener {
constructor(context: Context) : super(context)
init {
}
interface CustomViewListener {
fun onCancelled()
}
private var listener: CustomViewListener? = null
fun setCustomViewListener(listener: CustomerViewListener) {
this.listener = listener
}
Any idea please?
In Kotlin, it's more common to use Kotlin synthetic:
view_custom.setCustomViewListener(...)
Note: you seem to have written your listener implementation in Java, not Kotlin. Since your interface is defined in Kotlin you need something like this:
view_custom.setCustomViewListener(object : CustomView.MyCustomViewListener {
override fun onCancelled() {
...
}
})
SAM interfaces in Kotlin
Personally I like to use lambdas. Unfortunately you cannot use a lambda as a Kotlin SAM interface. You could however use a typealias instead of an interface:
typealias MyCustomerViewListener = () -> Void
Then you could use this instead:
view_custom.setCustomViewListener {
// listener code
}
How can an activity access the instance of the view, after it's been set using SetContentView?
Step #1: Add an android:id attribute to your root <FrameLayout> element.
Step #2: In onCreate() of your activity, after the setContentView() call, call findViewById() to retrieve the FrameLayout based on the ID that you assigned it in Step #1.
the activity uses a custom view which includes logic and I need this view to sent events to the activity, through a custom event listener that the activity needs to set in the view
You could also just call findViewById() and provide the ID of the custom view (findViewById(R.id.custom_view)).
Note that this covered by pretty much any book on Android app development.

Too many XML data bindings

I made a View that I want to reuse across many pages. It contains feedback elements for the user such as a ProgressBar, TextView etc.
Due to high amount of items within, binding all those turns out like this:
<layout ... >
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I find copy/pasting the whole XML block messy and error-prone. Is there any way to make this simpler ?
ScreenStateObserver is just a interface that I implement in my ViewModel and bind as follows:
override fun onCreateView(...): View? {
val factory = InjectorUtils.provideViewModelFactory()
viewmodel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
binding = MyFragmentBinding.inflate(inflater, container, false).apply {
screenObserver = viewmodel
}
}
class AtoZViewModel() : ViewModel(), ScreenStateObserver { ... }
interface ScreenStateObserver {
val isProgressVisible : MutableLiveData<Boolean>
val isTextVisible : MutableLiveData<Boolean>
val isButtonVisible : MutableLiveData<Boolean>
// [..]
}
Thanks !
Here is my suggestion to reduce code.
First declare a class like this
interface ScreenState {
class Loading : ScreenState
class Error(val errorMessage: String, val errorButtonText: String) : ScreenState
}
and inside you CustomView it will be
internal class ScreenStateView {
fun setState(state: ScreenState) {
if (state is ScreenState.Loading) {
// show loading
} else {
// hide loading
}
if (state is ScreenState.Error) {
//show {state.errorMessage} and {state.errorButtonText}
} else {
// hide error
}
}
}
using in xml
<my.namespace.view.ScreenStateView
...
app:state="#{screenObserver.screenState}"
...
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" /> // for onButtonClick I think it still better if we keep like this
Hope it help
You can use <include> in data binding layouts. Included layout file can have its own data and variables that you can access from the main binding class as well.
You have to create a layout file(such as layout_state_view.xml that contains your view and data variables relevant to your view:
<layout>
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</layout>
Now you can include this in your root layout file:
<layout>
<data>
...
</data>
<LinearLayout //Can be any layout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout:="#layout/layout_state_view">
</LinearLayout>
</layout>
Now when you are using binding class, if you root layout file was R.layout.mainActivity then it would look like this:
binding.layoutStateView.setScreenObserver(...)
You can also make a variable in root layout and pass that variable to child layout by using bind tag as mentioned on documentation but since you are looking to reduce code, it would be unnecessary.
Note: Since you only have a single view, you might be tempted to use <merge> tag. Databinding's layout tag does not support merge as a direct child.
Documentation Reference:
https://developer.android.com/topic/libraries/data-binding/expressions#includes
My solution to reduce code is first define class for ScreenStateView(different properties of ScreenStateView in this class) then use it as much times as you needed

Android DataBinding LiveData - not notify changed in DialogFragment & BottomSheetDialogFragment

I have an BottomSheetDialogFragment 's layout like this:
<data>
<variable
name="viewModel"
type="com.sample.MyViewModel" />
</data>
<TextView android:id="#+id/tvValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='#{String.format("%.1f", viewModel.weight)}'/>
<Button android:id="#+id/cmdUpdate"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:onClick="#{() -> viewModel.updateWeight()}"
android:text="#string/update" />
And here is the kotlin code:
// MyViewModel
val weight = MutableLiveData<Double>()
fun updateWeight() {
weight.value?.let {
weight.value = (it + 0.1)
}
}
// BottomSheetDialogFragment bind view model
val myViewModel = ViewModelProviders.of(it, factory).get(MyViewModel::class)
binding.viewModel = myViewModel
// code showing BottomSheet:
val fragment = MyBottomSheetFragment()
fragment.show(fragmentManager, "bottomsheet")
The first time open bottomsheet fragment, it can show the weight value, but when I click on button to update weight, there is nothing happen. From debugger, I can see that the updateWeight method is called, and the weight value is changed, but the TextView is not updated. This also happens on other DialogFragment.
The same code can work if I use normal Fragment
Is there something wrong with DialogFragment & DataBinding?
You need to call
binding.setLifecycleOwner(this)
According to documentation, it allows to update your view when LiveData was changed.

Android Databinding onLongClick not working

I have a text view to which I need to create a listener for onLongClick.
Right now for the respective viewmodel it has a function sendLogs() which deals with the logic for onClick. If I change onClick to onLongClick function never get call. Is there any way to make it work for onLongClick?
onClick is directly linked to my model class function but not the onLongClick. So I think model class binding is correct but I may need some extra work here.
<data>
<import type="android.view.View" />
<variable
type="com.aaa.bbb.viewmodel.SystemSettingsViewModel"
name="systemSettings"
</variable>
</data>
<TextView
android:gravity="end"
android:id="#+id/tv_logging"
android:layout_centerVertical="true"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_width="wrap_content"
android:onClick="#{() -> systemSettings.sendLogs()}"
android:text="#string/enable_logs"
android:textAlignment="viewEnd" />
I managed to work it correctly. I doubt this is properly documented.
In xml
android:onLongClick="#{(view) -> presenter.onLongClickOnHeading(view)}"
In presenter viewmodel class
public boolean onLongClickOnHeading(View v) {
//logic goes here
return false;
}
Note: this method signature should be exactly in this format. Otherwise biding errors will be thrown at runtime.
Here is the complete the code.
There is no such attribute for long click. So we have to create a binding adapter.
BindingUtils.kt
object BindingUtils {
private const val ON_LONG_CLICK = "android:onLongClick"
#JvmStatic
#BindingAdapter(ON_LONG_CLICK)
fun setOnLongClickListener(
view: View,
func : () -> Unit
) {
view.setOnLongClickListener {
func()
return#setOnLongClickListener true
}
}
}
Layout
<androidx.constraintlayout.widget.ConstraintLayout
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:onLongClick="#{() -> vm.onLongClick()}"/>
For it to work, the part in parenthesis has to match the method signature from the interface View.OnLongClickListener which looks like this :
boolean onLongClick(View view);
So this is how I got it to work :
<View
...
android:onLongClick="#{(view) -> listener.onLongClick(view, viewmodel)}"/>
...
In the xml section, you must refer to the Boolean return function, such as the following code, so as not to get into trouble.in build project android studio
in xml
android:onLongClick="#{(view) -> presenter.onLongClick(view)}"
in java
public boolean onLongClick(View v) {
return false;
}
You should look into this document
OnLongClick is as easy as onClick
Within your SystemSettingsViewModel you can have
public boolean onLongClick(){}
and in xml
android:onLongClick="#{() -> presenter.onLongClick()}"
As mentioned in the Google documentation Link there is no problem with what you wrote.
This is a sample of OnLongClick in XML:
android:onLongClick="#{(theView) -> presenter.onLongClick(theView, task)}"
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}

Categories

Resources