A declarative dependency injection library which use dart syntax and flutter style
- Configuration is aligned with syntax with dart language
- Scope strategy is aligned with scoping of functions
- Can handle async setup
- Using
Observable\States
as notification system with composition in mind - Using
StatesBuilder
to map a sequence of state to widget - Using
StatesListener
to add a listener in flutter layer
- dart_scope - a dart's declarative dependency injection library
- flutter_scope - a flutter's declarative dependency injection library
Let's explore with quick examples, assume we are developing a todos
apps using ValueNotifier:
class TodosNotifier extends ValueNotifier<Map<String, Todo>> {
TodosNotifier([super._value = const {}]);
void addTodo(Todo todo) { ... }
void toggleTodoCompleted(String todoId) { ... }
void removeTodo(String todoId) { ... }
}
enum TodoFilter { all, completed, uncompleted }
class TodoFilterNotifier extends ValueNotifier<TodoFilter> {
TodoFilterNotifier([super._value = TodoFilter.all]);
void updateFilter(TodoFilter filter) { ... }
}
Use FlutterScope(...)
to create a scope with configurations:
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
name: 'todosNotifier',
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
name: 'todoFilterNotifier',
equal: (_) => TodoFilterNotifier(),
),
],
child: Builder(
builder: (context) {
final myTodosNotifier = context.scope.get<TodosNotifier>(name: 'todosNotifier');
final myTodoFilterNotifier = context.scope.get<TodoFilterNotifier>(name: 'todoFilterNotifier');
return ...;
}
),
);
A FlutterScope
is created which expose singletons of TodosNotifier
and TodoFilterNotifier
. Later, these instances can be resolved by calling context.scope.get<T>(...)
.
Above example simulates:
void flutterScope() { // `{` is the start of scope
// create and exposed instances in current scope
final TodosNotifier todosNotifier = TodosNotifier();
final TodoFilterNotifier todoFilterNotifier = TodoFilterNotifier();
// resolve instances in current scope
final myTodosNotifier = todosNotifier;
final myTodoFilterNotifier = todoFilterNotifier;
} // `}` is the end of scope
This simple pseudocode shown:
- function scope that starts with
{
, ends with}
- how to create and expose instances in current scope
- how to resolve instances in current scope
Use different names to create multiple instances:
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
name: 'todosNotifier1',
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
name: 'todosNotifier2',
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
name: 'todosNotifier3',
equal: (_) => TodosNotifier(),
),
],
child: Builder(
builder: (context) {
final myTodosNotifier1 = context.scope.get<TodosNotifier>(name: 'todosNotifier1');
final myTodosNotifier2 = context.scope.get<TodosNotifier>(name: 'todosNotifier2');
final myTodosNotifier3 = context.scope.get<TodosNotifier>(name: 'todosNotifier3');
return ...;
},
),
);
Which simulates:
void flutterScope() {
final TodosNotifier todosNotifier1 = TodosNotifier();
final TodosNotifier todosNotifier2 = TodosNotifier();
final TodosNotifier todosNotifier3 = TodosNotifier();
final myTodosNotifier1 = todosNotifier1;
final myTodosNotifier2 = todosNotifier2;
final myTodosNotifier3 = todosNotifier3;
}
Name can be private, so instance will only be resolved in current library (mostly current file):
final _privateName = Object();
class SomeWidget extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
name: _privateName, // use private name
equal: (_) => TodosNotifier(),
),
],
child: Builder(
builder: (context) {
final myTodosNotifier = context.scope.get<TodosNotifier>(name: _privateName);
return ...;
},
),
);
}
}
Name can also be omitted, in this case null
is used as name:
FlutterScope(
configure: [
// assigned without name
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (_) => TodosNotifier(),
),
],
child: Builder(
builder: (context) {
// also resolved without name
final myTodosNotifier = context.scope.get<TodosNotifier>();
return ...;
},
),
);
Use FlutterScope.async(...)
to create a scope with async configurations.
If there is async setup like resolving SharedPreference
. We can follow this:
Future<Map<String, Todo>> resolveInitialTodosAsync() {
await Future<void>.delayed(Duration(seconds: 1));
return { ... };
}
...
FlutterScope.async( // use `async` constructor
configure: [
// using `AsyncFinal` to handle async setup
AsyncFinal<Map<String, Todo>>(
equal: (_) async {
return await resolveInitialTodosAsync();
},
),
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (scope) => TodosNotifier(
scope.get<Map<String, Todo>>(),
),
),
],
builder: (context, asyncScope) {
switch (asyncScope.status) {
case AsyncStatus.loading:
return ...; // loading widget
case AsyncStatus.error:
return ...; // error widget
case AsyncStatus.loaded:
final scope = asyncScope.requireData;
final myTodosNotifier = scope.get<TodosNotifier>();
return ...; // success widget
},
},
);
Which simulates:
void flutterScope() async {
final Map<String, Todo> initialTodos = await resolveInitialTodosAsync();
final TodosNotifier todosNotifier = TodosNotifier(initialTodos);
final myTodosNotifier = todosNotifier;
}
Use FlutterScope
to create a child scope which inherited getters from parent scope:
class AddTodoState { ... }
class AddTodoNotifier extends ValueNotifier<AddTodoState> { ... }
...
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
equal: (_) => TodoFilterNotifier(),
),
],
child: FlutterScope( // creating a new scope in subtree of parent scope
configure: [
FinalValueNotifier<AddTodoNotifier, AddTodoState>(
equal: (_) => AddTodoNotifier(),
),
],
child: Builder(
builder: (context) {
final myTodoNotifier = context.scope.get<TodosNotifier>();
final myTodoFilterNotifier = context.scope.get<TodoFilterNotifier>();
final myAddTodoNotifier = context.scope.get<AddTodoNotifier>();
return ...;
},
),
),
);
Which simulates:
void flutterScope() {
final TodosNotifier todosNotifier = TodosNotifier();
final TodoFilterNotifier todoFilterNotifier = TodoFilterNotifier();
void childFlutterScope() {
final AddTodoNotifier addTodoNotifier = AddTodoNotifier();
// resolve instances:
// `todosNotifier` is inherited from parent scope
// `todoFilterNotifier` is inherited from parent scope
// `addTodoNotifier` is exposed in current scope
final myTodosNotifier = todosNotifier;
final myTodoFilterNotifier = todoFilterNotifier;
final myAddTodoNotifier = addTodoNotifier;
}
}
Use InheritedScope
for making an exist scope available to subtree. This is useful when current route share scope with new route:
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
equal: (_) => TodoFilterNotifier(),
),
],
child: Builder(
builder: (context) {
return Scaffold(
...
floatActionButton: FloatActionButton(
onPressed: () => _showAddTodoDialog(context),
child: ...,
),
),
},
),
);
...
void _showAddTodoDialog(BuildContext context) {
showDialog( // show dialog will push a new route
context: context,
builder: (_) {
return InheritedScope( // use `InheritedScope` for
scope: context.scope, // making exist scope available to subtree
child: AlertDialog(
...,
content: Builder(
builder: (context) {
// resolve instance in new route
final myTodosNotifier = context.scope.get<TodosNotifier>();
return ...;
},
),
),
);
},
);
}
Above example shown:
- press
FloatActionButton
will push a new route - passing scope from current route to new route using
InheritedScope
- resolve
TodosNotifier
in new route
Use FlutterScope
's parentScope
parameter to create a new scope which is based on exist one, and has additional configurations.
void _showAddTodoDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) {
- return InheritedScope( // use `InheritedScope` for
- scope: context.scope, // making exist scope available to subtree
+ return FlutterScope(
+ parentScope: context.scope, // passing exist scope
+ configure: [ // with additional configurations
+ FinalValueNotifier<AddTodoNotifier, AddTodoState>(
+ equal: (_) => AddTodoNotifier(),
+ ),
+ ],
child: AlertDialog(
title: ...,
content: Builder(
builder: (context) {
// resolve instance in new route
final myTodosNotifier = context.scope.get<TodosNotifier>();
+ final myAddTodoNotifier = context.scope.get<AddTodoNotifier>();
return ...;
},
),
actions: ...,
),
);
},
);
}
Which simulates:
void flutterScope() {
final TodosNotifier todosNotifier = TodosNotifier();
final TodoFilterNotifier todoFilterNotifier = TodoFilterNotifier();
void childFlutterScope() {
final AddTodoNotifier addTodoNotifier = AddTodoNotifier();
final myTodosNotifier = todosNotifier;
final myAddTodoNotifier = addTodoNotifier;
}
}
We've covered the dependency injection part of FlutterScope
. Now, let's explore Observable/States
based notification system.
States
is a sequence of state
.
It will replay current state synchronously, then emit following state asynchronously or synchronously.
Example in pure dart:
void flutterScope() async {
final TodoFilterNotifier todosFilterNotifier = TodoFilterNotifier();
final States<TodoFilter> todoFilterStates = todosFilterNotifierAsStates(todosFilterNotifier);
late TodoFilter state;
final observation = todoFilterStates.observe((todoFilter) { // start observe states
print('simulate flutter set state');
state = todoFilter;
print('simulate map state to widget');
});
await Future<void>.delayed(const Duration(seconds: 3));
print('simulate `navigator.pop(...)`');
observation.dispose(); // stop observe
...// dispose `todosFilterNotifier`
}
// a function turns notifier to states
States<TodoFilter> todosFilterNotifierAsStates(TodoFilterNotifier notifier) { ... }
Above example shown:
late TodoFilter state
is a plain statefinal States<TodoFilter> todoFilterStates
is a sequence of plain state. SometimesStates
can be considered as plain state with a time dimension- use
todoFilterStates.observe(...)
to start observe states - use
observation.dispose()
to stop observe states
Note: States
is similar to dart Stream, but it is slightly different. States
promise replay current state synchronously to observer, while dart Stream has its trade off, is designed not support this feature.
Since States
has composition capability, let's introducing two common used operators.
Use States.computed
to combine multiple states into one States
.
When an item is emitted by one of multiple States, combine the latest item emitted by each States via a specified function and emit combined item that changed.
For example filteredTodos
is computed by combining todos
and todoFilter
:
List<Todo> filterTodos(Map<String, Todo> todos, TodoFilter filter) {
return todos.values
.where((todo) {
switch (filter) {
case TodoFilter.all: return true;
case TodoFilter.completed: return todo.isCompleted;
case TodoFilter.uncompleted: return !todo.isCompleted;
}
})
.toList();
}
...
void flutterScope() async {
...
final States<Map<String, Todo>> todosStates = ...;
final States<TodoFilter> todoFilterStates = ...;
final States<List<Todo>> filteredTodosStates = States.computed2(
states1: todosStates,
states2: todoFilterStates,
compute: filterTodos, // `filterTodos` is a pure function declared at top
);
late List<Todos> state;
final observation = filteredTodosStates.observe((filteredTodos) {
print('simulate flutter set state');
state = filteredTodos;
print('simulate map state to widget');
});
...
}
Above example shown:
filterTodos
is a pure function which compute plainfilteredTodos
by combining plaintodos
andtodoFilter
filteredTodosStates
is computed by combiningtodosStates
andtodoFilterStates
Use states.convert
to convert each item by applying a function and only emit result that changed.
For example todosLength
is converted from todos
:
void flutterScope() {
final TodosNotifier todosNotifier = TodosNotifier();
final States<Map<String, Todo>> todosStates = todosNotifierAsStates(todosNotifier);
// `todosLength` is converted from `todos`
final States<int> todosLengthStates = todosStates
.convert((todos) => todos.length);
final observation = todosLengthStates.observe((todosLength) {
print('todos length changed to $todosLength');
});
...
}
We've seen basic usage of States
, let's see how to integrate with flutter.
Use StatesBuilder(...)
to map a sequence of state to widget, as UI = f(state).
FlutterScope(
configure: [
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
equal: (_) => TodoFilterNotifier(),
),
],
child: StatesBuilder<TodoFilter>(
builder: (context, todoFilter, child) {
return ...; // map state to widget
},
),
);
Which simulates:
void flutterScope() async {
final TodoFilterNotifier todosFilterNotifier = TodoFilterNotifier();
final States<TodoFilter> todoFilterStates = todosFilterNotifierAsStates(todosFilterNotifier);
late TodoFilter state;
final observation = todoFilterStates.observe((todoFilter) {
print('simulate flutter set state');
state = todoFilter;
print('simulate map state to widget');
});
...
}
...
StatesBuilder
has composition capability, since it is based on States
.
Use StatesBuilder
with States.computed
operator to combine multiple states into one states, then map it to widget.
...
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (_) => TodosNotifier(),
),
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
equal: (_) => TodoFilterNotifier(),
),
],
child: Builder(
builder: (context) {
return StatesBuilder<List<Todo>>(
states: States.computed2(
states1: context.scope.getStates<Map<String, Todo>>(),
states2: context.scope.getStates<TodoFilter>(),
compute: filterTodos,
),
builder: (context, filteredTodos, child) {
return ...; // map state to widget
},
);
},
),
);
Which simulates:
...
void flutterScope() async {
final TodosNotifier todosNotifier = TodosNotifier();
final TodoFilterNotifier todosFilterNotifier = TodoFilterNotifier();
final States<Map<String, Todo>> todosStates = todosNotifierAsStates(todosNotifier);
final States<TodoFilter> todoFilterStates = todosFilterNotifierAsStates(todosFilterNotifier);
final States<List<Todo>> filteredTodosStates = States.computed2(
states1: todosStates,
states2: todoFilterStates,
compute: filterTodos,
);
late List<Todos> state;
final observation = filteredTodosStates.observe((filteredTodos) {
print('simulate flutter set state');
state = filteredTodos;
print('simulate map state to widget');
});
...
}
Use StatesListener(...)
to add a listener in flutter layer.
FlutterScope(
configure: [
FinalValueNotifier<TodoFilterNotifier, TodoFilter>(
equal: (_) => TodoFilterNotifier(),
),
],
child: StatesListener<TodoFilter>(
onData: (context, todoFilter) {
ScaffoldMessenger.of(context)
.showSnackbar(SnackBar(
content: Text('todo filter changed to $todoFilter'),
));
},
child: ...,
),
);
Which simulates:
void flutterScope() async {
final TodoFilterNotifier todosFilterNotifier = TodoFilterNotifier();
final States<TodoFilter> todoFilterStates = todosFilterNotifierAsStates(todosFilterNotifier);
final observation = todoFilterStates.observe((todoFilter) {
print('todo filter changed to $todoFilter');
});
...
}
...
StatesListener
also has composition capability, since it is based on States
.
Use StatesListener
with states.convert
operator to convert states to another states, then add a listener to the states.
FlutterScope(
configure: [
FinalValueNotifier<TodosNotifier, Map<String, Todo>>(
equal: (_) => TodosNotifier(),
),
],
child: Builder(
builder: (context) {
return StatesListener<int>(
states: context.scope.getStates<Map<String, Todo>>()
.convert((todos) => todos.length),
onData: (context, todosLength) {
ScaffoldMessenger.of(context)
.showSnackbar(SnackBar(
content: Text('todos length changed to $todosLength'),
));
},
child: ...,
);
},
),
);
Which simulates:
void flutterScope() {
final TodosNotifier todosNotifier = TodosNotifier();
final States<Map<String, Todo>> todosStates = todosNotifierAsStates(todosNotifier);
// `todosLength` is converted from `todos`
final States<int> todosLengthStates = todosStates
.convert((todos) => todos.length);
final observation = todosLengthStates.observe((todosLength) {
print('todos length changed to $todosLength');
});
...
}
...