Scope of variable defined in for loop header - kotlin

I noticed that the following Kotlin code compiles and executes successfully:
for (i in 1..2) {
val i = "a"
print(i)
}
This prints aa. However, I failed to find rationale behind the decision to allow this kind of variable shadowing. I would say that this is not a good practice, and is prohibited even in Java.
I think that Kotlin designers did a great work of improving Java syntax and accommodating it to the everyday practical use, so I must be missing something here?

Kotlin does not restrict variable shadowing in any way. The rationale is simple: "consistency."
Since you could shadow variables in most other places why would you exclude only some loop variables from the allowed options? Why would they be so special? It is an arbitrary difference.
Any scope can shadow a variable used in another scope. It is NOT good practice and does produce a compiler warning -- but it is allowed.
If you want to engage in a dialog with the contributors of the project, try the discussion forum or slack channel, both are linked from the Kotlin Community page. Otherwise if you feel it is a bug please add an Issue report to Kotlin YouTrack and the answer you receive there will be definitive as well.
In the meantime, you are free to write nonsensical code such as:
val i = 1
class Foo() {
val i = "monkey"
init { println(i) }
#Test fun boo() {
println(i)
val i = i.length
println(i)
if (i == 6) {
val i = Date(System.currentTimeMillis() + i) // Shadow warning
println(i)
}
for (i in 0..i) { // Shadow warning
val i = "chimp $i" // Shadow warning
println(i)
}
InnerFoo()
}
class InnerFoo() {
val i: Long = 100L
init { println(i) }
}
}
Which in Kotlin 1.0.3 produces 3 warnings.
Warning:(15, 21) Kotlin: Name shadowed: i
Warning:(18, 18) Kotlin: Name shadowed: i
Warning:(19, 21) Kotlin: Name shadowed: i
And outputs:
monkeymonkey6Sun Jul 17 11:31:23 UYT 2016chimp 0chimp 1chimp 2chimp 3chimp 4chimp 5chimp 6100

Related

Trying to get a when expression to show the result on the ui in kotlin

