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.
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
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:
...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:
As with other generate commands, the file will be generated in a location that relates to the command syntax:
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:
and, the file is placed in the expected folder:
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:
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);
})
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,
...
),
...
);
}
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:
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;
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):