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

BlockGenus attributes

ya-kind

Plugs (outputs) and Sockets

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

Updating BlockSaveFile.java

Testing

Special blocks that are not procedure calls (special forms)

Define the procedure in runtime.scm

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:

  1. Convert the parameters text and piece to Java String objects.
  2. Invoke Java’s indexOf() method, using the first String as the invoking object and the second String as the argument.
  3. Add 1 to the result.

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.

Add tests to YailEvalTest.java

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

Update the block specification file

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.

Define the block in OUTPUT_HEADER.txt

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

BlockGenus attributes

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.)

ya-kind

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.)

Plugs (outputs) and Sockets

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"/>

Specify the drawer in OUTPUT_FOOTER.txt

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>

Optionally specify a block family

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.

Update Blocks Language and Young Android version numbers

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.

Updating BLOCKS_LANGUAGE_VERSION

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.

Updating YOUNG_ANDROID_VERSION

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;

Updating BlockSaveFile.java

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().

Testing

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).

Special blocks that are not procedure calls (special forms)

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


[1] For the meaning of “Young Android”, see the glossary.