How to Add a Dropdown Block to a Component

Originally by Beka Westberg

This document describes how to add a dropdown block to a component. Currently it focuses on upgrading old components, not creating new ones.

1. Defining the Dropdown

import com.google.appinventor.components.common.OptionList;
import com.google.appinventor.components.common.Default;

public enum Animal implements OptionList<String> {
 Lion(
"lion"),
 
@Default
 Giraffe(
"giraffe"),
 Elephant(
"elephant"),
 
@Deprecated
 Dodo(
"dodo");

 
private String animal;

 Animal(String
 anim) {
   
this.animal = anim;
 }

 
public String toUnderlyingValue() {
   
return animal;
 }

 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }

 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }
}

1.1 Placement

Before defining your dropdown you need to make sure you are in the correct directory. This is because the component processor needs to get special information out of dropdowns that is only available if they are compiled first.

If you are working in the runtime directory:

components

  ﹂src

       ﹂com

             ﹂google

                   ﹂appinventor

                         ﹂components

                               ﹂annotations

                               ﹂common

                               ﹂runtime    ← here!

                               ﹂scripts

Then put your definition in the common directory:

components

  ﹂src

       ﹂com

             ﹂google

                   ﹂appinventor

                         ﹂components

                               ﹂annotations

                               ﹂common    ← here!

                               ﹂runtime

                               ﹂scripts

If you are working in your own extension directory:

components

  ﹂src

       ﹂MyDirectory    ← here!

       ﹂com / google / appinventor / components / …

Create a new directory named “helpers” and put your dropdown definition there:

components

  ﹂src

       ﹂MyDirectory

             ﹂helpers    ← here!

       ﹂com / google / appinventor / components / …

If you do not put your dropdown in the correct directory you will receive an error when you try to build the component that looks something like this:

OptionList Class: <classname> is not available. Make sure that it is available to the compiler.

1.2 Basic Implementation

Dropdowns are defined using special enums which implement the OptionList<T> interface. This interface requires that:

  1. The enum has a toUnderlyingValue() function which returns a value of type T.
  2. The enum has a static fromUnderlyingValue() function which takes in a value of type T and returns an instance of the enum type.

import com.google.appinventor.components.common.OptionList;

public enum Animal implements OptionList<String> {
 Lion(
"lion"),
 Giraffe(
"giraffe"),
 Elephant(
"elephant");

 
private String animal;

 Animal(String anim) {
   
this.animal = anim;

  }

 
public String toUnderlyingValue() {
   
return animal;
 }


 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }
 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }

}

→ For more information about defining enums see Java Enums and Java Enum String Example

1.2.1 Backing Values

If you are upgrading a component and using a dropdown to replace a set of constants the backing values should be the values of the constants.

For example if you have a function like this:

// Align takes in values: -1 (left) 0 (center) or 1 (right).
public void Align(int alignment) { }

Your enum should look like this:

import com.google.appinventor.components.common.OptionList;

public enum Alignment implements OptionList<Integer> {

  // These values are the values the Align function takes in.
 Left(-
1),
 Center(
0),
 Right(
1);

 
private int alignment;

 Alignment(
int align) {
   
this.alignment = align;
 }

 
public Integer toUnderlyingValue() {
   
return alignment;
 }


 
private static final Map<Integer, Alignment> lookup = new HashMap<>();

 
static {
   
for(Alignment align : Alignment.values()) {
     lookup.put(align.toUnderlyingValue(), align);
   }
 }

 
public static Alignment fromUnderlyingValue(Integer align) {
   
return lookup.get(align);
 }

}

1.2.2 Display Names

The names displayed in your dropdown block are the names of your enum options.

This is slightly more complicated if you are creating a build-in component, because they support i18n. For more information see i18n.

1.2.3 Ordering

The order of options in your dropdown is determined by the order of options in your enum.

Options at the top of your enum are displayed at the top of the dropdown list.

1.2 Default Value

The default value of a dropdown is the value that is selected when the dropdown block is first created. By default the default value is the first enum constant. So in the following definition:

import com.google.appinventor.components.common.OptionList;

public enum Animal implements OptionList {
 Lion(
"lion"),
 Giraffe(
"giraffe"),
 Elephant(
"elephant");

 
private String animal;

 Animal(String anim) {
   
this.animal = anim;
 }

 
public String getValue() {
   
return animal;
 }


 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }
 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }
}

Lion is the default value.

You can change this by importing the @Default tag, and adding it to one of your enum constants options. In this definition:

import com.google.appinventor.components.common.OptionList;
import com.google.appinventor.components.common.Default;

public enum Animal implements OptionList {
 Lion(
"lion"),

  @Default
 Giraffe(
"giraffe"),
 Elephant(
"elephant");

 
private String animal;

