chassis_flutter 0.0.1+2
chassis_flutter: ^0.0.1+2 copied to clipboard
An architectural framework for flutter applications
chassis_flutter 🏎️ #
Rigid in Structure, Flexible in Implementation.
This package provides Flutter widgets and helpers to integrate the core chassis
architecture. It connects your business logic to the UI using the provider
package, giving you the necessary tools to create a clean, reactive, and highly testable presentation layer following the MVVM pattern.
Learn more from the full documentation.
Core Components #
chassis_flutter
provides a few key components to bridge the gap between your domain logic (chassis
) and your user interface (Flutter).
ViewModel
: The bridge between your UI and your domain. It holds UI state, processes user input by sending messages to theMediator
, and exposes results for the View to display.ViewModelProvider
: A simple widget, built on top ofprovider
, for injecting yourViewModel
into the widget tree and making it accessible to your screens.ConsumerMixin
: A mixin forStatefulWidget
s to easily listen for one-time events (like showing a dialog or navigating) from theViewModel
without triggering a rebuild.
Getting Started #
This guide demonstrates how to build a simple feature that fetches a greeting.
1. Define UI State & Events #
First, create immutable classes for your UI's State (the data to render) and Events (one-time side effects like showing a snackbar).
💡 Why Events? Unlike state, events don't represent what the UI is, but rather what it should do. They are sent from the ViewModel
to the View
to trigger actions like navigation or alerts without cluttering the UI state.
// lib/features/greeting/greeting_view_model.dart
class GreetingState {
const GreetingState({this.isLoading = false, this.message = ''});
final bool isLoading;
final String message;
// A copyWith method is recommended for immutability
GreetingState copyWith({bool? isLoading, String? message}) {
return GreetingState(
isLoading: isLoading ?? this.isLoading,
message: message ?? this.message,
);
}
}
sealed class GreetingEvent {}
class ShowGreetingSuccess implements GreetingEvent {
const ShowGreetingSuccess(this.message);
final String message;
}
2. Create the ViewModel #
The ViewModel
connects to the Mediator
to fetch data and manages the GreetingState
. It exposes methods for the UI to call, like fetchGreeting()
.
// lib/features/greeting/greeting_view_model.dart
class GreetingViewModel extends ViewModel<GreetingState, GreetingEvent> {
GreetingViewModel(Mediator mediator) : super(mediator, const GreetingState());
Future<void> fetchGreeting() async {
setState(state.copyWith(isLoading: true));
final result = await read(const GetGreetingQuery()); // From 'chassis' core
result.when(
success: (greeting) {
setState(state.copyWith(isLoading: false, message: greeting));
sendEvent(ShowGreetingSuccess('Greeting loaded successfully!'));
},
failure: (error) {
setState(state.copyWith(isLoading: false, error: error.toString()));
},
);
}
}
3. Provide the ViewModel #
Use ViewModelProvider
(usually above your MaterialApp
or at the screen level) to make the ViewModel
available to the widget tree.
// lib/app.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// This makes the GreetingViewModel available to GreetingScreen and its children.
return ViewModelProvider(
create: (_) => GreetingViewModel(mediator), // Assumes 'mediator' is accessible
child: const MaterialApp(home: GreetingScreen()),
);
}
}
4. Consume State & Events in the View #
Finally, connect your UI to the ViewModel
.
- Use
context.watch<T>()
in thebuild
method to listen for state changes and rebuild the UI. - Use
context.read<T>()
in callbacks (likeonPressed
) to call methods on theViewModel
without rebuilding. - Use the
ConsumerMixin
to handle one-time events.
// lib/features/greeting/greeting_screen.dart
class _GreetingScreenState extends State<GreetingScreen> with ConsumerMixin {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Listen for one-off events with ConsumerMixin
onEvent<GreetingViewModel, GreetingEvent>((event) {
if (event is ShowGreetingSuccess) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(event.message)));
}
});
}
@override
Widget build(BuildContext context) {
// `watch` gets the ViewModel and rebuilds the widget when the state changes.
final viewModel = context.watch<GreetingViewModel>();
final state = viewModel.state;
return Scaffold(
appBar: AppBar(title: const Text('Chassis Quickstart')),
body: Center(
child: state.isLoading
? const CircularProgressIndicator()
: Text(state.message, style: Theme.of(context).textTheme.headlineMedium),
),
floatingActionButton: FloatingActionButton(
// `read` calls a method without subscribing to state changes.
onPressed: () => context.read<GreetingViewModel>().fetchGreeting(),
child: const Icon(Icons.refresh),
),
);
}
}
The Full Picture #
The chassis
and chassis_flutter
packages work together to create a clean separation of concerns:
- View (
GreetingScreen
) callsfetchGreeting()
on theViewModel
. - ViewModel dispatches a
GetGreetingQuery
to theMediator
. - Mediator finds the corresponding
GetGreetingQueryHandler
in your corechassis
layer. - Handler executes the business logic and returns the result.
- ViewModel receives the result, updates its
GreetingState
, and the View automatically rebuilds to show the new message.
Next Steps #
For more advanced concepts, tutorials, and best practices, please see the full documentation.