Transition to Coding in AFib
The trailer and tutorial videos are intended to be passively consumed, and to give you a solid overview of how AFib works, and how all its pieces fit together.
In my experience, YouTube videos are great for that level of learning, but poor for interactive coding tutorials, because it is difficult to constantly pause and rewind the video.
This section is intended to help you transition to coding in AFib. It steps you though the key ideas in the tutorial video - understanding the app-eval-demo, and touching on the essential elements of building a todo list screen. It is intended to help you confirm and consolidate what you learned in the tutorial video, without too much detail or repetition.
Basic Setup Steps
1. Create an new Flutter project
For the purpose of this demo, I recommend using the same project name and code as I do below.
Then, run it in an iOS or Android emulator using the flutter run
command:
You should see default Flutter startup screen:
2. Convert your project to an AFib project
From your new Flutter project's root folder, run:
dart pub global run afib:afib_bootstrap create app --package-name eval_example --package-code evex --project-style app-eval-demo --auto-install true
You should see output similar to:
creating project-style=app-eval-demo
created .vscode/extensions.json
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute require "meta, sqlite3, sqlite3_flutter_libs, path_provider, afib"
You are the following packages required by this project style:
meta
sqlite3
sqlite3_flutter_libs
path_provider
run flutter pub add meta
Resolving dependencies...
collection 1.17.1 (1.17.2 available)
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.5.0 available)
source_span 1.9.1 (1.10.0 available)
test_api 0.5.1 (0.6.0 available)
Got dependencies!
run flutter pub add sqlite3
Resolving dependencies...
collection 1.17.1 (1.17.2 available)
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.5.0 available)
source_span 1.9.1 (1.10.0 available)
+ sqlite3 1.11.2
test_api 0.5.1 (0.6.0 available)
Changed 1 dependency!
run flutter pub add sqlite3_flutter_libs
Resolving dependencies...
collection 1.17.1 (1.17.2 available)
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.5.0 available)
source_span 1.9.1 (1.10.0 available)
+ sqlite3_flutter_libs 0.5.15
test_api 0.5.1 (0.6.0 available)
Changed 1 dependency!
run flutter pub add path_provider
Resolving dependencies...
collection 1.17.1 (1.17.2 available)
+ file 6.1.4 (7.0.0 available)
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.5.0 available)
+ path_provider 2.0.15
+ path_provider_android 2.0.27
+ path_provider_foundation 2.2.3
+ path_provider_linux 2.1.11
+ path_provider_platform_interface 2.0.6
+ path_provider_windows 2.1.7
+ platform 3.1.0
+ process 4.2.4
source_span 1.9.1 (1.10.0 available)
test_api 0.5.1 (0.6.0 available)
+ xdg_directories 1.0.0
Changed 10 dependencies!
run flutter pub get
Resolving dependencies...
collection 1.17.1 (1.17.2 available)
file 6.1.4 (7.0.0 available)
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.5.0 available)
source_span 1.9.1 (1.10.0 available)
test_api 0.5.1 (0.6.0 available)
Got dependencies!
require meta, sqlite3, sqlite3_flutter_libs, path_provider, afib
execute generate id EVEXTestDataID.userWestCoast
execute generate id EVEXTestDataID.usersWestCoast
execute generate id EVEXTestDataID.userCredentialWestCoast
execute generate id EVEXTestDataID.userEastCoast
execute generate id EVEXTestDataID.userCredentialEastCoast
execute generate id EVEXTestDataID.userMidwest
execute generate id EVEXWidgetID.buttonSaveTransientCount
execute generate id EVEXWidgetID.buttonResetHistory
execute generate id EVEXTestDataID.countHistoryWestCoast
execute generate id EVEXWidgetID.buttonIHaveNoObjection
execute generate id EVEXWidgetID.textCurrentStanza
execute generate id EVEXWidgetID.buttonManageCount
execute generate ui EVEXDefaultTheme --parent-theme AFFunctionalTheme --parent-theme-id EVEXThemeID.defaultTheme --override-templates "core/files/theme=project_styles/app-eval-demo/files/start_here_theme"
execute generate state CountHistoryItem --add-standard-root --member-variables "int id; int count;" --resolve-variables "User user;" --override-templates "core/files/model=project_styles/app-eval-demo/files/model_count_history_item,core/files/model_root=project_styles/app-eval-demo/files/model_count_history_items_root,core/snippets/define_test_data=project_styles/app-eval-demo/snippets/define_count_history_root_test_data"
info Converting 'int id' to a String on the client, so that String test ids can be used
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate state UserCredentialRoot --member-variables "String storedEmail; String token" --resolve-variables "User user;" --override-templates "core/files/model_root=project_styles/app-eval-demo/files/model_user_credential_root,core/snippets/define_test_data=project_styles/app-eval-demo/snippets/define_user_credential_root_test_data"
execute generate state User --add-standard-root --member-variables "int id; String firstName; String lastName;String email;String zipCode" --override-templates "core/files/model=project_styles/app-eval-demo/files/model_user,core/files/model_root=project_styles/app-eval-demo/files/model_users_root,core/snippets/define_test_data=project_styles/app-eval-demo/snippets/define_referenced_users_root_test_data"
info Converting 'int id' to a String on the client, so that String test ids can be used
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate query ReadCountHistoryQuery --result-type CountHistoryItemsRoot --member-variables "String userId" --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_read_count_history"
execute generate query ReadUserQuery --result-type User --member-variables "String userId" --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_read_user"
execute generate query WriteCountHistoryItemQuery --result-type CountHistoryItem --member-variables "CountHistoryItem item" --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_write_count_history_item"
execute generate query StartupQuery --result-type AFUnused --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_startup"
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate query ResetHistoryQuery --result-type CountHistoryItemsRoot --member-variables "String userId" --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_reset_history"
execute generate query CheckSigninQuery --result-type UserCredentialRoot --override-templates "core/files/query_simple=project_styles/app-eval-demo/files/query_check_signin"
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate ui StartupScreen --no-back-button --override-templates "core/snippets/screen_build_with_spi_impl=project_styles/app-eval-demo/snippets/startup_screen_build_with_spi,core/snippets/empty_screen_build_body_impl=project_styles/app-eval-demo/snippets/startup_screen_build_body"
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate ui HomePageScreen --no-back-button --member-variables "int lineNumber;" --override-templates "core/snippets/extra_imports=project_styles/app-eval-demo/snippets/home_page_screen_extra_imports,core/snippets/standard_route_param=project_styles/app-eval-demo/snippets/home_page_screen_route_param,core/snippets/declare_spi=project_styles/app-eval-demo/snippets/home_page_screen_spi,core/snippets/navigate_push=project_styles/app-eval-demo/snippets/home_page_screen_navigate_push,core/snippets/screen_build_with_spi_impl=project_styles/app-eval-demo/snippets/home_page_screen_build_with_spi,core/snippets/empty_screen_build_body_impl=project_styles/app-eval-demo/snippets/home_page_screen_build_body,core/snippets/screen_additional_methods=project_styles/app-eval-demo/snippets/home_page_screen_additional_methods,core/snippets/smoke_test_impl=project_styles/app-eval-demo/snippets/home_screen_smoke_test"
execute generate ui CounterManagementScreen --member-variables "int clickCount;" --override-templates "core/snippets/extra_imports=project_styles/app-eval-demo/snippets/counter_management_screen_extra_imports,core/snippets/standard_route_param=project_styles/app-eval-demo/snippets/counter_management_screen_route_param,core/snippets/declare_spi=project_styles/app-eval-demo/snippets/counter_management_screen_spi,core/snippets/navigate_push=project_styles/app-eval-demo/snippets/counter_management_screen_navigate_push,core/snippets/screen_build_with_spi_impl=project_styles/app-eval-demo/snippets/counter_management_screen_build_with_spi,core/snippets/empty_screen_build_body_impl=project_styles/app-eval-demo/snippets/counter_management_screen_build_body,core/snippets/screen_additional_methods=project_styles/app-eval-demo/snippets/counter_management_screen_additional_methods,core/snippets/smoke_test_impl=project_styles/app-eval-demo/snippets/counter_management_smoke_test"
execute generate ui SignedInDrawer --override-templates "core/snippets/extra_imports=project_styles/app-eval-demo/snippets/signed_in_drawer_extra_imports,core/snippets/declare_spi=project_styles/app-eval-demo/snippets/signed_in_drawer_spi,core/snippets/screen_build_with_spi_impl=project_styles/app-eval-demo/snippets/signed_in_drawer_build_with_spi,core/snippets/drawer_build_body=project_styles/app-eval-demo/snippets/signed_in_drawer_build_body"
info Adding tempPlaceholder member variable due to issues with 'generate augment' in scenarios with no member variables.
execute generate test StartupUnitTest
execute generate test InitialWireframe --initial-screen HomePageScreen --override-templates "core/snippets/wireframe_impl=project_styles/app-eval-demo/snippets/wireframe_impl,core/snippets/wireframe_body=project_styles/app-eval-demo/snippets/initial_wireframe_body"
execute generate custom file --main-type EVEXSqliteDB --path lib/state/db --override-templates "core/files/custom=project_styles/app-eval-demo/files/sqlite_db"
rename test/widget_test.dart -> test/widget_test.old
rename lib/main.dart -> lib/main.old
create lib/evex_flutter.dart
create lib/evex_command.dart
create bin/evex_afib.dart
create lib/evex_id.dart
create lib/initialization/evex_define_core.dart
create lib/initialization/install/install_base.dart
create lib/initialization/install/install_base_library.dart
create lib/initialization/install/install_command.dart
create lib/initialization/install/install_command_library.dart
create lib/initialization/application.dart
create lib/initialization/install/install_core_library.dart
create lib/initialization/install/install_core_app.dart
create lib/initialization/install/install_test.dart
create lib/initialization/environments/debug.dart
create lib/initialization/environments/prototype.dart
create lib/initialization/environments/test.dart
create lib/initialization/environments/production.dart
create lib/initialization/create_dart_params.dart
overwrite lib/initialization/evex_config.g.dart
create lib/query/simple/startup_query.dart
create lib/state/evex_state_model_access.dart
create lib/state/evex_state.dart
create lib/state/stateviews/evex_default_state_view.dart
create test/afib/main_afib_test.dart
create lib/test/test_data.dart
create lib/test/evex_state_test_shortcuts.dart
create lib/test/wireframes.dart
create lib/test/ui_prototypes.dart
create lib/test/state_tests.dart
create lib/test/unit_tests.dart
create lib/test/state_tests/startup_state_test.dart
create lib/main.dart
create lib/app.dart
create lib/ui/evex_connected_base.dart
create lib/ui/themes/evex_default_theme.dart
create lib/ui/screens/startup_screen.dart
create lib/test/ui_prototypes/screens/startup_screen_tests.dart
create lib/state/models/count_history_item.dart
create lib/state/root/count_history_items_root.dart
create lib/state/root/user_credential_root.dart
create lib/state/models/user.dart
create lib/state/root/users_root.dart
create lib/query/simple/read_count_history_query.dart
create lib/query/simple/read_user_query.dart
create lib/query/simple/write_count_history_item_query.dart
create lib/query/simple/reset_history_query.dart
create lib/query/simple/check_signin_query.dart
create lib/ui/screens/home_page_screen.dart
create lib/test/ui_prototypes/screens/home_page_screen_tests.dart
create lib/ui/screens/counter_management_screen.dart
create lib/test/ui_prototypes/screens/counter_management_screen_tests.dart
create lib/ui/drawers/signed_in_drawer.dart
create lib/test/ui_prototypes/drawers/signed_in_drawer_tests.dart
create lib/test/unit_tests/startup_unit_test.dart
create lib/test/wireframes/initial_wireframe.dart
create lib/state/db/evex_sqlite_db.dart
success renamed 2 files, created 56 files, modified 0 files, created 0 folders
3. Open and Run the project folder in VS Code
Open a device emulator. I would start with an iOS device.
Open the project folder in VS Code, and choose Command-P to search for main.dart
. Just above the main function, you will see a debug
link. Click it. The example will start in the debugger:
You should see the app-eval-demo
startup screen in your emulator, which looks like this:
Play with the code in the debugger
Delete the welcome message
In VS Code, choose Command-P and search for home_page_screen.dart
. Within that file, search for _buildBody
, which will look like this:
Widget _buildBody(HomePageScreenSPI spi) {
final t = spi.t;
final rows = t.column();
rows.add(AFUIWelcomeWidget());
rows.add(_buildManageCountCard(spi));
rows.add(_buildStanzasCard(spi));
return ListView(
children: rows
);
}
Delete the line rows.add(AFUIWelcomeWidget());
, and save the file. You should see the welcome area at the top of the home screen disappear in real time, so that your screen now looks like this:
Change text on the home screen
Note that the home screen contains the text "Pride & Predjudice". Search for that string on the home page screen. You will find code like this:
t.buildCardHeader(rows: rows, title: "Pride & Predjudice, Jane Austen", subtitle: "(route parameter)");
Modify it to delete the author name, and save the file. Again, you will see the UI update in real time.
Practice Understanding a Production/Debug App
As in the tutorial app, we will start by understanding how the app-eval-demo
project style works in debug mode.
Throughout the remaining sections, I am trying to briefly touch on the topics you already learned in the tutorial video, allowing you to revisit what you already learned passively in an interactive way.
Route Parameters: Understand how the 'I have no objection to hearing it' button works
Still in the home_page_screen.dart
, search for the onPressedIHaveNoObjection
function implementation. Place a breakpoint on its implementation:
In addition, place a breakpoing on this code in the _buildStanzasCard
method:
rows.add(Container(
margin: t.margin.bigger,
height: (t.styleOnCard.bodyMedium?.fontSize ?? 12.0) * 4.0,
child: t.childText(
wid: EVEXWidgetID.textCurrentStanza,
text: spi.textCurrentStanza
)
));
Now, click the button in the UI. You can step into this function as much as you want. Then continue, and reach the breakpoint in the _buildStanzasCard
method. Inspect the value in spi.textCurrentStanza
, and its code, as much as you like.
Things to remember from the tutorial
- The route parameter in
context.p
is immutable, hence thereviseNextStanza
method, which returns a copy with the line number in the route parameter incremented by one. - The
context.updateRouteParam
function updates the route parameter with the new revised version. Within the SPI, think of thecontext
member variable as an API you use to manipulate AFib's internal state. - If you jump to the top of the file, and search for
HomePageScreenRouteParam
, you will see that the route parameter for this screen is defined at the top of the file, and that it contains a line number that tracks which stanza of pride and prejudice is showing. - If you search for
spi.onPressedIHaveNoObjection
, you will see that the button invokes the method that increments increments the stanza which is showing in the route parameter. - If, while broken within that function, you inspect the SPI's debugOnlyPublicState > route > screenHierarchy > active > [first segment] > param value, you will see the route parameter for this screen, as shown below. The point of seeing it there is to remember that all state in your app is tracked by the AFPublicState, but of course you can more easily access it via
context.p
in the SPI. - Anytime the route parameter or state view change, the rendering function gets re-executed, causing your UI to update.
Navigation: Understand how the 'Manage Count' button works
Next, remove those breakpoints, and search for onPressedManageCount
, and add a breakpoint within it:
void onPressedManageCount() {
context.navigatePush(CounterManagementScreen.navigatePush(clickCount: 0));
}
Next, use Command-P to open counter_management_screen.dart
, and place a breakpoint in the _buildIncrementParamCard, on this code:
rows.add(t.childText(
text: spi.clickCountParam.toString(),
wid: EVEXWidgetID.textCountRouteParam,
style: t.styleOnCard.displayMedium
));
Now, and click the 'Manage Count' button.
Again, you can debug the navigatePush
and spi.clickCountParam
calls to understand how the initial route parameter passed to navigatePush
becomes the initial route parameter for the new screen.
Things to remember from the tutorial
-
The static navigatePush method returns a navigate push action, which contains an initial route parameter for the manage count screen. That route parameter contains both the initial click count (which you might experiment with changing here), and the ID for the screen, which AFib knows how to load.
-
Again, the
context
member variable acts as an API for manipulating AFib's internal state, in this case modifying its route state. -
If you stop in the breakpoint in
counter_management_screen.dart
, you can again inspect thespi.debugOnlyPublicState
member variable, and see that there are now two screens in the route hierarchy, and that the CounterManagementScreen's initial route parameter came from theCounterManagementScreen.navigatePush
call you broke on.
Queries: Understand how the 'Save Transient Count in History' button works
Within the counter_management_screen.dart
, search for the onPressedPersistTransientCount
method, and put a breakpoint on the context.executeQuery
line, it looks like this:
void onPressedPersistTransientCount() {
context.executeWireframeEvent(EVEXWidgetID.buttonSaveTransientCount, context.p, onSuccess: _onClearClickCount);
final entry = CountHistoryItem.createNew(
userId: context.s.userCredential.userId,
count: context.p.clickCount
);
// execute the query which writes it to the history, and
context.executeQuery(WriteCountHistoryItemQuery(item: entry, onSuccess: (successCtx) {
_onClearClickCount();
}));
}
Again, you can debug into this code as much as you would like. You might also place a breakpoint on the _onClickClearCount
call in the onSuccess handler, and in the startAsync
and finishAsyncWithResponse
methods of the WritecountHistoryItemQuery
.
Things to remember from the tutorial
-
Again,
context.executeQuery
is a way to manipulate AFib's internal state. If you look at theWriteCountHistoryItemQuery
implementation, you will see that it updates your app's global component state, calledEVEXState
. -
If you place a breakpoint in the
buildWithSPI
button for this screen, and repeatedly increment the transient count and then click 'Save Transient Count', you can inspect thespi.debugOnlyPublicState
method, and see the new count history items get added under components > states > EVEXState > countHistoryItems. Again, the point of finding it there is to remember that theAFPublicState
tracks your entire application state. However, you can access the state (actually, a subset or superset of it in the state view) more easily using your SPI'scontext.s
variable. -
You can place breakpoints in the query's
startAsync
andfinishAsyncWithResponse
methods to get a better sense of what it is doing.
App Startup: Understand how the Startup Query works
Assuming you have created some history entries, if you refresh the app you will see that the persistent history is saved across app restarts. AFPublicState
is stored in transient memory, so your countHistoryItems
must be reloaded into it each time the app restarts.
Open the file startup_query.dart
. Note that in finishAsyncWithResponse
it executes CheckSigninQuery
. Navigate into that query, and note that if the user credential is signed in (which it always is in this example), it invokes ReadCountHistoryQuery
. Navigate into that query, and place a breakpoint within finishAsyncWithResponse
on context.updateComponentRootStateOne
, which looks like this:
Now, refresh the app, and inspect the count response that comes back. You can see that it contains the saved set of count history entries which were loaded from the sqlite database in startAsync
.
Things to remember from the tutorial
-
The startup query executes when your app starts.
-
Queries access data from persistent sources and device APIs in their
startAsync
methods, then integrate the results into your global component state in theirfinishAsyncWithResponse
methods. -
The data in your global component state is accessible via the
spi.context.s
orspi.context.stateView
in your UI rendering code. If you open thecounter_management_screen.dart
and search forget historyEntries
, you will see that it references data from the state view. If you search forspi.historyEntries
further down, you will see that it is used to render this count history in the UI. -
Because much of your testing will be done at the SPI level, your UI should rarely reach directly down to access
spi.context.s
orspi.context.p
. Instead, it should access SPI methods likespi.historyEntries
, which in turn accesscontext.s
andcontext.p
. You will use data-access methods likespi.historyEntries
to validate the state your UI is displaying in state tests later.
Tests: Run Tests from the command line
From the root of your project folder, on the command line, run:
You should see output similar to:
overwrite lib/initialization/evex_config.g.dart
success renamed 0 files, created 1 files, modified 0 files, created 0 folders
------------------------------------------------------------
Afib Unit Tests:
------------------------------------------------------------
------------------------------------------------------------
Afib State Tests:
------------------------------------------------------------
evex_statetest_startup
af_screentest_smoke:...................................... 12 passed evex_statetest_startup
TOTAL 12 passed
------------------------------------------------------------
Running at size phone-standard:portrait / 1170.0 w x 2532.0 h
------------------------------------------------------------
Running in locale en_us
------------------------------------------------------------
Afib Drawer Tests:
------------------------------------------------------------
evex_pr_signedInDrawerInitial
smoke..................................................... 0 passed
------------------------------------------------------------
Afib Single-Screen Tests:
------------------------------------------------------------
evex_pr_counterManagementScreenInitial
smoke..................................................... 3 passed
evex_pr_homePageScreenInitial
smoke..................................................... 3 passed
evex_pr_startupScreenInitial
smoke..................................................... 0 passed
TOTAL 6 passed
------------------------------------------------------------
GRAND TOTAL 18 passed (in 0.72s)
------------------------------------------------------------
00:01 +1: All tests passed!
Things to remember from the tutorial
- Remember that you can run all your UI and state tests from the command line.
Practice Using UI Prototypes: Create a TodoList screen
In this section, we will touch on creating your own state, UI, prototypes and tests in the simplest way possible. Our goal is not to create a functional todo list, but just to give you the experience of adding new UI to an AFib app. The idea of a todo list app is a framing device for the experience of creating some UI.
Switch to Prototype mode
Open evex_config.g.dart
, and switch to prototype mode, like this:
Then, hit the refresh button in the debugger. You should see the prototype mode home screen:
Then, tap UI prototypes, and navigate into the evex_pr_homePageScreenInitial
:
Doing so will display the home screen prototype:
Swipe in from the right to reveal the protoype mode drawer, and click Exit to exit the prototoype:
Return to the Home screen, and navigate into State Tests. Click on evex_statetest_startup
:
Note that within the state test, the full app appears to work. You can navigate down to the "Count Management" screen, you can save the transient count in the persistent history, etc. However, if you exit the state test and return to it again, or refresh the app and return to it, it will reset to its initial state.
Things to remember from the tutorial
- If you navigate into UI Prototypes > Home Page Screen, you will see the prototype of the home page screen.
- If you swipe in from the right, you can access the prototype mode drawer, and hit Exit to exit the prototype.
- When you enter a state test, you are viewing the UI associated with the terminal state of that test.
- In many apps, it is possible to treat a state test as a development environment, executing all the way down to the query level. The only difference is that rather than calling
startAsync
in the queries, AFib uses test results specified in the state test.
Create a TodoItem and TodoItemsRoot
From the root of your project folder, run the command:
dart bin/evex_afib.dart generate state TodoItem --member-variables "String id; String title" --add-standard-root
Open todo_item.dart
, and inspect the member variables (id and title). Note that AFib provides a lot of boilerplate code (copyWith, revise.. methods, basic serialization, and so on).
Open home_page_screen.dart
search for onPressedManageCount
, and in its body type:
The code should compile, as the todoItems
root should already be accessible through your state view.
Things to remember from the tutorial
- AFib generates model objects for you, and makes the boilerplate associated with immutable data structures easy to maintain.
- Because AFib data structures are immutable, you must use reviseMethods to create modified copies of them when something changes.
- It is slightly more convenient to create state before creating UI.
- When you add a model and include
--add-standard-root
, a set of your new models is automatically added to the root of your state (EVEXState
) and default state view (EVEXDefaultStateView
). - You can use
context.s
member variable in your SPI to access data in the state view.
Create a TodoListScreen
Now, let's create a screen which will list out some of our TodoItems using this command:
dart bin/evex_afib.dart generate ui TodoListScreen --member-variables "String filter;" --with-flutter-state
Refresh your app in the debugger, then navigate into the UI Prototypes section, and click on the evex_pr_todoListScreenInitial
entry. You should see an initial screen that looks like this:
Note the evex_pr_todoListScreenInitial
.
Things to remember from the tutorial
- You can use prototype mode to prototype UI without worrying about navigation or state management.
- You can watch your UI update in real time as you code.
- The
--with-flutter-state
flag helps AFib track Flutter widget state in your route parameter, which we will use later.
Feed test data into your new screen prototype
Now, we want to feed a few sample todo items into our UI. Open the file todo_list_screen_tests.dart
. You will see a prototype definition that looks like this:
var prototype = context.defineScreenPrototype(
id: EVEXPrototypeID.todoListScreenInitial,
stateView: EVEXTestDataID.evexStateFullLogin,
navigate: TodoListScreen.navigatePush(
filter: "",
//!af_push_params
),
timeHandling: AFTestTimeHandling.running,
);
prototype.defineSmokeTest(
body: (e) async {
});
EVEXTestDataID.evexStateFullLogin
. AFib creates, and to some extent maintains, that ID's value for you, but you can create your own custom IDs and states as well.
Use Command-Shift-f to search your project for that ID. You will find it in your test_data.dart file. Note that it contains entries for each of your root objects. In this case, we want to populate our new EVEXTestDataID.stateFullLoginCountHistoryItemsRoot
. Jump to the bottom of the test_data.dart
file, and replace this code:
void _defineTodoItemsRoot(AFDefineTestDataContext context) {
context.define(EVEXTestDataID.stateFullLoginTodoItemsRoot, TodoItemsRoot.initialState());
}
Which starts us with an empty set of todo items, with this code, which create two sample todo items:
void _defineTodoItemsRoot(AFDefineTestDataContext context) {
final todoBuyMilk = context.define(EVEXTestDataID.todoBuyMilk, TodoItem(
id: EVEXTestDataID.todoBuyMilk,
title: "Buy milk",
));
final todoMowGrass = context.define(EVEXTestDataID.todoMowGrass, TodoItem(
id: EVEXTestDataID.todoMowGrass,
title: "Mow Grass",
));
context.define(EVEXTestDataID.stateFullLoginTodoItemsRoot, TodoItemsRoot.fromList([todoBuyMilk, todoMowGrass]));
}
You will need to import TodoItem
, which you can do by placing your cursor on the symbol and hitting Command-.
Then, you will need to fix the missing test data identifiers, which you can do by placing your cursor within them and hitting Command-Shift-P
, then choosing AFib Fixup Identifier(s)
.
Hit refresh to reload the app, as test data is stored in a global variable.
Finally, open todo_list_screen.dart
and place a breakpoint within the _buildBody
function. Navigate back to your screen prototype, and from the breakpoint inspect spi.context.s.todoItems
. You will see that the state view now contains your two test todo items:
Things to remember from the tutorial
- You can hydrate your UI prototypes with data from
test_data.dart
. - You can fix missing test data IDs and widget ids using the
AFib Fixup Identifier(s)
command. - You need to refresh the app anytime you change anything test-related, as tests and test data are initialized at startup and stored in global variables.
Implement your Todo List Screen
First, begin by replacing the TodoListScreenRouteParam.create
method with this:
factory TodoListScreenRouteParam.create({
required String filter,
//!af_navigate_push_param_decl
}) {
final textControllers = AFTextEditingControllers.createOne(EVEXWidgetID.textTodoFilter, "");
final flutterState = AFFlutterRouteParamState(
textControllers: textControllers,
);
return TodoListScreenRouteParam(
filter: filter,
flutterState: flutterState,
//!af_create_params_call
);
}
Add this function to your SPI:
Iterable<TodoItem> get activeItems {
final allItems = context.s.todoItems.findAll;
if(filter.isEmpty) {
return allItems;
}
return allItems.where((e) => e.title.toLowerCase().contains(filter.toLowerCase()));
}
You will also need to replace the existing onChangedFilter
method, adding the updateTextField
call:
void onChangedFilter(String value) {
context.updateTextField(EVEXWidgetID.textTodoFilter, value);
final revised = context.p.reviseFilter(value);
context.updateRouteParam(revised);
}
Then, search for _buildBody
, and replace it with this code:
Widget _buildBody(TodoListScreenSPI spi) {
final t = spi.t;
final rows = t.column();
rows.add(Card(
key: t.keyForWID(EVEXWidgetID.cardSearchText),
child: t.childMargin(
wid: EVEXWidgetID.marginSearchText,
margin: t.margin.standard,
child: t.childTextField(
decoration: const InputDecoration(
hintText: "Search",
),
screenId: screenId,
wid: EVEXWidgetID.textTodoFilter,
parentParam: spi.context.p,
onChanged: spi.onChangedFilter,
expectedText: spi.context.p.filter,
)
)
));
for(final todoItem in spi.activeItems) {
rows.add(Card(
child: ListTile(
key: t.keyForWID(EVEXWidgetID.tileTodo.with1(todoItem.id)),
title: t.childText(text: todoItem.title),
)
));
}
return ListView(
key: t.keyForWID(EVEXWidgetID.listViewTodoItems),
children: rows
);
}
You will need to use the AFib Fixup Identifier(s)
command to create the various widget IDs.
Hit save, and you should see that your test data is hydrating your protoype UI, and displaying your two test todo items.
Things to remember from the tutorial
-
You can hydrate your UI prototypes with test data, then prototype them interactively.
-
Business and filtering logic belongs in your SPI. The
spi.activeItems
method hides some filtering logic, and will be useful for validating that filtering logic in state tests later. Your UI code should generally not reference thespi.context
directly. -
You can use
t.keyForWID
to create keys from widget ids, andwith1
to differentiate widget ids. -
Your SPI is also where handlers for user events should live. Your SPI will use it's
context
member variable as an API for manipulating AFib's internal state - updating the route parameter, navigating among screens, showing UI, and executing queries. Again, your UI should generally not reach down intospi.context
to manipulate AFib's state, it should call SPI methods which do so.
Test the filter functionality
Now, you will need to refresh the app to integrate changes to the route parameter. If you were viewing the TodoListScreen, you may get assertion failures, which is normal when the class definition of the route parameter changes:
Then, refresh your app, and return to the prototype. You should see that a text edit appears at the top, and typing text into it filters the set of todo items:
If you debug the activeItems
accessor in the SPI, you will see that it is filtering the items by name, and only one item contains the text "mow".
Things to remember from the tutorial
- You can maintain state for stateful Flutter widgets in the route parameter.
- You need to call
context.updateTextField
in the SPI to keep the edit text widget's state in sync with your AFib state.
Practice navigating to your todo list screen in a state test
We have a UI prototype which shows todo items, but it has not been integrated into our production app in any way. If we returned to debug mode, there would be no way to reach the todo list screen.
In the next few sections, we will begin using the existing state test to prototype integrating our new screen into the app eval demo.
First, refresh the app to return to the prototype mode home screen, then choose State Tests and evex_statetest_startup
. Doing so will work, as we have not yet created a query to load todo items.
Open the home_page_screen.dart
file, and search for "Manage Count", add this code just below it:
rows.add(t.childSingleRowButton(
button: t.childButtonPrimaryText(
wid: EVEXWidgetID.buttonTodoList,
text: "Todo List",
onPressed: spi.onPressedTodoList
)
));
Add the widget ID. Then, navigate to the HomePageScreenSPI
, and add this method:
You should see the button appear, and when you click the button, you should navigate to the screen. It will be empty, since no query is currently loading todo list data, and so the TodoItemsRoot
value just has its initial value, which is empty.
Things to remember from the tutorial
-
You can use the SPI's context member variable, and a screen's static
navigatePush
method, to navigate to a screen. -
AFib automatically initializes root variables with the result of their static
initialValue
method, which is empty.
Practice Creating a Query to Load Todo Items
Next, we will create a query to load all the todo list items at startup using:
dart bin/evex_afib.dart generate query ReadManyTodoItemsQuery --result-type "List<TodoItem>" --member-variables "String userId;"
read_many_todo_items_query.dart
.
Replace finishAsyncWithResponse
with this code, which integrates the results into the todoItems
root object of your component state:
@override
void finishAsyncWithResponse(AFFinishQuerySuccessContext<List<TodoItem>> context) {
final response = context.r;
final evexState = context.accessComponentState<EVEXState>();
final revised = evexState.todoItems.reviseSetItems(response);
context.updateComponentRootStateOne<EVEXState>(revised);
}
Normally, we would add code to startAsync
which would query data from our persistent store, which in this app is SQLite. However, to make that work, we would need to have a way to add items to SQLite, which is beyond the scope of this example. So, instead, add this code, which simply returns a hard-coded example todo item, which is coming from the startAsync method.
@override
void startAsync(AFStartQueryContext<List<TodoItem>> context) async {
final response = <TodoItem>[];
response.add(TodoItem(id: "start_async_example", title: "Example hard-coded in startAsync"));
context.onSuccess(response);
}
Finally, we need to execute our query, which we will do in check_signin_query.dart
, by adding the line just below ReadCountHistoryQuery
:
// load in the user record for this user.
final startupLoad = AFCompositeQuery.createList();
startupLoad.add(ReadUserQuery(userId: cred.userId));
startupLoad.add(ReadCountHistoryQuery(userId: cred.userId));
startupLoad.add(ReadManyTodoItemsQuery(userId: cred.userId));
Now, refresh your prototype mode app, and navigate back to your state test.
And, you should get an exception stating that there is no response to your new ReadManyTodoItemsQuery
:
Things to remember from the tutorial
-
You can generate a query, which provides a structured way to access data in persistent stores and device APIs. All external data should be accessed via queries. If you find yourself wanting to create a 'mock' object for an API, you probably should have wrapped access to that API in the startAsync method of a query.
-
Your query has two steps, one of which actually accesses the data, and calls
onSuccess
, the other of which integrates that data into your application's in-memory state. -
State tests skip the
startAsync
function, and return data specified by the state test itself. In this case, we haven't actually added the data for our new query in the state test, yielding an exception.
Specify a response for the new query in a state test
Open the file startup_state_test.dart
, and add this line after the ResetHistoryQuery
line:
testContext.defineQueryResponseFixed<ResetHistoryQuery>(CountHistoryItemsRoot.initialState());
testContext.defineQueryResponseFixed<ReadManyTodoItemsQuery>(EVEXTestDataID.todoItemsStateTest);
Define the EVEXTestDataID.todoItemsStateTest
identifier.
Then, open test_data.dart
and add these lines to the _defineTodoItemsRoot
method:
final todoBuyEpicPass = context.define(EVEXTestDataID.todoBuyEpicPass, TodoItem(
id: EVEXTestDataID.todoBuyEpicPass,
title: "Buy Epic pass"
));
final todoGetSkisWaxed = context.define(EVEXTestDataID.todoGetSkisWaxed, TodoItem(
id: EVEXTestDataID.todoGetSkisWaxed,
title: "Get skis waxed"
));
context.define(EVEXTestDataID.todoItemsStateTest, [todoBuyEpicPass, todoGetSkisWaxed]);
Things to remember from the tutorial
- You must specify results for queries in state tests.
-
When a state test starts, it executes your
StartupQuery
. The response to that query may result in the execution of additional queries. Each additional response influences the code path which gets executed, until eventually your app is left on a screen with no additional queries outstanding. This screen will often be your signin screen or a 'home page' screen, though it does not have to be. -
The
test_data.dart
contains a key-value lookup of test data values, which can be shared across multiple tests.
Extend the state test code
Next, re-open the startup_state_test.dart
file. Add the following line just below the countScreen declaration:
final counterScreen = shortcuts.createCounterManagementScreen();
final todoListScreen = shortcuts.createTodoListScreen();
Note that AFib automatically maintains these shortcuts for you.
And the following lines just below the final homeScreen.executeScreenBuild
section:
homeScreen.executeScreenBuild((e, spi) {
e.expect(spi.textCurrentStanza, ft.contains(secondStanza));
e.expect(spi.clickCountState, ft.equals(10));
});
homeScreen.executeScreenBuild((e, spi) {
spi.onPressedTodoList();
});
todoListScreen.executeScreen((e, screenContext) {
screenContext.executeBuild((spi) {
e.expect(spi.activeItems.length, ft.equals(2));
spi.onChangedFilter("pass");
});
screenContext.executeBuild((spi) {
e.expect(spi.activeItems.length, ft.equals(1));
});
});
Refresh your app, and navigate back to the state test. You will see that it now ends on the todo list screen, with the filter value you specified in the state test rather than on the home screen.
However, if you hit the back button, you will see that all the previous manipulations in the test were still executed, and that the resulting final state (e.g. with a click count of 10), still exists.
Execute your tests from the command line
Will produce output like:
------------------------------------------------------------
Afib Unit Tests:
------------------------------------------------------------
------------------------------------------------------------
Afib State Tests:
------------------------------------------------------------
evex_statetest_startup
af_screentest_smoke:...................................... 16 passed evex_statetest_startup
TOTAL 16 passed
------------------------------------------------------------
Running at size phone-standard:portrait / 1170.0 w x 2532.0 h
------------------------------------------------------------
Running in locale en_us
------------------------------------------------------------
Afib Drawer Tests:
------------------------------------------------------------
evex_pr_signedInDrawerInitial
smoke..................................................... 0 passed
------------------------------------------------------------
Afib Single-Screen Tests:
------------------------------------------------------------
evex_pr_todoListScreenInitial
smoke..................................................... 0 passed
evex_pr_counterManagementScreenInitial
smoke..................................................... 3 passed
evex_pr_homePageScreenInitial
smoke..................................................... 3 passed
evex_pr_startupScreenInitial
smoke..................................................... 0 passed
TOTAL 6 passed
------------------------------------------------------------
GRAND TOTAL 22 passed (in 0.78s)
------------------------------------------------------------
00:02 +1: AFib Test Afib Test 00:02 +1: All tests passed!
If you look carefully earlier in this section, you will see that the pass count for evex_statetest_startup
has increased from 12 to 16.
Things to remember from the tutorial
-
You can validate and manipulate the state of your app by accessing the SPIs of active screens, dialog, drawers, bottom sheets, and AFib widgets from the state test.
-
When you access a state test from within prototype mode, it executes completely, and AFib renders the resulting terminal state in UI form.
Switch to Debug Mode
Open evex_config.g.dart
and switch to debug mode:
ReadManyTodoItemsQuery.startAsync
function, which was hard-coded in this example, rather than being read from SQLite:
Things to remember from the tutorial
- In debug or production mode, AFib executes the
startAsync
method of your queries, yielding real results from your persistent store and device APIs. In this case, we hard-coded a result instartAsync
because writing/reading data from SQLite would not have had any educational value.
What's Next?
If you made it through this transition, you should have a pretty solid ability to understand, debug, and augment an AFib-related app. I recommend you skim through the remainder of this document, then return and create your own AFib project.