Skip to content

Using AFib Libraries

AFib libraries are standard dart packages, but with certain known AFib hooks and naming conventions. Those hooks allow the library to install its own custom state, startup queries, screens and other UI elements, commands, and more.

Why does AFib have libraries?

AFib libraries are a higher-level mechanism for code re-use. They are intended to support three specific scenarios - backend-agnostic open source UI libraries, backend-committed commercial libraries, and private libraries used to delineate code ownership across developers or teams.

Open Source - afib_signin and afib_chatsupport are examples of open source, backend-agnostic UI libraries. The former provides signin screens (and a flow between them). The latter provides widgets, screens, state and commands for providing simple integrated chat support.

Commercial - No backend-committed commercial libraries currently exist, but you can imagine that Olark might provide a library that integrates its chat-based customer-service offering, or that a net-promoter SAAS might provide a set of screens that feed data into its net-promoter score backend.

Team Organization - The project that spawned AFib has its own closed-source shared library which provides a variety of screens, widgets, state and commands shared between its main public app, and a private internal app used for support and human-assisted machine-learning. This usage was influenced by my time at Adobe (almost 20 years ago), where most desktop applications are created as a simple kernal and a large set of plug-ins, each of which is owned by a developer or team.

State Libraries vs UI Libraries

AFib supports two kind of libraries. State libraries provide state, commands and queries. UI Libraries provide UI to your app (screens, etc), in addition to everything a state library can provide.

For example, the two libraries mentioned above (afib_signin and afib_chatsupport) are UI libraries which provide screens and widgets to your app. afib_chatsupport also provides its own state. The afib_firebase_firestore library is a state library which provides only commands for generating firestore-specific AFib queries.

Installing and Integrating an AFib libary

In order to install an AFib library, you first add it to your pubspec.yaml as you would any other dart package, most likely by running flutter pub add.

Then, you need to run an integrate command, which should be provided by the install page of the library on pub.dev.

dart bin/xxx_afib.dart integrate library --package-name afib_signin --package_code afsi

Note that both of the arguments, --package-name and --package-code are intrinsic to the library itself. You don't get to choose them. If you do not enter the correct values, the integration will fail and will produce compilation errors (which you can resolve by just deleting the lines that fail to compile and re-running the command with the correct values).

The integrate command modifies several files under the lib/initialization folder. Those modifications allow the library to register its commands, state, screens, startup query, and more within your app.

Use a project style for afib_signin

Although you can integrate afib_signin just as you would another AFib library, you should prefer creating a new project with either the app-starter-signin or app-starter-signin-firebase project styles. Those styles integrate afib_signin for you, and provide significant additional code and testing functionality which you would otherwise have to write yourself.

Libraries in Protototype Mode

Once a UI library has been integrated, it will show up under the Libraries sub-section of your prototype mode home screen. When you select a library, you can access its UI Prototypes. The UI prototypes will automatically integrate your fundamental theme (e.g. colors, etc).

Prototyping Changes to Library UIs

If you override the library's functional theme (described below), your modifications will show up in real time as you save the override theme file. Consequently, you can use the UI prototypes included with the library to prototype your own UI modifications.

Viewing Widget and Translation IDs

For several reasons described below, you may want to view the widget and translation IDs used by widgets in a library UI. You can do so by navigating into one of the library's UI Prototypes, swiping in from the right to open the prototype drawer, selecting the theme tab, and under device attributes, select "identifiers" as the locale.

Having done so, you will see that the code for the widget id or translation ID for each widget. Widget ids contain _wid_ in the code, while translation ids contain _i18n_. You can use these text ids to reverse engineer the name of the constant AFib. For example, afsi_wid_buttonLogin will corelate to the constant AFSIWidgetID.buttonLogin in your code.

Interacting with Libraries via LPIs

Each library can expose one or more Library Programming Interfaces (LPIs). LPIs have two distinct purposes:

Allow the App to Manipulate the Library's Internal State

