flutter_scope
A declarative dependency injection library which use dart syntax and flutter style
Features
- Configuration is aligned with syntax with dart language
- Scope strategy is aligned with scoping of functions
- Can handle async setup
- Using
Observable\Statesas notification system with composition in mind - Using
StatesBuilderto map a sequence of state to widget - Using
StatesListenerto add a listener in flutter layer
Table Of Content
Packages
- dart_scope - a dart's declarative dependency injection library
- flutter_scope - a flutter's declarative dependency injection library
Quick Tour
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) { ... }
}
Usage of FlutterScope(...)
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
Usage of name
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 ...;
},
),
);
Usage of FlutterScope.async(...)
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;
}
Usage of child scope
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;
}
}
Usage of InheritedScope
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
FloatActionButtonwill push a new route - passing scope from current route to new route using
InheritedScope - resolve
TodosNotifierin new route
Usage of FlutterScope's parentScope parameter
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.
Usage of States
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 stateis a plain statefinal States<TodoFilter> todoFilterStatesis a sequence of plain state. SometimesStatescan 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.
Usage of States.computed
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:
filterTodosis a pure function which compute plainfilteredTodosby combining plaintodosandtodoFilterfilteredTodosStatesis computed by combiningtodosStatesandtodoFilterStates
Usage of states.convert
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.
Usage of StatesBuilder(...)
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.
Usage of StatesBuilder with States.computed operator
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');
});
...
}
Usage of StatesListener(...)
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.
Usage of StatesListener with states.convert operator
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');
});
...
}
...