Scaling to a Real Project Structure¶
While placing everything in a single file is great for learning, real-world applications require a structured approach. Organizing your files properly ensures that your project remains maintainable as it grows.
In this section, we will refactor the single-file example into a standard directory structure.
Important
Import Consistency: In Dart, always use absolute package imports (e.g., package:your_project/ui/app_state.dart). Mixing relative and absolute imports can cause Dart to treat the same file as two different instances, which will break your application state.
A safe way to do this is to use:
Simple Lint: https://pub.dev/packages/simple_lint
Recommended Directory Structure¶
lib/
├── ui/
│ ├── app_state.dart # All StateSlots are defined here
│ └── pages/
│ └── counter_page.dart # UI Widgets
└── main.dart # Entry point
Step 1: Define Application State (lib/ui/app_state.dart)¶
First, move all your state logic into a dedicated file. This file becomes the “Source of Truth” for your entire app.
import 'package:simple_app_state/simple_app_state.dart';
final appState = SimpleAppState();
final countSlot = appState.slot<int>('count', initial: 0);
final logsSlot = appState.slot<List<String>>(
'logs',
initial: [],
caster: (raw) => (raw as List).cast<String>(),
);
Tip
The Golden Rule: app_state.dart should never import any files from the UI layer (like widgets or pages). This ensures a clean data flow.
Step 2: Create the UI Layer (lib/ui/pages/counter_page.dart)¶
Next, move your widgets to the pages/ directory. Use the package import to access the state.
import 'package:flutter/material.dart';
import 'package:simple_app_state/simple_app_state.dart';
// Use absolute package import
import 'package:simple_app_state_demo/ui/app_state.dart';
class CounterPage extends SlotStatefulWidget {
const CounterPage({super.key});
@override
List<StateSlot> get slots => [countSlot, logsSlot];
@override
SlotState<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends SlotState<CounterPage> {
@override
Widget build(BuildContext context) {
final count = countSlot.get();
final logs = logsSlot.get();
return Scaffold(
appBar: AppBar(title: const Text('SimpleAppState Example')),
body: Column(
children: [
const SizedBox(height: 32),
Text(
'Count: $count',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
appState.batch(() {
countSlot.update((v) => v + 1);
logsSlot.update((oldCopy) {
oldCopy.add('Increment at ${DateTime.now()}');
return oldCopy;
});
});
},
child: const Text('Increment (batched)'),
),
const Divider(height: 32),
Expanded(
child: ListView.builder(
itemCount: logs.length,
itemBuilder: (context, index) {
return ListTile(title: Text(logs[index]));
},
),
),
],
),
);
}
}
Step 3: Cleanup the Entry Point (lib/main.dart)¶
Finally, main.dart becomes very slim. Notice that all imports are absolute.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:simple_app_state_demo/ui/app_state.dart';
import 'package:simple_app_state_demo/ui/pages/counter_page.dart';
void main() {
if (kDebugMode) {
appState.setDebugListener((slot, oldV, newV) {
debugPrint("Changed Slot:${slot.name}, to:$newV");
});
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CounterPage());
}
}
Why This Structure?¶
Prevent Instance Duplication: By using absolute package imports everywhere, you ensure that appState remains a single, consistent instance throughout the app.
Discoverability: New team members always know where to look for data: ui/app_state.dart.
Predictable Dependencies: The dependency always flows from UI → State.
Testability: You can write unit tests for your logic in app_state.dart without needing to build widgets.
Next step¶
Now that your project is well-organized, let’s look at some common mistakes to avoid when using SimpleAppState.
Go to Common Mistakes section.