How to Add a Built-In Library Block
Note: This is for App Inventor Classic. It does not apply to App Inventor 2.
This document describes how to add a built-in library block (static function) to the App Inventor language.
As an example, we will show how this block was added to the Text drawer:
Note that the label is starts at, shown in blue throughout this document, and there are two arguments, text and piece, shown in red throughout this document.
Define the procedure in runtime.scm
Add tests to YailEvalTest.java
Update the block specification file
Define the block in OUTPUT_HEADER.txt
Specify the drawer in OUTPUT_FOOTER.txt
Optionally specify a block family
Update Blocks Language and Young Android version numbers
Updating BLOCKS_LANGUAGE_VERSION
Updating YOUNG_ANDROID_VERSION
Special blocks that are not procedure calls (special forms)
Assuming the new functionality can be implemented as a Scheme procedure call, add the procedure to buildserver/src/com/google/appinventor/buildserver/resources/runtime.scm. (If it cannot, see the summary on special forms below.) In our example, the procedure would be:
(define (string-starts-at text piece)
(+ ((text:toString):indexOf (piece:toString)) 1))
The colons are used to call Java methods. Thus, the meaning of the above (from inside out) is:
Note that the name of the Scheme procedure, string-starts-at, is generally not the same as the label on the block, starts at.
There are many other examples in runtime.scm.
Tests should be added to the appropriate methods in buildserver/tests/com/google/appinventor/buildserver/YailEvalTest.java. For example, the following lines were added to the method testTextGroup():
assertEquals("3", scheme.eval("(string-starts-at \"abc\" \"c\")").toString());
assertEquals("0", scheme.eval("(string-starts-at \"abc\" \"x\")").toString());
You can run the tests by executing the following command from the topmost buildserver directory:
ant BuildServerTests
The language used by the Blocks Editor is specified in the ya_lang_def.xml file, which is automatically generated from the static files OUTPUT_HEADER.txt and OUTPUT_FOOTER.txt (in the directory components/src/com/google/appinventor/components/scripts/templates) and from annotations in the component source code.
Figure 1 shows the addition to OUTPUT_HEADER.txt for the new block. Note that string-starts-at is both the internal name of the block and the name of the Scheme function and that text is both a formal parameter name and a type, which could be confusing to the reader of this document. Unfortunately, I was unable to find any good examples that did not include such puns so I tried to distinguish between the different usages by color (Figure 2).
<!-- String-Starts-At Block -->
<BlockGenus name="string-starts-at" decorator="call" kind="function"
initlabel="starts at" color="text">
<description>
<arg-description n="1" doc-name="text">
The text to search for the piece.
</arg-description>
<arg-description n="2" doc-name="piece">
The piece (a text string) to search for in the text.
</arg-description>
<text>Returns the starting index of the piece in the text, where index 1 denotes the beginning of the text. Returns 0 if the piece is not in the text.</text>
</description>
<BlockConnectors>
<BlockConnector connector-kind="plug" connector-type="poly"/>
<BlockConnector label="text" connector-kind="socket"
connector-type="poly"/>
<BlockConnector label="piece" connector-kind="socket"
connector-type="poly"/>
</BlockConnectors>
<LangSpecProperties>
<LangSpecProperty key="ya-kind" value="primitive"/>
<LangSpecProperty key="ya-rep" value="string-starts-at"/>
<LangSpecProperty key="plug-type-1" value="number"/>
<LangSpecProperty key="socket-allow-1" value="text/text"/>
<LangSpecProperty key="socket-allow-2" value="text/value"/>
<LangSpecProperty key="socket-allow-3" value="piece/text"/>
<LangSpecProperty key="socket-allow-4" value="piece/value"/>
</LangSpecProperties>
</BlockGenus>
Figure 1: Addition to OUTPUT_HEADER.txt
content | meaning |
string-starts-at | the internal name for the block |
starts at | the user-visible name for the block |
text piece | the formal parameters |
string-starts-at | the name of the Scheme function, which happens to be the same as the internal name in this example |
text number value | type information |
Figure 2: Color key for Figure 1
The BlockGenus kind attribute value of function indicates that the block returns a value. (For blocks that don’t return a value, such as replace-list-item, the value should be command.)
The first two LangSpecProperty tags (1) specify that the functionality is implemented by a Scheme procedure with a ya-kind value of primitive and (2) provide the procedure’s name. (For blocks not implemented as procedure calls, see the summary on special forms below.)
The BlockConnectors section describes the output connector (plug) and any input connectors (socket). Since the BlockGenus kind is function, there must be exactly one plug, and there may be any number of (including zero) sockets. (If there were no output, the kind would be command.) Type information is in the LangSpecProperties section, which in our example specifies that the type of the output is number and that the actual parameters can be of type text (a string literal) or value, which means a value computed at run-time (such as the value of a variable). Note that there can be multiple socket-allow-# entries (e.g., socket-allow-1 and socket-allow-2) for a single formal parameter. For each BlockGenus entry, the first trailing number must be 1, and later ones must be sequential (2, 3, etc.).
Alternately, parameter types can be specified with the socket-exclude-# key, indicating that all types except for the specified ones are legal. This is typically used when any type is allowed except for the argument block used to declare an argument to a user-defined procedure. For example, the definition of Add-Items-to-List includes the following for its second parameter, item:
<LangSpecProperty key="socket-exclude-1" value="item/argument"/>
For plugs (outputs), the type of the result is usually specified with the plug-type-1 key. For example, the definition of the number block includes:
<LangSpecProperty key="plug-type-1" value="number"/>
The type-exclude-1 key is used when all types except for the specified one are allowed, as with get-list-item, which can return any type of block except for the argument type:
<LangSpecProperty key="type-exclude-1" value="argument"/>
The “drawers” within the Blocks Editor and their contents are defined in OUTPUT_FOOTER.txt. For example, the change to add string-starts-at to the Text drawer is shown in bold below below, with unchanged lines referencing other blocks replaced with vertical ellipses (:):
<BlockDrawer name="Text" button-color="red">
:
<BlockGenusMember>string-starts-at</BlockGenusMember>
</BlockDrawer>
It is also possible to specify a BlockFamily of related blocks, such as:
<BlockFamily>
<FamilyMember>number-plus</FamilyMember>
<FamilyMember>number-minus</FamilyMember>
<FamilyMember>number-times</FamilyMember>
<FamilyMember>number-divide</FamilyMember>
</BlockFamily>
This creates a drop-down menu in these blocks so it is easy to switch among them. The following picture shows two number-plus blocks, where the right one is being changed through the drop-down menu to a number-minus block.
When the language is changed, version numbers need to be incremented with appropriate documentation. These version numbers appear in saved projects to ensure they are only run on compatible servers.
The Blocks Language and Young Android[1] version numbers are defined in components/src/com/google/appinventor/components/common/YaVersion.java.
To update the Blocks Language version number, find the part of the file where the constant BLOCKS_LANGUAGE_VERSION is defined. Increment the value and add descriptive comments. For example, if this line appeared:
public static final int BLOCKS_LANGUAGE_VERSION = 16;
you would replace it with:
// For BLOCKS_LANGUAGE_VERSION 17
// Added starts-at to Text drawer.
public static final int BLOCKS_LANGUAGE_VERSION = 17;
Other examples in the file show how to document multiple additions and other changes.
Next, in the same file, you would update the Young Android version number from:
public static final int YOUNG_ANDROID_VERSION = 53;
to:
// FOR YOUNG_ANDROID_VERSION 54:
// - BLOCKS_LANGUAGE_VERSION was incremented to 17.
public static final int YOUNG_ANDROID_VERSION = 54;
Finally, you need to update blockslib/src/openblocks/yacodeblocks/BlockSaveFile.java to indicate how to update a saved blk file from an old version to the new version. Assuming you are only adding, not removing or changing, library functions, this is straightforward. Find the section of the method upgradeLanguage() in which the parameter blkYaVersion is checked and incremented. It will end with something like this:
if (blkLangVersion < 16) {
// In BLOCKS_LANGUAGE_VERSION 16, we added make-color to the Color drawer.
// No language blocks need to be modified to upgrade to version 16.
blkLangVersion = 16;
}
Insert similar code right after with the new Blocks Language version number:
if (blkLangVersion < 17) {
// In BLOCKS_LANGUAGE_VERSION 17, we added starts-at to the Text drawer.
// No language blocks need to be modified to upgrade to version 17.
blkLangVersion = 17;
}
If you were removing or changing library blocks, you would have to write and call a method to transform the block file or issue warnings. An example of such a method is changeGetStartTextAndOpenCloseScreenBlocks().
In addition to creating and running unit tests, as described above, you should of course build a server with your changes and create and run test applications. Make sure you can open an old project (to test your changes to BlockSaveFile.java) and that when you download the source of a new or changed project that the .blk file has the appropriate YA and Blocks Language version numbers, such as:
<YACodeBlocks ya-version="54" lang-version="17">
When making a code review request, send the server address and test applications to your reviewer(s).
A few blocks cannot be implemented as Scheme procedure calls. One example is the if block
which only evaluates its “then-do” parameter if the “test” parameter has a value of true. (With ordinary procedure calls, all parameters are evaluated before the procedure is applied.) To implement such a special form, it is necessary to determine what Scheme expression needs to be generated and then write a transformer in blockslib/src/openblocks/yacodeblocks/BlockParser.java that translates the block structure into that expression. This defines a new ya-kind value, which allows us to specify if blocks as shown in Figure 3. Other special forms include ifelse, makeList, and forEach, which are also defined in BlockParser.java.
<!-- If Block -->
<BlockGenus name="if" kind="command" initlabel="if" color="control">
<description>
<arg-description n="1" name="test">The condition to test.
</arg-description>
<arg-description n="2"
name="then-do">The actions to be performed when
the condition is true.
</arg-description>
<text>Tests a given condition. If the result is true, performs
the actions in the 'then-do' sequence of blocks.</text>
</description>
<BlockConnectors>
<BlockConnector label="test"
connector-kind="socket"
connector-type="poly"/>
<BlockConnector label="then-do"
connector-kind="socket"
is-indented="yes"
connector-type="cmd"/>
</BlockConnectors>
<LangSpecProperties>
<LangSpecProperty key="ya-kind" value="if"/>
<LangSpecProperty key="socket-allow-1" value="test/value"/>
<LangSpecProperty key="socket-allow-2" value="test/boolean"/>
</LangSpecProperties>
</BlockGenus>
Figure 3: The BlockGenus definition in OUTPUT_HEADER.txt for if