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:



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?

  1. Prevent Instance Duplication: By using absolute package imports everywhere, you ensure that appState remains a single, consistent instance throughout the app.

  2. Discoverability: New team members always know where to look for data: ui/app_state.dart.

  3. Predictable Dependencies: The dependency always flows from UI → State.

  4. 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.