mavix 0.1.1
mavix: ^0.1.1 copied to clipboard
A lightweight Flutter state manager with separate streams for persistent state and one-time UI activities.
Mavix #
A lightweight Flutter state manager that keeps persistent UI state and one-time UI activities in separate streams.
Mavix is designed for a very common Flutter problem:
statedescribes what the UI should displayactivitydescribes what the UI should do once
That means navigation, snackbars, dialogs, clipboard actions, downloads, and similar one-shot effects do not need to live inside persistent state.
Why Mavix? #
Many state management solutions handle one-time effects indirectly:
- storing a "show error" flag in state
- resetting that flag after the widget reacts
- creating extra streams next to the main state object
This works, but it adds ceremony and can replay UI actions after rebuilds or a new subscription.
Mavix makes the distinction explicit:
State -> MavixBuilder / MavixSelector
Activity -> MavixListener
An activity is never stored and is never replayed to a new listener.
Features #
- Small and explicit API
- Separate streams for state and one-time activities
- No dependency on third-party state management packages or
rxdart MavixBuilderfor state-driven rebuildsMavixSelectorfor selective rebuildsMavixListenerfor one-time UI reactions- Built on standard Dart streams
- Automatic lifecycle management with
MavixProvider - Suitable for immutable state models
Installation #
After publishing to pub.dev:
dependencies:
mavix: ^0.1.1
Before publishing, you can use Git:
dependencies:
mavix:
git:
url: https://github.com/RfMakar/mavix.git
ref: main
Then run:
flutter pub get
Core idea #
update(nextState);
send(activity);
update(...)changes persistent statesend(...)emits a one-time activity
Basic structure #
A feature typically contains three files:
login/
├── login_activity.dart
├── login_mavix.dart
└── login_state.dart
1. Define State #
class LoginState {
const LoginState({
this.isLoading = false,
});
final bool isLoading;
LoginState copyWith({
bool? isLoading,
}) {
return LoginState(
isLoading: isLoading ?? this.isLoading,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is LoginState &&
runtimeType == other.runtimeType &&
isLoading == other.isLoading;
}
@override
int get hashCode => isLoading.hashCode;
}
2. Define Activity #
sealed class LoginActivity {
const LoginActivity();
}
final class LoginCompleted extends LoginActivity {
const LoginCompleted();
}
final class LoginFailed extends LoginActivity {
const LoginFailed(this.message);
final String message;
}
3. Create a Mavix #
import 'package:mavix/mavix.dart';
class LoginMavix extends Mavix<LoginState, LoginActivity> {
LoginMavix(this._authenticationRepository)
: super(const LoginState());
final AuthenticationRepository _authenticationRepository;
Future<void> login() async {
if (state.isLoading) {
return;
}
update(
state.copyWith(isLoading: true),
);
try {
await _authenticationRepository.login();
update(
state.copyWith(isLoading: false),
);
send(
const LoginCompleted(),
);
} catch (error, stackTrace) {
reportError(error, stackTrace);
update(
state.copyWith(isLoading: false),
);
send(
LoginFailed(error.toString()),
);
}
}
}
4. Provide It #
Use MavixProvider to create and expose an instance:
MavixProvider<LoginMavix>(
create: (_) => LoginMavix(authenticationRepository),
child: const LoginScreen(),
);
Instances created through create are closed automatically.
If you already have an instance:
MavixProvider<LoginMavix>.value(
value: loginMavix,
child: const LoginScreen(),
);
An instance passed through .value is not closed automatically.
5. Read It #
Read a Mavix without subscribing:
context.readMavix<LoginMavix>().login();
You can also use the shorter alias:
context.mavix<LoginMavix>().login();
6. Build UI From State #
Use MavixBuilder when the whole widget depends on state:
MavixBuilder<LoginMavix, LoginState>(
builder: (context, state) {
return LoginForm(
isLoading: state.isLoading,
);
},
);
Optional filtering:
MavixBuilder<LoginMavix, LoginState>(
buildWhen: (previous, current) {
return previous.isLoading != current.isLoading;
},
builder: (context, state) {
return LoadingIndicator(
visible: state.isLoading,
);
},
);
7. Rebuild Only What Changed #
Use MavixSelector when a widget depends on only part of the state:
MavixSelector<LoginMavix, LoginState, bool>(
selector: (state) => state.isLoading,
builder: (context, isLoading) {
return FilledButton(
onPressed: isLoading
? null
: context.readMavix<LoginMavix>().login,
child: Text(
isLoading ? 'Loading...' : 'Login',
),
);
},
);
Selected values are compared using ==.
8. React To One-Time Activities #
MavixListener handles activities and never rebuilds its child:
MavixListener<LoginMavix, LoginActivity>(
listener: (context, activity) {
switch (activity) {
case LoginCompleted():
Navigator.of(context).pushReplacementNamed('/home');
case LoginFailed(:final message):
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
},
child: const LoginView(),
);
Optional filtering:
MavixListener<LoginMavix, LoginActivity>(
listenWhen: (activity) => activity is LoginFailed,
listener: (context, activity) {
// Handle only failures.
},
child: const LoginView(),
);
Complete Example #
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return MavixProvider<LoginMavix>(
create: (_) => LoginMavix(authenticationRepository),
child: MavixListener<LoginMavix, LoginActivity>(
listener: (context, activity) {
switch (activity) {
case LoginCompleted():
Navigator.of(context).pushReplacementNamed('/home');
case LoginFailed(:final message):
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
},
child: const _LoginView(),
),
);
}
}
class _LoginView extends StatelessWidget {
const _LoginView();
@override
Widget build(BuildContext context) {
return MavixSelector<LoginMavix, LoginState, bool>(
selector: (state) => state.isLoading,
builder: (context, isLoading) {
return FilledButton(
onPressed: isLoading
? null
: context.readMavix<LoginMavix>().login,
child: Text(
isLoading ? 'Loading...' : 'Login',
),
);
},
);
}
}
Lifecycle #
Do not use a Mavix instance after close():
await mavix.close();
Calling update, send, or reportError after closing the instance throws a StateError.
State Equality #
Mavix does not publish a state when:
nextState == state
For best results:
- use immutable state objects
- implement
==andhashCode - or use a helper such as
equatableorfreezed
API #
Mavix<State, Activity>
MavixProvider<M>
MavixBuilder<M, State>
MavixSelector<M, State, Selected>
MavixListener<M, Activity>
MavixMultiProvider
MavixMultiListener
MavixRepositoryProvider<R>
MavixMultiRepositoryProvider
Core members:
state
states
activities
isClosed
update(state)
send(activity)
reportError(error, stackTrace)
close()
Example App #
The repository includes a small notes app in example/ that demonstrates:
- loading state
- selective rebuilds
- snackbars
- navigation through activities
- repository injection
License #
Mavix is available under the MIT License.