Apparence.io studio flutter management library. This lib is used for our apps and is open for all.
This force split business logic / view for your pages.
This is not a state management lib
You can combine Alfreed with bloc, rxdart or riverpod for reactive update.
See examples for more infos...(bloc, riverpod example coming next)
Add alfreed in your pubspec and import it.
import 'package:alfreed/alfreed.dart';
class SecondPage extends AlfreedPage<MyPresenter, MyModel, ViewInterface> {
SecondPage({Object? args}) : super(args: args);
@override
AlfreedPageBuilder<MyPresenter, MyModel, ViewInterface> build() {
return AlfreedPageBuilder(
key: ValueKey("presenter"),
builder: (ctx, presenter, model) {
return Scaffold(
appBar: AppBar(title: Text(model.title ?? "")),
// ... you page widgets are here
);
},
presenterBuilder: (context) => MyPresenter(),
interfaceBuilder: (context) => ViewInterface(context),
);
}
}
Every build is defered to make our navigation responsible for building them. (see routing section).
- builder: build your flutter page content (widgets...)
- presenterBuilder: build your presenter (business logic class)
- interfaceBuilder: build your view interface (business logic calls this class to interact with our application without knowing flutter). Goal is to hide flutter from our business logic. (Example: showSnackBar(String message) Without any context in parameters)
- key(optionnal): used to get a reference to the presenter
Note: we extends [AlfreedPage] to handle hot reload. Without hot reload we could remove this layer.
Create a presenter extending Presenter
class.
This is where you write your business logic.
There is one and only one presenter instance available in the widget tree.
An instance of this will be available in the builder method seen in the previous stage (Create a page builder).
class MyPresenter extends Presenter<MyModel, ViewInterface> {
// ... write you business logic methods here
}
Basically this will contain everything you want to show on your page. This model is a simple class where you can wrap models ValueNotifier
or Stream
.
class ViewInterface extends AlfreedView {
ViewInterface(BuildContext context) : super(context: context);
/// ... all your attributes you want to use in your page
void showMessage(String message) {
final snackBar = SnackBar(content: Text(message));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
You can push our page widget in your app router.
Like this:
Route<dynamic> route(RouteSettings settings) {
switch(settings.name) {
case "/":
return MaterialPageRoute(builder: FirstPage());
default:
return MaterialPageRoute(builder: SecondPage());
}
}
Use case: you want to navigate to a tab and keep your page state across rebuild. You can use this
Use rebuildIfDisposed
parameter.
Example:
class SecondPage extends AlfreedPage<MyPresenter, MyModel, ViewInterface> {
@override
AlfreedPageBuilder<MyPresenter, MyModel, ViewInterface> build() {
return AlfreedPageBuilder(
key: ValueKey("presenter"),
builder: (ctx, presenter, model) => Scaffold(appBar: AppBar(title: Text(model.title ?? ""))),
presenterBuilder: (context) => MyPresenter(),
interfaceBuilder: (context) => ViewInterface(context),
rebuildIfDisposed: false, /// this force the builder to not recreate our presenter. Only content will be rebuilt after recreating page.
);
}
}
Our build method contains a special context named AlfredContext
.
This class contains a Device
attribute you can use to make your view for different device sizes.
We used twitter bootstrap sizes ref to create range of devices
Device type can be :
- phone (0px - 576px])
- tablet ([576px - 992px])
- large ([992px - 1200px])
- xlarge (more than 1920px large)
Use example:
AlfreedPageBuilder<MyPresenter, MyModel, ViewInterface>(
key: ValueKey("presenter"),
builder: (ctx, presenter, model) {
return Scaffold(
floatingActionButton: ctx.device < Device.large()
? FloatingActionButton(
backgroundColor: Colors.redAccent,
onPressed: () =>
presenter.addTodoWithRefresh("Button Todo created"),
child: Icon(Icons.plus_one),
)
: null,
);
},
presenterBuilder: (context) => MyPresenter(),
interfaceBuilder: (context) => ViewInterface(context),
);
Here our floating button will be available only for devices smaller than large (phone and tablets).
To create animations on your page you can use AlfreedPageBuilder
factories:
AlfreedPageBuilder<MyPresenter, MyModel, ViewInterface>.animated(...)
for single animation controllerAlfreedPageBuilder<MyPresenter, MyModel, ViewInterface>.animatedMulti(...)
for multiple animations controller. Then you have access to builder methods for your animations.
Basically animations are accessed through a map where you name them. This can help finding your animations back when you need them.
Animation(s) controller(s) and their animations will be available in your presenter and AlfreedPageBuilder builder methods within context.
AlfreedPageBuilder<MyPresenter, MyModel, ViewInterface>.animated(
singleAnimControllerBuilder: (ticker) {
var controller =
AnimationController(vsync: ticker, duration: Duration(seconds: 1));
var animation1 = CurvedAnimation(
parent: controller, curve: Interval(0, .4, curve: Curves.easeIn));
var animation2 = CurvedAnimation(
parent: controller, curve: Interval(0, .6, curve: Curves.easeIn));
return {
'': AlfreedAnimation(controller, subAnimations: [animation1, animation2])
};
},
animListener: (context, presenter, model) {
if (model.animate) {
context.animations!.values.first.controller.forward(); //SIMPLIFY
}
},
builder: (ctx, presenter, model) => ...,
presenterBuilder: (context) => MyPresenter(),
interfaceBuilder: (context) => ViewInterface(context),
);
Access to animations within presenter and start the first one:
animations!.values.first.controller.forward();
You can pass arguments from routing directly to your presenter like this
Route<dynamic> route(RouteSettings settings) {
switch (settings.name) {
case '/second':
return MaterialPageRoute(builder: SecondPage(args: settings.arguments));
default:
return MaterialPageRoute(builder: myPageBuilder.build);
}
}
now you can access args directly inside your presenter using:
this.args
Presenter has a rebuildOnHotReload
that will trigger every time the user hot reloads and call onInit again.
By default rebuildOnHotReload is False, so state will be preserved.
Override onReassemble
in your presenter.
Use AlfreedUtils
to get a reference of your presenter instance.
var presenter = AlfreedUtils.getPresenterByKey<MyPresenter, MyModel>(
tester, ValueKey("presenter"));
- The Key must be unique and added to the
AlfreedPageBuilder
seen on step (Create a page builder) - The application must be started using a pumpWidget
- the page is correctly built
You can directly get your viewmodel from an Alfreedpage child like using this :
final myModel = PresenterInherited.dataOf<MyPresenter, MyModel>(context);
Prefer using a real presenter but in some case this helps.
Doc incoming
Preferences > User snippets
"Alfreed template": {
"prefix": "alf",
"description": "create an Alfreed templated page",
"body": [
"import 'package:flutter/material.dart';",
"import 'package:alfreed/alfreed.dart';",
"",
"class ${1:name}ViewModel {}",
"",
"class ${1:name}ViewInterface extends AlfreedView {",
" ${1:name}ViewInterface(BuildContext context) : super(context: context);",
"}",
"",
"class ${1:name}Page extends AlfreedPage<${1:name}Presenter, ${1:name}ViewModel, ${1:name}ViewInterface> {",
"",
" ${1:name}Page({Object? args, Key? key}) : super(args: args, key: key);",
"",
" @override",
" AlfreedPageBuilder<${1:name}Presenter, ${1:name}ViewModel, ${1:name}ViewInterface> build() {",
" return AlfreedPageBuilder(",
" key: ValueKey('presenter'),",
" presenterBuilder: (context) => ${1:name}Presenter(),",
" interfaceBuilder: (context) => ${1:name}ViewInterface(context),",
" builder: (context, presenter, model) => null, //TODO",
" );",
" }",
"}",
"",
"class ${1:name}Presenter extends Presenter<${1:name}ViewModel, ${1:name}ViewInterface> {",
"",
" ${1:name}Presenter() : super(state: ${1:name}ViewModel());",
"}"
]
}
Developed with 💙 by Apparence.io