Mutators

A mutator is like a special extension; in addition to changing the block, it defines how those changes will be saved to XML and loaded from XML. Extensions, by comparison, may not save changes to or load changes from XML. The most visible example of a mutator is the pop-up dialog which allows if statements to acquire extra else if and else clauses. But not all mutations are so complex.

All mutators must define a mutator class and a factory for creating instances of that class and be registered on the BlockFactory. Mutators that show UI to the user may also define a mutator fragment containing the UI and register it with the BlockViewFactory.

Registering a mutation

Just like extensions, mutations must be registered with the BlockFactory. This is typically done by overriding configureMutators() on AbstractBlocklyActivity.

// Extending AbstractBlocklyActivity
public void configureMutators() {
    super.configureMutators(); // Adds the default mutators
    BlockFactory blockFactory = mController.getBlockFactory();
    blockFactory.registerMutator("my_mutator_extension",
            MyMutator.FACTORY);
    // Optionally add a UI for the mutator
    BlockViewFactory bvFactory = mController.getBlockViewFactory();
    bvFactory.registerMutatorUi("my_mutator_extension",
            MyMutatorFragment.FACTORY);
}

Mutator class

The Mutator class defines a small set of methods for interacting with Blockly. At a minimum, serialize() and update() must be defined by any subclasses. There are also two optional methods, onAttach() and onDetach(), which may be overridden to do any setup or teardown when the mutator is attached to or detached from a block.

serialize and update

The XML format used to load, save, copy, and paste blocks automatically captures and restores all data stored in editable fields. However, if the block contains additional information, this information would be lost when the block is saved and reloaded. Each block's XML has an optional mutator element where arbitrary data may be stored.

An example of this is IfElseMutator's controls_if block. By default it has one input value and one input statement:

If the user adds an else if clause it modifies the block to look like this:

To persist this change when saving and loading blocks, the serialize function writes the mutator element recording that this block has an extra elseif clause. When the block is loaded the mutator tag is passed to the update function which adds a second if statement named IF1. The logic_boolean block can then be correctly attached to it.

  <block type="controls_if">
    <mutation elseif="1"></mutation>
    <value name="IF1">
      <block type="logic_boolean">
        <field name="BOOL">TRUE</field>
      </block>
    </value>
  </block>

Saving mutation data is done by implementing serialize on your mutator class. Here is the example from the controls_if block's IfElseMutator:

@Override
public void serialize(XmlSerializer serializer) throws IOException {
    if (mElseIfCount == 0 && !mHasElseStatement) {
        return;
    }
    serializer.startTag(null, "mutation")
            .attribute(null, "elseif", String.valueOf(mElseIfCount))
            .attribute(null, "else", mHasElseStatement ? "1" : "0");
    serializer.endTag(null, "mutation");
}

This function is called whenever a block is being written to XML. If the function returns null, then no mutation is recorded. If the function returns a 'mutation' XML element, then this element (and any properties or child elements) will be stored at the beginning of the block's XML representation.

The inverse function is update which is called whenever a block is being restored from XML. Here is the controls_if block's:

@Override
public void update(XmlPullParser parser) throws IOException, XmlPullParserException {
    int elseIfCount = 0;
    boolean hasElse = false;

    // Note: parser.next should be called to get the <mutation> element.
    int tokenType = parser.next();
    if (tokenType != XmlPullParser.END_DOCUMENT) {
        parser.require(XmlPullParser.START_TAG, null, TAG_MUTATION);
        String elseIfValue = parser.getAttributeValue(null, "elseif");
        if (!TextUtils.isEmpty(elseIfValue)) {
            try {
                elseIfCount = Integer.parseInt(elseIfValue);
            } catch (NumberFormatException e) {
                Log.e(TAG, "Error reading mutation elseif count.", e);
            }
        }
        String elseValue = parser.getAttributeValue(null, "else");
        if (TextUtils.equals("1", elseValue)) {
            hasElse = true;
        }
    }
    updateImpl(elseIfCount, hasElse); // Performs the mutation on the block
}

It is passed a parser where the next element is the 'mutation' XML element. The function may parse the element and reconfigure the block based on the element's attributes and child elements.

Performing a mutation

To provide consistency, it is recommended that mutations be handled in update regardless of the source. When triggering an update from UI, mBlock.setMutation(String) should be called with the serialized mutation to trigger a call to update and send an event for the change. This also means that there is only one code path for mutations to be applied. The IfElseMutator.mutate method performs mutations in this way.

Changing the shape of a block

Mutations often involve changing the shape of their block. To do this, a new list of inputs should be created which define the blocks updated shape. The list may contain existing inputs on the block if they will also exist after the mutation. The list of inputs, as well as the output, previous, or next connections, should then be passed to mBlock.reshape.

mBlock.reshape(newInputList, outputConnection, previousConnection, nextConnection);

For convenience, reshape(newInputList) may be called to use the existing output, previous, and next connections for the block. The new list of inputs and connections must still follow the guidelines for creating custom blocks. In addition, any inputs that are being removed and are not included in the new list must have had any connected blocks disconnected.

See the IfElseMutator.updateImpl for an example of how this can be done for a reasonably complex mutation.

Mutator editing UI

The mutator also needs UI if the user should be able to edit the block's shape. Unlike web, there is currently no default UI for mutators. To build a mutator UI you must extend MutatorFragment and register a factory for it on the BlockViewFactory.

BlockViewFactory bvFactory = mController.getBlockViewFactory();
bvFactory.registerMutatorUi("my_mutator_extension",
        MyMutatorFragment.FACTORY);

Registration is typically done in configureMutators but can happen at any point before blocks are shown to the user.

Custom editor UIs

MutatorFragment extends DialogFragment, so any UI that can be put inside a Fragment may be used for a mutator's UI. The controller on Android will request a new Fragment from the Factory that was registered when it is ready to be shown.

The IfElseMutatorFragment uses an AlertDialog with a custom content view to render controls and updates the block when the user clicks the done button. You could also update the block on every change instead of waiting until the user confirms the changes.

AlertDialog dialog = new AlertDialog.Builder(getActivity())
        .setTitle(R.string.mutator_if_else_title)
        .setPositiveButton(R.string.mutator_done, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                finishMutation();
            }
        })
        .setView(contentView)
        .create();
return dialog;