Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin-Java interop not working with varargs

I wanted to abstract out getting resources in Android, so I have written a class ResourceProvider that actually provides resources:

@Singleton
class ResourceProvider @Inject constructor(private val context: Context) {
fun getString(@StringRes id: Int): String {
    return context.getString(id)
}

fun getString(@StringRes id: Int, vararg formatArgs: Any): String {
    return context.getString(id, formatArgs)
}
...
}

Nothing special here, just calling methods on Context. I have a problem when I want to get String with parameters, I created following example:

var fromContext = requireContext().getString(R.string.one_parameter_string, "Text")
Log.i("fromContext", fromContext)
var fromWrapper = resourceProvider.getString(R.string.one_parameter_string, "Text")
Log.i("fromWrapper", fromWrapper)

fromContext = requireContext().getString(R.string.two_parameter_string, "Text", "Text")
Log.i("fromContext", fromContext)
fromWrapper = resourceProvider.getString(R.string.two_parameter_string, "Text", "Text")
Log.i("fromWrapper", fromWrapper)

Here are String resources:

<string formatted="false" name="two_parameter_string">Text with parameters: %s, %s</string>
<string formatted="false" name="one_parameter_string">Text with parameter: %s</string>

as you can see I'm calling the same methods directly on Context and on my ResourceProvider class. I would expect the same results, but in fact this is what is printed in console:

I/fromContext: Text with parameter: Text
I/fromWrapper: Text with parameter: [Ljava.lang.Object;@6d43f06
I/fromContext: Text with parameters: Text, Text
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: xxx.xxx.xxx, PID: 22963
    java.util.MissingFormatArgumentException: Format specifier '%s'
        at java.util.Formatter.format(Formatter.java:2522)
        at java.util.Formatter.format(Formatter.java:2458)
        at java.lang.String.format(String.java:2814)
        at android.content.res.Resources.getString(Resources.java:472)
        at android.content.Context.getString(Context.java:572)
        at xxx.xxx.xxx.utils.ResourceProvider.getString(ResourceProvider.kt:21)
        at xxx.xxx.xxx.views.trial.TrialFragment.onViewCreated(TrialFragment.kt:45)
        at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1471)
        at androidx.fragment.app.FragmentManagerImpl.addAddedFragments(FragmentManager.java:2646)
        at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2416)
        at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2372)
        at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2273)
        at androidx.fragment.app.FragmentManagerImpl$1.run(FragmentManager.java:733)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6944)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

As you can see calling it directly on Context works without the flaws, but calling the same method with my wrapper makes it print Object.toString() and in second case it crashes.

Here is the decompiled version of the getString(@StringRes id: Int, vararg formatArgs: Any) method:

@NotNull
public final String getString(@StringRes int id, @NotNull Object... formatArgs) {
  Intrinsics.checkParameterIsNotNull(formatArgs, "formatArgs");
  String var10000 = this.context.getString(id, new Object[]{formatArgs});
  Intrinsics.checkExpressionValueIsNotNull(var10000, "context.getString(id, formatArgs)");
  return var10000;
}

What is the problem and how to avid it?

like image 236
user3448282 Avatar asked Jan 25 '19 09:01

user3448282


People also ask

Is Kotlin interoperable with Java?

Kotlin is designed with Java interoperability in mind. Existing Java code can be called from Kotlin in a natural way, and Kotlin code can be used from Java rather smoothly as well.

How to use varargs in Kotlin?

Kotlin doesn’t provide varargs parameters, however, to be fully operational with Java, it supports a special spread operator (*) to call the functions which have varargs parameters. Types in Kotlin are different from the types in Java.

How to expose Kotlin functions to Java callers?

Normally, if you write a Kotlin function with default parameter values, it will be visible in Java only as a full signature, with all parameters present. If you wish to expose multiple overloads to Java callers, you can use the @JvmOverloads annotation. The annotation also works for constructors, static methods, and so on.

How do you escape a method in Kotlin from Java?

If a Java library uses a Kotlin keyword for a method, you can still call the method escaping it with the backtick (`) character: Copied! Any reference in Java may be null, which makes Kotlin's requirements of strict null-safety impractical for objects coming from Java.


1 Answers

You need to use the spread operator (*) to call context.getString, i.e. you need to use *formatArgs:

@Singleton
class ResourceProvider @Inject constructor(private val context: Context) {
  fun getString(@StringRes id: Int): String {
    return context.getString(id)
  }

  fun getString(@StringRes id: Int, vararg formatArgs: Any): String {
    return context.getString(id, *formatArgs)
  }
...
}

You can read more about it in the kotlin reference regarding variable number of arguments (varargs).

If you don't do it, then the given object (in this case a formatArgs-array) is treated as a single object which you want to pass to the vararg-method, which will therefore be wrapped into a Object[] { formatArgs }.

like image 151
Roland Avatar answered Sep 22 '22 00:09

Roland