Skip to content

Create a Wireframe

What is a wireframe?

A wireframe stitches together multiple different UI prototype screens into a simulated app with navigation and state transfer between screens. You can configure AFib to startup directly into a wireframe, making it look like you are starting a real app.

Wireframes are potentially useful for creating 'walk through' videos of planned functionality which can be used to gather feedback. They might also be used to enable a product manager to walk through a demo with a potential customer.

Can't I just roll my own?

As you come to understand AFib, you will see that it would be fairly easy to create your own wireframes using the standard AFib APIs.

The value of the wireframe is that it allows simulated functionality to co-exist with production code:

  • Wireframes can co-exist with real production code. One team can be implementing stable parts of your application, while another team creates an extended or alternate wireframe. The two will not conflict with each other.

  • Wireframes centralize all simulated functionality in a single file. This makes it easy to tell which parts of your app are simulated in the wireframe.

Generating a wireframe

A wireframe is considered a test, and is generated via the generate test subcommand. It must end in Wireframe, as the help for that command describes. This command:

dart bin/xxx_afib.dart generate test DemoWireframe --initial-screen TodoListScreen

Will produce lib/test/wireframes/demo_wireframe.dart.

Note that the --initial-screen flag specifies the name of a screen that already exists in your app, and will be the initial screen loaded by the wireframe.

A Wireframe starts like a UI Prototype

A wireframe definition looks very much like a UI protoype. It starts with a stateView and a navigate push call that directs it to an initial screen. For example:

void defineDemoWireframe(AFWireframeDefinitionContext definitions) {

  definitions.defineWireframe(
    id: TODOWireframeID.demo,
    navigate: AFSISigninScreen.navigatePushReady(),
    stateView: [TODOTestDataID.todoStateFullLogin, AFTimeState.createNow()],
    body: _executeHandleEventDemoWireframe,
    enableUINavigation: true,
    timeHandling: AFTestTimeHandling.running
  );
}
The primary difference is the body parameter, which is a function which handles wireframe events. It is described below.

The State View is shared among all screens in a wireframe

In a wireframe, AFib builds an initial state view using the data you provide, then continues to use that state view for all the screens that the wireframe visits. You can update that state view in response to wireframe events. As a result, you can use that state view to make it appear that data is moving between screens.

Since a state view is just a mapping of type names to objects, AFib can convert between different state view types for you. As long as the objects your UI accesses via a state view exist at the root of the state view you provide, everything will work properly.

Instrumenting wireframe events

Because wireframes are built on UI prototypes, route parameter updates, and context.show... calls already work as expected.

Unlike UI prototypes, context.navigate... calls also work in Wireframes, so you can place them directly in your UI code. If you prefer to have your wireframe control navigation, you can omit context.navigate... calls from your UI code, and perform them in your wireframe.

context.execute calls, which are used to issue queries that synchonize with persistant external state, do not have any effect in a wireframe.

Anytime you'd like a user event (e.g. button press, etc) to update 'persistent' state that is shared across screens, you will add wireframe instrumentation using context.executeWireframeEvent.

For example, in your SPI you might have something like:

    void onPressedSaveTodoItem() {
        // this works in production, but does nothing in a UI prototype or wireframe.
        context.executeQuery(WriteTodoItemQuery(item: context.p.itemInProgress, onSuccess: (successCtx) {
          context.navigatePop();
        }));

        // so, add an instrumented call to a wireframe.  What this does will be specified in the wireframe
        // itself. It does nothing if you are not executing within a wireframe.
        context.executeWireframeEvent(this, TDLEWidgetID.buttonSave, context.p.itemInProgress,    onSuccess: () {
          context.navigatePop()
        });
    }
Note that the wireframe event takes a widget ID, indicating what widget created the event. The context already knows what screen generated the event. It also takes an arbitrary piece of data. If your wireframe event is meant to transfer state across screens, you will generally pass that state into the wireframe event.

Wireframe events only work within an executing wireframe. In other contexts, they do nothing.

Implementing wireframe events

A wireframe is just a function that takes an AFWireframeExecutionContext. Within the function, you can detect wireframe events using the context.isScreenAndWidget function, then modify the shared state view context.updateStateView.... Here is an example:

void defineInitialWireframe(AFWireframeDefinitionContext definitions) {

  definitions.defineWireframe(
    id: EVEXWireframeID.initial,
    navigate: HomePageScreen.navigatePush(lineNumber: 0),
    stateView: [EVEXTestDataID.evexStateFullLogin, AFTimeState.createNow()],
    body: _executeHandleEventInitialWireframe,
    enableUINavigation: true,
    timeHandling: AFTestTimeHandling.running
  );
}

bool _executeHandleEventInitialWireframe(AFWireframeExecutionContext context) {
  final stateView = context.accessStateView<EVEXDefaultStateView>();

  if(context.isScreenAndWidget(EVEXScreenID.counterManagement, EVEXWidgetID.buttonSaveTransientCount)) {
    final param = context.accessEventParam<CounterManagementScreenRouteParam>();
    final countHistory = stateView.countHistoryItems;
    final revised = countHistory.reviseSetItem(
      CountHistoryItem.createNew(
        count: param.clickCount, 
        userId: stateView.userCredential.userId,
        idPrefix: "count_wireframe"
      )
    );
    context.updateStateViewRootOne(revised);
    return true;
  }  else if(context.isScreenAndWidget(EVEXScreenID.counterManagement, EVEXWidgetID.buttonResetHistory)) {
    context.updateStateViewRootOne(CountHistoryItemsRoot.initialState());
    return true;
  }
  return false;
}

As you can see, the function detects which event is executing using context.isScreenAndWidget. It then accesses the data passed to the event using context.accessEventParam. Finally, it uses context.updateStateView... to update the state view that is shared among all the screens the wireframe visits.

Using a Wireframe

When you are developing a wireframe, you can enter it by entering prototype mode, then navigating into the Wireframes menu item.

Starting directly into a wireframe

For maximum demo impact, you can also configure AFib to start directly into a wireframe. To do so, call

config.setStartupWireframe(XXXWireframeID.yourWireframe);

From within lib/initialization/environments/prototype.dart. In addition, you need to set the environment to:

config.setValue("environment", AFEnvironment.startupInWireframe);

within lib/initialization/xxx_config.g.dart.