 Animal(String anim) {
   
this.animal = anim;
 }

 
public String getValue() {
   
return animal;
 }


 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }
 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }
}

Giraffe is the default value.

1.3 Versioning

Versioning means changing your enum/dropdown after you have already released a version of your component including the dropdown.

1.3.1 Deprecation

You deprecate an option when you no longer want users to use it. If someone has a project that is currently using that option the block will be highlighted in red:

But it will still generate code correctly. The option will also be removed from the dropdown so that users can no longer select it.

To deprecate an option simply add an @Deprecated tag to it.

import com.google.appinventor.components.common.OptionList;

public enum Animal implements OptionList {
 Lion(
"lion"),
 Giraffe(
"giraffe"),
 Elephant(
"elephant"),
 
@Deprecated
 Dodo(
"dodo");

 
private String animal;

 Animal(String anim) {
   
this.animal = anim;
 }

 
public String getValue() {
   
return animal;
 }


 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }
 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }

}

1.3.2 Reordering

Reordering the options of a dropdown is very simple. Just reorder them in your Java definition and that will be reflected by the blocks.

import com.google.appinventor.components.common.OptionList;

public enum Animal implements OptionList {

  Elephant("elephant"),

  Giraffe("giraffe"),
 Lion(
"lion");

 
private String animal;

 Animal(String anim) {
   
this.animal = anim;
 }

 
public String getValue() {
   
return animal;
 }


 
private static final Map<String, Animal> lookup = new HashMap<>();

 
static {
   
for(Animal anim : Animal.values()) {
     lookup.put(anim.toUnderlyingValue(), anim);
   }
 }

 
public static Animal fromUnderlyingValue(String anim) {
   
return lookup.get(anim);
 }

}

1.4 I18n

Internationalization support is only available for built-in components. You can internationalize both the “tag” of the dropdown block, and the actual options.

1.4.1 Tags

To internationalize the tag of a dropdown block add the following to the correct OdeMessages file:

<camelCaseTagName>OptionList = Translated Tag

For example if I wanted to translate the Animal tag into Hungarian I would say:

animalOptionList = Állat

1.4.2 Options

To internationalize an option of a dropdown block add the following to the correct OdeMessages file:

<camelCaseTagName><OptionName>Option = Translated Option

For example if I wanted to translate all of the Animal options into Hungarian I would say:

animalLionOption = Oroszlán
animalGiraffeOption = Zsiráf
animalElephantOption = Elefánt

2. Upgrading Old Components

This section is for people that have already released a component into the wild, and they would like to associate some existing properties/methods/events with dropdowns.

 

@DesignerComponent(version = 1,
  description =
"This is my custom zoo component",
  category = ComponentCategory.EXTENSION,
  nonVisible =
true,
  iconName =
"images/extension.png")
@SimpleObject(external = true)
public class ZooOld extends AndroidNonvisibleComponent {

 
private String favoriteAnimal = "lion";

 
public ZooOld(ComponentContainer container) {
   
super(container.$form());
 }

 
@SimpleProperty
 
public @Options(Animal.class) String FavoriteAnimal() {
   
return favoriteAnimal;
 }

 
@SimpleProperty
 
public void FavoriteAnimal(@Options(Animal.class) String animal) {
   favoriteAnimal = animal;
 }


 
@SimpleFunction
 
public void MakeDance(@Options(Animal.class) String animal) {
   
// Make the animal dance.
 }


 
@SimpleFunction
 
public @Options(Animal.class) String MakeRun(

      @Options(Animal.class) String animal

  ) {
   
// Make the animal run.
 }

 
@SimpleEvent
 
public void OnHungry(@Options(Animal.class) String animal) {
   EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
 }
}

The basic idea is that you take any parameter or return type that is the more primitive type (e.g. String, int, etc) and annotate it with an @Options annotation. You pass the annotation the dropdown definition you created in Part 1.

In this example we have a ZooOld component that we have already released. In the past we had users give it one of the strings “lion”, “giraffe”, or “elephant”. But now we would like them to be able to use a dropdown.

2.1 Properties

Our FavoriteAnimal property looked like this before:

@SimpleProperty
public String FavoriteAnimal() { //Getter
 
return favoriteAnimal;
}

@SimpleProperty
public void FavoriteAnimal(String animal) { //Setter
 favoriteAnimal = animal;
}

And then we add annotations to make it look like this:

@SimpleProperty
public @Options(Animal.class) String FavoriteAnimal() { //Getter
 
return favoriteAnimal;
}

@SimpleProperty
public void FavoriteAnimal(@Options(Animal.class) String animal) { //Setter
 favoriteAnimal = animal;
}

The annotation on the getter’s return type allows it to be compared against a dropdown.

And the annotation on the setter’s parameter allows it to accept a dropdown.

