When you create a custom view you can define custom attributes. These elements are declared in an XML resources file. More information can be found at

https://developer.android.com/training/custom-views/create-view.html

All well and good except that there's no Java enum equivalent generated. Sure, one could be created by hand, but that seems prone to getting out of sync. Wouldn't it be good if there was a way to generate a Java enum class from the XML enum? Well here's a way. (Let's be honest, if I didn't have a way to do it, this blog post would be a bit pointless!)

By the power of Gradle! With a little help from Square's JavaPoet.

Now this may not be the most elegant way, nor the best way, nor even a good idea. It's probably brittle, doesn't scale, and there are probably a dozen other reasons to not use it. If someone with more experience than me who actually knows what they are doing tells you not to even look at this site, listen to them.

It's all in the github repo

https://github.com/afterecho/create_enum_from_xml

The explanation is in the README.md file and may be more up to date. In the event of a conflict, trust the repo.

This chunk is placed in the build.gradle at the module level. Explanation below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 android.applicationVariants.all { variant ->

   File outdir = new File("${project.buildDir}/generated/source/attrenum/${variant.dirName}")
   File attrsfile = new File("${project.projectDir}/src/main/res/values/attrs.xml")

   def attrEnumTask = tasks.create("attrenum${variant.name}") << {
     def attrs = new XmlParser().parse(attrsfile)
     def styleable = attrs.depthFirst().find {
       it.name() == "declare-styleable" && it.@name == "MyEnumStyle"
     }
     def enumlist = styleable.depthFirst().find { it.name() == "attr" && it.@name == "fubar" }
     def vals = enumlist.depthFirst().findAll { it.name() == "enum" }

     com.squareup.javapoet.TypeSpec.Builder attrEnumBuilder = com.squareup.javapoet.TypeSpec.enumBuilder("AttrEnum").addModifiers(javax.lang.model.element.Modifier.PUBLIC)
     vals.each {
       attrEnumBuilder.addEnumConstant(it.@name, com.squareup.javapoet.TypeSpec.anonymousClassBuilder('$L', it.@value as int).build())
     }

     attrEnumBuilder.addField(com.squareup.javapoet.TypeName.INT, "index", javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.FINAL)
       .addMethod(com.squareup.javapoet.MethodSpec.constructorBuilder()
       .addParameter(com.squareup.javapoet.TypeName.INT, "index")
       .addStatement('this.$N = $N', "index", "index")
       .build())

     com.squareup.javapoet.TypeSpec attrEnum = attrEnumBuilder.build();

     com.squareup.javapoet.JavaFile javaFile = com.squareup.javapoet.JavaFile.builder("com.afterecho.android.util", attrEnum).build();
     javaFile.writeTo(outdir)
   }
   attrEnumTask.description = 'Turns a stylable enum into a Java enum'
   variant.registerJavaGeneratingTask attrEnumTask, outdir
 }

Line 1 - This runs against all variants of build that are configured. For more information about variants, see

https://developer.android.com/tools/building/configuring-gradle.html

Lines 3-4 - Two File objects are defined. outdir is the base directory where the generated Java file will be placed. I have hardcoded some of the path (generated/source) as I don't know if it is available in some other way. The attrenum is chosen by me. You can choose anything you like but don't use something that is already in use. (Have a look in the generated/source directory after you've done a build to see what is already in use.)

attrsfile is where the XML with the declared styleable enums are. As you can see I've hardcoded a large chunk of the path as I don't know any other way to get it.

Line 6 - This creates a new task. The ${variant.name} makes the task name unique per build variant otherwise you'll get complaints.

Lines 7-12 - Ok, not very pretty, but it parses the attrs.xml file, finds the declare-styleable block that has a name of MyEnumStyle and then finds the attr element that has a name of fubar and then gets all the enums within. I have no doubt that there are cleaner, prettier ways, but I can't do all the work for you. :)

Lines 14-29 - This uses Square's JavaPoet to create an enum. Each enum is named after the attribute name in the XML so make sure you use valid Java identifiers. I've also given the enum a parameter which is the value attribute in case you don't use consecutive values. JavaPoet is very powerful so you can do so much more than this if you wish. Or replace it all with a load of printlns if you want.

The string parameter in the builder() call is the package name for your class.

Line 30 - This writes the Java source file. Luckily it also creates the package directory structure too.

Lines 32-33 - These last two lines give the task a description and register it.

Once you have all this, a rebuild of your project and Hey Presto! There's a new Java file that looks something like this:

 1  package com.afterecho.android.util;
 2 
 3  public enum AttrEnum {
 4    FirstEnum(0),
 5 
 6    SecondEnum(1),
 7 
 8    ThirdEnum(2),
 9 
10    FourthEnum(3);
11 
12    public final int index;
13 
14    AttrEnum(int index) {
15      this.index = index;
16    }
17  }

You can see it in AndroidStudio through the normal method of opening Java files (CTRL-N on Linux). You'll notice when you open it that there's the warning "Files under the build folder are generated and should not be edited." That shows it's in the right place.

Use as you see fit!

No licence. Public Domain. No rights reserved. E&OE. Contents may settle in transit. Not responsible for any damage the code does. For novelty purposes only. Not to be taken internally.

Darren @ Æ


Comments

comments powered by Disqus