mavix 0.1.0 copy "mavix: ^0.1.0" to clipboard
mavix: ^0.1.0 copied to clipboard

A lightweight Flutter state manager with separate streams for persistent state and one-time UI activities.

Mavix #

style: flutter_lints

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:

  • state describes what the UI should display
  • activity describes 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 bloc, provider, or rxdart
  • MavixBuilder for state-driven rebuilds
  • MavixSelector for selective rebuilds
  • MavixListener for 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.0

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 state
  • send(...) 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 == and hashCode
  • or use a helper such as equatable or freezed

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

Comparison #

Mavix is closer to Cubit than to full Bloc, but with first-class one-time activities built into the core model.

In practice:

  • choose Mavix when you want a small API and explicit UI side effects
  • choose Cubit when you want a familiar ecosystem and only need state
  • choose Bloc when you need an event-driven architecture with a larger ecosystem

License #

Mavix is available under the MIT License.

1
likes
0
points
115
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

A lightweight Flutter state manager with separate streams for persistent state and one-time UI activities.

Repository (GitHub)
View/report issues

Topics

#flutter #state-management #state-manager #reactive #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on mavix