Welcome to my latest project! I'm excited to introduce Trent
β a state management library I built from the ground up while tackling the challenges of large-scale app development with Flutter. If you're new to Flutter, it's a powerful UI toolkit for building natively compiled applications across mobile, web, and desktop. As I was working on ambitious projects like Dormside, I found that existing state management solutions were often too verbose or complex for real-world needs.
Project links:
My experience building applications like Dormside and Confesi highlighted key needs that Trent
directly addresses:
Trent
is designed to minimize the code you need to write to manage even intricate states. You define your core logic in lean classes, and Trent
handles the reactivity and plumbing.Trent
's alert()
method became essential for managing these without complicating the primary state flow in Dormside.Trent
's core API is minimal: emit()
, set()
, alert()
, and supporting utilities like watch<T>()
, watchMap<T, S>()
, Digester
, and Alerter
provide all you need without overwhelming you.Trent
is built to handle both β whether you have a single configuration object or a multi-faceted state with loading, data, and error variations.Trent
's adaptability is best seen by comparing how it handles both simple and more complex state scenarios. Let's look at two contrasting examples:
For basic UI elements, you often need to manage simple configurations. Imagine managing a layout with a navigation index. Here's how Trent
keeps it straightforward:
import 'package:trent/trent.dart';
// Define a simple state class
class LayoutState extends EquatableCopyable<LayoutState> {
final int homeNavIdx;
LayoutState({required this.homeNavIdx});
@override
LayoutState copyWith({int? homeNavIdx}) {
return LayoutState(homeNavIdx: homeNavIdx ?? this.homeNavIdx);
}
@override
List<Object> get props => [homeNavIdx];
}
// Create your Trent class
class LayoutTrent extends Trent<LayoutState> {
LayoutTrent() : super(LayoutState(homeNavIdx: 0));
void updateHomeNavIdx(int idx) {
emit(state.copyWith(homeNavIdx: idx));
}
}
Here, LayoutTrent
extends Trent<LayoutState>
. Updating the homeNavIdx
is as simple as calling emit()
with a new state. Trent
handles the UI updates automatically β clean and direct.
Now consider a more complex scenario: managing weather data which can be in various states β No Data, Sunny, or Rainy. This is where Trent
's ability to handle multi-class states shines. Such scenarios are common in real-world applications and Trent
's way of managing this is reminiscent of a "state machine"-like approach, keeping your code organized and maintainable.
import 'package:trent/trent.dart';
// Define your state types using a base abstract class
abstract class WeatherState extends EquatableCopyable<WeatherState> {
@override
List<Object?> get props => [];
}
class NoDataState extends WeatherState {
@override
NoDataState copyWith() => this;
}
class SunnyState extends WeatherState {
final double temperature;
SunnyState(this.temperature);
@override
List<Object?> get props => [temperature];
@override
SunnyState copyWith({double? temperature}) {
return SunnyState(temperature ?? this.temperature);
}
}
class RainyState extends WeatherState {
final double rainfall;
RainyState(this.rainfall);
@override
List<Object?> get props => [rainfall];
@override
RainyState copyWith({double? rainfall}) {
return RainyState(rainfall ?? this.rainfall);
}
}
// Create your Trent class for the complex state
class WeatherTrent extends Trent<WeatherState> {
WeatherTrent() : super(NoDataState()); // Initial state is NoData
void updateToSunny(double temperature) {
emit(SunnyState(temperature));
}
void updateToRainy(double rainfall) {
emit(RainyState(rainfall));
}
void resetWeather() {
reset(); // Resets back to NoDataState
}
}
In this multi-class example, WeatherTrent
manages a WeatherState
which can be NoDataState
, SunnyState
, or RainyState
. Despite the increased complexity of state variations, the Trent
class remains clean. The UI logic, as we'll see, also benefits from Trent
's Digester
and watchMap
components, which are designed to handle these multi-state scenarios elegantly.
Direct Comparison:
Single State (Layout example): Ideal for simple UI configurations or isolated data points. Trent
's straightforward Trent<LayoutState>
setup is perfect for these cases, minimizing code and maximizing clarity.
Multi-Class State (Weather example): Necessary for scenarios with distinct state variations like loading states, error states, or different data representations. Trent
, using Trent<WeatherState>
with an abstract base state and concrete subclasses, handles this complexity without becoming convoluted. The Digester
and watchMap
widgets then make consuming these complex states in your UI just as easy as consuming simple states.
Trent
's architecture is specifically designed to smoothly handle both ends of this spectrum, making it a versatile choice regardless of your state complexity.
Trent
's internal structure is what allows it to be so adaptable and efficient, especially in larger applications:
The Trent<S extends EquatableCopyable<S>>
Class: This abstract class is the foundation. It uses BehaviorSubject
for managing both the main state stream (stateStream
) and a separate stream for "alert" states (alertStream
). It also intelligently caches previous states, useful for state history management, and extends ChangeNotifier
to efficiently notify Flutter widgets about state changes. Here, S
represents the type of your Trent
's State.
Core Methods: emit(newState)
, set(newState)
, and alert(alertState)
:
emit(newState)
: Updates the current state and triggers necessary UI rebuilds, ensuring your UI is always in sync with your state.set(newState)
: Updates the state without triggering UI rebuilds. This is valuable for state changes that don't directly impact the visible UI, optimizing performance.alert(alertState)
: Sends an ephemeral state for alerts, notifications, or transient UI elements like toast messages. These alerts are managed separately from your main application state, preventing interference.Widget Integration: watch<T extends Trent<S>>(context)
and get<T extends Trent<S>>(context)
:
get<T extends Trent<S>>(context)
: Provides non-reactive access to a Trent
instance. Use this when you need to call methods on your Trent
but don't need the widget to rebuild when the state changes (e.g., button presses triggering state updates). Here, T
is your specific Trent
class and S
is its State type.watch<T extends Trent<S>>(context)
: Provides reactive access. Widgets using watch
will automatically rebuild whenever the Trent
's state changes, ensuring your UI dynamically reflects the latest state. Again, T
is your Trent
class and S
is its State type.Utility and State Management Methods: Trent
provides a rich set of utility methods for advanced state manipulation:
getExStateAs<DesiredStateType>()
: Retrieves the last known state that was of DesiredStateType
. Useful for scenarios like navigating back to a previous state.getCurrStateAs<DesiredStateType>()
: Checks if the current state is of DesiredStateType
and returns it if it is.stateMap
: A powerful method for mapping the current state to different values or actions based on its type, using .as<StateType>(...)
for type-specific handling and .orElse(...)
for a default case.clearEx<StateType>()
, clearAllExes()
): For managing state history and memory, allowing you to clear specific past states or all of them.reset()
: Returns the Trent
's state to its initial value, effectively resetting a part of your application state.dispose()
: Important for resource management; releases resources and closes the underlying streams when your Trent
is no longer needed.stateStream
and alertStream
: For advanced use cases, these provide direct access to the underlying BehaviorSubject
streams, allowing for custom stream handling if needed.Under the hood, Trent
smartly utilizes the Provider
package for efficient dependency injection, making Trent
instances readily accessible throughout your widget tree.
Conditional UI Building with watchMap<T extends Trent<S>, S>(context, ...)
: Especially valuable for complex states, watchMap
lets you define different UI outputs based on different state subtypes (like Data, Loading, Error states in the Weather example). It eliminates nested if-else chains in your UI, leading to cleaner and more maintainable widget code. For example:
watchMap<WeatherTrent, WeatherState>(
context,
(mapper) {
mapper
..as<SunnyState>((s) => Text("It's Sunny: ${s.temperature}Β°C"))
..as<RainyState>((r) => Text("It's Rainy: ${r.rainfall}mm"))
..orElse((_) => const Text("No Weather Data Available"));
},
);
Trent
provides widget building blocks specifically designed to make reactive UI development easier:
Digester<T extends Trent<S>, S>
: The Digester
widget is your primary tool for building UIs that react to main state changes. It listens to the stateStream
of your Trent
and rebuilds its UI based on the current state. You use a WidgetSubtypeMapper
within Digester
to define how different state subtypes should be rendered as widgets.
Digester<WeatherTrent, WeatherState>(
child: (mapper) {
mapper
..as<SunnyState>((s) => Text("It's Sunny: ${s.temperature}Β°C"))
..as<RainyState>((r) => Text("It's Rainy: ${r.rainfall}mm"))
..orElse((_) => const Text("No Weather Data Available"));
},
);
Alerter<T extends Trent<S>, S>
: The Alerter
widget is dedicated to handling ephemeral "alert" states. It listens to the alertStream
of your Trent
. When you call alert(...)
, Alerter
can display transient UI elements like SnackBars
, toasts, or pop-up dialogs, based on the alert state, without interfering with the main UI driven by the main state. But, Alerter
is more versatile than just reacting to alerts! It also provides fine-grained control over responding to both alert state changes and even main state changes, if needed, through its fields: listenAlertsIf
and listenStates
. Let's explore these with an example:
class WeatherScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Alerter<WeatherTrent, WeatherState>(
listenAlerts: (mapper) {
mapper
..as<WeatherAlertState>((alert) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Alert: ${alert.message}")),
);
});
},
listenAlertsIf: (oldState, newState) => newState is WeatherAlertState,
listenStates: (mapper) {
mapper
..as<SunnyState>((state) => print("Weather is Sunny: ${state.temperature}Β°C"))
..as<RainyState>((state) => print("Weather is Rainy: ${state.rainfall}mm"))
..orElse((_) => print("Weather state: No Data"));
},
child: Digester<WeatherTrent, WeatherState>(
child: (mapper) {
mapper
..as<SunnyState>((state) => Text("Sunny: ${state.temperature}Β°C"))
..as<RainyState>((state) => Text("Rainy: ${state.rainfall}mm"))
..orElse((_) => const Text("No Data"));
},
),
);
}
}
listenAlerts
: This field is the primary way Alerter
reacts to alert states. Just like in our previous examples, you use a WidgetSubtypeMapper
here to define how to handle different alert state types. In the example, we show a SnackBar
when a WeatherAlertState
is emitted via alert(...)
.
listenAlertsIf
: (oldState, newState) => ...
: This optional function provides conditional listening to alert states. The function takes the oldState
and newState
as arguments and should return a boolean. Alerter
will only execute the listenAlerts
mapper if listenAlertsIf
returns true. In our example, listenAlertsIf: (oldState, newState) => newState is WeatherAlertState
ensures that the SnackBar
is only shown when the new alert state is indeed a WeatherAlertState
. This level of control is crucial for fine-tuning when alert reactions should occur, preventing unnecessary UI updates or side effects.
listenStates:
While Alerter
is primarily designed for alerts, listenStates
allows it to also react to changes in the main application state if you need to perform actions in response to main state changes within the scope of an Alerter
. This is less common but can be useful for specific scenarios. In our example, we are simply printing messages to the console when the weather state changes to Sunny, Rainy, or No Data. Importantly, the UI building with Digester
remains the primary way to visually represent the main WeatherState
; listenStates
in Alerter
provides a mechanism for side-effects or secondary reactions to these main state changes if needed.
Both Digester
and Alerter
offer optional conditional functions (listenStatesIf
, listenAlertsIf
) for advanced scenarios where you need even finer-grained control over when they react to state or alert changes.
Register Your Trent Instances: In your main.dart
file, use TrentManager
at the top of your widget tree to register all your Trent
instances. TrentManager
leverages MultiProvider
for efficient dependency injection.
void main() {
runApp(
TrentManager(
trents: [
register(LayoutTrent()),
register(WeatherTrent()),
// ... register other Trents
],
child: const MyApp(),
),
);
}
Accessing Trent Instances in Your UI: Within your widgets, use watch<T extends Trent<S>>(context)
for reactive access to your Trent
instances:
final weatherTrent = watch<WeatherTrent>(context);
Interacting with Your Trent: Call methods on the Trent
instance to manipulate state. Because you're using watch
, UI updates are automatic when you emit a new state:
ElevatedButton(
onPressed: () {
weatherTrent.updateToSunny(28.0); // UI will automatically rebuild
},
child: const Text('Make it Sunny'),
);
Building Reactive UIs with Digester and watchMap: Use Digester
for declarative UI building based on state, and watchMap
for conditionally rendering different widgets based on different state subtypes, as shown in the Weather example earlier.
Key Advantages of Trent:
watch<T>()
, watchMap<T, S>()
, and get<T>()
. Mapped outputs (.as<SomeType>(...)
....orElse(...)
) simplify complex state-based UI logic. Ephemeral states via alert(...)
provide a clean way to handle transient UI elements. Access to state history with getExStateAs<T>()
adds powerful state management capabilities.Equatable
for reliable state equality checks and Option.Some(...)/Option.None()
for safe handling of optional states. Encourages clear separation of UI and business logic.Core UI Layer Components:
Alerter<T extends Trent<S>, S>
: Widget for handling alert and optionally main states, ideal for transient messages and for fine-grained control over reaction to state changes via listenAlertsIf
and listenStates
. You can also listen to ephemeral alert states with listenAlerts
and listenAlertsIf
.Digester<T extends Trent<S>, S>
: Widget for dynamically building UI based on current state subtypes, ensuring type-safe state rendering.watch<T extends Trent<S>>(BuildContext context)
: Reactively retrieves a Trent
instance in your widgets, causing rebuilds upon state changes.get<T extends Trent<S>>(BuildContext context)
: Non-reactive Trent
instance retrieval, for actions not requiring UI updates.watchMap<T extends Trent<S>, S>(BuildContext context, void Function(WidgetSubtypeMapper<S>) configure)
: Maps state subtypes to specific widgets for conditional UI rendering.Business Logic Layer Functions (within Trent<S>
class):
emit(state)
: Updates the state and triggers UI rebuilds for reactive updates.set(state)
: Updates the state without triggering UI rebuilds, for non-visual state changes.alert(state)
: Sends an ephemeral state, used for alerts and transient notifications.getExStateAs<StateType>()
: Retrieves the last known state of type StateType
.getCurrStateAs<StateType>()
: Retrieves the current state if it is of type StateType
.stateMap
: Provides a way to map the current state to different outputs or actions using .as<StateType>(...)
and .orElse(...)
.clearEx<StateType>()
: Clears the cached "last known state" of type StateType
from memory.clearAllExes()
: Clears all cached "last known states" from memory.reset()
: Resets the Trent
's state back to its initial state.dispose()
: Releases resources and closes the underlying streams when the Trent
is no longer needed.stateStream
: Provides direct access to the main state BehaviorSubject
stream.alertStream
: Provides direct access to the alert state BehaviorSubject
stream.Define Your State Classes:
Start by defining your state classes, inheriting from EquatableCopyable
for efficient state management and equality comparisons.
// Example Weather State Classes (as defined earlier)
abstract class WeatherState extends EquatableCopyable<WeatherState> { ... }
class NoDataState extends WeatherState { ... }
class SunnyState extends WeatherState { ... }
class RainyState extends WeatherState { ... }
Create Your Trent Classes:
Extend Trent<YourStateType>
for each distinct piece of state you need to manage. Implement methods to update the state using emit()
, set()
, and alert()
.
// Example WeatherTrent Class (as defined earlier)
class WeatherTrent extends Trent<WeatherState> { ... }
// Example LayoutTrent Class (as defined earlier)
class LayoutTrent extends Trent<LayoutState> { ... }
Organize Your Files (Optional):
A good practice is to create a trents
directory in your lib
folder and place each Trent
class in its own file (e.g., weather_trent.dart
, layout_trent.dart
).
Initialize TrentManager in main.dart:
Register all your Trent
instances within the TrentManager
widget at the root of your runApp
hierarchy.
void main() {
runApp(
TrentManager(
trents: [
register(WeatherTrent()),
register(LayoutTrent()),
],
child: const MyApp(),
),
);
}
Connect UI to Business Logic:
In your widgets, access your Trent
instances using watch<T>()
or get<T>()
. Call methods on the Trent
to update the state in response to user interactions or application events.
class WeatherScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () => get<WeatherTrent>(context).updateToSunny(25.0),
child: const Text("Make it Sunny"),
),
// ... more buttons and UI elements interacting with WeatherTrent and LayoutTrent
],
);
}
}
Build Reactive UIs with Alerter, Digester, and watchMap:
Use Trent
's specialized widgets within your widget tree to build reactive UI components that automatically update based on state changes. Refer to the examples provided earlier for Alerter
, Digester
, watch
, and watchMap
usage, and especially the Alerter
example showcasing listenAlertsIf
and listenStates
for advanced control.
Trent
is more than just another state management library; it's a response to the complexities of building real-world, large-scale Flutter applications like Dormside. It provides a reactive, efficient, and exceptionally intuitive approach to managing state, whether you're dealing with simple UI configurations or orchestrating complex data flows across your application.
Trent
simplifies development with its focus on ease of use, fine-grained control, and performance. If you're building ambitious Flutter projects and are looking for a state management solution that scales with your application's complexity without adding unnecessary boilerplate, Trent
is designed for you.
Explore the Trent
repository on GitHub and the package on Pub.dev. I'm happy to accept feedback, suggestions, and contributions!