A clean, lightweight Flutter application demonstrating Vanilla State Management patterns. This app serves as a practical example of how to manage complex application state (like a shopping cart) using only the built-in tools provided by the Flutter SDK, without relying on third-party libraries like Provider, Riverpod, or Bloc.
The Candy Store app allows users to browse a delicious selection of sweets, add them to a shopping cart, and manage their selections. The primary focus of this project is to illustrate the "Flutter Way" of handling state using:
ChangeNotifierfor business logic and state notifications.InheritedWidgetfor efficient dependency injection and data propagation.ListenableBuilderfor targeted UI rebuilds.
- Product Catalog: A scrollable list of various candies with descriptions, images, and prices.
- Dynamic Shopping Cart:
- Add items to the cart directly from the product list.
- Increment/decrement item quantities within the cart.
- Real-time updates of total item count and total price.
- Persistent State: The cart state is maintained across different screens (Products Page and Cart Page) using a centralized provider.
- Responsive UI: Built with Material Design 3 principles.
The app follows a "Vanilla" approach to state management:
- Model/Notifier (
CartNotifier): ExtendsChangeNotifier. It holds the cart items and encapsulates the logic for adding/removing products. Whenever the state changes, it callsnotifyListeners(). - Provider (
CartProvider): A customInheritedWidgetthat wraps theCartNotifier. This allows any widget in the tree to access the cart state viaCartProvider.of(context). - UI Updates: Widgets use
ListenableBuilderto listen to specific changes in theCartNotifier, ensuring that only the necessary parts of the UI (like the cart badge or the list items) are rebuilt.
- Flutter SDK installed on your machine.
- A code editor (Android Studio, VS Code, or IntelliJ).
- Clone the repository:
git clone https://github.com/your-username/candy-store-state.git cd candy-store-state - Get dependencies:
flutter pub get
- Run the app:
flutter run
Building this app using only vanilla Flutter tools was an enlightening experience. Here are the key lessons I learned from this perspective:
When I use libraries like Provider or Riverpod, a lot of the heavy lifting is hidden behind elegant APIs. By implementing InheritedWidget myself, I truly understood how Flutter's BuildContext works and how the framework propagates data down the widget tree. It removed the "black box" feeling and gave me a deeper appreciation for the framework's architecture.
I realized that for many apps, ChangeNotifier is more than enough. It's simple, intuitive, and extremely powerful when paired with ListenableBuilder. I learned that I don't always need complex state machines or streams; sometimes, a simple "hey, something changed" is the most efficient way to handle UI updates.
Without a high-level library managing everything, I had to be very intentional about where I placed my builders. I learned to use ListenableBuilder at the leaf nodes of my widget tree to prevent unnecessary rebuilds of large layouts. This "manual" optimization taught me to be more mindful of performance from the start.
I used to think of Dependency Injection (DI) as a separate architectural layer. Building the CartProvider taught me that in Flutter, DI is just another widget. The tree is the architecture. Being able to inject my CartNotifier at the root and access it anywhere via context proved that Flutter’s native capabilities are incredibly robust.
By avoiding external packages, the project has zero external dependencies (beyond standard Flutter). This results in smaller binary sizes, fewer versioning conflicts, and a codebase that is easier to maintain long-term as the framework evolves. I learned that "standard" doesn't mean "basic"—it means "reliable."