-
-
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.
- Business logic, models, and services are left out; the generated target may need mocks or values supplied by the benchmark harness.
- 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 you define
alongside the file:
| 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 seeded by hand.
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 (State, BlocBuilder, 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.
- Because SPM skips non-UI dependencies, code involving complex models or
services may need manual adjustment or mocking in the generated file. The
fixture…and…Valuesymbols named by the generatedinitStateare yours to define; until they exist, the isolated file does not compile. - The
Skeletonizerhandles images, but custom fonts or localized strings may still need manual setup if critical to the widget.
Commands
Reference
Internals
Contributing