I have been stuck making this metric converter app for over a week I keep going back to the previous codelabs that explain how to make a tip calculator but when I try to apply the same method to this app that I'm trying to make it doesn't have any errors but the app crashes whenever I try to test it to see if I can get the functionality to work this is my last resort. I've tried to do when statements but I kept getting the error that certain branches would never be reached. if anyone can point me in the right direction as to what I'm doing wrong or what's missing from my code. I'm all ears.
package com.example.metricconversion
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.metricconversion.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(R.layout.activity_main)
setContentView(binding.root)
binding.conversionTo.setOnClickListener { convertMetric() }
}
fun convertMetric() {
val stringInTextField = binding.howMuchStuffWeTalking.text.toString()
val amountIQ = stringInTextField.toDouble()
val selectedId = binding.unitThatNeedsToBeConverted.checkedRadioButtonId
val selectedId2 = binding.toBeConverted.checkedRadioButtonId
// val grainToQtr = R.id.Grain and R.id.quarter1
//val grainToSt= R.id.Grain and R.id.quarter1
//val grainToLb = R.id.Grain and R.id. pound1
//val grainToTon= R.id.Grain and R.id.Ton1
val st = R.id.Grain and R.id.stone1
// code to convert grain to the other options
//code to convert grain to the other options
when (selectedId and selectedId2) {
R.id.Grain and R.id.Ounce1 -> {
amountIQ / 437.5
}
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.quarter1 -> amountIQ * 0.000005714
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.pound1 -> amountIQ * 0.0001429
}
when (selectedId and selectedId2) {
R.id.Grain and R.id.Ton1 -> amountIQ / 1.4e+7
}
when (selectedId and selectedId2) {
st -> {
amountIQ * 0.0000102
}
}
binding.metricConverted.text=getString(R.string.end_result,convertMetric())
}
}
Problems with your code:
Using and to check two values for equality at the same time is not viable. The and function does a bitwise operation to merge the two numbers together. There are many possible inputs that could merge to the same solution, so you will get false positives. Instead you could use the to infix function to combined the two numbers into a Pair wrapper object so they are both preserved for the comparison.
You have a series of individual when statements, each with only a single condition. It doesn't make sense to use a when statement with a single condition. Usually a single condition would be represented with an if statement instead of when statement. But I'm guessing you didn't mean for these to be separate when statements, based on what I'm seeing.
Your when statements are not actually doing anything. Their branches resolve to a number, but you're not doing anything with that number (not assigning it to a variable or logging it or showing it in the UI, etc.). So they are useless. Your statement at the bottom isn't getting the result of any of the when statements, but is instead recursively calling the same function again. This function doesn't return anything, so that's useless. Even if it did return a number, the recursive call will create an infinite loop, resulting in a stack overflow.
So first, to just fix your code (we can discuss a better way of doing it later):
We replace and with to.
We merge all the when statements into a single statement.
We store the result of the when statement in a variable and use that variable to set the text in the last line of the function. When you use a when statement with a subject (something in parentheses that is compared for each branch) or to get a result, you have to cover every possible case, so an else branch must also be added.
fun convertMetric() {
val stringInTextField = binding.howMuchStuffWeTalking.text.toString()
val amountIQ = stringInTextField.toDouble()
val selectedId = binding.unitThatNeedsToBeConverted.checkedRadioButtonId
val selectedId2 = binding.toBeConverted.checkedRadioButtonId
// code to convert grain to the other options
val result = when (selectedId to selectedId2) {
R.id.Grain to R.id.Ounce1 -> amountIQ / 437.5
R.id.Grain to R.id.quarter1 -> amountIQ * 0.000005714
R.id.Grain to R.id.pound1 -> amountIQ * 0.0001429
R.id.Grain to R.id.Ton1 -> amountIQ / 1.4e+7
R.id.Grain to R.id.stone1 -> amountIQ * 0.0000102
else -> error("Unsupported selection pair")
}
binding.metricConverted.text = getString(R.string.end_result, result.toString())
}
Above, the error() call will crash your app. You need to make sure you cover every possible combination that could occur. During development, this is suitable, but for production you might want to change the behavior so it shows an error message in the UI of the app and doesn't crash.
Now, regarding the overall design, it is quite fragile because you have UI layout details so tightly coupled to your app's behavior. All these formula calculations should probably be defined in a separate class, possibly an Enum class. You could create a Map in your Activity file that links the UI elements to the behavior class(es), and then in your when statement, you could use the UI elements to pull the associated behavior from the map. This would be more maintainable, and make it easier for you to avoid forgetting something as you add/modify functionality. I say this just to get you thinking about it, but it's probably too much for a beginner project right now. I don't have time to explain in detail how I would do all of that for your case.

How to modify variables outside of their scope in kotlin?

I understand that in Kotlin there is no such thing as "Non-local variables" or "Global Variables" I am looking for a way to modify variables in another "Scope" in Kotlin by using the function below:
class Listres(){
var listsize = 0
fun gatherlistresult(){
var listallinfo = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
listallinfo.addOnSuccessListener {
listResult -> listsize += listResult.items.size
}
}
}
the value of listsize is always 0 (logging the result from inside of the .addOnSuccessListener scope returns 8) so clearly the listsize variable isn't being modified. I have seen many different posts about this topic on other sites , but none fit my usecase.
I simply want to modify listsize inside of the .addOnSuccessListener callback
This method will always be returned 0 as the addOnSuccessListener() listener will be invoked after the method execution completed. The addOnSuccessListener() is a callback method for asynchronous operation and you will get the value if it gives success only.
You can get the value by changing the code as below:
class Demo {
fun registerListResult() {
var listallinfo = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
listallinfo.addOnSuccessListener {
listResult -> listsize += listResult.items.size
processResult(listsize)
}
listallinfo.addOnFailureListener {
// Uh-oh, an error occurred!
}
}
fun processResult(listsize: Int) {
print(listResult+"") // you will get the 8 here as you said
}
}
What you're looking for is a way to bridge some asynchronous processing into a synchronous context. If possible it's usually better (in my opinion) to stick to one model (sync or async) throughout your code base.
That being said, sometimes these circumstances are out of our control. One approach I've used in similar situations involves introducing a BlockingQueue as a data pipe to transfer data from the async context to the sync context. In your case, that might look something like this:
class Demo {
var listSize = 0
fun registerListResult() {
val listAll = FirebaseStorage.getInstance()
.getReference()
.child("MainTimeline/")
.listAll()
val dataQueue = ArrayBlockingQueue<Int>(1)
listAll.addOnSuccessListener { dataQueue.put(it.items.size) }
listSize = dataQueue.take()
}
}
The key points are:
there is a blocking variant of the Queue interface that will be used to pipe data from the async context (listener) into the sync context (calling code)
data is put() on the queue within the OnSuccessListener
the calling code invokes the queue's take() method, which will cause that thread to block until a value is available
If that doesn't work for you, hopefully it will at least inspire some new thoughts!

