letsar/binder

Should logic loader be part of the library?

chimon2000 opened this issue · 6 comments

Seems like a fairly common pattern.

Yes and it will, I'm just looking for the simplest API.
For example, should we have:

LogicLoader(
  loader: (context) => context.use(myLogicRef).load(),
  child: MyChild(),
);

or

LogicLoader(
  ref: myLogicRef,
  loader: (myLogic) => myLogic.load(),
  child: MyChild(),
);

or even having an interface, let's say this one:

abstract class Loadable {
  Future<void> load();
} 

and having this LogicLoader:

LogicLoader(
  ref: myLogicRef,
  child: MyChild(),
);

The first one is the most flexible, as we can load multiple refs or call multiple methods.
The second one loose the ability to call another ref, so we would have to insert another LogicLoader.
The third one loose also the ability to call multiple methods, but we can always call multiple methods in the load(), so that's not an issue I think.

We could also go with the Loadable interface and having:

LogicLoader(
  refs: [myFirstLogicRef, mySecondLogicRef],
  child: MyChild(),
);

What do you think?

I really like the last example. Fits the same paradigm as Disposable. I could also see something similar to FutureBuilder/StreamBuilder being useful:

//Naming things
LogicLoaderBuilder(    
  refs: [myFirstLogicRef, mySecondLogicRef],
  //Only check for isLoading and/or hasException/exception here; not model.
  builder: (context, state) => state.isLoading ? LoadingChild() : MyChild(),
);

Thank you for your ideas.
What do you think about this:

/// Interface for business logic components that can be used with [LogicLoader].
abstract class Loadable {
  /// Loads data.
  Future<void> load();
}

/// Signature for a function that builds a widget given the [loading] state.
///
/// Used by [LogicLoader.builder].
typedef LoadableWidgetBuilder = Widget Function(
  BuildContext context,
  bool loading,
  Widget child,
);

/// A widget which can be used to load resources when it's inserted in the tree.
///
/// For example, you can use this widget to load data from the repository, the
/// first time this widget builds.
class LogicLoader extends StatefulWidget {
  /// Creates a [LogicLoader] which will call the [Loadable.load] method of all
  /// the [LogicRef]s of the given [refs], when it's inserted in the tree.
  ///
  /// [refs] must not be null.
  /// [child] and [builder] must not be both null.
  ///
  /// If [builder] is set, [child] can be used as a subtree that does not
  /// depends on the loading argument.
  const LogicLoader({
    Key key,
    this.refs = const <LogicRef<Loadable>>[],
    this.builder,
    this.child,
  })  : assert(refs != null),
        assert(
          child != null || builder != null,
          'Either child or builder must be not null',
        ),
        super(key: key);

  /// Logic references that needs to be loaded when this widget is inserted in
  /// the tree.
  final List<LogicRef<Loadable>> refs;

  /// The builder that creates a child to display in this widget, which will
  /// use the provided loading state to show whether data is being fetched.
  final LoadableWidgetBuilder builder;

  /// The widget to pass to [builder] if it's not null, or the child to
  /// directly display in this widget.
  final Widget child;

  @override
  _LogicLoaderState createState() => _LogicLoaderState();
}

class _LogicLoaderState extends State<LogicLoader> {
  bool loading = true;

  @override
  void initState() {
    super.initState();
    // We delay the loading, because if one of the `load` method is calling
    // `write` synchronously, we would end up with an error, because we can't
    // rebuild a parent here.
    Future.microtask(() => load());
  }

  Future<void> load() async {
    final futures = widget.refs.map((ref) => context.use(ref).load());
    try {
      await Future.wait(futures);
    } finally {
      if (widget.builder != null) {
        setState(() {
          loading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.builder != null) {
      return widget.builder(context, loading, widget.child);
    }

    return widget.child;
  }
}

Would you also handle an exception being thrown or would you let that fall into the implementation? Looks good!

In my opinion, error handling, should be in the logic. So LogicLoader will not catch any exceptions.

LogicLoader has been added in v0.2.2