udf
A package that enables you to use a Unidirectional Data Flow, inspired by the Elm and Erlang/otp languages and platform.
setting up
It is important to initialise your providers and model before you run your app, otherwise there might be nullpointers.
example:
void main() {
initAppState();
runApp(ScannerApp());
}
void initAppState() {
LoginModelProvider.init());
MenuModelProvider.init());
}
##Intellij templates
git clone git@github.com:koenusz/udf-intellij-plugin.git
Router
The router allows you to navigate between screens. All the views require a routeName string to identify them for the router. To enable the routing functionality you need to place a widget in the widget tree that enables the routing.
example:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My amazing app',
home: RouterWidget(),
routes: {
Login.routeName: (context) => Login(), //all your views need to be added here
Menu.routeName: (context) => Menu(),
});
}
}
class RouterWidget extends StatelessWidget {
final Router router = Router();
RouterWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
router.init(Navigator.of(context));
return Login(); //return the view where you want to start
}
}
This allows you to navigate as such in your app.
example:
RaisedButton(
child: Text("Login"),
onPressed: () => {
Router().navigateTo(routeName: Menu.routeName),
},
),
The router is cached, so calling the constructor a second time will retrieve the same instance that you created the first time.
Package structure
Model
Immutable class that contains your data. Make sure to use final keyword to make sure your data
stays consistent. Updating the Model data is by creating a new copy of it with the copyWith method.
Data consistency is one of the most important things to prevent bugs in your app. The type system is
a good tool to make certain bugs impossible to occur. Make sure as many inconsistent states are
disallowed in your app by using the type system smartly. (Make impossible states impossible)
https://www.youtube.com/watch?v=IcgmSRJHu_8&ab_channel=elm-conf
example:
class LoginModel extends Model<LoginModel> {
final String email;
final String password;
LoginModel._(this.email, this.password);
static LoginModel init() {
return LoginModel._("", "");
}
LoginModel copyWith({String email, String password}) {
return LoginModel._(email ?? this.email, password ?? this.password);
}
@override
String toString() {
return 'LoginModel{email: $email, password: $password}';
}
}
bonus: adding a proper toString override will make debugging your app a lot easier.
ViewNotifier
Changes made to the model most of the time require your app to render these changes. To enable this you need to place a viewNotifier in your widget tree.
example:
@override
Widget build(BuildContext context) {
return ViewNotifier<MenuModelProvider>(
stateProvider: StateProvider.providerOf(MenuModelProvider),
child: MenuView(),
);
}
This enables the using data in your views like this:
example
class MenuView extends StatelessWidget {
@override
Widget build(BuildContext context) {
var provider = StateProvider.providerOf(MenuModelProvider);
MenuModel model = provider.model();
return Scaffold(
appBar: AppBar(
title: Text("menuView"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(model.user.email),
RaisedButton(child: Text("Menu"), onPressed: () => {}),
],
),
),
);
}
}
Message
An Immutable class that is the relay for any of the app's functionality. Its handle method defines how the app state is updated.
example:
class EmailChangedMessage extends Message<LoginModel, EmailChangedMessage> {
final String email;
EmailChangedMessage(this.email);
@override
LoginModel handle(StateProvider<LoginModel> provider, EmailChangedMessage msg, LoginModel model) {
return model.copyWith(email: msg.email);
}
@override
String toString() {
return 'EmailChangedMessage{email: $email}';
}
}
bonus: adding a proper toString override will make debugging your app a lot easier.
StateProvider
Managing class that handles Messages and updates your Model.
There is not much you need to do to instantiate the provider. It basically only needs to know its own name and what model it will use.
It is recommended though that you add the following: - An init factory method where you instantiate your model's initial state. - A getModel static method to get the current state of the model. - A static send method to be able to send messages to the provider.
These are not strictly needed but it hides some of the boilerplate and keeps your business logic a bit cleaner.
example:
class LoginModelProvider extends StateProvider<LoginModel> {
@Protected
LoginModelProvider(LoginModel model) : super(model);
static send(Message msg) => StateProvider.providerOf(LoginModelProvider).receive(msg);
static getModel() => StateProvider.providerOf(LoginModelProvider).model();
factory LoginModelProvider.init() => LoginModelProvider(model: LoginModel(email: "", password: ""));
}
Sending messages
receive
This is done by calling the receive method on the stateProvider. The receive method takes a Message object and executes the handle method on the message data provided. Messages are resolved in the order in which they are received.
The Provider needs to be retrieved form the ViewNotifier in order to trigger re-rendering.
example:
class LoginView extends StatelessWidget {
@override
Widget build(BuildContext context) {
var provider = ViewNotifier.of(LoginModelProvider);
var model = provider.model();
return Scaffold(
appBar: AppBar(
title: Text("Login"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MyWidgets.textField(
"email",
"enter your email",
(email) => {provider.receive(EmailChangedMessage(email))},
),
RaisedButton(
child: Text("Login"),
onPressed: () => {
Router().navigateTo(routeName: Menu.routeName),
},
),
],
),
),
);
}
}
bonus:
static Widget textField(String label, String hint, ValueChanged<String> onChanged) {
return Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
child: TextField(
decoration: InputDecoration(labelText: label, hintText: hint, fillColor: Colors.white), onChanged: onChanged),
);
}
sendWhenCompletes
Sometimes you would like to trigger something in your app on a reaction to some stimulus. For instance when you receive a response from an Api. This method handles this usecase by executing the future (1st argument) and afterwards either sending a message to the provider or logging an (Optional) Message.
example:
stateProvider.sendWhenCompletes(
API.getEmployees(),
(employees) => EmployeesReceivedMessage(employees),
errorMessage: "fetching the employees failed",
);
passing data between views
Each view should have exactly one model and one provider related to that view. Passing data between view is as simple as retrieving the provider related to the view that you want to pass data to and sending it a message. This will never result in re-rendering your current view because the other provider is not added to the notifier in your current widget tree.
example:
class MenuView extends StatelessWidget {
@override
Widget build(BuildContext context) {
...
var provider = StateProvider.providerOf(PaymentModelProvider);
provider.receive(PaymentMessage(amount: 10, currency: EURO));
...
}
}