NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #4395 most downloaded on pub.dev
Flutter CLI — scaffold Clean Architecture projects with Riverpod or flutter_bloc, FVM and your own conventions.
Last release today
07 Oct 2026
Ships on a steady schedule
a new release about every 9 days
Nearly every release is documented
notes for 59 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
7 months old
214 releases · first in 2026
One column per month.
`AppDropdownInput` and `AppMultiSelectInput` no longer crash when given only `onSelected`. A field the user can pick in needs onChanged or onSelected
AppDropdownInput and AppMultiSelectInput no longer crash when given
only onSelected. A field the user can pick in needs onChanged or
onSelected now. Before, a field with onSelected and no onChanged failed
an assert in debug builds. Existing projects get the fix from
moarch update dropdown and moarch update multi-select.The AI agent guide now includes a Claude Code mod, in .claude/skills/moarch-mod/. A mod is a plugin of function hooks, and Claude Code loads this one
.claude/skills/moarch-mod/. A mod is a plugin of function hooks, and
Claude Code loads this one from the skills folder once you trust the
workspace. Other agents keep reading AGENTS.md and the skills.
*.g.dart and *.freezed.dart and names the source
to edit instead. It also refuses an edit that removes a // moarch:routes,
// moarch:registrations or // moarch:endpoints anchor.part, the status line shows
build_runner pending until build_runner runs./moarch opens a pane with the stack, the features, the files that
differ from .moarch.yaml (the ones moarch update skips) and the
session cost.moarch-mod:runner subagent on haiku that runs analyze, test,
build_runner and moarch commands and reports only what failed.claude plugin test .claude/skills/moarch-mod runs its tests. Existing
projects with the skills get it from moarch doctor --fix, and
moarch update ai refreshes it. init also adds
.claude/skills/*/.claude-plugin/types/ to .gitignore: Claude Code
writes the API types there each time it loads a mod.`docs/GENERATE_JKS_FILE.md` fixes the command for the certificate fingerprints. The keytool call in "Get the certificate SHA fingerprints" now passes
docs/GENERATE_JKS_FILE.md fixes the command for the certificate
fingerprints. The keytool call in "Get the certificate SHA
fingerprints" now passes the keystore before -list -v. Run moarch update jks-doc to refresh it.Breaking — `AppOtpInput` no longer depends on `mo_2fa_code`. The code field is now part of the generated app_otp_input.dart. Its cells take their deco
AppOtpInput no longer depends on mo_2fa_code. The code
field is now part of the generated app_otp_input.dart. Its cells take
their decoration from AppInputStyle.decoration, the same as every other
input, so a field with no variant follows the theme instead of the
package's primary color. Pass charset: (AppOtpCharset.numeric by
default, also alphanumeric and any) to accept more than digits. The
controller is now AppOtpController: replace Mo2FACodeController in
any screen that uses one. Once nothing else imports mo_2fa_code, you can
remove it from pubspec.yaml.`moarch create tests` finds the state holders of a feature with a folder per screen. Notifiers, blocs and cubits under presentation/list/, presentatio
moarch create tests finds the state holders of a feature with a folder
per screen. Notifiers, blocs and cubits under presentation/list/,
presentation/create/ and similar folders were skipped. Now the whole
feature is scanned (except data/ and domain/). A file counts if it sits
in a notifiers/, blocs/, cubit/ or logic/ folder at any depth, or
if its name ends in _notifier, _bloc, _cubit or _event. States are
found in any states/ folder or by a _state suffix, and their imports
now resolve wherever they sit under presentation/.Breaking — `Paginated ` handles page, offset and cursor APIs. The page / limit / pageCount / nextPage fields are replaced by next, the key of the next
Paginated<T> handles page, offset and cursor APIs. The
page / limit / pageCount / nextPage fields are replaced by next,
the key of the next page (null on the last one), and total is now
nullable. Paginated.fromJson is replaced by fromPageJson,
fromOffsetJson and fromCursorJson. Without a total, a short page ends
the list. append is removed (it is now PagedList.append). No generated
code used the old fields. If your code does, moarch update paginated will
not overwrite a file you edited, so move it over yourself.moarch create widget paged-list adds lib/shared/widgets/lists/app_paged_list.dart, which holds
AppPagedList, AppPagedGrid and AppPagedSliver (plus
AppPagedSliver.grid). They load the next page a few items before the end,
show a loading row or a retry row at the end of the list, support
pull-to-refresh on vertical lists, and accept separators. Unlike the
package, they hold no state. You pass in the items and the loading and
error fields, plus an onLoadMore callback. The design-system preview
includes a demo feed of paged rows.lib/core/utils/paged_list.dart is written alongside paginated.dart
in Dio projects. It contains PagedList<T>, the value a state holds for a
list that loads in pages, and a "load more" mixin for each stack:
PagedNotifierMixin on AsyncNotifier and PagedBlocMixin on Bloc.
Both load one page at a time, turn a failed page into a retry row (the
items already loaded stay), and drop a page that finishes loading after
the list was refreshed. The bloc mixin does not need bloc_concurrency.
Refresh it with moarch update paging. Projects scaffolded earlier get it
from moarch doctor --fix.AGENTS.md and the
moarch-build-screen skill now say that a paged list keeps a PagedList
in its state, loads further pages through the stack's mixin, passes the
next key back without reading it, and is drawn with the paged widgets.
Refresh them with moarch update ai.Offline-first cache, a new `init` option. Tick *Offline-first cache (drift)* under *Backend / networking* and the project gets lib/core/database/app_d
init option. Tick Offline-first cache
(drift) under Backend / networking and the project gets
lib/core/database/app_database.dart (a drift database with one table of
records as JSON, keyed by feature) and lib/core/database/local_cache.dart
(readAll, watchAll, replaceAll, clear, clearAll). AppDatabase is
registered in external_module.dart, LocalCache in core_module.dart, and
drift, drift_flutter and drift_dev are added to the pubspec. It needs
Dio, and it replaces the offline screen when both are ticked.moarch create feature caches into it. In a project with the cache, the
local datasource is ticked by default and written against LocalCache. The
repository's fetchAll() saves the API's list to the cache and answers from
it on a NetworkException, and the new watchAll() follows the cache. On
Riverpod the notifier loads through fetchAll(), follows watchAll() and
gets a refresh(). A bloc keeps loading through fetchAll(). Firestore
features are never cached, since Firestore keeps its own offline copy.init option. Tick Offline sync (outbox +
background) with the cache and writes stop needing a connection:
create feature gives a cached feature create / update / delete.
Each changes the cache at once (a created record gets a temporary
negative id) and queues the SyncRequest the remote datasource describes
(method, path, body) in a new PendingWrites table.lib/core/sync/sync_service.dart sends the queue in order: at start-up,
after each write, on reconnect and on resume. A 2xx leaves the queue;
offline or a 401 stops and keeps it; any other 4xx is dropped and
reported on SyncService.rejected; a 5xx is retried after 10s, 20s, 40s
and 80s, then dropped. Every collection a sync touched is refetched, so
the server's answer wins. The pending count is pendingSyncCountProvider
on Riverpod and PendingSyncCubit on bloc.lib/core/sync/background_sync.dart sends it every ~15 minutes while the
app is closed, through workmanager (added to the pubspec). init
declares UIBackgroundModes (fetch) and
BGTaskSchedulerPermittedIdentifiers in Info.plist, merged into an
existing array rather than beside it. doctor checks them;
AppDelegate.swift needs no change. docs/SYNC_SETUP.md has the rules and
how to trigger a background run on Android and iOS.LocalCache.clearAll(), which sign-in and sign-out already call, empties
the queue too. LocalCache gains put and remove for single records.docs/OFFLINE_FIRST.md is generated with the cache: a step-by-step
explanation of the tables, the read path, and (with sync) the write path,
the queue's rules, the background task and a full timeline, for the
project's stack. AGENTS.md points agents at it. Refresh with moarch update offline-doc.moarch update app-database brings it to cache
projects.AuthRepositoryImpl
takes LocalCache and clears it on logout and account deletion. The REST
variant also clears it at every sign-in, which covers a session that expired
while the app was away. data_module.dart passes it in.getIt..registerX(...) with a single section tripped
avoid_single_cascade_in_expression_statements, so flutter analyze
failed out of the box on, for example, a bloc project with auth and no
localization (presentation_module.dart) or a core module holding only
PermissionService. moarch update di-presentation di-core fixes existing
projects.AGENTS.md (a new Offline-first section) and the
moarch-add-feature / moarch-add-endpoint skills describe the cache when
the project has it.lib/core/database/local_cache.dart. Both options are init-only; moarch update app-database local-cache refreshes the cache files, and moarch update sync-queue sync-service background-sync sync-setup the sync ones.`moarch create feature` writes a loading skeleton. A feature with a state holder and a view now gets presentation/widgets/ _skeleton.dart: a ListView.
moarch create feature writes a loading skeleton. A feature with a
state holder and a view now gets presentation/widgets/<name>_skeleton.dart:
a ListView.separated over a list of fake models, with a note on the
BoneMock values (BoneMock.name, BoneMock.words(3), BoneMock.date…)
that size its bones. The view passes it to skeleton: on both stacks.placeholder. The skeleton file holds the
fake data now, so a new state field only has to reach the constructor,
copyWith and (bloc) props. Features created before this keep their
placeholder and still compile; nothing rewrites them.AppStatusView, AppAsyncView, runAction), DI
modules, network, services, gates and the UI kit were cut down to what a
reader needs; TODOs, moarch: anchors and examples stay.fvm in front of build_runner in app_env.dart's comment, the env
docs page and moarch doctor's hint for a missing app_env.g.dart.AppException.fromError is generated without its stray indentation, and the
Riverpod feature state without a double blank line.AGENTS.md and skills describe the skeleton file
instead of placeholder.moarch update all refreshes the generated files with
the shorter comments (edited files are left alone without --force), and
moarch update ai refreshes the agent guide.A hugging Material `AppBottomNav` is roomier. Each destination now gets the widest label plus 32px rather than plus 16px, so an icon-only tab is 96px
AppBottomNav is roomier. Each destination now gets
the widest label plus 32px rather than plus 16px, so an icon-only tab is
96px wide instead of 80px and the longest labels sit 32px apart. 80px is the
share a tab gets in a crowded five-tab bar, which is not what a card sized
to two or three destinations should look like. The bar is still capped by
the screen and by floatingMaxWidth.moarch update bottom-nav.A hugging `AppBottomNav` gives each labelled tab room of its own. With floatingWidth: AppBottomNavWidth.hug, the classic style — and dot with labels:
AppBottomNav gives each labelled tab room of its own. With
floatingWidth: AppBottomNavWidth.hug, the classic style — and dot with
labels: AppBottomNavLabels.below — set its tabs 4px apart, so two long
labels nearly read as one word. Each of those tabs now carries 8px on
either side of its content, which puts 20px between neighbouring labels.
The pill, which pads its label inside the fill, icon-only tabs and
Material's own bar are unchanged, as is every bar that fills the width.moarch update bottom-nav.A hugging Material `AppBottomNav` is no longer an empty card. With the default style and floatingWidth: AppBottomNavWidth.hug, the bar was measured wi
AppBottomNav is no longer an empty card. With the
default style and floatingWidth: AppBottomNavWidth.hug, the bar was
measured with an IntrinsicWidth — and NavigationBar answers that with
zero, so the card shrank to its padding with no destinations in it. The
widget now works the width out itself: every destination gets the share the
widest label needs, never less than the indicator, which is all that is
counted with labels: AppBottomNavLabels.none. The styles the widget draws
itself were not affected. AppAdaptiveNav(bottomNavWidth: …) gets the fix
through the same widget.moarch update bottom-nav, and moarch update design-system for the preview.Two new agent skills, in .agents/skills/ with their .claude/skills/ pointers, written for the project's stack and options like the others:
.agents/skills/ with their .claude/skills/
pointers, written for the project's stack and options like the others:
moarch-plan-feature — an interview before any code. The agent asks
about a feature's data, screens and their states, actions, route and
platform needs in rounds, every question with a recommended answer, looks
up in the code what the code can answer, never reopens what AGENTS.md
settles, and ends with the decisions and the skills that build them. It
runs only when the user asks to plan or be grilled.moarch-fix-bug — reproduce first. A table from symptom to the layer that
owns it, then the loop that makes it fail on command (a state-holder test
against a mocked repository, a model test on the real payload, a widget
test, an integration test against the API, or a tagged log from a
device), the fix in that layer, and the test kept.moarch doctor --fix, which now offers
any skill a project that has the others was never given; one it generated
and deleted is left alone. Then moarch update agents lists them in
AGENTS.md.AGENTS.md names mattpocock/skills (grill-me, domain-modeling,
handoff) beside flutter/skills as generic skills that can be installed
next to the project's own, and adds "dropping a repository interface
because it has one implementation" to what the project's rules overrule.Generated `AGENTS.md` asks for named `required` parameters. A new "Dart style" section tells agents to write delete({required int id}), not delete(int
AGENTS.md asks for named required parameters. A new
"Dart style" section tells agents to write delete({required int id}), not
delete(int id), in every function and method, keeping positional
parameters only where an override or typedef fixes the signature. The
review skill checks it, and the examples that show a method — the
add-action skills, the README's state walkthroughs and the Firestore
datasource create feature writes (fetchOne, delete) — follow it.equatable moves to ^3.0.0 (the templates never
used the removed EquatableMixin). Raised floors: firebase_core ^4.15.0,
firebase_messaging ^16.7.0, firebase_crashlytics ^5.4.0,
firebase_auth ^6.7.0, cloud_firestore ^6.10.0, google_sign_in ^7.2.0,
get_it ^9.3.0, dio ^5.11.1, flutter_secure_storage ^11.2.0,
envied / envied_generator ^1.3.10.The REST auth feature asks `GET /auth/me` who is signed in. Login and register save the tokens, then fetch the user into a new freezed UserModel (feat
GET /auth/me who is signed in. Login and
register save the tokens, then fetch the user into a new freezed
UserModel (features/auth/domain/models/user_model.dart, catalog entry
auth-me-model), and the state carries it: AuthState.user on Riverpod,
AuthAuthenticated.user on bloc (and AuthFailure.session, replacing
AuthFailure.userId). A failed /auth/me clears the half-saved session, so
the login screen shows the error. Session restore fetches it too; offline it
keeps the session with no user until reloadUser() /
AuthUserReloadRequested runs. TokenStorage no longer decodes a user id
out of the access token (it still deletes the old user_id key on logout),
and AuthRepository.currentUserId() is gone.refreshToken and the refresh response was read as accessToken /
refreshToken, while the models were already snake_case through
build.yaml. All of them are now refresh_token / access_token.ApiConstants. api_constants.dart gains a
// moarch:endpoints anchor, and moarch create feature declares the new
feature's path above it (static const orderHistory = '/order_history';),
with the datasource calling ApiConstants.orderHistory. The FCM device-token
path (authDeviceToken) and the Dio-backed maintenance and update gates'
config paths (configMaintenance, configAppVersion) moved there too. A
refreshed file in a project whose constants lack one writes the path inline
instead, so it still compiles.init now adds to
AndroidManifest.xml: CAMERA, READ_MEDIA_IMAGES and
READ_EXTERNAL_STORAGE (up to API 32) for the media service, without which
permission_handler reports them denied without asking; POST_NOTIFICATIONS,
RECEIVE_BOOT_COMPLETED, SCHEDULE_EXACT_ALARM and the
flutter_local_notifications receivers for scheduled notifications;
POST_NOTIFICATIONS for FCM; <queries> intents for the URL launcher; and
INTERNET for Dio, which flutter create declares only in the debug
manifest. The table is utils/platform_requirements.dart, shared with
doctor.controller.trimmed and trimmedOrNull on TextEditingController, in
core/utils/extensions.dart, for what a text field submits. AGENTS.md
tells agents to use them.AppSingleScrollView defaults to AppConstants.paddingPage instead of
padding12. They are the same value today; pages now follow the page token.moarch doctor reports a service missing its platform declarations, a
REST auth feature without UserModel, and an api_constants.dart without
the anchor. --fix patches the manifest and Info.plist, and writes the
model.moarch doctor --fix first (it writes
user_model.dart, which the refreshed auth files import), then
moarch update auth secure-storage api-constants together, then
build_runner. If you edited the auth repository, it still reads
_tokens.userId, which TokenStorage no longer has — switch it to
me().AppDragSection uses onReorderItem instead of onReorder, which is deprecated from Flutter 3.41. The indices your onReorder receives are the same as bef…
create feature adds the route. The new view gets AppRoutes.<name>
(order_history → /order-history) and a GoRoute pointing at it — the
page on bloc, which provides the bloc — inserted above a new
// moarch:routes anchor in app_routes.dart and app_router.dart, the
way the locator is patched above // moarch:registrations. Until now every
new feature's screen was unreachable until someone wrote the route by hand.
A router without the anchor is left alone and the route is printed instead.
Existing projects: moarch update router routes if you never edited them,
otherwise add the two // moarch:routes lines; moarch doctor notes a
router without them.core/services/theme_mode_service.dart (themeModeProvider on Riverpod,
ThemeModeCubit on bloc), watched by main.dart for themeMode, and
stored through the new PreferencesService
(core/services/preferences_service.dart, shared_preferences). The locator
loads it before runApp, so the saved theme is there on the first frame.
Without the dark theme there is no switch either. moarch create theme --dark adds both, and adds the switch to a project that already has the
dark theme; new catalog entries preferences and theme-mode.init option. OfflineGate
covers the app with an offline screen while there is no connection — the
app stays mounted underneath, so nothing the user was doing is lost — and
ConnectivityService.onReconnect runs work when the connection comes back,
with an app-wide hook in main.dart for a sync. ConnectivityService's
stream now starts with the current state and drops repeats, and bloc gets a
ConnectivityCubit beside Riverpod's hasInternetProvider. New catalog
entries connectivity and offline-gate. Existing projects:
moarch update connectivity.init option (needs the router): the Android App
Links intent filter in MainActivity, and docs/DEEP_LINKS.md with the
iOS Associated Domains steps and the assetlinks.json /
apple-app-site-association files the domain serves, filled in with the
project's ids. GoRouter does the routing, so there is no package. moarch doctor notes the placeholder example.com until it is replaced.?from= and continues there; only in-app paths
are followed. Existing projects: moarch update router.AGENTS.md names the theme-mode switch, and — with deep links or the
offline gate — says that a route must load from its URL alone and that
reconnect work goes through onReconnect. The moarch-add-feature and
moarch-build-screen skills say create feature adds the route. Existing
projects: moarch update ai.create feature's repository no longer fails flutter analyze with
two unused_field warnings. The REST datasource's placeholder is now
fetchAll(), the method the repository interface declares, and the
repository hands it on as the Firestore one already did; the local
datasource's field carries an ignore until the cache has methods.AndroidManifest.xml (the biometric permission) left a CRLF
manifest with mixed line endings.moarch doctor no longer reports the other stack's widgets as missing.
The design-system preview lists the whole kit as its dependencies, so a
Riverpod project was told AppStatusView was missing, and a bloc project
AppAsyncView and ActionListener. Neither stack imports them. doctor --fix would have generated them, and the bloc project could not compile
the Riverpod-only ones.moarch create tests now tests a Riverpod notifier's build() (build() loads without an error), the same as the bloc generator's initial-state
test. A notifier fresh from create feature has no public methods yet, so
its test file used to contain no tests and import the notifier without
using it (unused_import).AppDragSection uses onReorderItem instead of onReorder, which is
deprecated from Flutter 3.41. The indices your onReorder receives are the
same as before. Existing projects: moarch update drag-section.const (two
prefer_const infos in flutter analyze). Existing projects: moarch update design-system.create feature now says to register its
storage in config/di/external_module.dart, where the locator has kept
externals since 9.0.0, not in injector.dart.Every widget a screen is split into now gets its own file. AGENTS.md, the moarch-build-screen skill and the project README tell agents (and people) to
AGENTS.md, the
moarch-build-screen skill and the project README tell agents (and people)
to write each one as a public class in the feature's
presentation/widgets/ (order_header.dart → OrderHeader), or in
lib/shared/widgets/ once a second feature needs it, instead of a private
_Header class at the bottom of the view. The layout in AGENTS.md and the
README shows the new folder. Existing projects: moarch update ai and
moarch update readme.The "AI agent guide" option in init now writes step-by-step skills beside AGENTS.md, in .agents/skills/: moarch-add-feature, moarch-add-endpoint, moar
init now writes step-by-step skills
beside AGENTS.md, in .agents/skills/: moarch-add-feature,
moarch-add-endpoint, moarch-add-action, moarch-add-model,
moarch-build-screen, moarch-add-env-key, moarch-write-tests,
moarch-update-scaffold and moarch-review. Each one is written for the
project's stack and options: bloc gets events, handlers and scopes, and
Riverpod gets notifier methods and listenAction. They also cover Dio and/or
Firestore, the router, localization and the CI workflows. They feed
create feature its checklist on stdin and pass -y to update, so an
agent never waits on a prompt it cannot answer..claude/skills/, so each skill also
gets a pointer there to its .agents copy, the same way CLAUDE.md imports
AGENTS.md..claude/settings.json allows the format / analyze / test / build_runner
commands without a prompt, and denies reading .env and editing
*.g.dart, *.freezed.dart and .moarch.yaml. .gemini/settings.json
points Gemini CLI at AGENTS.md. .claude/settings.local.json is added
to .gitignore.AGENTS.md lists the skills when the project has them, and says that
where a generic Flutter skill disagrees with the project's rules, the rules
win.ai group: moarch update ai. update never adds
files, so a project from before 9.1.0 gets them from moarch doctor --fix,
then moarch update agents to list them in AGENTS.md.moarch doctor --fix now applies the fixes that come with a note, not
only those on a warning or an error. Until now the 9.0.0 offer to generate
AGENTS.md was printed but never applied. A note still does not make
doctor exit 1.Bloc projects get lib/core/utils/app_bloc_observer.dart, and main.dart installs it with Bloc.observer = const AppBlocObserver(); before the locator is
lib/core/utils/app_bloc_observer.dart, and main.dart
installs it with Bloc.observer = const AppBlocObserver(); before the
locator is set up. runAction hands any error that is not an
AppException to addError, and bloc's default observer ignores it, so
until now the screen showed "Unknown error" and the exception behind it
was lost. The observer logs it through appLogger, which also sends it to
Crashlytics when the project has it.moarch update bloc-observer refreshes the file. update never adds files,
so an existing bloc project has to create it by hand (copy it from a fresh
init) before moarch update main installs it. Until the file is there,
main.dart refreshes without it.The generated build.yaml sets json_serializable's field_rename: snake, so every model field reads and writes a snake_case key (createdAt is created_at
build.yaml sets json_serializable's field_rename: snake,
so every model field reads and writes a snake_case key (createdAt is
created_at) with no @JsonKey(name:). A comment above it lists the other
values (none, kebab, pascal, screamingSnake).create model --from-json annotates only the keys the rename would not
reach, like a camelCase key.moarch update build-yaml hands an existing project the same option, and
that changes the key of every camelCase field in its models: check the
models first, or keep your build.yaml as it is.moarch needs Dart 3.9+ (Flutter 3.35+). The test generator below reads source with analyzer (10–14 supported), which needs 3.9.
Breaking
analyzer (10–14 supported), which needs 3.9.init no longer adds mogen_unit_tests / mogen_integration_tests to the
app. It adds mocktail (and bloc_test on bloc) instead, which is what the
generated tests import. For an existing project, moarch doctor --fix makes
the same swap.moarch create scope <feature> <name> (bloc) — carries a screen's
blocs to the routes, sheets and dialogs it opens. Those are siblings in the
Navigator, so context.read from inside them finds nothing.
presentation/scopes/<name>_scope.dart:
of(context) collects the blocs;provide(child:) provides the same instances again;context.show<Name>Sheet / show<Name>Dialog do both.--blocs picks them
from any feature.--parent XScope nests scopes: the outer scope's blocs are provided around
this one's.ShellRoute for nested routes, which survives a deep link where
extra does not.AGENTS.md tells agents to use it rather than create a second
instance of a bloc.moarch create tests [feature] — the two mogen packages, merged in and
adapted to a moarch project:
Unit: a test per notifier, bloc and cubit under test/unit/features/.
getIt<X>() dependencies (behind a getter, or inline) are
registered as mocks in getIt, with getIt.reset() in tearDown. mogen
only understood ref.read, so it missed every repository a moarch
notifier uses.runAction has its failure asserted on
errorMessage.if (state is! X) return; gets X as its seed:.true, so the success path reaches the rest of the
handler.ServerException instead of a test-only
AppException.test().const.Integration: a test per GET endpoint under test/integration/features/.
ApiConstants.x paths are resolved (mogen only read literals).flutter_test, not package:test.NetworkException fails a test, so safeApiCall's mapping is
respected.dio_helper.dart is built from AppEnv.baseUrl and ApiConstants, and is
never overwritten.Both:
GENERATED BY moarch marker, and a file without it is left alone
unless --force. Files mogen wrote are still refreshed.--dry-run, --no-unit / --no-integration, --verbose..empty() the tests call and that
lack it.The generator runs inside the globally-activated CLI, so analyzer no
longer enters the app's dependency graph. That was the source of the
bloc_lint / mogen_integration_tests resolution conflicts.
Generated tests analyze clean and pass on fresh Riverpod and bloc projects (Flutter 3.44).
AGENTS.md and CLAUDE.md. init writes the project's rules for coding
agents (a new checklist item, on by default): the layout, no entity layer and
no use-case classes, the stack's state patterns (runAction, AppAsyncView /
ref.listenAction or BlocConsumer / AppStatusView), the locator modules
and their anchors, the UI kit and tokens, and the fvm commands that prove a
change is done. Built from what the project has, like the README. CLAUDE.md
is only @AGENTS.md. Both are Docs catalog entries (moarch update agents claude-md), and moarch doctor --fix adds them to a project scaffolded
before this release.
Update gate. A new update-gate widget and init checklist item: while
the installed version is below the backend's minimum, the app is replaced by
an "update required" screen that opens the store. Per-platform
min_version / store_url, a live Firestore document or a Dio endpoint read
on resume, and it fails open like the maintenance gate. Both stacks. Mounted
inside MaintenanceGate in main.dart. Adds package_info_plus (and
url_launcher).
Status colors follow the theme. New config/theme/app_status_colors.dart:
success / warning / info as a ThemeExtension, registered on AppTheme.light
and .dark, read with context.statusColors. AppTag and AppBanner
previously drew the light colors in dark mode (successDark & co. were
declared but never read), and AppToast no longer branches on brightness
itself. Detected from disk: a project without the file keeps the
AppConstants lookups. Run moarch doctor --fix, then
moarch update theme tag banner toast, to adopt it.
FCM in the background actually shows data-only pushes. The generated background handler used to only log. A background message runs in its own isolate, with no Firebase app, no locator and no initialized notifications plugin, so a data-only push arrived and nothing showed. With the local notifications service also generated, the handler now sets up exactly what it needs in that isolate (idempotently) and shows the message. Foreground messages the OS will not display (any message on Android, data-only on iOS) are shown too. The data goes along as the payload.
Bloc projects get bloc_concurrency. The generated auth blocs register
their submit events (login, register, Google sign-in, password reset, logout,
delete) with droppable(), so a double tap sends one request, not two.
Detected from the pubspec: a bloc project without the package keeps plain
handlers after update.
config/di/feature_module.dart. The locator gains a layer for the
long-lived services one feature owns (a socket, a call engine), which belong
in neither core_module.dart nor data_module.dart. It also has
openScope / closeScope, get_it scopes for what one flow owns (disposed
when the flow ends). injector.dart calls it only when the file exists.
App lifecycle service (checklist option):
core/services/app_lifecycle_service.dart exposes foreground/background as
streams. resumed emits how long the app was away, timed from
hidden/paused rather than inactive, so a notification-shade glance does not
count. For revalidating state that went stale while backgrounded. Registered
in core_module.dart with a dispose.
Motion curves in AppConstants: curveStandard, curveEnter,
curveExit, used by AppToast and AppCarousel. If a project's
app_constants.dart predates them, widgets are written with the literal
curves instead, so a refresh never breaks the build.
Bloc gets `runAction`. core/utils/app_status.dart now carries a StatusState contract and an ActionBlocMixin, the bloc counterpart to Riverpod's Action
runAction. core/utils/app_status.dart now carries a
StatusState contract and an ActionBlocMixin, the bloc counterpart to
Riverpod's ActionNotifierMixin: a handler is
runAction(emit, (current) async { … }) and returns the next state, with no
try / on AppException of its own. Where the screen already is decides
what a run looks like. With nothing on screen it emits loading and a
failure lands on failure. Over loaded data it emits no loading, and a
failure keeps success with only errorMessage set, so the body stays and a
toast shows. Errors that are not an AppException go to addError as well,
so BlocObserver still sees them.StatusState (one withStatus
override), and generated blocs mix in ActionBlocMixin and load through
runAction. The auth blocs keep their sealed states and are unchanged.moarch update app-status before creating a
feature or bloc — create feature and create bloc warn when the file
predates the mixin. Features already generated keep compiling as they are.
create bloc now also writes app_status.dart when it is missing.`build_apk.yml` skips cleanly when the keystore is not set up. A new check-android-secrets job looks for ANDROID_KEYSTORE_BASE64, KEYSTORE_STORE_PASSW
build_apk.yml skips cleanly when the keystore is not set up. A new
check-android-secrets job looks for ANDROID_KEYSTORE_BASE64,
KEYSTORE_STORE_PASSWORD, KEYSTORE_KEY_PASSWORD and KEYSTORE_KEY_ALIAS,
and the build runs only when all four exist — the same as build_ipa.yml, so
a fresh clone or a fork stays green. moarch update workflow-android picks
it up.init no longer writes
.github/workflows/deploy_stores.yml, and workflow-deploy is no longer a
scaffold slug. It ran lanes moarch never generated, so on a new project it
could only fail. A project that already has the file keeps it — update
leaves it alone, and it can be deleted by hand.Breaking: the scaffold is model-only. A feature no longer has an entity. Its one data type is the freezed model in domain/models/ _model.dart, which t
Breaking: the scaffold is model-only. A feature no longer has an entity. Its
one data type is the freezed model in domain/models/<x>_model.dart, which the
repository interface returns, the state holds and the screens draw. Every field
is declared once, and there is no fromEntity() / toEntity() mapping to keep
in step with it.
The model lives in domain/ — the place the entity was — so the dependencies
keep pointing inward and domain/ imports nothing from data/. The trade is
deliberate: domain/ now imports the freezed / json_serializable
annotations, and a change to a payload's shape reaches the screens that read
it, where an entity layer would have absorbed it.
create feature writes no domain/entities/. The model gains the
.empty() factory the entity used to carry, and the Riverpod state's
skeleton placeholder is built from <X>Model. The repository implementation
passes the datasource's models straight through — on Firestore,
fetchAll() and watchAll() are one line each.AuthTokensEntity is gone (nothing
read it), and AuthUserEntity with it: the Firebase auth repository, state
and event now speak AuthUserModel. auth-entity and auth-user-entity are
no longer scaffold slugs.create model writes the model alone, with its .empty() factory.
--from-entity is removed, and --empty now patches
domain/models/<x>_model.dart. --from-json and --doc work as before, and
the inferred model carries .empty() and the private constructor the
template has.create empty-factories walks domain/models/*_model.dart instead of
domain/entities/.moarch update auth-model auth-user-model relocates the auth models from
data/models/ to domain/models/, the way update already relocates a
moved widget: one copy, at the new path. doctor and the update catalog
still recognise the Firebase auth variant in a project that has the model at
either path.Migrating an existing project: move each data/models/<x>_model.dart to
domain/models/, delete domain/entities/, point each repository and state at
the model, and drop toEntity() / fromEntity() from the models. moarch update does the move for the two auth models only — every other model and
repository has your fields in it, so it is yours to migrate.
AppBottomNav now lives in the navigation templates. The template moved
from SharedTemplates to NavigationTemplates, beside AppNavRail,
AppDrawer and AppAdaptiveNav that read its AppNavDestination. The
generated file, its slug and its path are unchanged, so nothing regenerates
differently.A nav destination can carry a badge. AppNavDestination takes badgeCount (an unseen-messages number, a cart count) and showBadgeDot (a plain "something
A nav destination can carry a badge. AppNavDestination takes
badgeCount (an unseen-messages number, a cart count) and showBadgeDot (a
plain "something is new" dot, read only when there is no count). Null or 0
shows nothing, and past 99 it reads "99+". AppBottomNav, AppNavRail and
AppDrawer all draw it through AppNavDestination.badged, on the selected
icon as well as the idle one, so it looks the same on a phone and a tablet:
AppNavDestination(
icon: Icons.chat_bubble_outline,
selectedIcon: Icons.chat_bubble,
label: 'Chats',
badgeCount: unseenMessagesNumber,
)
The count is state, so a destination list that uses it is built where that
state is read rather than held const. In the pill style that opens sideways
the badge is no longer clipped by the pill's AnimatedSize, and the custom
styles now announce the count to a screen reader. bottom-nav now depends on
badge, so moarch create widget bottom-nav brings app_badge.dart along.
moarch update bottom-nav nav-rail drawer picks it up.
A floating `AppBottomNav` clears the system bar instead of sitting on it. The card was placed at exactly MediaQuery.paddingOf(context).bottom, but tha
AppBottomNav clears the system bar instead of sitting on it.
The card was placed at exactly MediaQuery.paddingOf(context).bottom, but
that inset is the room the system took, not a margin — so on Android's
three-button navigation the card came to rest flush against the buttons, and
read as a second bar stacked on the first rather than as something floating.
The margin is now spent on top of the inset: AppConstants.space8 above
whatever the system reserved, and the full space16 where it reserved
nothing. moarch update bottom-nav picks it up.A bloc view's builder is one `AppStatusView` call, not a `switch`. New in the kit for bloc projects: shared/widgets/app_status_view.dart, the counterp
A bloc view's builder is one AppStatusView call, not a switch. New in
the kit for bloc projects: shared/widgets/app_status_view.dart, the
counterpart to Riverpod's AppAsyncView. It owns the three shells every
screen repeats — the skeleton, the failure screen and the empty state — so a
generated view names only its body:
builder: (context, state) => AppStatusView(
status: state.status,
message: state.errorMessage,
onRetry: () => context.read<OrdersBloc>().add(const OrdersStarted()),
skeleton: (context) => _body(context, OrdersState.placeholder),
builder: (context) => _body(context, state),
),
It takes no type parameter: the caller has the state in hand and closes
over it, so both builders are plain WidgetBuilders. isEmpty is a plain
bool (state.items.isEmpty) for the same reason.
A bloc feature is one state class with a shared AppStatus, not four
sealed states. moarch create feature / create bloc now generate a
single OrdersState carrying an AppStatus from the new
core/utils/app_status.dart:
class OrdersState extends Equatable {
const OrdersState({this.status = AppStatus.initial, ...});
static const placeholder = OrdersState(status: AppStatus.success);
final AppStatus status;
}
With a state class per phase, a screen that keeps its list up while a save
runs had to re-declare that list on every phase that could show it, and the
view grew a body per shape — _body took OrdersSuccess and nothing else
could reach it. Now every status hands _body the same class. The status is
shared rather than declared per feature precisely so one widget can switch
over it; a phase belonging to one screen alone is a field on that screen's
state, not a value on the enum.
A refresh should leave the status on success and emit the new data
when it lands — moving it to loading trades the body for a skeleton and
the screen flickers. The old data is still on the state to draw, which is
the point of the shape.
errorMessage and successMessage on the bloc state, both one-shot.
copyWith drops them unless they are passed again, so the state that sets
one is the only state that carries it — the listener fires a toast once
instead of on every rebuild, and an action that fails without blanking the
screen is copyWith(errorMessage: e.message) with the status left on
success. This is the shape Riverpod's ActionState already had, so the two
stacks now read the same.
The _seq counter that made two identical OrdersFailures unequal is gone
with the sealed states: a retry passes through loading, so the second
failure is never dropped as equal to the current one.
OrdersState.placeholder is the fake state the loading skeleton is
traced from, matching the Riverpod side. A new field now has four places to
reach — the constructor, copyWith, props and placeholder — and the
generated TODO names all four.
moarch update app-status and create widget status-view are the new
catalog entries behind the two files, so both refresh like everything else.
hasActionBase is now true on both stacks: Riverpod's shared base is
core/utils/action_notifier.dart, bloc's is core/utils/app_status.dart.
Auth is unchanged: AuthState stays a sealed family, because signed in
versus signed out is a real either/or the router guard switches on and the
two carry different things.
Fixed: a bloc project's design-system preview did not compile. The
generated shared/views/design_system_view.dart previewed AppAsyncView on
both stacks, but the widget is Riverpod's — so a bloc project got a screen
importing shared/widgets/app_async_view.dart and core/utils/action_bloc.dart,
neither of which moarch ever writes. The section is now Riverpod-only, along
with the three imports that existed solely for it (app_async_view.dart,
app_exception.dart, and skeletonizer for its BoneMock skeleton).
Riverpod projects are unaffected.
`AppCalendar` dots can say what they are, not just how many. Alongside events, the widget takes an optional eventColors: Map > — one dot per color, so
AppCalendar dots can say what they are, not just how many. Alongside
events, the widget takes an optional
eventColors: Map<DateTime, List<Color>> — one dot per color, so a month can
show a status, a category or which calendar an entry came from rather than
only how busy the day was.
AppCalendar(
selected: _day,
eventColors: {
for (final a in appointments) a.startsAt: [a.status.color],
},
onSelected: (day) => setState(() => _day = day),
)
A day named there takes both its dots and how many of them from the list,
so there is no count to keep in sync; events still speaks for every day
that is not named. The keys are re-keyed to the day exactly the way
events is, so entries at 09:00 and 14:00 are two dots on one day. An
empty list is a non-entry rather than a suppression — {day: []} leaves
the day to events, the same way a count of zero there says nothing, and
neither map can blank a day the other filled.
Uncolored dots are untouched. A null color falls through to
CalendarStyle.markerDecoration, so a calendar that never mentions colors
draws what it always drew. moarch update calendar refreshes an unedited
copy; both parameters default to empty, so no existing call site changes.
A day with more events than fit now says so. The grid used to stop at
three dots, which made a four-event day look identical to a three-event one.
The last marker is now a +N standing for everything the dots did not show,
so the two add up to the day's real count.
Three is a cap on markers, not dots: a day with more draws two dots and
the counter. Three dots plus a counter is ≈34px of markers, and a day cell
inside page padding is ≈33px wide on a 320px screen — wide enough to
overflow the row table_calendar lays the markers out in. Spending a dot
on the count keeps them inside the cell at every width.
The dot's shape is stated once. _dotDecoration backs both
markerDecoration and the colored dots, so a fork that wants pills or a glow
changes one function instead of finding two definitions and splitting its own
kit.
`AppException` is a sealed family. The single class with an AppExceptionType field is now a sealed base with six kinds under it — NetworkException, Se
AppException is a sealed family. The single class with an
AppExceptionType field is now a sealed base with six kinds under it —
NetworkException, ServerException, NotFoundException, AuthException,
CancelledException, UnknownException — so a switch over a failure is
checked for completeness, and a kind added later cannot be silently missed at
the places that branch on one. Nothing outside app_exception.dart can join
the family, which is what makes that hold.
Catching gets shorter wherever it used to test a field:
// before
} on AppException catch (e) {
if (e.type == AppExceptionType.network) return true;
await _tokens.clearSession();
return false;
}
// now
} on NetworkException {
return true;
} on AppException {
await _tokens.clearSession();
return false;
}
The generated auth repository, the Dio 401 interceptor and both Google
sign-in flows are written that way now. Nothing above the datasource
changed: runAction, AppAsyncView and the bloc failure states still catch
the base class and show message, which is what most code should keep
doing.
AppExceptionType and .type stay. The enum is still generated and
AppException.type still hands out the same values, so a project that
branched on it keeps compiling straight through
moarch update app_exception. New code does not need it — a switch on the
exception itself is exhaustive — and the generated file says so, along with a
note that adding a subclass makes the getter's switch incomplete on purpose.
One narrow break: a class outside app_exception.dart can no longer
implements AppException, because sealed forbids it. A test double that
did should throw one of the six kinds instead.
Every factory keeps its name. AppException.noInternet(),
.sessionExpired(), .cancelled(), .fromError(), .fromDioError(),
.fromFirebaseError() and .fromFirebaseAuthError() are unchanged at the
call site — each now returns the kind it describes.
- Adjustments
`readOnly`, across the whole input family. Fifteen more widgets take it — checkbox, checkbox label, switch, segmented, choice chip, radio group, slide
readOnly, across the whole input family. Fifteen more widgets take it —
checkbox, checkbox label, switch, segmented, choice chip, radio group, slider,
stepper, OTP, rating, dropdown, multi-select, file picker, country picker and
calendar — joining the fields that already had it. Read-only is not disabled:
a disabled control greys out because its value is not the user's to set yet,
while a read-only one keeps every colour at full strength — the value it is
showing is real and worth reading — and only stops answering, to the pointer
and to the keyboard both. It keeps validating too, so a required box the
user cannot reach still fails the form rather than passing quietly.
AppCheckboxLabel(label: 'Terms', value: true) // greyed out
AppCheckboxLabel(label: 'Terms', value: true, readOnly: true) // normal, inert
readOnly: true is the whole of what a caller writes. The controls'
callbacks stopped being required to make that possible, and each one now
hands Material a no-op of its own when it is read-only — a Checkbox or a
Slider greys itself out the moment its callback goes null, which is the
one thing read-only must not do. Nobody should have to invent an
onChanged: (_) {} to keep a frozen control from looking disabled. The
four picker-backed fields assert that a field the user can pick in was
given an onChanged, so the looser signature cannot hide a forgotten one.
A new ReadOnlyGate in app_input_style.dart is the one place that
behaviour lives. Three widgets have a reason not to reach for it and say so
in their docs: the picker-backed fields hand readOnly to the
TextFormField underneath, the file picker drops its add area rather than
leaving a live-looking one inert, and the calendar freezes only the day tap
because paging to another month changes no value.
The app theme is what paints the kit now. AppInputConfig.variant starts
at null, and that null is the rule: no variant means the theme paints it
— a checkbox by checkboxTheme, a card by cardTheme, a chip by chipTheme,
a field by inputDecorationTheme. Until now app_theme.dart declared
twenty-five component themes and the kit read one property from one of them,
so editing it moved almost nothing. Naming a variant still hands that widget's
colours back to the kit, and geometry — AppInputType, AppInputShape,
AppInputSize — stays the kit's either way, because a theme has no way to
describe four variants at once.
A filled input and an AppCard are the same colour again. The field's
fill was the theme's fillColor with 6% of the variant blended over it, while
the card painted surfaceContainerLowest flat, so the two never quite
matched. With no variant there is nothing to tint with, and both now take the
theme's own surface — inputDecorationTheme.fillColor and cardTheme.color.
AppCard, AppListTile, AppBadge, AppAppBar, AppTabs,
AppProgressBar and AppExpansionTile read their component themes instead
of painting past them. AppCard takes its colour, corner radius and shadow
from cardTheme; AppListTile, a hand-built row rather than a Material
ListTile, reads listTileTheme for its inset and icon colour.
Some defaults move with all this — every one of them into app_theme.dart,
where they can now be edited, rather than staying locked inside a widget:
cardTheme is rounded to 16 and chipTheme to a pill, which is what those
widgets were already drawing; listTileTheme.contentPadding becomes the inset
AppListTile was using; and appBarTheme becomes surface-on-onSurface,
which is the bar AppAppBar was drawing over the theme's brand-coloured one.
A project that had edited any of these gets to keep its edit, since update
never overwrites a changed file.
moarch update widgets theme refreshes the lot.
`SearchPickerSheet` paints its surface as a `Material`. The sheet opens transparent, so the Container it drew its background with sat in front of the
SearchPickerSheet paints its surface as a Material. The sheet opens
transparent, so the Container it drew its background with sat in front of
the nearest Material — the one every ListTile in the list paints its
ripple onto. Rows took taps but never flashed, and debug builds logged a
framework assertion on every row build. moarch update search-sheet
refreshes it.`ref.listenChange(...)`, beside `ref.listenAction(...)`. Not every result of an action is a message. The same extension now hands a screen the rest of
ref.listenChange(...), beside ref.listenAction(...). Not every result
of an action is a message. The same extension now hands a screen the rest of
them: select any value off the state, be called once when it appears or
changes.
ref.listenChange<OrdersState, String>(
context,
ordersNotifierProvider,
select: (state) => state.createdOrderId,
onChange: (id) => ref.read(routerProvider).push(AppRoutes.orderOf(id)),
);
A null selection means there is nothing to react to, so a request the
notifier has already cleared does not fire again, and onChange never
receives one — (state) => state.isDone ? true : null is how a plain flag
says now. It compares against the previous state, so a value that stays
put through later rebuilds fires once rather than on every one, and it
holds the same rules listenAction does: nothing while the state is still
in flight, nothing once the context is gone.
It knows nothing about routes, which is the point — navigation, popping,
focus, scroll controllers and analytics all come out of one helper, and the
notifier keeps reporting what happened while the screen decides what to
do about it. listenAction is unchanged, so nothing generated before this
needs touching.
The get_it registrations are split one file per layer. moarch init now writes lib/config/di/ as five files rather than one: injector.dart holds getIt
The get_it registrations are split one file per layer. moarch init now
writes lib/config/di/ as five files rather than one: injector.dart holds
getIt and a setupInjector() that calls one registrar per layer, and the
registrations live beside the layer they belong to —
external_module.dart (Dio, the Firebase handles, secure storage),
core_module.dart (the services under lib/core), data_module.dart
(datasources and repositories) and, on bloc, presentation_module.dart (the
blocs). injector.dart is the same length in a one-feature app and a
fifty-feature one.
On Riverpod there is no presentation module at all, which is the layout
saying out loud what was already true: a notifier needs the Ref Riverpod
owns, so it lives behind its provider and reads what it depends on out of
the locator. Everything else — clients, services, datasources,
repositories — is in get_it in both stacks.
AppButton gains isDisabled. The same end state as onPressed: null,
said the other way round — isDisabled: !form.isValid rather than
onPressed: form.isValid ? _submit : null — so the handler stays visible
at the call site. Both routes work and they compose. A loading button still
keeps its full color, because busy is not the same as unavailable; a
disabled one fades, and stays faded even while it loads.
For Failure bloc states, failure is unique
Entities and models are now `freezed` classes, and models add `json_serializable`. Every generated entity and model changes shape, and a generated pro
Entities and models are now freezed classes, and models add
json_serializable. Every generated entity and model changes shape, and a
generated project gains four dependencies (freezed_annotation and
json_annotation at runtime, freezed and json_serializable in dev). The
entity/model split is unchanged: domain/entities/ stays JSON-free,
data/models/ keeps fromEntity() / toEntity(). What is gone is
class XModel extends XEntity — freezed generates the concrete class, so
there is no constructor left to inherit. The model declares its own fields
and maps them explicitly.
This does **not** migrate a project that already exists: `moarch update`
compares hashes and refuses to overwrite an edited file, and it never touches
`pubspec.yaml`. In particular, refreshing the auth feature (`moarch update
auth`) on a project scaffolded before this release writes freezed sources into a project that has no freezed. Add the four dependencies first.
moarch create entity-copys is removed. It injected copyWith, == and
hashCode into an entity; freezed writes all three, and injecting them on
top is a duplicate-member compile error. moarch create model <feature> <name> --from-entity is what replaces it — see below.
moarch create empty-factories stays: .empty() is a hand-written
convenience freezed does not produce.
== keyed on id alone silently dropped state. Both
stacks' entity templates, and create model --from-json, generated
other is XEntity && other.id == id. A multi-step create form builds drafts
that all share an empty id, so every draft compared equal to the last —
Bloc.emit short-circuits on state == _state, and a Riverpod notifier only
rebuilds listeners on a changed state, so every update after the first was
dropped and the form quietly lost what had been typed into it. The same
equality was on AuthUserEntity, where a changed display name or photo never
reached the UI. Freezed's equality covers every field and compares
collections by content.copyWith could not set a nullable field back to null.
x ?? this.x cannot tell "not passed" from "passed null". Freezed uses a
sentinel and gets it right.id into the document body. add()
assigns the id only once the write lands, so the copy stored beside the data
was wrong from the moment it was written. The id now carries
@JsonKey(includeToJson: false) and is read back off the snapshot —
fromDoc folds doc.id into the payload before parsing.DateTime was stored as an ISO string, which the server
sorts and ranges as text, so a range query compared character by character
and an index on the field bought nothing. A generated TimestampConverter
(lib/core/network/timestamp_converter.dart) stores it as a real
Timestamp, and reads it back in UTC so the same document does not decode to
a different DateTime on two devices.moarch create model <feature> <name> --from-entity — writes the model for
an entity you wrote by hand, mapping its fields in both directions. A field
whose type is another entity is converted, not assigned:
DatasModel.fromEntity(entity.datas) one way and datas.toEntity() the
other, element-wise for a List<XEntity>, null-guarded when the field can be
absent. It also imports the sibling models it maps to and names the ones that
do not exist yet.--doc marks a Firestore document root, on --from-entity and --from-json
alike: the model gets fromDoc and keeps its id out of the body. Leave it off
for a value object nested inside a document, which can carry an id of its
own and still be a plain map. On --from-json it also supplies the String id the sample could not — a document's id is its name, so an exported payload
does not carry one — and retypes an id the sample inferred as something
else, since doc.id is always a String.build.yaml is now generated, carrying explicit_to_json: true. Without it
json_serializable leaves a nested model in toJson()'s map as the object it
is rather than as a map — jsonEncode papers over that, but
FirebaseFirestore.add(model.toJson()) throws on it.*.freezed.dart and *.g.dart are added to .gitignore. CI already runs
build_runner build before analyze and test, so they are a command away.moarch create model now reads Firestore off the project like every other
command — on the plain scaffold and on --from-json — so a Firestore project
gets the document-shaped model rather than a REST one its own datasource
cannot use, and its dates are stored as Timestamps.bloc_lint is floored at ^0.4.1 rather than ^0.4.2. 0.4.2 needs
_fe_analyzer_shared >=100, which only analyzer 13 brings, while freezed 3.x
caps analyzer below 11 — the two do not resolve together, and a bloc project
simply failed pub get. 0.4.1 accepts what analyzer 10 ships.freezed is pinned below 4 on purpose: 4.0.0 raised its floor to Dart 3.13,
which no released Flutter stable ships yet. The class shape moarch writes is
the same in both, so this is a floor to raise, not a rewrite.AppInputStyle.decorationError is the one place that cancels the
indent, and every field in the kit goes through it — TextFormField and
DropdownButtonFormField via errorBuilder, the FormField-based pickers
via InputDecoration.error, and the controls that draw their own line
(checkbox, radio, slider, file picker) via AppInputStyle.errorStyle.AppInputConfig.requiredMessage. It was spelled out in nine widgets;
translating it is now a one-line assignment to AppInputConfig.defaults.12 also dropped the FilePickerResult wrapper (pickFiles returns List ) and deprecated pickFiles(allowMultiple: false), so media_service.dart now branc…
moarch init --all did not resolve on either stack. file_picker: ^11
pins win32 ^5.9, and flutter_secure_storage: ^11 reaches win32 ^6.0.1
through flutter_secure_storage_windows — the two cannot both be installed.
file_picker is now ^12.0.0, which is where it stopped depending on win32
directly. 12 also dropped the FilePickerResult wrapper (pickFiles returns
List<PlatformFile>) and deprecated pickFiles(allowMultiple: false), so
media_service.dart now branches to FilePicker.pickFile for a single pick.mogen_integration_tests. 1.1.2
needs analyzer >=13, which a Riverpod app cannot reach: riverpod 3.4.2
declares test as a regular dependency, and test resolved against
flutter_test's pinned matcher/test_api caps analyzer below 13. The
constraint is now ^1.1.1, which gives pub somewhere to back off to — a bloc
project still installs 1.1.2, a Riverpod one takes 1.1.1. (The real fix is
upstream: mogen_unit_tests declares analyzer >=10 <15 and never
conflicts; mogen_integration_tests narrowed its floor to 13.)ProviderListenable — the type every ref.listen takes, and the one
listenAction is declared with — left the main barrel in Riverpod 3.
action_listener.dart now imports package:flutter_riverpod/misc.dart,
where it lives.AppAsyncView built its stream/future states with AsyncValue.copyWithPrevious,
which went @internal in Riverpod 3 and warned in every generated project.
It now tracks hasValue / value / error / isLoading itself and adapts
the provider's AsyncValue through public getters only. Behaviour is
unchanged: a reload or a failed refresh still leaves loaded data on screen.moarch create feature generated the repository, its implementation, the
entity and the model even when the Repository row was left unticked —
the state holder was assumed to need one, so the layer was silently added
back. The checklist is now taken literally: unticking Repository generates a
bloc that takes nothing and loads nothing until you point it somewhere, and
a Riverpod notifier whose build() is a TODO with no locator behind it.
Both are registered and both compile.Initial, Loading,
Success, Failure, and Started — dispatched when the screen opens and
again to refresh or retry. Refreshed is gone: a retry is the same load,
and two names for it is one too many.Success is generated empty. It carried a List<XEntity> items, a
copyWith and a placeholder of fake rows. What a screen shows is the
screen's business, and a scaffolded list half the features do not want is a
line to delete rather than a head start — so it ships as const XSuccess()
with a TODO saying where the fields and their props go. The state no
longer names an entity at all, which is what lets a feature without a data
layer compile.create bloc --firestore. It brought two events of its own
(ItemsUpdated, Failed), a StreamSubscription and a close() override.
Every bloc is now the one-off await _repo.fetchAll() shape whatever the
backend is; a live query is the project's to wire. The data layer is
untouched — a Firestore datasource still has watchAll() and the repository
still declares it.const XSuccess() instead of a placeholder static, with a comment saying
to give the fields fake values as they are added. It no longer imports
EmptyView, which only the live variant had a use for.moarch create bloc registers the bloc even when the feature has no
repository — it takes nothing, so there is nothing to wait for.AppBottomNav takes a borderColor. Floating, it is a hairline around the card, drawn as part of the shape Material already cuts the corner with; docked
AppBottomNav takes a borderColor. Floating, it is a hairline around the
card, drawn as part of the shape Material already cuts the corner with;
docked, it replaces the outlineVariant line between the bar and the
content, and gives Material's own bar a top edge it never drew. Null is the
bar every project had before this — a flat dark theme is where it earns its
place, since the shadow holding a floating card up is invisible there.AppBottomNav takes a floatingWidth. AppBottomNavWidth.hug sizes the
floating card to its destinations and centers it, instead of spanning the
screen — two or three tabs stretched across a phone is mostly empty card.
floatingMaxWidth caps the width of either, which is how a fill bar stops
short of the edges on a tablet. Both are only read when floating: a docked
bar is the bottom edge.
NavigationBar divides whatever width it is handed, so hugging
measures it with an IntrinsicWidth instead.AppAdaptiveNav hands the phone layout the three new knobs as
bottomNavBorderColor, bottomNavWidth and bottomNavMaxWidth.core/utils/extensions.dart gains FormX.isValid on GlobalKey , so a submit reads if (_formKey.isValid) instead of _formKey.currentState?.validate() ??
core/utils/extensions.dart gains FormX.isValid on
GlobalKey<FormState>, so a submit reads if (_formKey.isValid) instead of
_formKey.currentState?.validate() ?? false. An unmounted form reports
invalid, which is the safe half of that ??.core/utils/extensions.dart gains FormX.isValid on GlobalKey , so a submit reads if (_formKey.isValid) instead of _formKey.currentState?.validate() ??
core/utils/extensions.dart gains FormX.isValid on
GlobalKey<FormState>, so a submit reads if (_formKey.isValid) instead of
_formKey.currentState?.validate() ?? false. An unmounted form reports
invalid, which is the safe half of that ??.The generated main.dart now removes the native splash after runApp()
rather than before it. Removing it first hands the screen back to the
framework before a single frame has been painted, which is a blank window
for as long as the first build takes — on a cold start with a router that
parks on a loading route, that flash is visible. The comment above the call
says how to push it later still (your own async init, or a post-frame
callback) if something has to land before the app is on screen.
flutterbloc: the app-wide BlocProvider<AuthBloc> is now lazy: false.
A lazy provider builds its bloc on the first read _from the widget tree,
and nothing reads this one that way — the router's redirect and its
refreshListenable both take the bloc out of the locator. So the
..add(const AuthStarted()) in create never ran, session restore never
started, and the router sat on the splash route waiting for a state change
that could not arrive.
init now writes lib/core/network/paginated.dart alongside safe_api_call.dart whenever Dio is part of the stack — Paginated , the envelope a REST list
init now writes lib/core/network/paginated.dart alongside
safe_api_call.dart whenever Dio is part of the stack — Paginated<T>, the
envelope a REST list endpoint answers with. It exists so the first feature
to paginate has one shape to share rather than one per repository.
Shaped for the common {page, limit, total, data} response, but written to
be edited: the envelope is the backend's choice, not the app's. The item key
is a dataKey argument rather than a literal, and the counts are read
leniently — a missing or stringified total, which a last page or a PHP
backend will hand you, costs a fallback instead of a TypeError that
safeApiCall can only report as an unknown failure. A null item list reads
as an empty page for the same reason. fromJsonT takes an Object? rather
than a Map, so a page of scalars parses through the same factory as a page
of models.
What survives renaming the fields is the arithmetic and the two members the
Clean Architecture boundary is there for: map, so a repository hands the
domain a page of entities instead of a page of models, and append, which
is the whole of "load more" — the state holds one Paginated and replaces
it with state.append(page). pageCount treats a zero limit as a single
page, so an envelope that carried no page size cannot loop a load-more
forever.
Nothing generated consumes it yet: fetchAll() still returns a whole list
and the feature states still hold a plain List. Wiring a loadMore path
through the notifier and bloc templates is a separate change; a project
whose API never pages can delete the file.
paginated is a catalog entry, so moarch update paginated refreshes it,
moarch update network includes it, and --diff shows what a refresh would
change.
init now writes the project's own README.md — the one generated document aimed at a person rather than a task, written for someone who has never worke
init now writes the project's own README.md — the one generated document
aimed at a person rather than a task, written for someone who has never
worked on a Clean Architecture Flutter app. Thirteen sections: what the
project is and why the dependency rule shapes the folders; a first-run
walkthrough from installing FVM through .env and build_runner to
fvm flutter run, with a table of what to do when each step fails; the
tooling and the packages that shape how the code is written; an annotated
tree; one piece of data traced entity → model → repository → get_it →
AppException; the state stack the project actually took; moarch create feature and the five steps that are still yours; flavors; the build
commands and where the artefacts land; the workflows and every secret they
read; the conventions the analyzer cannot check; contacts and access; and
where the rest of docs/ picks up.
It replaces the README `flutter create` leaves behind and only that one —
matched on two of its own sentences, the same way `main.dart` and
`widget_test.dart` already are, so a README you wrote is never touched.
The README is a catalog entry, so moarch update readme refreshes it and
--diff shows what a refresh would change. Like every other template it
reads its options back off the project rather than remembering them: the
state stack, the packages, the workflows, and now the flavors — init runs
before moarch create flavors exists, so a fresh project gets the flavor
setup walkthrough, and the same file rewrites itself with the real flavor
names, run commands and artefact paths once flavorizr.yaml is on disk.
Two sections are [bracketed] placeholders instead: the owner line and
Contacts & access, which no detection can fill in.
ScaffoldContext gained projectName, flavorNames and hasWorkflows.
flavorNames scans flavorizr.yaml rather than parsing it as YAML: that
file is edited by hand, and a malformed one should cost the README its
flavor list, not make every template in the catalog throw.
Breaking on update, not on init. A project scaffolded with 6.0.0 is fine. An existing 5.x project is only affected if you run moarch update — and then
Breaking on update, not on init. A project scaffolded with 6.0.0 is fine.
An existing 5.x project is only affected if you run moarch update — and then
in one specific way: buildDioClient takes a new required refreshSession:
argument, and injector.dart is the file that passes it. update refreshes
dio_client.dart silently (you almost certainly never edited it) but leaves
injector.dart alone, because moarch create feature writes into it and that
counts as edited. The two then disagree and the project stops compiling.
Refreshing both together is the fix:
moarch update dio-client
moarch update injector --force # only if you have no hand-edits to keep
Or add the argument by hand:
..registerLazySingleton<Dio>(
() => buildDioClient(
getIt<TokenStorage>(),
refreshSession: () => getIt<AuthRepository>().refresh(),
),
)
Two smaller ones in the same shape:
auth_remote_datasource.dart now calls ApiConstants.authLogin and friends,
so refresh api-constants alongside it or the datasource will not resolve
them.GetX to XRepository. That
compiles as-is — the repository was always registered too — but leaves
domain/usecases/get_x.dart and its registerLazySingleton<GetX> orphaned.
Both can be deleted.pubspec.yaml now carries a caret constraint on every dependency
instead of any, from one table in lib/src/utils/package_versions.dart
that is bumped per release. intl stays unconstrained on purpose:
flutter_localizations pins it exactly from the SDK.GetX that only forwarded
_repository.fetchAll() added a name and no behaviour, and the auth feature
init scaffolds never had one — so the two halves of the generator
disagreed about whether the layer existed. State holders take the
repository in both stacks.dio_client.dart no longer
carries its own bare Dio, endpoint and JSON keys beside the datasource's:
the 401 interceptor takes a refreshSession callback, injector.dart
passes the auth repository's refresh (as a callback, since the repository
is built on that same client), and the repository — a lazy singleton —
owns the single-flight guard that used to be a top-level mutable global./auth/* paths moved into ApiConstants, so the datasource and the Dio
client's public-route list read the same strings.safeApiCall recognises offline from what Dio throws rather than asking
connectivity_plus before every request — that cost a platform round-trip
per call and reported a captive portal as online. safeFirebaseCall keeps
its pre-flight; its doc comment now says so.moarch create subcommand accepts either a lib/ or the project
root for --path.moarch create feature in a bloc project scaffolded Riverpod when --path
pointed at the project root: StateManagement.detect only looked one
directory up for pubspec.yaml, and falls back to Riverpod when it finds
none. It now walks up until it finds one.moarch update rewrote a bloc project's analysis_options.yaml as the
Riverpod one, dropping the bloc: ruleset — the catalog spec called
analysisOptions() without the stack.AuthFailure
now carries the session it failed from (userId), so the router redirect
can tell "signed out, with a reason" from "still signed in, and something
went wrong" — which is what made the failure unemittable before. The same
change gives the Firebase password-reset handler distinct states for two
identical failures in a row, where Equatable had deduped the second emit
and shown nothing.debugLogDiagnostics and Dio's LogInterceptor are debug-only. appLogger
already dropped the log records in release, but msg.toString() runs at the
call site, so every response body in the app was still serialised in full
first.AppRoutes.designView (pointed at a screen init does not generate) and
AppRoutes.forgotPassword (unrouted, and silently in publicRoutes).AppExceptionType.cache and .parsing, which nothing ever constructed,
and the AppException.test() factory — a test helper shipped in lib/..env.example, committed beside the gitignored .env. Without it a fresh
clone had no .env at all and build_runner failed on app_env.dart
before a new developer could run anything.- Adjustments
- Adjustments
- Transformers on bloc
moarch create feature on the bloc stack writes presentation/pages/ _page.dart beside presentation/views/ _view.dart. The page is the BlocProvider — it
moarch create feature on the bloc stack writes
presentation/pages/<x>_page.dart beside presentation/views/<x>_view.dart.
The page is the BlocProvider — it builds the bloc out of the locator, opens
it with its first event, and is what a GoRoute points at. The view is left a
plain widget that reads the bloc off the context, so a widget test can pump it
with a bloc of its own without the locator being set up.
Riverpod generates no page: a notifier is read through a provider wherever it is needed, so there would be nothing to wrap the screen in. The auth screens are unchanged in both stacks — their holder is provided once, above the router.
Breaking for both stacks. A project generated with 5.0.0 does not look like one generated with 4.x. Nothing migrates an existing project: moarch updat
Breaking for both stacks. A project generated with 5.0.0 does not look like
one generated with 4.x. Nothing migrates an existing project: moarch update
refreshes a file where the current templates put it, so a 4.x bloc project
keeps its presentation/states/ folder — and update stops refreshing what is
in it — until the files are moved by hand. A 4.x Riverpod project keeps its
providers and is not touched.
Riverpod declared a provider beside every class it built. It no longer does.
lib/config/di/injector.dart — the file the bloc stack has had since 4.0.0 —
is now generated for both stacks, and holds the same things in both:
clients, services, datasources, repositories and use cases.
dioClientProvider, secureStorageProvider,
tokenStorageProvider, firebaseAuthProvider, firebaseDbProvider,
permissionProvider, mediaServiceProvider, urlLauncherProvider,
notificationServiceProvider, firebaseNotificationsServiceProvider,
biometricServiceProvider, connectivityProvider, debouncerProvider,
dialogProvider, modalProvider, and the per-feature
<x>RemoteDataSourceProvider / <x>LocalDataSourceProvider /
<x>RepositoryProvider / get<X>Provider. Each is a getIt<Thing>() now.authNotifierProvider, languageProvider, routerProvider,
hasInternetProvider and maintenanceStatusProvider — and those read their
dependencies out of the locator.AsyncNotifier needs the Ref only
Riverpod can hand it, so it is not registered in get_it; it declares
OrdersRepository get _repo => getIt<OrdersRepository>(); in place of
ref.watch(ordersRepositoryProvider).moarch create feature now registers what it generated in a Riverpod
project too — the datasource, the repository and the use case, at the
// moarch:registrations anchor. The notifier is the only thing it leaves
out, because there is nothing to register.main.dart calls await setupInjector() before runApp in both stacks.
The ProviderContainer / UncontrolledProviderScope dance a Riverpod
project needed when a service held a Ref is gone with the services: it is
a plain ProviderScope again, whatever is selected.get_it is a dependency of both stacks. moarch doctor checks for it and
for the locator in both.AppButton — now have one body instead of two.
config/firebase/firebase_providers.dart and core/network/dio_client.dart
are the same file in both stacks and moved out of the per-stack
AppTemplates.presentation/states/<x>_state.dart moves to presentation/blocs/<x>_state.dart
on the bloc stack, beside the events and the bloc. The three are one unit — the
handlers emit the states — and a change to any of them is usually a change to
all three. Riverpod is unchanged: presentation/states/ beside
presentation/notifiers/.
BlocConsumerThe generated view is a BlocConsumer rather than a BlocBuilder, with a
listener prepared for the states the feature has and
listenWhen: (previous, current) => previous != current. It is the bloc answer
to ref.listen: listener runs once per new state — where a toast, a dialog
or a context.push belongs — while builder runs on every rebuild.
moarch create bloc wrote Riverpod's core/utils/action_notifier.dart into
a bloc project, importing flutter_riverpod in a project that does not have
it. It also named a mixin (ActionBlocMixin) that has never existed. The
file is no longer written.build() and takes the use
case when there is one — the same rule the bloc has followed since 4.0.0
(the repository regardless on the Firestore variant, whose live query no use
case wraps).app_exception.dart without
using it, and the local datasource imported its model without using it.--firestore variant declared its constructor as
const OrdersSuccess{...} — no parentheses around the parameter list, so
the generated file did not parse. It is const OrdersSuccess({...}) now.Riverpod projects are unaffected by this release — every template, field and file path on that side is unchanged. Everything below is the new stack.
Riverpod projects are unaffected by this release — every template, field and file path on that side is unchanged. Everything below is the new stack.
moarch init now asks which state
management the project uses before anything else — Riverpod, as before, or
flutter_bloc with get_it for dependency injection. Every state-bearing
template exists in both, under lib/src/templates/riverpod/ and
lib/src/templates/bloc/: the feature scaffold, both auth features,
AppAsyncView, the action listener, the maintenance gate, the router, the
Dio client and main.dart. Same layers, same file names, same layer
boundaries — a project reads the same way whichever it took.<Feature>Event family, and a
<Feature>State family of Initial / Loading / Success / Failure, so
the view is a switch the compiler checks for completeness. The family is
the status — there is no flag or enum on top of it. States and events extend
Equatable, which is load-bearing: bloc drops an emit equal to the current
state and BlocBuilder rebuilds on the same test, so without it every emit
repaints.AuthState follows the same shape: AuthInitial (restoring, which is what
parks the router on splash), AuthLoading, AuthAuthenticated,
AuthUnauthenticated, AuthFailure.BlocProvider in a Page,
BlocBuilder and a switch in the View. AppAsyncView,
ref.listenAction and any shared action base are not generated into a bloc
project — they exist to map Riverpod's opaque AsyncValue onto four
screens, and a sealed family needs no wrapper. moarch create widget async-view says so rather than writing a file that cannot compile.moarch create feature reads the stack off pubspec.yaml and generates for
it — no new flag. In a bloc project it also registers what it generated
in lib/config/di/injector.dart, at the // moarch:registrations anchor.moarch create bloc <feature> <name> adds a state + event + bloc trio to a
feature that already exists, wired to that feature's repository.moarch init --state riverpod|bloc picks the stack without the checklist,
so --all can reach either one.bloc_lint in dev dependencies, the recommended
ruleset in analysis_options.yaml, and a bloc lint step in the CI
workflow. A freshly scaffolded project passes it with no findings.moarch doctor checks what the project's own stack needs: get_it and a
locator with its anchor comment for bloc, flutter_riverpod otherwise.main.dart, action_notifier.dart, app_router.dart, dio_client.dart,
firebase_providers.dart and language_service.dart moved out of
CoreTemplates / ConfigTemplates / ServicesTemplates into the per-stack
AppTemplates. Nothing changes in what a Riverpod project generates.AppButton, the dialog and modal helpers,
the design-system preview — take the stack as a parameter instead of being
duplicated, so there is one body to maintain.moarch create widget recorded only the files it wrote this run, so a widget already on disk stayed out of .moarch.yaml — and moarch update then read i
moarch create widget recorded only the files it wrote this run, so a widget
already on disk stayed out of .moarch.yaml — and moarch update then read
it as a file it could not vouch for. create widget <name> and
create widget all now also record a widget they skipped when its content is
still exactly what the current templates generate, whether it got there from
an earlier run, a copy, or a run that stopped before saving. A file that
differs is still left out: that content is yours.AppBottomNav takes two more looks apart from its style. labels (auto / below / none) says where the destination names are written, so the pill can sta
AppBottomNav takes two more looks apart from its style. labels
(auto / below / none) says where the destination names are written, so
the pill can stack over its label instead of opening sideways, the dot can
carry one at all, and any style can drop to icons only — a label that is not
drawn still reaches a screen reader and still names its icon on a long press.
floatingShape (full / rounded / square) cuts the floating card's
corner, and pillShape the corner of the fill behind the selection — which
Material's own bar reads too, as its indicator. Both take a
BorderRadius of the project's own (floatingBorderRadius,
pillBorderRadius) where the three names are not the number wanted.
AppAdaptiveNav passes all four down as bottomNavLabels,
bottomNavShape, bottomNavPillShape and their radius pair. Defaults are
what the bar drew before.init asks for it (Dark theme, off by
default): with it off, AppConstants declares one brand palette and
AppTheme one light getter — around 290 fewer lines in the files you
actually edit. With it on, every color token gains its *Dark counterpart,
AppTheme.dark is generated, and main.dart gets darkTheme +
themeMode: ThemeMode.system.moarch create theme --dark adds the dark half to a project scaffolded
without it, and --no-dark takes it away again. The palette, the theme,
main.dart, AppToast and the design-system preview are generated against
each other, so the switch is all of them at once: files moarch wrote and
nobody edited are rewritten silently, and an edited one stops the run with a
diff instead (--diff, --dry-run, --force, --yes).app_theme.dart rather than remembered, so
moarch update and moarch create widget follow what the project actually
is — including after switching.main.dart shipped darkTheme: AppTheme.dark commented out, so a generated
app was light-only whatever the palette said. The design-system preview had
the same line commented out under a working brightness toggle, leaving a
button that did nothing. Both are now wired when the project takes dark.AppConstants drops the tokens nothing in the kit read: accentActive,
accentRestorative, accentEnergetic (the tab indicator uses primary),
padding8, paddingH16, paddingH24, paddingV16, borderRadius24 and
duration100. The remaining colors are grouped brand → surfaces → status,
with the dark palette (when present) in one block rather than three.An existing project keeps its dark theme — the scope is read off
app_theme.dart, so moarch update sees what is already there. Two things to
look at in the diff it offers:
moarch update constants removes the tokens listed above. If your own code
reads one of them, keep it: it is your palette now.moarch update main uncomments darkTheme and sets themeMode, which is
what the dark palette was always for — but it does mean the app starts
following the system brightness. moarch create theme --no-dark is the way
out if it was never meant to.Nothing published for this version
### Fixes - Maintence Gate
AppTextButton alignment now moves the label: it reaches the row and the text, and claims the parent's width to align inside of
AppTextButton alignment now moves the label: it reaches the row and the
text, and claims the parent's width to align inside ofAppTextButton gains bare, dropping the button box around the label### Features - Padding page to 12
### Features - Adjustments
Adapt widget for size dimensions
The design-system preview renders in the app's real theme. It built its own ThemeData(useMaterial3: true) behind a TODO, so the one screen whose job i
The design-system preview renders in the app's real theme. It built its
own ThemeData(useMaterial3: true) behind a TODO, so the one screen whose
job is showing what the kit looks like was the one screen not showing it —
every widget previewed in stock Material colors and type instead of the
project's. It now uses AppTheme.light / AppTheme.dark, the same themes
main.dart mounts, so editing lib/config/theme/app_theme.dart moves the
preview with it. app_theme.dart is written unconditionally by init, so
the new import needs nothing the scaffold did not already have.
init and doctor now surface the fvm use step. init writes a
.vscode/settings.json pointing dart.flutterSdkPath at .fvm/flutter_sdk,
but only fvm use creates that symlink and .fvm/ is gitignored — so on a
fresh scaffold or a fresh clone the path did not exist. Nothing reports that:
the Dart extension silently falls back to the first Flutter on PATH, and
debug, hot reload and the analyzer all run the SDK the .fvmrc pin exists to
avoid. The only symptom is analyzer output that disagrees with
fvm flutter analyze. init now prints fvm use as the first step, ahead of
pub get, and moarch doctor grew a check for it:
dart.flutterSdkPath pointing at a path that does not exist — error,
with the fvm use fix.
the symlink present but dangling, the pinned SDK not installed — error,
pointing at fvm install.
a versioned .fvm/versions/<version> path, which is what fvm use rewrites
the setting to and which stops following .fvmrc — warning, and
doctor --fix points it back at .fvm/flutter_sdk.
settings.json missing, or carrying no dart.flutterSdkPath — warning.
An absolute path is left alone as a deliberate override, and a project with no
`.fvmrc` gets none of these findings.
The README documents that the generated .fvmrc pins the stable alias
rather than a version, so fvm install on CI or a teammate's machine can
resolve to a different SDK than your cache holds, and how to pin for real once
the project ships.
### Features - AppCard adjustment
`MaintenanceGate` — a kill switch the backend owns. While a flag says maintenance, it replaces the whole app with a screen carrying the title and mess
MaintenanceGate — a kill switch the backend owns. While a flag says
maintenance, it replaces the whole app with a screen carrying the title and
message the backend sent, so the team taking the API down can empty the app,
and reword the notice, without a release. Mounted in MaterialApp.builder so
it wraps the Navigator: above every route the router can reach, including
anything pushed after the flag flips. It replaces rather than covers, so
nothing is left to tap and the back button has nothing to pop.
It fails open — loading, offline, endpoint down or rules denied all read
as "up", because a fault in the check must not lock out every user at once.
The provider follows the project's backend: a live Firestore snapshots()
listener, a polled Dio endpoint (five minutes, plus on resume), or a stub to
point at your own source. Available in the init checklist and as
moarch create widget maintenance-gate.
Widgets whose source varies with the project are now resolved in one place,
WidgetCatalog.sourceFor, instead of being special-cased separately in
init, create widget and update — three copies that had to agree, or
update would report a file as edited the moment it was generated.
init writes android/app/proguard-rules.pro — the keep rules that were
until now only printed in docs/SECURITY_BEFORE_DEPLOYMENT.md for you to
copy across: the Flutter engine, Play Core, Firebase, OkHttp, coroutines,
enums, native methods, and SourceFile,LineNumberTable so a release stack
trace still de-obfuscates. The file is inert until the release build type
turns R8 on, so enabling minification before a release is now just that
gradle block rather than that block plus a round of release-only crashes.
The doc renders the same template, so the two cannot drift. Refreshable with
moarch update proguard (new android group).
CHECKLIST_BEFORE_DEPLOYMENT.md and SECURITY_BEFORE_DEPLOYMENT.md
reconciled with what the scaffold actually does. Both were generic
checklists that asked you to do work init had already done. Items the
scaffold handles now arrive ticked and name the file that handles them
(config/env/app_env.dart, TokenStorage, ValidationService,
app_logger.dart, the CI jobs), so the OWASP mapping stays complete but you
can see at a glance what is left. Everything unticked is genuinely yours..env.example, no network_security_config.xml, R8 rules
written but not enabled, build/debug-info/ never uploaded by the Android
workflow, and build_ipa.yml archiving through xcodebuild without carrying
the Dart obfuscation flags.envied example
pointed at lib/core/env/env.dart and class Env (the scaffold generates
lib/config/env/app_env.dart and AppEnv), the R8 block was Groovy
build.gradle where the scaffold patches build.gradle.kts, and two code
examples had Portuguese UI strings in an otherwise English doc.### Features - AppHeading
Your coding agent can read these notes before it upgrades. Set up the MCP server →