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?
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.
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.
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.
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.
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 }
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With