Skip to content

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.

flutter create eval_example

Then, run it in an iOS or Android emulator using the flutter run command:

cd eval_example
flutter run

You should see default Flutter startup screen:

The Afib Startup Screen

The 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:

The main function and debug link

The main function and debug link

You should see the app-eval-demo startup screen in your emulator, which looks like this:

App Eval Demo

App Eval Demo Home Page

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:

App Eval Demo

App Eval Demo Home Page

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:

  void onPressedIHaveNoObjection() {
    context.updateRouteParam(context.p.reviseNextStanza());
  }

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 the reviseNextStanza 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 the context 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.

App Eval Demo

Route Parameter in the Debugger

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 the spi.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 the CounterManagementScreen.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 the WriteCountHistoryItemQuery implementation, you will see that it updates your app's global component state, called EVEXState.

  • 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 the spi.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 the AFPublicState 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's context.s variable.

  • You can place breakpoints in the query's startAsync and finishAsyncWithResponse 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:

  // just save the count to our global state.
  context.updateComponentRootStateOne<EVEXState>(count);

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 their finishAsyncWithResponse methods.

  • The data in your global component state is accessible via the spi.context.s or spi.context.stateView in your UI rendering code. If you open the counter_management_screen.dart and search for get historyEntries, you will see that it references data from the state view. If you search for spi.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 or spi.context.p. Instead, it should access SPI methods like spi.historyEntries, which in turn access context.s and context.p. You will use data-access methods like spi.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:

dart bin/evex_afib.dart test

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:

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

Then, hit the refresh button in the debugger. You should see the prototype mode home screen:

App Eval Demo

App Eval Demo Prototype mode

Then, tap UI prototypes, and navigate into the evex_pr_homePageScreenInitial:

App Eval Demo

App Eval Demo Prototype mode

Doing so will display the home screen prototype:

App Eval Demo

App Eval Demo Prototype mode

Swipe in from the right to reveal the protoype mode drawer, and click Exit to exit the prototoype:

App Eval Demo

App Eval Demo Prototype mode

Return to the Home screen, and navigate into State Tests. Click on evex_statetest_startup:

App Eval Demo

App Eval Demo Prototype mode

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:

final confirm = context.s.todoItems;

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:

App Eval Demo

App Eval Demo Prototype mode

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 {

  });
Note that the state view is populated from your test data using the id 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]));
}
Note that this syntax defines IDs for each todo item separately, in addition to defining the overall root. That is not required, but it might be convenient later, if you want to load a specific todo item from the test data.

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:

App Eval Demo

App Eval Demo Initial Todo List Screen Data

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.

App Eval Demo

App Eval Demo Prototype mode

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 the spi.context directly.

  • You can use t.keyForWID to create keys from widget ids, and with1 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 into spi.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:

App Eval Demo

Todo List All

App Eval Demo

Todo List Mow

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:

void onPressedTodoList() {
  context.navigatePush(TodoListScreen.navigatePush(filter: ""));
}

App Eval Demo

Todo List Button

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;"
and open the file 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:

App Eval Demo

Missing Query Response

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]);
Refresh your app once more, and then navigate back into the state test again. This time, it should start successfully, and will display the two skiing related todo items in the todo list screen.

App Eval Demo

Todo List Button

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.

App Eval Demo

Todo List Button

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

 dart bin/evex_afib.dart test

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:

  config.setValue("environment", AFEnvironment.debug);
Refresh the app. Click on the 'Todo List' button, and you will see the result from your ReadManyTodoItemsQuery.startAsync function, which was hard-coded in this example, rather than being read from SQLite:

App Eval Demo

Todo List Button

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 in startAsync 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.