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.
Related
I have 5 fragments and one parent fragment which takes care of the fragment navigation. All these fragments use a common (shared) view model to share data across all fragments. But when I try to call a method from XML on button's onClick, it doesn't get called. I added breakpoint but the debugger didn't stop there at all.
Moreover, I have some visibility conditions defined in the view which are also using viewmodel reference and those don't execute either.
Below is the code of button click
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/previousBTN"
style="#style/buttonStyleWithFillColor"
android:layout_width="wrap_content"
android:layout_height="#dimen/common_44"
android:layout_marginEnd="#dimen/common_50"
android:layout_marginBottom="#dimen/common_15"
android:clickable="#{!viewModel.isHorizontalProgress()}"
android:fontFamily="#font/lato_bold_style"
android:gravity="center"
android:onClick="#{()->viewModel.previous()}"
android:paddingStart="#dimen/common_20"
android:paddingEnd="#dimen/common_20"
android:text="#string/submit"
android:visibility="#{viewModel.currentTabIndex==5? View.VISIBLE:View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
In the above code, you can see a method call on button click and a visibility condition . None of them works.
CurrentTabIndex is a mutable live data defined in viewModel
var currentTabIndex = MutableLiveData(1)
This is the viewModel class
class EmpGPViewModel #ViewModelInject constructor(application: Application): BaseViewModel<EmpGPNavigator>(application) {
var model=MutableLiveData<EmpGPModel>()
var currentTabIndex = MutableLiveData(1)
fun selectDate() {
navigator.selectDate()
}
fun selectOutTime() {
navigator.selectOutTime()
}
fun next() {
navigator.next()
}
fun previous() {
navigator.previous()
}
fun submit() {
navigator.submit()
}}
I am also using Hilt in this project for dependency injection. I also have the same code setup on a different screen only difference is that it doesn't use a shared view model and there all of this code works.
I have a base fragment class which provides an overridden method to instantiate viewModel.
This is how I do it.
override val viewModel: EmpGPViewModel by viewModels()
Base fragment class has calls below code
viewDataBinding?.lifecycleOwner = viewLifecycleOwner
viewDataBinding?.setVariable(bindingVariable, mViewModel)
viewDataBinding?.executePendingBindings()
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 want to create an "inflateWhen" BindingAdapter and attach it to a viewstub to have it inflate when a boolean value is true. However, the BindingAdapter keeps trying to operate on the root view of the viewstub, causing it to fail to compile. Is there any way to do this as a bindingadapter rather than having to do it programmatically in the activity?
Here's what I have so far:
#BindingAdapter("inflateWhen")
fun inflateWhen(viewstub: ViewStub, inflate: Boolean) {
if (inflate) {
viewstub.inflate()
} else {
viewstub.visibility = View.GONE
}
}
This is what I have, but when attached to a viewstub like
<ViewStub
android:id="#+id/activity_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:inflateWhen="#{viewmodel.userid != 0}" />
it fails to compile. The error is:
ActivityMyAccountSectionedBindingImpl.java:1087: error: cannot find symbol
if (this.pageFooter.isInflated()) this.pageFooter.getBinding().setVariable(BR.inflateWhen, viewmodelRatingInt0);
Looks like it's trying to apply the binding to the inflated view, but that's not what I want here.
08.10.2020 Update:
I have written an article on Medium where I provide an example of how to switch between layouts on the fly depending on the screen state using ViewStub and DataBinding:
https://medium.com/#mxdiland/viewstub-databinding-handle-screen-states-easily-2f1c01098b87
Old accepted answer:
I also faced the problem to write #BindingAdapter for the ViewStub to control layout inflation using databinding instead of direct referencing to the ViewStub and calling inflate()
Along the way, I did some investigation and studied the following things:
ViewStub must have android:id attribute to avoid build errors like java.lang.IllegalStateException: target.id must not be null;
any custom attribute declared for ViewStub in an XML, databinding tries to set as a variable to the layout which will be inflated instead of the stub;
... that's why any binding adapter is written for ViewStub will never be used by databinding
there is only one but pretty tricky #BindingAdapter which works: androidx.databinding.adapters.ViewStubBindingAdapter and allows setting ViewStub.OnInflateListener trough XML attribute android:onInflate
the ViewStubBindingAdapter's first argument is ViewStubProxy not View or ViewStub!;
any different adapter written similarly does not work - databinding tries to set variable to the future layout instead of using the adapter
BUT it is allowed to override existing androidx.databinding.adapters.ViewStubBindingAdapter and implement some desired logic.
Because this adapter is one and the only option to interact with ViewStub using databinding I decided to override the adapter and use not for its intended purpose
The idea is to provide specific ViewStub.OnInflateListener which will be the listener itself and at the same time will be a signal that ViewStub.inflate() should be called:
class ViewStubInflationProvoker(
listener: ViewStub.OnInflateListener = ViewStub.OnInflateListener { _, _ -> }
) : ViewStub.OnInflateListener by listener {
companion object {
#JvmStatic
fun provideIf(clause: Boolean): ViewStubInflationProvoker? {
return if (clause) {
ViewStubInflationProvoker()
} else {
null
}
}
}
}
and overriding binding adapter:
#BindingAdapter("android:onInflate")
fun setOnInflateListener(
viewStubProxy: ViewStubProxy,
listener: ViewStub.OnInflateListener?
) {
viewStubProxy.setOnInflateListener(listener)
if (viewStubProxy.isInflated) {
viewStubProxy.root.visibility = View.GONE.takeIf { listener == null } ?: View.VISIBLE
return
}
if (listener is ViewStubInflationProvoker) {
viewStubProxy.viewStub?.inflate()
}
}
and XML part
...
<ViewStub
android:id="#+id/no_data_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="#layout/no_data"
android:onInflate="#{ViewStubInflationProvoker.provideIf(viewModel.state == State.Empty.INSTANCE)}"
app:viewModel="#{viewModel.noDataViewModel}"
/>
...
So now the inflation will happen only when the state is State.Empty and databinding will set viewModel variable to the inflated #layout/no_data layout.
Not really graceful but working solution.
I want to create a custom onClickListener to use for databinding. This custom click listener prevents the user from spamming the button and triggering the event twice(like showing two dialogues at the same time). I made a custom listener below that works in normal code, but I don't know how to implement it for databinding like the android:onClick="" in xml.
abstract class OneClickListener(var delay: Long) : View.OnClickListener {
private var hasClicked: Boolean = true
constructor() : this(1000)
override fun onClick(it: View) {
if (!hasClicked) {
return
} else {
hasClicked = false
onClicked(it)
GlobalScope.launch {
delay(delay)
hasClicked = true
}
}
}
abstract fun onClicked(it: View)
}
Is it possible to use this listner in databinding like for example
app:OneClickListener="#{viewModel::MyMethod}" in XML? and if yes, could you please tell me how?
Using data binding you can specify which listener to call when an event is fired just by calling it in a lambda. For example, let's say you have a method in your viewmodel, called myOnClick(). You can use it with data binding this way:
android:onClick="#{() -> viewModel.myOnClick()}"
Defining a custom binding adapter called OneClickListener is something different and it would not be called when the click event is fired, unless you use a trick: registering a click listener inside the custom binding adapter. This means you would have to call a method that register an other method: not really the cleanest way to add a listener.
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>