-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commands isolate
isolate extracts rebuild scopes such as a Flutter State class or a BlocBuilder callback into
standalone Dart files. The smaller files make it possible to profile one UI segment without running
the rest of the application.
spm isolate --output-dir ./isolated_widgets /path/to/project
spm isolate -o ./isolation-output -j map.jsonl /path/to/project| Flag / Option | Short | Description |
|---|---|---|
--output-dir <path> |
-o |
Required. Directory where the isolated files are saved. |
--jsonl <path> |
-j |
Output JSONL mapping original source paths → isolated file paths. |
--verbose |
-v |
Enable verbose logging of the isolation process. |
<dir> [dir…] |
One or more directories to scan for rebuild scopes. |
Isolation combines static analysis with source transformation.
An AST visitor scans the target directories for rebuild boundaries:
- Classes that extend
State,ConsumerWidget, orHookConsumerWidget. - Anonymous builder functions passed to
BlocBuilder,BlocSelector,BlocConsumer,Consumer,Selector,Obx,GetX,GetBuilder, orObserver.
The TransplantExtractor converts a discovered scope into a new, self-contained StatefulWidget
(GeneratedWidget).
- For a
Stateclass, SPM also finds its companionStatefulWidgetto extract fields and constructors. - For a builder function, SPM extracts parameters such as
stateormodeland converts them into fields on the new state. -
contextis never lifted, sinceStatealready supplies one. A source class that declares its owncontextfield is skipped for the same reason: copying it across shadowsState.context.
To keep the generated file compilable, SPM recursively crawls the code for every referenced symbol:
- Methods, fields, and getters used within the same class or file move into the generated file.
- A custom widget, enum, or painter from another file is extracted along with its own dependencies,
including the base class it extends. A widget whose supertype went missing would stop reading as a
widget, which changes what
analyzecounts rather than merely failing to compile. - A class that is not a widget but hands one out, such as a theme helper with a
Widget buildDivider(), is also extracted whole. Its body is part of the tree the scope builds. - Business logic, models, services and constants are not copied. They are replaced by a
declaration-only stand-in: the name, the members the scope actually reaches, and nothing else.
Bodies become
throw UnimplementedError()and constants becomenull. - Third-party packages are never imported back into the isolated file; their symbols get the same stand-in treatment. A stand-in mirrors whether the original was a widget, so a third-party widget still reads as one and a value object still reads as a value object.
- Required
package:flutteranddart:imports are collected automatically.
A scope also reads names it does not declare, and those do not travel with the transplanted source.
Two kinds are lifted onto the generated State:
-
Locals and parameters of the enclosing method. A
BlocBuildercallback sitting inside a method can read that method'sbool onlyNKN; the declaration stays behind when the callback moves. -
Members inherited from a package supertype.
controlleronGetViewfrompackage:getis the recurring case. The transplant never emits such a class, so the member becomes a field.
Names declared inside the scope, members of the enclosing class, and top-level declarations are left alone: the first travel with the source and the other two are already handled by steps 2 and 3.
Lifting a parameter to a field costs it type promotion, which Dart applies to locals but not to
fields. Uses whose promoted type was narrower than the declared one are rewritten with an explicit
cast, so state.wallets inside an if (state is WalletLoaded) becomes
(state as WalletLoaded).wallets.
Lifted fields have no values, because the isolated scope is no longer called with arguments. SPM
generates an initState that assigns each one from a conventionally named symbol:
| Lifted binding | Assignment generated |
|---|---|
field wallets
|
wallets = fixtureWallets; |
field _controller
|
_controller = fixtureController; |
cross-file project global application
|
application = applicationValue; |
Globals are assigned first, since a field initialiser may read one. A scope whose transplanted class
already brought its own initState keeps it: overwriting a real one would discard setup the scope
depends on, so its lifted fields must be assigned by hand. The symbols below are still declared;
only the assignments are missing.
Each of those symbols is declared at the bottom of the same file, as a late variable with no
value:
late WalletState fixtureWallets;Assign one before the widget builds and it behaves as the original did. Leave it and reading it
throws, which is deliberate: a fabricated default would be measured as though it were the value
that was really there. Declaring them keeps the file analyzable, because spm analyze skips any
file carrying an error-severity diagnostic, and an undefined name is one.
Isolated widgets often lack the original project's assets. The Skeletonizer:
- Identifies widgets like
Image.network,AssetImage, orSvgPicture. - Replaces them with lightweight placeholders.
- Keeps missing image files from preventing layout and rendering.
After the files are written, SPM runs dart format over the output directory. The transplant
concatenates fragments that keep their original indentation, so unformatted output differs between
runs in layout as well as in code. Formatting is best-effort: a scope that produced unparseable Dart is still written out for
inspection.
output/
├── State/
│ ├── login_form_state.dart
│ └── product_list_state.dart
├── BlocBuilder/
│ └── user_profile_builder.dart
└── mapping.jsonl
Links each isolated file back to its original source. One JSON object per line:
{
"originalPath": "/abs/path/to/project/lib/ui/login_form.dart",
"nodeType": "State",
"isolatedPath": "/abs/path/to/output/State/login_form_state.dart",
"name": "LoginFormState"
}| Field | Description |
|---|---|
originalPath |
Absolute path of the source file the scope was extracted from. |
nodeType |
Scope kind, which is also the output subdirectory name, such as State, BlocBuilder, or Consumer. |
isolatedPath |
Absolute path of the generated standalone file. |
name |
Name of the extracted class or builder-owning widget. |
Use an isolated scope when a whole application adds unrelated work to frame timings or when a
benchmark needs direct control of the values passed into one builder. The generated
GeneratedWidget can be placed in a benchmark loop and supplied with controlled inputs.
- The stand-ins that replace non-UI and third-party dependencies resolve, but they do not run: every
body throws and every constant is
null. An isolated file is meant to be analyzed as it stands; running one means assigning thefixture…and…Valueseeds and replacing whichever stand-ins the scope actually calls into. - A stand-in for a third-party widget has an empty
build. Its own subtree is not part of the isolated file, soanalyzereports a smaller tree there than it would against the full project. Repo-local widgets are unaffected, since those are extracted whole. - The
Skeletonizerhandles images, but custom fonts or localized strings may still need manual setup if critical to the widget.
Commands
Reference
Internals
Contributing