Using lambda in custom BindingAdapter using Android Databinding and Kotlin


#1

I’m trying to have a custom binding with a lambda handler for Android Databinding using Kotlin. It’s working well as long as my ViewModel handler is explicitly returning Void. But if it returns Kotlin Unit instead, I get an error: cannot generate view binders java.lang.StackOverflowError.

Everything seems to be hooked-up correctly as all the other custom bindings works (convertBooleanToViewVisibility, toTestString and even customOnClick as long as I’m calling onClickVoid [see example below]).

The issue is when I’m trying to invoke a lambda returning Unit in my app:customOnClick instead of returning Void. In the example below, it’s to invoke mainViewModel.onClick() instead of mainViewModel.onClickVoid(). There must be a way of making it work as the android:onClick is able to make to call to the Unit version. But if I’m using the same syntax for customOnClick calling onClick, I’m getting this error:

:app:kaptGenerateStubsDebugKotlin
Using kotlin incremental compilation
:app:kaptDebugKotlin
e: error: cannot generate view binders java.lang.StackOverflowError
e: 

e:      at android.databinding.tool.writer.Scope.access$getCurrentScope$cp(LayoutBinderWriter.kt:49)
e:      at android.databinding.tool.writer.Scope$Companion.getCurrentScope(LayoutBinderWriter.kt:58)
e:      at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:196)
e:      at android.databinding.tool.expr.Expr.toCode(Expr.java:776)
e:      at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt:203)
e:      at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
e:      at android.databinding.tool.ext.LazyExt.getValue(ext.kt:27)
e:      at android.databinding.tool.writer.LayoutBinderWriterKt.getCallbackLocalName(LayoutBinderWriter.kt)
e:      at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:197)
e: java.lang.IllegalStateException: failed to analyze: java.lang.RuntimeException: failure, see logs for details.
cannot generate view binders java.lang.StackOverflowError
    at android.databinding.tool.writer.Scope.access$getCurrentScope$cp(LayoutBinderWriter.kt:49)
    at android.databinding.tool.writer.Scope$Companion.getCurrentScope(LayoutBinderWriter.kt:58)
    at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:196)
    at android.databinding.tool.expr.Expr.toCode(Expr.java:776)
    at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt:203)
    at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
    at android.databinding.tool.ext.LazyExt.getValue(ext.kt:27)
    at android.databinding.tool.writer.LayoutBinderWriterKt.getCallbackLocalName(LayoutBinderWriter.kt)
    at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:197)
    .............................. (TRUNCATED)  ...................................
    at android.databinding.tool.expr.Expr.toCode(Expr.java:776)
    at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt:203)
    at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
    at android.databinding.tool.ext.LazyExt.getValue(ext.kt:27)
    at android.databinding.tool.writer.LayoutBinderWriterKt.getCallbackLocalName(LayoutBinderWriter.kt)
    at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:197)

    at org.jetbrains.kotlin.analyzer.AnalysisResult.throwIfError(AnalysisResult.kt:57)
    at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:144)
    at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:167)
    at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:55)
    at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.java:182)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.execCompiler(CompileServiceImpl.kt:397)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.access$execCompiler(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:365)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$2$$special$$inlined$withValidClientOrSessionProxy$lambda$1.invoke(CompileServiceImpl.kt:798)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$2$$special$$inlined$withValidClientOrSessionProxy$lambda$1.invoke(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.common.DummyProfiler.withMeasure(PerfUtils.kt:137)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.checkedCompile(CompileServiceImpl.kt:825)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.access$checkedCompile(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$2.invoke(CompileServiceImpl.kt:797)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$2.invoke(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.ifAlive(CompileServiceImpl.kt:1004)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.ifAlive$default(CompileServiceImpl.kt:865)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.doCompile(CompileServiceImpl.kt:791)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.access$doCompile(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1.invoke(CompileServiceImpl.kt:364)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1.invoke(CompileServiceImpl.kt:99)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.ifAlive(CompileServiceImpl.kt:1004)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.ifAlive$default(CompileServiceImpl.kt:865)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:336)
    at sun.reflect.GeneratedMethodAccessor86.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:324)
    at sun.rmi.transport.Transport$1.run(Transport.java:200)
    at sun.rmi.transport.Transport$1.run(Transport.java:197)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
    at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
    at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
    at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)



    ... 39 more


FAILED

Any idea how to make it work with Unit so I do not need to change my ViewModel to explicitly return Void? for all my handlers?

Sample project

Project build.gradle

buildscript {
ext.kotlin_version = '1.1.3-2’
ext.plugin_version = ‘2.3.3’

repositories {
    jcenter()
}

dependencies {
    classpath "com.android.tools.build:gradle:$plugin_version"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

}

allprojects {
repositories {
jcenter()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

Module (app) build.gradle

buildscript {
repositories {
jcenter()
mavenCentral()
}

dependencies {
    classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
}

}

apply plugin: 'com.android.application’
apply plugin: "kotlin-android"
apply plugin: 'kotlin-android-extensions’
apply plugin: ‘kotlin-kapt’

android {
compileSdkVersion 24
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.example.kotlindatabinding"
minSdkVersion 24
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner”

    dataBinding {
        enabled = true
    }
}
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

}

dependencies {
compile fileTree(dir: ‘libs’, include: [’*.jar’])
androidTestCompile(‘com.android.support.test.espresso:espresso-core:2.2.2’, {
exclude group: ‘com.android.support’, module: ‘support-annotations’
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2’
testCompile 'junit:junit:4.12’
compile “org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version”

kapt "com.android.databinding:compiler:$plugin_version"

}

kapt {
generateStubs = true
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    binding.setMainViewModel(MainViewModel())
}

}

MainViewModel.kt

class MainViewModel {
val myName: String
get() {
return “Hello world!”
}

fun onClick(){
    Log.i("ME", "Logging...")
}

fun onClickVoid(): Void? {
    onClick()
    return null as Void?
}

}

Bindings.kt

object Bindings{
@BindingConversion
@JvmStatic fun convertBooleanToViewVisibility(isVisible: Boolean): Int {
// Working
return if (isVisible) View.VISIBLE else View.GONE
}

@BindingAdapter("customOnClick")
@JvmStatic fun setOnItemClicked(textView: TextView, consumer: (String) -> Any?) {
    // Working
    consumer("test")
}

@JvmStatic fun toTestString(input: Any) : String {
    return "Test"
}

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<data>
    <import type="com.example.kotlindatabinding.Bindings"/>
    <variable
        name="mainViewModel"
        type="com.example.kotlindatabinding.MainViewModel" />
</data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.kotlindatabinding.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{Bindings.toTestString(mainViewModel.myName)}"
        android:visibility="@{mainViewModel.myName != null}"
        android:clickable="true"
        android:onClick="@{() -> mainViewModel.onClick()}"
        app:customOnClick="@{(someInput) -> mainViewModel.onClickVoid()}" />
</LinearLayout>

emphasized text


#2

You pasted quite a bit of code and config, I will say that I got a SOE with data binding, was quite confused, created a ticket on Android issue tracker; the issue was replied to prompty by their data binding library developer. Although it sounds like they haven’t released an update which solves the SOE, the developer did point out the mistake I was making and how to fix it.

https://issuetracker.google.com/issues/37127904

My scenario didn’t even involve BindingAdapter, but I’ll bet same issue can happen if your adapter function doesn’t match expected signature.

Hope this helps!


#3

It also seems it try to convert to the right type but whatever I accept in my BindingAdapter as a return value for my function parameter, nothing works if my ViewModel is returning Unit. I’ll also create a support ticket for android data binding team also to see what they have to say.

Thank.


#4

Binding expressions aren’t required to return anything, you should be able to do something identical to onClick handler. You tried fun onClickVoid() {...}?


#5

Yes, onClickVoid does work, but not onClick. And it seems that whatever lambda return type I try to define in @BindingAdapter, I’m not able to have anything that works with Unit, only Void or other standard Java types.


#6

@instriker Make sense to report this to Android issue tracker