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

Hi everyone,

I’m working on a Flutter application with several connected workflows, including products, sales, inventory, purchases, suppliers, and expenses.

I’m currently thinking about how to structure state management as different parts of the application can modify the same data.

For example, when a sale is completed, the available stock for one or more products needs to change. Stock can also change when new items are purchased, products are returned, or manual adjustments are made.

I’m trying to avoid a situation where every screen manages its own copy of the same product or inventory data.

A few things I’m considering are:

  • Keeping product and inventory state separate

  • Updating inventory only after the backend confirms a transaction

  • Refreshing related screens when stock changes

  • Moving business calculations outside widgets

  • Using repositories/services between the UI and API

  • Making sales and stock updates easier to test

For developers who have worked on larger Flutter applications, how do you handle shared data between multiple features?

Would you use one central state for products and inventory, or keep each feature independent and synchronize data when needed?

I’m also interested in how you structure the flow between UI → state management → repository → API for workflows where a sale or purchase affects multiple pieces of data.

For context, this is the codebase I’m currently working with:

https://github.com/studentsdav/RetailSale

I’d appreciate hearing about approaches that have worked well for you, especially any lessons learned when maintaining a Flutter application with multiple connected business workflows.

Thanks!

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.