KFlect is a Kotlin library that provides dynamic access to class members. It enables you to inspect and manipulate classes at runtime, including private members, companion objects, and extension functions. This can be useful for testing, debugging, and advanced programming techniques.
Add the following to your build.gradle.kts
in your project:
dependencies {
implementation("io.goatbytes:kflect:1.0.2")
}
This guide demonstrates how kflect
provides an efficient approach to interacting with classes dynamically. Using a Person
class as our example, we’ll walk through instance creation, modifying private members, invoking companion object functions, and even accessing top-level extension functions.
Here’s a simple Person
class with a private constructor, private fields, and a private function. Normally, these members would be inaccessible from the outside, but we can work with them flexibly through kflect
.
package io.goatbytes.kflect.example
class Person private constructor(
val name: String,
val age: Int,
private val ssn: String // Private property
) {
constructor(name: String, age: Int) : this(name, age, generateSSN())
fun getIntroduction(): String = "Hi, I'm $name and I'm $age years old."
private fun getPrivateInfo(): String = "SSN: $ssn"
companion object {
private fun generateSSN(): String {
return listOf(3, 2, 4).joinToString("-") {
(1..it).joinToString("") { (1..9).random().toString() }
}
}
}
}
// Top-level extension functions
fun Person.greet() = println("Hello, $name!")
private fun Person.is21() = age >= 21
With kflect
, you can create an instance of Person
even though the constructor is private:
kflect {
val person = "io.goatbytes.kflect.example.Person"("Jared", 39) as Person
println(person.getIntroduction()) // Output: "Hi, I'm Jared and I'm 39 years old."
}
Sometimes, it’s necessary to read or modify private fields or call private methods. kflect
allows this by letting us reach into the class and interact with its internal details:
kflect {
val getPrivateInfo = person.function("getPrivateInfo")
println(getPrivateInfo(person)) // Output: "SSN: XXX-XX-XXXX"
val generateSSN = person.function("generateSSN")
person["ssn"] = generateSSN()
println(getPrivateInfo(person)) // Updated SSN
}
This method is particularly useful for testing, debugging, and working with data that would otherwise be restricted.
We can also access companion object functions, including private ones. Here’s an example of calling the private generateSSN
method:
kflect {
val generateSSN = person.function("generateSSN")
println("Generated SSN: ${generateSSN()}")
}
You can even invoke top-level extension functions dynamically. Here’s how we use greet
and check the is21
function:
kflect {
val greet = person.topLevelExtensionFunction(Person::class, "greet")
greet(person) // Output: "Hello, Jared!"
val is21 = person.topLevelExtensionFunction(Person::class, "is21")
if (is21(person) == true) {
println("${person.name} is old enough to attend the conference.")
} else {
println("${person.name} isn't old enough for the conference yet.")
}
}
With kflect
, you can simplify standard reflection tasks, such as invoking a method by name or accessing private fields in Java. This flexibility can save time during debugging or when working with restricted APIs.
val substringMethod = "java.lang.String".method("substring", Int::class, Int::class)
val result = substringMethod("Hello, World!", 7, 12)
println(result) // Output: "World"
In this example, we’re calling substring
on a String
instance directly by the method name, which reduces the usual reflection boilerplate.
val person = "com.example.Person"("Jane Doe", 25) as Person
person["privateField"] = "newValue" // Modify a private field directly
Here, kflect
enables modification of private fields without the usual setAccessible
calls, making code more readable and concise.
On Android, kflect
becomes particularly valuable when working with the platform's reflection-heavy APIs, such as accessing hidden fields or methods in the Android SDK. Here’s an example of how to use kflect
for tasks like obtaining the current activity:
Android’s framework classes often require reflection for accessing certain hidden APIs or working with internal details. Using kflect
, you can dynamically obtain the current Activity
without direct references to ActivityThread
:
val currentActivity: Activity = kflect {
("android.app.ActivityThread"("currentActivityThread")["mActivities"] as Map<*, *>)
.values.firstNotNullOf { record ->
when (record["paused"] == false) {
true -> record["activity"] as Activity // return the active android.app.Activity
else -> null
}
}
}
This example is practical for use in debugging scenarios, dynamic feature delivery, and analytics libraries where obtaining the current activity context is essential.
The predicates
package in KFlect enables you to filter and search class members—like methods, properties, fields, and constructors—using declarative conditions. This approach simplifies reflection by enabling selective access to class members based on your specific criteria.
KFlect’s predicates provide a highly readable, fluent interface for querying classes. Rather than manually iterating through members, predicates offer a way to dynamically filter the members you need, reducing boilerplate and increasing readability.
KFlect’s predicates
package offers a range of filters that target different kinds of class members. Each predicate can be chained to form complex criteria, providing you with fine-grained control over member selection.
Here’s a quick overview of the available predicate classes:
- ConstructorPredicates: Filters constructors based on parameters, annotations, and accessibility.
- ExecutablePredicates: General filters for executable members, like functions and methods.
- FieldPredicates: Allows filtering fields based on type, visibility, and annotations.
- KCallablePredicates: A generic set of filters for Kotlin callables, covering properties and functions.
- KFunctionPredicates: Targets Kotlin-specific functions with conditions like return type and parameters.
- KPropertyPredicates: Specific to properties, with filters for mutability, visibility, and initialization.
- MemberPredicates: Broad member-level predicates to filter by name, annotations, and accessibility.
- MethodPredicates: Focused on Java methods, filtering by return type, parameters, and annotations.
Using KFlect’s predicates, you can filter and select specific members of a class without looping manually. Below are some examples illustrating various use cases.
// Find functions in `Random` with a `Unit` return type and an `Int` parameter
val functions = Random::class.filterFunctions {
returnType(Unit::class) and hasParameterType(Int::class)
}
// Filter public methods in `String` with one parameter and a return type of `String`
val methods = String::class.java.filterMethods {
hasParameterCount(1) and returnType(String::class.java) and isPublic()
}
// Locate a property in `StringBuilder` with an `Int` return type and the name "length"
val property = StringBuilder::class.findProperty {
returnType(Int::class) and name("length")
}
// Find a method in `StringBuilder` named "length" with an `Int` return type
val method = StringBuilder::class.java.findMethod {
name("length") and returnType(Int::class.java)
}
In these examples, filterFunctions
, filterMethods
, findProperty
, and findMethod
leverage predicates to locate members matching specific conditions.
KFlect’s predicates can be chained using and
, or
, and not
, making it easy to build complex conditions. For instance:
val functionsWithConditions = MyClass::class.filterFunctions {
returnType(Unit::class) and (isPublic() or hasAnnotation<Deprecated>())
}
This query retrieves all Unit
-returning functions in String
that are either public
or annotated with @Deprecated
.
KFlect includes LazyKFlect
and SynchronizedLazyKFlect
for situations where on-demand initialization of reflection data is preferred. These classes are particularly useful for performance-sensitive applications or cases where reflection data might not be required immediately.
LazyKFlect
enables lazy initialization of reflection-related objects, ensuring that they’re only created when accessed for the first time. This approach minimizes the initial memory footprint and computation cost, as resources are allocated only when necessary.
val lazyReflection by LazyKFlect {
"io.goatbytes.kflect.example.Person"("Alice", 28) as Person
}
For multithreaded environments, SynchronizedLazyKFlect
provides thread-safe lazy initialization by ensuring that only one thread can initialize the reflection data at a time.
val synchronizedLazyReflection by SynchronizedLazyKFlect {
"io.goatbytes.kflect.example.Person"("Bob", 35) as Person
}
src
└── main
└── kotlin
└── io
└── goatbytes
└── kflect
├── cache // Caching mechanism for reflection data
├── dsl // DSL for simplified reflection queries
├── exceptions // Custom exceptions for reflection handling
├── ext // Extension functions for reflection utilities
├── lazy // Lazy initialization utilities for reflection
├── misc // Miscellaneous helpers, like unsafe operations
├── os // OS-specific utilities
├── predicates // Predicates for filtering reflection data
└── traverser // Traversal utilities for classes and functions
└── ext // Extensions supporting traversal operations
Contributions are welcome! Please read our contributing guide and submit pull requests to our repository.
This project is licensed under the Apache 2.0 License — see the LICENSE file for details.
At GoatBytes.IO, our mission is to develop secure software solutions that empower businesses to transform the world. With a focus on innovation and excellence, we strive to deliver cutting-edge products that meet the evolving needs of businesses across various industries.