ExotikArch – Flutter setState on steriods ⚡

ExotikArch for Flutter. Deliver production apps fast with flutter.

  • Global State Management
  • Navigation Service
  • Helper Widgets, i.e. loaders and processors.
  • Seamless Exception Handling with zero code repeats
  • Support for Isolates to offload API calls.

*Note

ExotikArch exists in form of this app project at the moment. If this project gathers enough attention then I’ll make a package out of it and publish that over to pubdev.

Run it

flutter run

Modules

State Managment

ExotikArch uses the built-in setState function to cater for the global state managment. It is designed to work well with MVC pattern. We make controllers to handle the business logic. To construct a simple controller just extend it with ExoController

import 'package:exotik_todos_app/exotik_arch/exotik_arch.dart';

class TodosController extends ExoController<TodosRetainer> {

  void Function(void Function() fn)? setState;
  TodosRetainer? retainer;

  TodosController(
    this.setState,
    {
      TodosRetainer?initRetainer
    }
  ) : super(
    setState,
    initRetainer: initRetainer ?? TodosRetainer()
  );

  void dispose() async {
    super.dispose();
  }

}

The ExoController takes in a generic type which will serve as a model for controller data. For instance in case of this TodosController, this TodosRetainers reference is defined as,

class TodosRetainer {

  late List<Todo> todosList;

  TodosRetainer(){
    todosList = [];
  }

}

Its a convention to keep retainer references in the same file as the controllers. To call the controller from the UI use this syntax,

  late TodosController todosController;

  @override
  void initState() {

    todosController = TodosController(
      this.setState,
      initRetainer: anaGlobalRetainers.todosRetainer
    );

    super.initState();
  }

Note that there is a initRetainer being passed to TodosContoller constructor. That’s how the global state is supported. All the global retainers are put in a AnaGlobalRetinaters reference and passed to a controller on demand. If a initRetainer is passed then controller is initialized with existing state, hence the global state effect.

Navigation Service

Navigate to anywhere from anywhere. Just use,

import 'package:exotik_todos_app/exotik_arch/exotik_arch.dart';

ExoNavService.push(
    MainPage()
);

Helper Widgets

ExotikArch ships with 2 main helper widgets. These are constructed on a very simple idea. All most 90% of apps do only 2 things. They load and display data. Or they submit data to server and wait for response. ExotikArch treats them as data loading and process handling respectively. To load the data there is a helper widget called ExoDataLoader. It takes in a reference of ExoControler. And whenever the controller is in loading state it renders a spinner automatically and then stops once data has been rendered.

We expose the logic in controllers, and UI is updated automatically.

Future<void> getTodos({
    bool reload = true
}) async {

    try {

      if(!reload){
        if(retainer!.todosList.isNotEmpty){
          return;
        }
      }

      List<Todo> _todosList;

      exoDataLoaderHelper.startLoading(); // Start Data Loader

      _todosList = await apiInterface.getTodos();

      exoDataLoaderHelper.stopLoading(); // Stop Data Loader

      if(_todosList != null){
        retainer!.todosList = _todosList;
      }

    } on ExoException catch (e, s){

      exoDataLoaderHelper.showError(e); // Handle Error Messages

      return null;

    }

    return;

}

Same goes for processes. There is ExoProcessHandler, which will display a overlay spinner and stops it once the process has been finished.

Future<bool> login(
    String email,
    String password,
    {
      bool rememberMe: true
    }
) async {

    try{

      AppUser? _appUser;

      exoProcessHandlerHelper.startLoading(); // Start Process Loader

      _appUser = await apiInterface.logIn(
        email,
        password,
      );

      exoProcessHandlerHelper.stopLoading(); // Stop Process Loader

      appUser = _appUser;

      if(appUser != null){
        if(rememberMe){
          appUser?.cache();
        }
        return true;
      } else {
        return false;
      }

    } on ExoException catch (e, s){

      exoProcessHandlerHelper.showError(e); // Handle Error Messages

      return false;

    }

}

And ExoDataLoader, ExoProcessHandler are called just like regular widgets,

@override
  Widget build(BuildContext context) {
    return ExoProcessHandler(
      exoProcessHandlerHelper: todosController.exoProcessHandlerHelper,
      child: Container(),
    );
  }

@override
  Widget build(BuildContext context) {
    return ExoProcessHandler(
      exoProcessHandlerHelper: authController.exoProcessHandlerHelper,
      child: Container()
    );
}

Exception Handling

ExotikArch has a built-in mechanism to handle all the exceptions internally. This works v.i.a contollers and helper widgets. All the exceptions are parsed to one single class ExoException which has a friendlyErrorMessage field in it. This message is automatically displayed when something goes wrong on ExoProcessHandler or ExoDataLoader widgets. Flash messages are constructed and prompted to user.

Work offload – Support for Isolates

Isolates support is very basic at the moment. We use build-in compute() function for that. Which will spawn the isolates, make a network request in it and then despawn it. The implementation for that is found is api_interface.dart file.

      ComputeRequest computeRequest = ComputeRequest(
        requestURL,
        RequestType.POST,
        body: body,
      );

      CDioResponse<Map<String, dynamic>> response = await compute<ComputeRequest, CDioResponse<Map<String, dynamic>>>(
        cRequest,
        computeRequest
      ); // Execute the network request With Isolate

      CDioResponse<Map<String, dynamic>> response = await cRequest(
        computeRequest
      ); // Execute the Network Request WithOut Isolate

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

MIT

GitHub

View Github