flutter_modular_widget
A tiny base layer for Flutter screens that depend on multiple Modular repositories.
It wires your UI to repositories from mcquenji_core
/ flutter_modular
, handles loading/error states for you, and gives you a clean place to derive view data before building.
✨ What you get
- Declarative dependencies — declare which repositories your screen needs by watching them inside
query
. - Built-in loading & error UI — if any watched repo is loading you get
buildLoader()
, if any errored you getbuildError(error)
. - Derived view model — compute and pass a typed
T
into your content widget once all repos are healthy. - Non-reactive access in build — use
get<MyRepo>()
insidebuildContent
for event handlers, helpers, etc. - Optional local state — mix in
StatefulContent
to keep widget-local state in a familiarState
subclass (ModularState<T>
).
📦 Install
dependencies:
flutter_modular_widget:
git:
url: https://github.com/necodeIT/flutter_modular_widget
flutter_modular: ^6.3.4
mcquenji_core:
git:
url: https://github.com/mcquenji/mcquenji_core.git
The package expects repositories from
mcquenji_core
(which expose anAsyncValue
state) to be registered in your Modular graph.
🧠 Core concepts
- Repository: a piece of business logic (from
mcquenji_core
) whosestate
is anAsyncValue
(loading / data / error
). - watch
() : declare a reactive dependency on a repo inquery
. The screen will rebuild when that repo’s state changes. - get
() : fetch a repo non-reactively (use inbuildContent
for callbacks, methods, etc.). - query() -> FutureOr
: derive your view data T
from watched repositories. Runs once all watched repos are ready; re-runs when any of them changes. It is safe to throw here; the error will be caught and passed tobuildError
. - buildLoader / buildError / buildContent: the three render phases that
ModularWidget<T>
manages for you. - StatefulContent + ModularState: opt-in mixin to keep local widget state while still receiving the derived
T
and theget
accessor.
🚀 Quick start
import 'package:flutter/widgets.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:mcquenji_core/mcquenji_core.dart';
import 'package:flutter_modular_widget/modular_widget.dart';
/// Example repositories
class UserRepo extends Repository<AsyncValue<User>> { /* ... */ }
class SettingsRepo extends Repository<AsyncValue<Settings>> { /* ... */ }
/// The screen’s derived view data
class MyViewData {
final User user;
final Settings settings;
const MyViewData(this.user, this.settings);
}
class MyScreen extends ModularWidget<MyViewData> with StatefulContent<MyViewData> {
// 1) Show while any watched repo is loading
@override
Widget buildLoader(BuildContext context) => const Center(child: Text('Loading…'));
// 2) Show when any watched repo errors
@override
Widget buildError(BuildContext context, Object error) =>
Center(child: Text('Oops: $error'));
// 3) Derive view data once repos are healthy (declare deps here!)
@override
FutureOr<MyViewData> query<R extends Repository>(R Function<R extends Repository>() watch) async {
final userRepo = watch<UserRepo>(); // reactive
final settingsRepo = watch<SettingsRepo>(); // reactive
final user = (await userRepo.state.requireData);
final settings = (await settingsRepo.state.requireData);
return MyViewData(user, settings);
}
// 4) Build the actual UI
@override
Widget buildContent(
BuildContext context,
MyViewData data,
R Function<R extends Repository>() get, // non-reactive accessor
) {
final userRepo = get<UserRepo>(); // for actions/callbacks, not reactive
return Column(
children: [
Text('Hello, ${data.user.name}'),
ElevatedButton(
onPressed: () => userRepo.refresh(), // example side-effect
child: const Text('Refresh'),
),
],
);
}
// 5) (Optional) Keep local widget state
@override
ModularState<MyViewData> createContentState() => _MyScreenState();
}
class _MyScreenState extends ModularState<MyViewData> {
// Local UI state (text controllers, animations, focus nodes, etc.)
@override
Widget build(BuildContext context) {
final data = widget.data; // derived MyViewData
final settings = widget.get<SettingsRepo>(); // non-reactive access
// Compose additional UI here if you prefer keeping it in a State subclass
return Container(); // or return null and keep all UI in buildContent above
}
}
🔄 Lifecycle & reactivity
-
On first build,
ModularWidget
subscribes to every repository you watch inquery
. -
If any watched repo is
loading
, you getbuildLoader
. -
If any watched repo is in
error
, you getbuildError(error)
(first error wins). -
Once all watched repos are ready,
query
runs to produceT
, thenbuildContent
renders with thatT
. -
On later changes, if any watched repo’s state changes,
query
runs again and the widget rebuilds accordingly. -
Subscriptions are disposed automatically.
-
Declare all dependencies in
query
usingwatch
. Don’t callget
inquery
(that would be non-reactive). -
Keep
buildContent
pure. Useget
only for callbacks, methods, or one-off, non-reactive reads. -
Short-circuit heavy work: if you need expensive mapping, do it in
query
and pass the result down. -
Local UI state? Use
with StatefulContent
and aModularState<T>
to keep animations, controllers, etc. -
Error/Loading precedence: error beats loading; otherwise loading beats content.
abstract class ModularWidget<T> extends StatefulWidget {
const ModularWidget({super.key});
// Render phases
Widget buildLoader(BuildContext context);
Widget buildError(BuildContext context, Object error);
Widget buildContent(
BuildContext context,
T data,
R Function<R extends Repository>() get,
);
// Declare reactive dependencies & derive view data
FutureOr<T> query(R Function<R extends Repository>() watch);
}
Optional local state:
mixin StatefulContent<T> on ModularWidget<T> {
ModularState<T> createContentState(); // provide your State subclass
}
typedef ModularState<T> = State<StatefulModularWidget<T>>;
🙋 FAQ
Q: How are errors “resolved”?
When a previously errored repository transitions to a non-error state, the screen will leave buildError
automatically and re-evaluate query
. You don’t need to track which repo failed; subscriptions handle that.
Q: Can I mix watch
and get
?
Yes. Use watch
inside query
(reactive). Use get
inside buildContent
or your ModularState
for callbacks and helpers (non-reactive).
Q: Do I need StatefulContent
?
Only if you want local UI state (animations, controllers, etc.). Otherwise, keep everything in buildContent
.