You should always include both.

2.2 Methods

Our ZooOld component has two methods. MakeDance has a void return type, and MakeRun has a String return type (which returns a String representing an animal).

Before our methods looked like this:

@SimpleFunction
public void MakeDance(String animal) {
 
// Make the animal dance.
}

@SimpleFunction
public String MakeRun(
   String animal
) {
 
// Make the animal run.
}

And then we add annotations to make it look like this:

@SimpleFunction
public void MakeDance(@Options(Animal.class) String animal) {
 
// Make the animal dance.
}

@SimpleFunction
public @Options(Animal.class) String MakeRun(
   
@Options(Animal.class) String animal
) {
 
// Make the animal run.
}

The annotations on the functions’ parameters allows them to accept dropdowns.

And the annotations on the MakeRun function’s return type allows it to be compared against a dropdown.

2.3 Events

Our OnHungry function looked like this before:

@SimpleEvent
public void OnHungry(String animal) {
 EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
}

And then we add an annotation to make it look like this:

@SimpleEvent
public void OnHungry(@Options(Animal.class) String animal) {
 EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
}

The annotation on the parameter adds a dropdown block to the variable setter in the flydown.

And the annotation allows that variable to be compared against a dropdown.

3. Writing New Components

Including dropdowns in new components is even easier than including them in upgraded components.

@DesignerComponent(version = 1,
 description =
"This is my custom zoo component",
 category = ComponentCategory.EXTENSION,
 nonVisible =
true,
 iconName =
"images/extension.png")
@SimpleObject(external = true)
public class ZooNew extends AndroidNonvisibleComponent {

 
private Animal favoriteAnimal = Animal.Lion;

 
public ZooNew(ComponentContainer container) {
   
super(container.$form());
  }

 
@SimpleProperty
 
public Animal FavoriteAnimal() {
   
return favoriteAnimal;
 }

 
@SimpleProperty
 
public void FavoriteAnimal(Animal animal) {
   favoriteAnimal = animal;
 }

 
@SimpleFunction
 
public void MakeDance(Animal animal) {
   
// Make the animal dance.
 }

 
@SimpleFunction
 
public Animal MakeRun(Animal animal) {
   
// Make the animal run.
 }

 
@SimpleEvent
 
public void OnHungry(Animal animal) {
   EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
 }
}

All you have to do is use the enum/dropdown you defined in Part 1 instead of the more primitive type.

3.1 Why can’t I use this to upgrade?

You cannot use this method to upgrade components you have already released because those components need to be backwards compatible.

Take the FavoriteAnimal function for example. If it previously accepted a string:

You want it to still accept that string, even after you’ve allowed it to also accept dropdowns.

New components don’t need this backwards compatibility. It’s actually better from an abstraction/design point of view if new blocks only accept dropdowns.

So if you try to upgrade your FavoriteAnimal function by changing the parameter type (instead of using an @Option annotation) this will break:

And your users won’t be happy :/

3.2 Properties

Instead of defining our FavoriteAnimal property using Strings (or another primitive type):

@SimpleProperty
public String FavoriteAnimal() {
 
return favoriteAnimal;
}

@SimpleProperty
public void FavoriteAnimal(String animal) {
 favoriteAnimal = animal;
}

We define it using Animals:

@SimpleProperty
public Animal FavoriteAnimal() {
 
return favoriteAnimal;
}

@SimpleProperty
public void FavoriteAnimal(Animal animal) {
 favoriteAnimal = animal;
}

This means that your setter can accept Animal dropdowns (and only Animal dropdowns)

And your getter can be compared against Animal dropdowns (and only Animal dropdowns)

3.3 Methods

Instead of defining our methods using Strings (or another primitive type):

@SimpleFunction
public void MakeDance(String animal) {
 
// Make the animal dance.
}

@SimpleFunction
public String MakeRun(String animal) {
 
// Make the animal run.
}

We define them using Animals:

@SimpleFunction
public void MakeDance(Animal animal) {
 
// Make the animal dance.
}

@SimpleFunction
public Animal MakeRun(Animal animal) {
 
// Make the animal run.
}

This means that your methods can accept Animal dropdowns (and only Animal dropdowns)

And that your MakeRun function can be compared against Animal dropdowns (and only Animal dropdowns)

3.4 Events

Instead of defining our event using a String (or another primitive type):

@SimpleEvent
public void OnHungry(String animal) {
 EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
}

We define it using an Animal:

@SimpleEvent
public void OnHungry(Animal animal) {
 EventDispatcher.dispatchEvent(
this, "OnHungry", animal);
}

This allows that variable to be compared against an Animal dropdown (and only an Animal dropdown).

And in the future it may allow a dropdown block to be included in the parameter’s flydown: