How would you structure state management for a Flutter app with sales and inventory workflows?

The short answer to your central question (single central state vs. independent feature state) is to split them by layer: keep your canonical data (the single source of truth for inventory stock) down in your repository layer as reactive state, and keep your screen controllers as lightweight facades that just observe it. That way, when checkout completes a sale, the repository updates stock once, and every active screen reflecting that stock updates automatically without cross-bloc stream spaghetti.

I actually just published an architectural deep dive on this exact problem with a runnable reference repo:

Beyond Clean Architecture: The Iceberg Pattern for Real-Time Flutter Apps with BlocSignal

It walks through structuring repository-level domain state, handling optimistic vs. pessimistic backend updates, and keeping multi-screen data in sync.

EDIT: I just took a quick look at your codebase, and I have a few notes:

Right now, the exact source of your sync pain is that each of your ChangeNotifier classes is acting as a data-fetcher, JSON parser, and isolated state container all at once:

  • SalesController maintains its own List<Item> items = []; fetched directly via ApiClient.get(ApiEndpoints.items).

  • ItemController maintains a completely separate List<Item> list = []; fetched from the same endpoint.

  • DamageController, ReceivingController, and ReturnController are also separate islands.

Because there’s no repository layer between your controllers and ApiClient, there is no shared source of truth. When SalesController records a sale, or DamageController logs broken goods, none of the other controllers have any way of knowing stock changed unless they trigger a full HTTP refetch.

This is where the submerged engine from the article comes in:

  1. Pull the data ownership out of the controllers: Create an ItemRepository (or InventoryRepository). Move the ApiClient calls there, and let the repository hold the single, canonical collection of items/stock as reactive state.

  2. Turn the controllers into consumers: SalesController and ItemController shouldn’t own List<Item> fields anymore. Instead, they just observe the repository’s reactive state.

  3. Trigger actions through the repository: When a sale or stock adjustment completes, the controller tells inventoryRepository.recordSale(...). The repository hits the backend, updates its internal cache upon confirmation, and both your Sales screen and Inventory screen update in the same frame without either controller needing to know the other exists.