How to register AndroidLintInspectionBase in IntellijIdea Plugin

I'm creating a costum Android Lint Inspection and I need to register the inspection, to be run. Where do I need to register it?
I've already tried to register the inspection which provides the inspection inside plugin.xml file.
The actual inspection:
class HardcodedDimensionsInspection : AndroidLintInspectionBase("Hardcoded dimensions", HardcodedDimensDetector.ISSUE) {
override fun getShortName(): String {
return "AndroidLintHardcodedDimension"
}
}
The entry in plugin.xml file
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<!-- <inspectionToolProvider implementation="JavaInspectionProvider"/>-->
<globalInspection shortName="AndroidLintHardcodedDimension" displayName="Hardcoded dimensions"
enabledByDefault="true" level="WARNING"
implementationClass="HardcodedDimensionsInspection"/>
</extensions>
The actual detector
class HardcodedDimensDetector : LayoutDetector() {
override fun getApplicableAttributes(): Collection<String>? {
return Arrays.asList(
// Layouts
ATTR_TEXT
)
}
override fun appliesTo(folderType: ResourceFolderType): Boolean {
return (folderType == ResourceFolderType.LAYOUT ||
folderType == ResourceFolderType.MENU ||
folderType == ResourceFolderType.XML)
}
override fun visitAttribute(context: XmlContext, attribute: Attr) {
val value = attribute.value
}
companion object {
/** The main issue discovered by this detector */
#JvmField
val ISSUE = Issue.create(
id = "HardcodedDimension",
briefDescription = "Hardcoded dimens",
explanation = """
Brief
""",
category = Category.I18N,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
HardcodedDimensDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}
}
I've expected to hit the breakpoints in any of the functions for Detector but the code is never called. Seems like my detector is not registered. Can you please point me to the missing part, is there a class where I should register my Detector?
Thank you.
The link to the full project: https://github.com/magicbytes/Android-Lint-Inspection
I don't see anything obvious wrong from these snippets. Could you please post on our forum and link to the full sources of your plugin? Thanks. https://intellij-support.jetbrains.com/hc/en-us/community/topics/200366979-IntelliJ-IDEA-Open-API-and-Plugin-Development
I have a workaround for now, not sure it's the official way to do it. Android Lint has a registry with all the Issue classes (built-in), the class is called LintIdeIssueRegistry. When it runs the Android Lint, it's looking in this registry for Issue processors. Since the list is hardcoded, we need to inject ours in the list. I'm using the following code for that:
val registry = LintIdeIssueRegistry()
val issue = registry.getIssue(HardcodedDimensDetector.ISSUE.id)
if (issue == null) {
val list = registry.issues as MutableList<Issue>
list.add(HardcodedDimensDetector.ISSUE)
}
Hopefully in future we will have a method called addIssue inside the LintIdeIssueRegistry.

Kotlin dynamically compile a class from source code at runtime

Is it possible to compile and instantiate Kotlin class at runtime? I'm talking about something like that but using Kotlin API: How do I programmatically compile and instantiate a Java class?
As example:
I'm getting full class definition as String:
val example = "package example\n" +
"\n" +
"fun main(args: Array<String>) {\n" +
" println(\"Hello World\")\n" +
"}\n"
And then inserting it into some class.kt and running it so I'm getting "Hello World" printed in console at runtime.
You might want to look at Kotlin Scripting, see https://github.com/andrewoma/kotlin-script
Alternatively, you'll need to write your own eval(kotlin-code-string-here) method which will dump the text inside blah.kt file for example, compile it using an external Kotlin compiler into blah.class then dynamically load those classes into the runtime using the Java Classloader doing something like this:
MainClass.class.classLoader.loadClass("com.mypackage.MyClass")
This might be very slow and unreliable.
Another no so great option is to make use of Rhino and run JavaScript inside your Kotlin code. So once again, you'll have an eval(kotlin-code-string-here) method which will dump the content to a blah.kt file, then you would use a Kotlin2JS compiler to compile it to JavaScript and directly execute the JavaScript inside Kotlin using Rhino which is not great either.
Another option is to make use of Kotlin Scripting or an external Kotlin compiler (in both cases, the Kotlin compiler will have to start up) and doing something like this will also allow you to execute dynamically, albeit, only on Unix systems.
Runtime.getRuntime().exec(""" "kotlin code here" > blah.kts | sh""")
I'm not aware of a clean solution for this, Kotlin was not designed to be run like like PHP / JavaScript / Python which just interprets text dynamically, it has to compile to bytecode first before it can do anything on the JVM; so in each scenario, you will need to compile that code first in one way or another, whether to bytecode or to javascript and in both cases load it into you application using the Java Classloader or Rhino.
Please check this solution for dependencies, jar resources, etc. Code below isn't enough for successful execution.
However, to compile dynamic class you can do the following:
val classLoader = Thread.currentThread().contextClassLoader
val engineManager = ScriptEngineManager(classLoader)
setIdeaIoUseFallback() // hack to have ability to do this from IntelliJ Idea context
val ktsEngine: ScriptEngine = engineManager.getEngineByExtension("kts")
ktsEngine.eval("object MyClass { val number = 123 } ")
println(ktsEngine.eval("MyClass.number"))
Please note: there is code injection possible here. Please be careful and use dedicated process or dedicated ClassLoader for this.
KotlinScript can be used to compile Kotlin source code (e.g. to generate a jar file that can then be loaded).
Here's a Java project which demonstrates this (code would be cleaner in Kotlin):
https://github.com/alexoooo/sample-kotlin-compile/blob/main/src/main/java/io/github/alexoooo/sample/compile/KotlinCompilerFacade.java
Note that the code you provide would be generated as a nested class (inside the script).
Here is a Kotlin version:
#KotlinScript
object KotlinDynamicCompiler {
//-----------------------------------------------------------------------------------------------------------------
const val scriptClassName = "__"
const val classNamePrefix = "${scriptClassName}$"
private val baseClassType: KotlinType = KotlinType(KotlinDynamicCompiler::class.java.kotlin)
private val contextClass: KClass<*> = ScriptCompilationConfiguration::class.java.kotlin
//-----------------------------------------------------------------------------------------------------------------
fun compile(
kotlinCode: String, outputJarFile: Path, classpathLocations: List<Path>, classLoader: ClassLoader
): String? {
Files.createDirectories(outputJarFile.parent)
val scriptCompilationConfiguration = createCompilationConfigurationFromTemplate(
baseClassType, defaultJvmScriptingHostConfiguration, contextClass
) {
jvm {
val classloaderClasspath: List<File> = classpathFromClassloader(classLoader, false)!!
val classpathFiles = classloaderClasspath + classpathLocations.map { it.toFile() }
updateClasspath(classpathFiles)
}
hostConfiguration(ScriptingHostConfiguration (defaultJvmScriptingHostConfiguration) {
jvm {
compilationCache(
CompiledScriptJarsCache { _, _ ->
outputJarFile.toFile()
}
)
}
})
}
val scriptCompilerProxy = ScriptJvmCompilerIsolated(defaultJvmScriptingHostConfiguration)
val result = scriptCompilerProxy.compile(
kotlinCode.toScriptSource(KotlinCode.scriptClassName), scriptCompilationConfiguration)
val errors = result.reports.filter { it.severity == ScriptDiagnostic.Severity.ERROR }
return when {
errors.isEmpty() -> null
else -> errors.joinToString(" | ")
}
}
}

Is there any way to iterate all fields of a data class without using reflection?

I know an alternative of reflection which is using javassist, but using javassist is a little bit complex. And because of lambda or some other features in koltin, the javassist doesn't work well sometimes. So is there any other way to iterate all fields of a data class without using reflection.
There are two ways. The first is relatively easy, and is essentially what's mentioned in the comments: assuming you know how many fields there are, you can unpack it and throw that into a list, and iterate over those. Or alternatively use them directly:
data class Test(val x: String, val y: String) {
fun getData() : List<Any> = listOf(x, y)
}
data class Test(val x: String, val y: String)
...
val (x, y) = Test("x", "y")
// And optionally throw those in a list
Although iterating like this is a slight extra step, this is at least one way you can relatively easy unpack a data class.
If you don't know how many fields there are (or you don't want to refactor), you have two options:
The first is using reflection. But as you mentioned, you don't want this.
That leaves a second, somewhat more complicated preprocessing option: annotations. Note that this only works with data classes you control - beyond that, you're stuck with reflection or implementations from the library/framework coder.
Annotations can be used for several things. One of which is metadata, but also code generation. This is a somewhat complicated alternative, and requires an additional module in order to get compile order right. If it isn't compiled in the right order, you'll end up with unprocessed annotations, which kinda defeats the purpose.
I've also created a version you can use with Gradle, but that's at the end of the post and it's a shortcut to implementing it yourself.
Note that I have only tested this with a pure Kotlin project - I've personally had problems with annotations between Java and Kotlin (although that was with Lombok), so I do not guarantee this will work at compile time if called from Java. Also note that this is complex, but avoids runtime reflection.
Explanation
The main issue here is a certain memory concern. This will create a new list every time you call the method, which makes it very similar to the method used by enums.
Local testing over 10000 iterations also show a general consistency of ~200 milliseconds to execute my approach, versus roughly 600 for reflection. However, for one iteration, mine uses ~20 milliseconds, where as reflection uses between 400 and 500 milliseconds. On one run, reflection took 1500 (!) milliseconds, while my approach took 18 milliseconds.
See also Java Reflection: Why is it so slow?. This appears to affect Kotlin as well.
The memory impact of creating a new list every time it's called can be noticeable though, but it'll also be collected so it shouldn't be that big a problem.
For reference, the code used for benchmarking (this will make sense after the rest of the post):
#AutoUnpack data class ExampleDataClass(val x: String, val y: Int, var m: Boolean)
fun main(a: Array<String>) {
var mine = 0L
var reflect = 0L
// for(i in 0 until 10000) {
var start = System.currentTimeMillis()
val cls = ExampleDataClass("example", 42, false)
for (field in cls) {
println(field)
}
mine += System.currentTimeMillis() - start
start = System.currentTimeMillis()
for (prop in ExampleDataClass::class.memberProperties) {
println("${prop.name} = ${prop.get(cls)}")
}
reflect += System.currentTimeMillis() - start
// }
println(mine)
println(reflect)
}
Setting up from scratch
This bases itself around two modules: a consumer module, and a processor module. The processor HAS to be in a separate module. It needs to be compiled separately from the consumer for the annotations to work properly.
First of all, your consumer project needs the annotation processor:
apply plugin: 'kotlin-kapt'
Additionally, you need to add stub generation. It complains it's unused while compiling, but without it, the generator seems to break for me:
kapt {
generateStubs = true
}
Now that that's in order, create a new module for the unpacker. Add the Kotlin plugin if you didn't already. You do not need the annotation processor Gradle plugin in this project. That's only needed by the consumer. You do, however, need kotlinpoet:
implementation "com.squareup:kotlinpoet:1.2.0"
This is to simplify aspects of the code generation itself, which is the important part here.
Now, create the annotation:
#Retention(AnnotationRetention.SOURCE)
#Target(AnnotationTarget.CLASS)
annotation class AutoUnpack
This is pretty much all you need. The retention is set to source because it has no value at runtime, and it only targets compile time.
Next, there's the processor itself. This is somewhat complicated, so bear with me. For reference, this uses the javax.* packages for annotation processing. Android note: this might work assuming you can plug in a Java module on a compileOnly scope without getting the Android SDK restrictions. As I mentioned earlier, this is mainly for pure Kotlin; Android might work, but I haven't tested that.
Anyways, the generator:
Because I couldn't find a way to generate the method into the class without touching the rest (and because according to this, that isn't possible), I'm going with an extension function generation approach.
You'll need a class UnpackCodeGenerator : AbstractProcessor(). In there, you'll first need two lines of boilerplate:
override fun getSupportedAnnotationTypes(): MutableSet<String> = mutableSetOf(AutoUnpack::class.java.name)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
Moving on, there's the processing. Override the process function:
override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
// Find elements with the annotation
val annotatedElements = roundEnv.getElementsAnnotatedWith(AutoUnpack::class.java)
if(annotatedElements.isEmpty()) {
// Self-explanatory
return false;
}
// Iterate the elements
annotatedElements.forEach { element ->
// Grab the name and package
val name = element.simpleName.toString()
val pkg = processingEnv.elementUtils.getPackageOf(element).toString()
// Then generate the class
generateClass(name,
if (pkg == "unnamed package") "" else pkg, // This is a patch for an issue where classes in the root
// package return package as "unnamed package" rather than empty,
// which breaks syntax because "package unnamed package" isn't legal.
element)
}
// Return true for success
return true;
}
This just sets up some of the later framework. The real magic happens in the generateClass function:
private fun generateClass(className: String, pkg: String, element: Element){
val elements = element.enclosedElements
val classVariables = elements
.filter {
val name = if (it.simpleName.contains("\$delegate"))
it.simpleName.toString().substring(0, it.simpleName.indexOf("$"))
else it.simpleName.toString()
it.kind == ElementKind.FIELD // Find fields
&& Modifier.STATIC !in it.modifiers // that aren't static (thanks to sebaslogen for issue #1: https://github.com/LunarWatcher/KClassUnpacker/issues/1)
// Additionally, we have to ignore private fields. Extension functions can't access these, and accessing
// them is a bad idea anyway. Kotlin lets you expose get without exposing set. If you, by default, don't
// allow access to the getter, there's a high chance exposing it is a bad idea.
&& elements.any { getter -> getter.kind == ElementKind.METHOD // find methods
&& getter.simpleName.toString() ==
"get${name[0].toUpperCase().toString() + (if (name.length > 1) name.substring(1) else "")}" // that matches the getter name (by the standard convention)
&& Modifier.PUBLIC in getter.modifiers // that are marked public
}
} // Grab the variables
.map {
// Map the name now. Also supports later filtering
if (it.simpleName.endsWith("\$delegate")) {
// Support by lazy
it.simpleName.subSequence(0, it.simpleName.indexOf("$"))
} else it.simpleName
}
if (classVariables.isEmpty()) return; // Self-explanatory
val file = FileSpec.builder(pkg, className)
.addFunction(FunSpec.builder("iterator") // For automatic unpacking in a for loop
.receiver(element.asType().asTypeName().copy()) // Add it as an extension function of the class
.addStatement("return listOf(${classVariables.joinToString(", ")}).iterator()") // add the return statement. Create a list, push an iterator.
.addModifiers(KModifier.PUBLIC, KModifier.OPERATOR) // This needs to be public. Because it's an iterator, the function also needs the `operator` keyword
.build()
).build()
// Grab the generate directory.
val genDir = processingEnv.options["kapt.kotlin.generated"]!!
// Then write the file.
file.writeTo(File(genDir, "$pkg/${element.simpleName.replace("\\.kt".toRegex(), "")}Generated.kt"))
}
All of the relevant lines have comments explaining use, in case you're not familiar with what this does.
Finally, in order to get the processor to process, you need to register it. In the module for the generator, add a file called javax.annotation.processing.Processor under main/resources/META-INF/services. In there you write:
com.package.of.UnpackCodeGenerator
From here, you need to link it using compileOnly and kapt. If you added it as a module to your project, you can do:
kapt project(":ClassUnpacker")
compileOnly project(":ClassUnpacker")
Alternative source setup:
Like I mentioned earlier, I bundled this into a jar for convenience. It's under the same license as SO uses (CC-BY-SA 3.0), and it contains the exact same code as in the answer (although compiled into a single project).
If you want to use this one, just add the Jitpack repo:
repositories {
// Other repos here
maven { url 'https://jitpack.io' }
}
And hook it up with:
kapt 'com.github.LunarWatcher:KClassUnpacker:v1.0.1'
compileOnly "com.github.LunarWatcher:KClassUnpacker:v1.0.1"
Note that the version here may not be up to date: the up to date list of versions is available here. The code in the post still aims to reflect the repo, but versions aren't really important enough to edit every time.
Usage
Regardless of which way you ended up using to get the annotations, the usage is relatively easy:
#AutoUnpack data class ExampleDataClass(val x: String, val y: Int, var m: Boolean)
fun main(a: Array<String>) {
val cls = ExampleDataClass("example", 42, false)
for(field in cls) {
println(field)
}
}
This prints:
example
42
false
Now you have a reflection-less way of iterating fields.
Note that local testing has been done partially with IntelliJ, but IntelliJ doesn't seem to like me - I've had various failed builds where gradlew clean && gradlew build from a command line oddly works fine. I'm not sure whether this is a local problem, or if this is a general problem, but you might have some issues like this if you build from IntelliJ.
Also, you might get errors if the build fails. The IntelliJ linter builds on top of the build directory for some sources, so if the build fails and the file with the extension function isn't generated, that'll cause it to appear as an error. Building usually fixes this when I tested (with both modules and from Jitpack).
You'll also likely have to enable the annotation processor setting if you use Android Studio or IntelliJ.
here is another idea, that i came up with, but am not satisfied with...but it has some pros and cons:
pros:
adding/removing fields to/from the data class causes compiler errors at field-iteration sites
no boiler-plate code needed
cons:
won't work if default values are defined for arguments
declaration:
data class Memento(
val testType: TestTypeData,
val notes: String,
val examinationTime: MillisSinceEpoch?,
val administeredBy: String,
val signature: SignatureViewHolder.SignatureData,
val signerName: String,
val signerRole: SignerRole
) : Serializable
iterating through all fields (can use this directly at call sites, or apply the Visitor pattern, and use this in the accept method to call all the visit methods):
val iterateThroughAllMyFields: Memento = someValue
Memento(
testType = iterateThroughAllMyFields.testType.also { testType ->
// do something with testType
},
notes = iterateThroughAllMyFields.notes.also { notes ->
// do something with notes
},
examinationTime = iterateThroughAllMyFields.examinationTime.also { examinationTime ->
// do something with examinationTime
},
administeredBy = iterateThroughAllMyFields.administeredBy.also { administeredBy ->
// do something with administeredBy
},
signature = iterateThroughAllMyFields.signature.also { signature ->
// do something with signature
},
signerName = iterateThroughAllMyFields.signerName.also { signerName ->
// do something with signerName
},
signerRole = iterateThroughAllMyFields.signerRole.also { signerRole ->
// do something with signerRole
}
)