aero_mvvm 0.0.1
aero_mvvm: ^0.0.1 copied to clipboard
A zero-dependency, highly performant MVVM state management framework for Flutter. Provides context-less routing, async state handling, and granular reactive UI rebuilds.
// ignore_for_file: depend_on_referenced_packages
import 'package:flutter/material.dart';
import 'package:aero_mvvm/aero_mvvm.dart';
// =============================================================================
// 1. GLOBAL STATE (Simulated App-Wide State)
// =============================================================================
class CartViewModel extends ViewModel {
int itemCount = 0;
double totalPrice = 0.0;
void addToCart(double price) {
itemCount++;
totalPrice += price;
rebuild();
}
}
final globalCart = CartViewModel();
// =============================================================================
// 2. LOCAL ASYNC STATE (Screen-Specific Logic)
// =============================================================================
class ProductViewModel extends AsyncViewModel {
String selectedColor = "Matte Black";
double productPrice = 299.99;
String productTitle = "AeroSonic Pro ANC";
final List<String> availableColors = [
"Matte Black",
"Lunar Silver",
"Navy Blue",
];
void changeColor(String color) {
selectedColor = color;
rebuild();
}
void fetchProductDetails() {
runAsync(() async {
await Future.delayed(const Duration(seconds: 2));
// Data is now "loaded"
});
}
}
// =============================================================================
// 3. THE APPLICATION
// =============================================================================
void main() {
runApp(const AeroStoreApp());
}
class AeroStoreApp extends StatelessWidget {
const AeroStoreApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Aero MVVM Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.black,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ProductScreen(),
);
}
}
// =============================================================================
// 4. THE UI (Pure layout, zero business logic)
// =============================================================================
class ProductScreen extends StatelessWidget {
const ProductScreen({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ProductViewModel>(
viewModelBuilder: () => ProductViewModel(),
onModelReady: (viewModel) => viewModel.fetchProductDetails(),
builder: (context, productViewModel) {
return Scaffold(
backgroundColor: const Color(0xFF121212),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text(
'Aero Store',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
// ---------------------------------------------------------------
// DEMO: ViewModelBuilder listening to GLOBAL state
// ---------------------------------------------------------------
ViewModelBuilder<CartViewModel>(
viewModelBuilder: () => globalCart,
builder: (context, cart) {
return Padding(
padding: const EdgeInsets.only(right: 20.0),
child: Center(
child: Badge(
isLabelVisible: cart.itemCount > 0,
label: Text('${cart.itemCount}'),
child: const Icon(Icons.shopping_bag_outlined),
),
),
);
},
),
],
),
// -------------------------------------------------------------------
// DEMO: AsyncViewModel handling loading states automatically
// -------------------------------------------------------------------
body: productViewModel.isBusy
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
"Fetching premium products...",
style: TextStyle(color: Colors.grey),
),
],
),
)
: _buildProductDetails(productViewModel),
// -------------------------------------------------------------------
// DEMO: MultiViewModelBuilder merging global AND local state
// -------------------------------------------------------------------
bottomNavigationBar: MultiViewModelBuilder(
viewModels: [globalCart, productViewModel],
builder: (context) {
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(30),
),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20)],
),
child: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Total Cart",
style: TextStyle(color: Colors.grey),
),
Text(
"\$${globalCart.totalPrice.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// Add the item to the global cart when clicked!
onPressed: () =>
globalCart.addToCart(productViewModel.productPrice),
child: Text("Add ${productViewModel.selectedColor}"),
),
],
),
),
);
},
),
);
},
);
}
// A helper method to keep the main build method clean
Widget _buildProductDetails(ProductViewModel viewModel) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Simulated Product Image Area
Container(
height: 300,
width: double.infinity,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.headphones,
size: 120,
color: Colors.white24,
),
),
const SizedBox(height: 32),
Text(
viewModel.productTitle,
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
"\$${viewModel.productPrice}",
style: const TextStyle(fontSize: 24, color: Colors.grey),
),
const SizedBox(height: 32),
const Text(
"Select Color",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
// Color Selector Chips
Wrap(
spacing: 12,
children: viewModel.availableColors.map((color) {
final isSelected = viewModel.selectedColor == color;
return ChoiceChip(
label: Text(color),
selected: isSelected,
onSelected: (_) => viewModel.changeColor(color),
selectedColor: Colors.white,
labelStyle: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
backgroundColor: const Color(0xFF2A2A2A),
);
}).toList(),
),
],
),
);
}
}