If a library provides its own state (e.g. afib_chatsupport has AFCSState), then in theory you could directly modify that state by calling revise... methods on its root objects, and then calling

context.updateComponentStateOne<AFAHState>(revisedRoot);

However, often the root state objects provide relatively low-level functionality which is better considered private. Instead, a library can expose a higher-level LPI which is designed to allow you to manipulate the library's internal state in supported ways.

Consequently, when you begin using an AFib library, you typically want to familiarize yourself with its LPIs.

How to access an LPI

You can access an LPI from any runtime context (e.g. query context, spi.context, etc) using the accessLPI method:

final lpi = context.accessLPI<AFAHManipulateStateLPI>(AFAHLibraryProgrammingID.manipulateState);
Each LPI has an identifier in the library's ...LibraryProgrammingID class.

Allow the App to Provide Functionality to the Library

An app can also override an LPI, providing its own implementation for the functions of the LPI (one library can also override another library's LPI). This allows the application to provide functionality to the library. For example, the afib_signin library contains an AFSISigninActionsLPI, which itself contains the method:

void onResetPassword(String email) {
    context.navigateToUnimplementedScreen("You must override AFSISigninActionsLPI.onResetPassword");
}

An app that uses afib_signin provides this functionality by overriding the LPI and providing its own implementation of that function.

How to Override an LPI

You can override an LPI using the generate override command, with the syntax:

dart bin/xxx_afib.dart generate override XXXSigninActionsLPI --parent-type AFSISigninActionsLPI

As with other generate commands, the file will be generated in a location that relates to the command syntax:

lib/override/lpi/xxx_signin_actions_lpi.dart

Overriding Themes

AFib also aspires to allow developers to enhance the style and functionality of the UIs provided by AFib libraries. In some sense, a heavy-weight UI library like afib_signin or afib_chatsupport is more likely to be successful if apps that use it can significantly alter the appearance and functionality of the UI that those libraries provide, without having to either reimplement or fork them.

The root of this extensibility is the ability to override the library's AFFunctionalTheme theme(s).

How to override a theme

You override a theme using syntax that parallels the LPI override above:

dart bin/xxx_afib.dart generate override XXXSigninTheme --parent-type AFSIDefaultTheme

and, the file is placed in the expected folder:

lib/override/themes/xxx_signin_theme.dart

Overriding Theme Methods

If you intend to customize the UI of a third-party AFib library, you should start by reviewing the documentation for its theme(s). Most libraries will have only a single default theme (e.g. AFSIDefaultTheme for afib_signin).

Overriding conceptual methods

Hopefully, the library provides a conceptual method which anticipates that you might want to override it. For example, afib_chatsupport's theme provides:

Widget iconHelp({
    double? size
}) {
    return Icon(Icons.help,
        size: size,
        color: colorPrimary
    );
}

This method is clearly intended to allow you to provide your own help icon.

Overriding AFFunctionalTheme methods

Note that even if the library author has not provided conceptual methods for you to override, you may still be able to customize the UI by overriding one of the default child... methods provided by AFFunctionalTheme, and testing for a particular widget id.

The technique for discovering widget IDs described in the internationalization section above is useful in this case.

Remember Everything is a Widget in Flutter

Remember that the child... methods in a theme return a Widget. You can often substantially alter an existing UI by calling super.child... to obtain the library's original intended widget, and then wrapping it in additional widgets (containers, columns, rows, tables), coupled with other widgets that you add. You can then return this composite Widget, and the original library will insert it into the UI without knowing the difference.

AFib-Aware Widgets Facililite UI Extensibility

Theme overrides are not limited to purely cosmetic changes. If you want to add interactive UI elements to an existing third-party UI, or elements that display data that is specific to your app, an AFib-aware widget is usually the answer. To do so, you simply return an AFib-aware widget as part of the return value of a theme function override. You will specify an actual route parameter instance as the launchParam for your widget. That launchParam value will provide the initial route parameter for your widget, within the screen's child parameter pool

Because each AFib-aware widget can have its own child route parameter, access its own state view, and manipulate its own SPI, an AFib-aware widget is a way to insert data and functionality specific to your app inside a third-party UI that has no way of anticipating the data and functionality that your app would require.

Note that the app-starter-signin and app-starter-signin-firebase project styles provide a good example of this, returning a RegistrationDetailsWidget from a theme override which collects additional information on the registration screen. Within its AFSISigninActionsLPI override, it looks up child parameter for that widget, and uses the information within it to create a user with attributes that afib_signin itself never anticipated.

Accessing Your State Within A Theme Override Method

Sometimes, you may need to pass an element from your state to the launchParam specified in a theme override method. However, because the data specified in the launchParam is used in the rendering of the UI, it must come from the state view for the third-party UI elements which is calling the theme method. That requirement exists because the UI only re-renders when data in the state view (or route param) changes, so you cannot use data that is not in your state view to render UI -- it won't update properly.

Third party UI elements have no way of knowing about your custom state and including it in their state views.

You can address this case using the addStateViewAugmentationHandler call within your xxx_define_core.dart file. This method allows you to dynamically add data to an existing state view. Once the data is included in the state view, the third-party UI will re-render any time that data changes. For example, the app-starter-signin-firebase example contains this code:

context.addStateViewAugumentationHandler<AFSIDefaultStateView>((context, result) {
  final xxxState = context.accessComponentState<XXXState>();
  result.add(xxxState.userCredential);
  result.add(xxxState.users);
})
This code adds those two root objects to afib_signin's default state view. As a result, when we return a registration details widget from our AFSIDefaultTheme override, we can do this:

@override
Widget? childExtraDetailsRegistration() {
  // context is the build context for an afib_signin screen.  It references
  // AFSIDefaultStateView, which has no knowledge of UsersRoot or UserCredentialRoot, both
  // of which are specific to the app.  However, because we added those two root objects into
  // the state view the code above, we can look them up and use them here.
  // anytime they change in the state, the afib_signin screen will be re-rendered since
  // its state view changed, causing this method to be re-invoked.
  final userCred = context.s.findType<UserCredentialRoot>();
  final users = context.s.findType<UsersRoot>();
  final activeUser = users.findById(usersCred.userId);
  ...

  return RegistrationWidget(
    launchParam: RegistrationWidgetRouteParam(
      userToEdit: activeUser,
      ...
    ),
    ...
  );
}
Note that the context.s.findType code is not as special as it seems. findType is the exact same call that a state view normally makes. In that example, the app's state view returns the user credential like this:

  UserCredentialRoot get userCredential => findType<UserCredentialRoot>();

In theory, you could also cast to your own state view, using:

final xxxStateView = context.castToStateView<XXXDefaultStateView>(XXXDefaultStateView.create);
final userCred = xxxStateView.userCredential;
final users = xxxStateView.users;
This works similarly. I choose the former because as this is written, the only methods that will work properly on xxxStateView are users and userCredential, because only those objects exist in the state view. Any other state view methods will throw an exception, unless the corresponding object is also added to addStateViewAugumentationHandler above. I think the original formulation makes it more clear that you do not have access to the entire state view, only those root elements that you choose to 'augment'.

Providing Library Translations

Discovering Translation IDs

You can use the technique described above to discover the widget and translation ids for various widgets on a UI Library prototoype screen.

Overriding Library Translations

Within xxx_define_core.dart, you can specify translations for a particular locale using primary.setTranslations. Note that although third-party libraries are encouraged to use translation ids for text, your app need not do so - it can start by just hard-coding strings during prototyping.

Also note that often a widget id is used as a translation ID, as many widgets will require only a single translatable piece of text (e.g. a button):

  primary.setTranslations(AFUILocaleID.englishUS, {
    AFUITranslationID.appTitle: "Todo Example",
    ///
    AFSIWidgetID.buttonSignin: "My Login",
    ...
  });