kotlin-default-args-util is a library that attempts to bring two of the most prominent features in Kotlin to the Java reflection world:
- Named parameters
- Default parameters
Click to expand
Default and named parameters are undeniably one of the most favored features in Kotlin, and while it is possible to interop them with Java using annotations such as `@JvmOverload`, using them in Java's Reflection API is very tricky to get right, as it requires dealing with synthetic compiler functions, classes and arguments, and accommodating the many edge cases with it.The official solution proposed by JetBrains is kotlin-reflect; a library that introspects Kotlin classes and metadata to allow easy and ergonomic access to functions and properties.
The problem, however, was with the slow performance and vast bundle size of kotlin-reflect
(~2.8 MB). JetBrains
addresses this problem by providing a simpler, smaller, and lighter version of kotlin-reflect, kotlin.reflect.lite.
While it sounds promising, it has been marked as experimental, possibly abandoned, and far from being production-ready.
Out of the need for something small that gets the job done, kotlin-default-args-util was born.
- Small size 🔥: The library is tiny, measuring about 30 kilobytes.
- Zero dependencies 🔥: The library does not require any dependencies, not even the Kotlin standard library. This makes it convenient to bundle anywhere.
- Lazy reflection: Generating reflection elements is a resource-intensive process. The library caches reflection elements and only fetches them on demand.
- Thread-safe: All API classes are immutable and thread-safe, and lazy fetching is synchronized on first call
- Support for Kotlin ergonomics:
- Suspend functions
- Named arguments
- Default arguments
- Index-based arguments
object
s andcompanion object
s@JvmStatic
functions
- Friendly and accurate error messages
- A nice and convenient API
<!-- Add JitPack repository -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<!-- Add the dependency -->
<dependency>
<groupId>com.github.Revxrsal</groupId>
<artifactId>kotlin-default-args-util</artifactId>
<version>(version)</version>
</dependency>
repositories {
maven { url = "https://jitpack.io" }
}
dependencies {
implementation("com.github.Revxrsal:kotlin-default-args-util:(version)")
}
repositories {
maven(url = "https://jitpack.io")
}
dependencies {
implementation("com.github.Revxrsal:kotlin-default-args-util:(version)")
}
Function class:
class Test {
companion object {
/**
* Greets the given name
*/
fun greet(name: String = "John") {
println("Hello, $name!")
}
}
}
Calling the function:
// fetch the function
Method greetMethod = Test.Companion.getClass().getDeclaredMethod("greet", String.class);
// the KotlinFunction wrapper
KotlinFunction greet = KotlinFunction.wrap(greetMethod);
// note: the instance can be null if the function has @JvmStatic.
greet.call(
/* instance = */ Test.Companion,
/* arguments = */ emptyList(),
/* isOptional = */ parameter -> true // All parameters are optional
);
greet.call(
/* instance = */ Test.Companion,
/* arguments = */ singletonList("my friend"),
/* isOptional = */ parameter -> true // All parameters are optional
);
Output:
Hello, John! (*default parameter was used*)
Hello, my friend!
Warning
⚠️ : Parameter names at runtime may not necessarily match the ones at compile-time, in which cases, the function will throw an exception if an invalid name was provided. To prevent this, configure the compiler to preserve parameter names at runtime
Function class
object Numbers {
fun numbers(
a: Int = 10,
b: Int = 30,
c: Int = -5
) {
println("A: $a")
println("B: $b")
println("C: $c")
}
}
Calling the function
// fetch the function
Method sumMethod = Numbers.class.getDeclaredMethod("numbers", int.class, int.class, int.class);
// the KotlinFunction wrapper
KotlinFunction sum = KotlinFunction.wrap(sumMethod);
// note: the instance can be null if the function has @JvmStatic.
sum.callByNames(
/* instance = */ Numbers.INSTANCE,
/* arguments = */ new HashMap<String, Object>() {{
put("a", 20);
put("c", 400);
}},
/* isOptional = */ parameter -> true // All parameters are optional
);
Output:
A: 20
B: 30
C: 400
Call the function using the indices of parameters. Zero represents the first parameter.
// fetch the function
Method sumMethod = Numbers.class.getDeclaredMethod("numbers", int.class, int.class, int.class);
// the KotlinFunction wrapper
KotlinFunction sum = KotlinFunction.wrap(sumMethod);
// note: the instance can be null if the function has @JvmStatic.
sum.callByIndices(
/* instance = */ Numbers.INSTANCE,
/* arguments = */ new HashMap<Integer, Object>() {{
put(0, 20); // parameter 'a'
put(2, 400); // parameter 'c'
}},
/* isOptional = */ parameter -> true // All parameters are optional
);
- Due to the hairy nature of the Kotlin synthetics, this library tries its best to find the correct candidates for functions, singletons, parameters, etc. While it should work well in most cases, it is not perfect. In such cases, please feel free to file an issue with code that reproduces the problem
- To avoid any sort of dependency on
kotlin-reflect
, the library has no way of knowing which parameters are optional and which ones are not, which is why allcall___
functions require anisOptional
parameter. - Interface methods that have default values are not supported yet.