Skip to content

Instantly share code, notes, and snippets.

@HenriqueNas
Last active July 4, 2024 16:53
Show Gist options
  • Save HenriqueNas/dedb6e6523f1ba03acedf780d2c89ef9 to your computer and use it in GitHub Desktop.
Save HenriqueNas/dedb6e6523f1ba03acedf780d2c89ef9 to your computer and use it in GitHub Desktop.
Easiest way to manager your Flutter page/widget state without any package.
import 'package:flutter/material.dart';
/// Function that receives a [BuildContext] and a [PageState] and returns a [Widget]. </br>
typedef WidgetBuilder = Widget Function(BuildContext context, PageState state);
/// Helper class to manage the state of a page/widget. </br>
class PageStateNotifier<Error> extends ChangeNotifier {
PageState _state = PageState.idle;
/// The current [PageState].
/// This is the main state of the notifier
/// and should be used to control the UI.
PageState get state => _state;
/// Set the current [PageState] and notify the listeners.
set state(final PageState state) {
_state = state;
notifyListeners();
}
Error? _error;
/// If the current state is [PageState.error], this will return the error.
Error? get error => _error;
/// Set the error and change the state to [PageState.error].
void setError([final Error? error]) {
_error = error;
_state = PageState.error;
notifyListeners();
}
/// Returns if the current state is [PageState.idle].
bool get isIdle => _state.isIdle;
/// Returns if the current state is [PageState.error].
bool get isError => _state.isError;
/// Returns if the current state is [PageState.loading].
bool get isLoading => _state.isLoading;
/// Returns if the current state is [PageState.success].
bool get isSuccess => _state.isSuccess;
/// Set the state to [PageState.loading] and notify the listeners.
void setLoading() {
_state = PageState.loading;
notifyListeners();
}
/// Set the state to [PageState.success] and notify the listeners.
void setSuccess() {
_state = PageState.success;
notifyListeners();
}
/// Set the state to [PageState.idle] and notify the listeners.
void setIdle() {
_state = PageState.idle;
notifyListeners();
}
/// Add a listener to the notifier and execute the
/// callback when the state changes to [PageState.error].
void onError(final void Function(Error? error) callback) {
addListener(() {
if (isError) callback(error);
});
}
/// Add a listener to the notifier and execute the
/// callback when the state changes to [PageState.success].
void onSuccess(final VoidCallback callback) {
addListener(() {
if (isSuccess) callback();
});
}
/// Add a listener to the notifier and execute the
/// callback when the state changes to [PageState.loading].
void onLoading(final VoidCallback callback) {
addListener(() {
if (isLoading) callback();
});
}
/// Add a listener to the notifier and execute the
/// callback when the state changes to [PageState.idle].
void onIdle(final VoidCallback callback) {
addListener(() {
if (isIdle) callback();
});
}
/// Returns a [ListenableBuilder] that listens to any change.
Widget builder(final WidgetBuilder builder) => ListenableBuilder(
listenable: this,
builder: (final context, final _) => builder(context, state),
);
/// Returns a [ListenableBuilder] that listens to any change.
Widget stateBuilder({
final Widget Function() idle = SizedBox.shrink,
final Widget Function()? loading,
final Widget Function()? success,
final Widget Function(Error? error)? error,
final PageState fallbackState = PageState.idle,
}) {
assert(
fallbackState == PageState.loading && loading != null,
'Your fallbackState is loading, but you did not provide a loading widget',
);
assert(
fallbackState == PageState.error && error != null,
'Your fallbackState is error, but you did not provide an error widget',
);
assert(
fallbackState == PageState.success && success != null,
'Your fallbackState is success, but you did not provide a success widget',
);
final fallbackWidget = switch (fallbackState) {
PageState.idle => idle(),
PageState.loading => loading!.call(),
PageState.error => error!.call(_error),
PageState.success => success!.call(),
};
final widget = switch (_state) {
PageState.idle => idle(),
PageState.loading => loading?.call(),
PageState.error => error?.call(_error),
PageState.success => success?.call(),
};
return ListenableBuilder(
listenable: this,
builder: (final _, final ___) => widget ?? fallbackWidget,
);
}
}
enum PageState {
idle,
loading,
error,
success;
bool get isIdle => this == idle;
bool get isLoading => this == loading;
bool get isSuccess => this == success;
bool get isError => this == error;
}
@HenriqueNas
Copy link
Author

HenriqueNas commented Jul 4, 2024

how to use it:

Usually you will create a "controller" (or whatever you want to call it) that extends the PageStateNotifier and you can bind to the page that you want to control!

Example:

class HomeController extends PageStateNotifier<String> {
  // you can use a `ValueNotifier<String?>` to make this reactable too !!
  String? userName;

  Future<void> fetch() async {
    try {
      setLoading();

      final result = await Future.delayed(const Duration(seconds: 2), () {
        final isSuccess = Random().nextBool();

        if (isSuccess) {
          return 'Success';
        } else {
          throw 'Error';
        }
      });

      userName = result;

      setSuccess();
    } catch (error) {
      setError(error.toString());
    }
  }
}

@HenriqueNas
Copy link
Author

your page:

your page can build widgets with the builder and stateBuilder methods of your HomeController:

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final controller = HomeController();

  @override
  void initState() {
    super.initState();

    // when the state is set to Error, call this:
    controller.onError((final error) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(error ?? 'Unknown error')),
      );
    });
  }

  @override
  Widget build(final BuildContext context) {
    return controller.stateBuilder(
      idle: () => Center(
        child: ElevatedButton(
          onPressed: controller.fetch,
          child: const Text('Fetch'),
        ),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      success: () => Center(child: Text(controller.userName ?? 'No user name')),
    );
  }
}

@lucasdealmeida91
Copy link

otima abordagem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment