nylo-state-management
Scannednpx machina-cli add skill nylo-core/claude-code/nylo-state-management --openclawNylo State Management
Overview
Nylo v7 state management is built on two core classes: NyState (for reusable widgets) and NyPage (for pages, extending NyState). State updates propagate through named state identifiers, state actions enable cross-widget communication, and the provider/event systems handle app-level initialization and event-driven logic. Decoders bridge API responses to typed models.
When to Use
- Building stateful widgets that need to update from external triggers
- Managing page lifecycle (init, loading, reboot)
- Communicating between widgets via state actions
- Locking UI during async operations (e.g. preventing double-submit)
- Registering app services and packages via providers
- Dispatching and listening to application events
- Registering model decoders for API response parsing
- When NOT to use: for simple local state within a single widget, standard Flutter
setStateis sufficient
Quick Reference
| Task | Code |
|---|---|
| Create stateful widget | metro make:stateful_widget my_widget |
| Create state-managed widget | metro make:state_managed_widget cart |
| Update a widget's state | updateState(Cart.state) |
| Update with data | updateState(Cart.state, data: "value") |
| Fire a state action | stateAction('refresh', state: MyWidget.state) |
| Lock during async | lockRelease('key', perform: () async { ... }) |
| Check if locked | isLocked('key') |
| Check if loading | isLoading(name: 'key') |
| Reboot page init | reboot() |
| Create provider | metro make:provider cache_provider |
| Create event | metro make:event payment_event |
| Dispatch event | event<PaymentEvent>(data: {...}) |
NyState
Basic Structure
class MyWidget extends StatefulWidget {
static String state = "my_widget";
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends NyState<MyWidget> {
_MyWidgetState() {
stateName = MyWidget.state;
}
@override
get init => () async {
// Async initialization - loader shown automatically
};
@override
void stateUpdated(data) {
// Called when updateState(MyWidget.state) is invoked
reboot(); // Re-run init to refresh data
}
@override
Widget view(BuildContext context) {
return Scaffold(
body: Text("My Widget"),
);
}
}
Loading Styles
Configure what users see during init():
// Default loader widget
@override
LoadingStyle get loadingStyle => LoadingStyle.normal();
// Custom loading widget
@override
LoadingStyle get loadingStyle => LoadingStyle.normal(
child: Center(child: Text("Loading...")),
);
// Skeleton shimmer effect
@override
LoadingStyle get loadingStyle => LoadingStyle.skeletonizer();
// No loading indicator
@override
LoadingStyle get loadingStyle => LoadingStyle.none();
Lifecycle Methods
| Method | When Called |
|---|---|
init | During state initialization (async supported, shows loader) |
stateUpdated(data) | When updateState(stateName) is called externally |
view(context) | Renders the widget UI (replaces build) |
NyPage
NyPage extends NyState with page-specific capabilities. Pages use path instead of state for identification.
class _HomePageState extends NyPage<HomePage> {
@override
get init => () async {
// Page initialization
};
@override
Widget view(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Home")),
body: Text("Welcome"),
);
}
}
Enable state management on a page:
class _MyPageState extends NyPage<MyPage> {
@override
bool get stateManaged => true;
@override
get stateActions => {
"refresh-page": () {
reboot();
},
};
}
Updating State
Trigger a widget rebuild from anywhere in the app:
// Simple update
updateState(Cart.state);
// Update with data
updateState(Cart.state, data: {"count": 5});
The target widget's stateUpdated(data) method is called with the provided data.
Full Example: Cart Counter
// Widget definition
class _CartState extends NyState<Cart> {
int _count = 0;
_CartState() {
stateName = Cart.state;
}
@override
get init => () async {
_count = int.parse(await getCartValue());
};
@override
void stateUpdated(data) {
reboot();
}
@override
Widget view(BuildContext context) {
return Text("$_count");
}
}
// Update from anywhere
Future incrementCart() async {
String count = await getCartValue();
await storageSave(Keys.cart, (int.parse(count) + 1).toString());
updateState(Cart.state);
}
State Actions
Trigger specific behaviors on widgets without full state rebuilds.
Defining Actions
class _MyWidgetState extends NyState<MyWidget> {
_MyWidgetState() {
stateName = MyWidget.state;
}
@override
get stateActions => {
"hello_world": () {
print('Hello world');
},
"reset_data": (data) async {
_textController.clear();
setState(() {});
},
"show_high_score": (data) {
_score = data["high_score"];
setState(() {});
},
};
}
Alternative definition using whenStateAction in init:
@override
get init => () async {
whenStateAction({
"reset_badge": () {
_count = 0;
setState(() {});
},
});
};
Firing Actions
// Without data
stateAction('hello_world', state: MyWidget.state);
// With data
stateAction('show_high_score', state: HighScore.state, data: {"high_score": 100});
// On a NyPage (use path)
stateAction('refresh-page', state: MyPage.path);
StateAction Helper Class
Built-in actions available: refreshPage(), pop(), showToastSuccess(), showToastDanger(), showToastWarning(), validate(), changeLanguage(), confirmAction().
Helper Methods
Loading State
// Check if page is loading
if (isLoading()) return AppLoader();
// Named loading states
setLoading(true, name: 'refreshing');
await fetchData();
setLoading(false, name: 'refreshing');
if (isLoading(name: 'refreshing')) { ... }
// afterLoad - show loader until init completes
@override
Widget view(BuildContext context) {
return afterLoad(child: () {
return Text("Loaded");
});
}
Lock/Release (Prevent Double-Submit)
_login() async {
await lockRelease('login', perform: () async {
await Future.delayed(Duration(seconds: 4));
print('Login attempt...');
});
}
// Show loader while locked
if (isLocked('login'))
CircularProgressIndicator()
else
Button.primary(text: "Login", onPressed: _login)
// afterNotLocked helper
afterNotLocked('login', child: () {
return Button.primary(text: "Login", onPressed: _login);
})
Reboot
Re-executes the init method to refresh page data:
reboot();
afterNotNull
Display loader until a variable is populated:
User? _user;
@override
get init => () async {
_user = await api<ApiService>((request) => request.fetchUser());
setState(() {});
};
@override
Widget view(BuildContext context) {
return afterNotNull(_user, child: () {
return Text(_user!.name);
});
}
Validation
validate(rules: {
"email address": [textEmail, "email"],
}, onSuccess: () {
print('Validation passed');
});
Environment-Conditional Code
@override
get init => () {
whenEnv('developing', perform: () {
_emailController.text = 'test@example.com';
});
};
Confirmation Dialog
confirmAction(() {
logout();
}, title: "Logout of the app?");
Providers (NyProvider)
Providers initialize services and packages before the app starts.
Creating a Provider
metro make:provider cache_provider
Provider Structure
class CacheProvider implements NyProvider {
@override
Future<Nylo?> setup(Nylo nylo) async {
// Runs first during bootstrap
// Initialize packages, register services
await CacheManager.init();
return nylo; // Must return Nylo or null
}
@override
Future<void> boot(Nylo nylo) async {
// Runs after ALL providers complete setup()
// Safe to use services from other providers
User user = await Auth.user();
if (!user.isSubscribed) {
await Auth.remove();
}
}
}
Bootstrap Lifecycle
Boot.nyloloops through providers registered inconfig/providers.dart- Each provider's
setup()runs in order - After all
setup()calls complete, each provider'sboot()runs Boot.finishedbinds the Nylo instance to Backpack as'nylo'- Access via:
Backpack.instance.read('nylo')
Registration
Providers are registered in lib/config/providers.dart:
final providers = [
AppProvider(),
CacheProvider(),
// ...
];
Events (NyEvent / NyListener)
Creating an Event
metro make:event PaymentSuccessfulEvent
Event Structure
class PaymentSuccessfulEvent implements NyEvent {
final listeners = {
DefaultListener: DefaultListener(),
};
}
class DefaultListener extends NyListener {
handle(dynamic event) async {
// Process event data
}
}
Multiple Listeners
class PaymentSuccessfulEvent implements NyEvent {
final listeners = {
NotificationListener: NotificationListener(),
AnalyticsListener: AnalyticsListener(),
OrderProcessingListener: OrderProcessingListener(),
};
}
Dispatching Events
// Without data
event<PaymentSuccessfulEvent>();
// With data
event<PaymentSuccessfulEvent>(data: {
'user': user,
'amount': amount,
'transactionId': 'txn_123456',
});
// Broadcasting (for external listeners)
event<PaymentSuccessfulEvent>(
data: {'user': user},
broadcast: true,
);
Listening to Events
// Manual subscription (requires manual cancel)
NyEventSubscription subscription = listenOn<PaymentSuccessfulEvent>((data) {
showSuccessMessage("Payment received");
});
subscription.cancel(); // when done
// In NyPage/NyState (auto-cleanup on dispose)
@override
get init => () {
listen<PaymentSuccessfulEvent>((data) {
routeTo(OrderConfirmationPage.path);
});
};
Global Broadcasting
Enable auto-broadcast for all events in AppProvider:
@override
boot(Nylo nylo) async {
nylo.broadcastEvents();
}
Decoders
Decoders convert API responses into typed Dart objects. Configured in lib/config/decoders.dart.
Model Decoders
final modelDecoders = {
User: (data) => User.fromJson(data),
List<User>: (data) => List.from(data)
.map((json) => User.fromJson(json)).toList(),
};
Used automatically by network() in API services:
class ApiService extends NyApiService {
ApiService({BuildContext? buildContext})
: super(buildContext, decoders: modelDecoders);
Future<User?> fetchUser() async {
return await network<User>(
request: (request) => request.get("/user"),
);
}
}
API Decoders
Register API service instances for the api() helper:
final Map<Type, dynamic> apiDecoders = {
ApiService: ApiService(),
AuthApiService: AuthApiService(),
};
Usage:
User user = await api<ApiService>(
(request) => request.fetchUser(),
);
Common Mistakes
| Mistake | Fix |
|---|---|
stateUpdated not called | Ensure stateName is set in the constructor and matches the value passed to updateState() |
| State action not firing | Verify the action name string matches exactly between definition and stateAction() call |
| NyPage state actions not working | Set stateManaged => true in the NyPage subclass |
| Double-tap causing duplicate requests | Use lockRelease('key', perform: ...) to prevent concurrent execution |
| Loading indicator never disappears | Ensure init completes (resolves or throws); unhandled async errors can hang loading |
| Provider boot code failing | boot() runs after all setup() calls; ensure dependencies are initialized in setup() of their respective providers |
| Event listeners accumulating | Use listen() in NyPage/NyState (auto-cleanup) instead of listenOn() which requires manual cancel() |
| Model decoder returning null | Ensure the decoder function in modelDecoders matches the exact type used in network<T>() |
api<T>() throwing "not found" | Register the API service in apiDecoders in config/decoders.dart |
updateState from NyPage | Use the page's path for NyPage state actions, not a state field |
Source
git clone https://github.com/nylo-core/claude-code/blob/main/skills/nylo-state-management/SKILL.mdView on GitHub Overview
Nylo v7 state management uses NyState for reusable widgets and NyPage for pages, enabling named state identifiers, cross-widget state actions, and app-level providers and events. It also uses decoders to map API responses to typed models, streamlining data flow across the app.
How This Skill Works
Create a NyState-based widget with a static state name; override init for async setup (with loader) and stateUpdated to react to updateState calls. NyPage extends NyState for pages with path and optional stateManaged, actions, and event wiring; providers and events bootstrap services and app logic, while decoders translate API data to models.
When to Use It
- Building stateful widgets that need to update from external triggers
- Managing page lifecycle (init, loading, reboot)
- Communicating between widgets via state actions
- Locking UI during async operations (e.g. preventing double-submit)
- Registering model decoders for API response parsing
Quick Start
- Step 1: Run metro make:stateful_widget my_widget to create a NyState widget
- Step 2: Run metro make:state_managed_widget cart to create a NyPage-managed widget
- Step 3: In code, call updateState(Cart.state) (and data: ...) and use stateAction and lock helpers as needed
Best Practices
- Use named state identifiers for clear updates across widgets
- Leverage NyState and NyPage lifecycles instead of ad-hoc setState
- Use lockRelease and isLocked to guard async actions
- Register providers for app-wide services and packages
- Register decoders to map API responses to typed models
Example Use Cases
- A Cart widget updates when external stock info arrives via updateState
- A Home page reboots after a user action triggers new data
- A form submit button is locked during async submission
- An app-wide service provider is registered at startup
- API responses are decoded into Cart and User models via decoders