Flutter Provider v3 Architecture using ProxyProvider for Injection

Dane Mackier
Jun 16 · 15 min read

Architecture overview

Implementation

provider: ^3.0.0

Dependency Injection Setup

import 'package:provider/provider.dart';...  @override
Widget build(BuildContext context) {
return MultiProvider(
providers: [],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: RoutePaths.Login,
onGenerateRoute: Router.generateRoute,
),
);
}
// provider_setup.dart
import 'package:provider/provider.dart';
List<SingleChildCloneableWidget> providers = [
...independentServices,
...dependentServices,
...uiConsumableProviders,
];
List<SingleChildCloneableWidget> independentServices = [];List<SingleChildCloneableWidget> dependentServices = [];List<SingleChildCloneableWidget> uiConsumableProviders = [];
MultiProvider(
providers: providers,
child: MaterialApp(...),
)

Dependency Injection Implementation

List<SingleChildCloneableWidget> independentServices = [
Provider.value(value: Api())
];
List<SingleChildCloneableWidget> dependentServices = [
ProxyProvider<Api, AuthenticationService>(
builder: (context, api, authenticationService) =>
AuthenticationService(api: api),
)
];

ViewModels and Injection

Implementing a ViewModel

class LoginViewModel extends ChangeNotifier {
AuthenticationService _authenticationService;
LoginViewModel({ @required AuthenticationService authenticationService})
: _authenticationService = authenticationService;
Future<bool> login(String userIdText) async {
}
}
bool _busy = false;
bool get busy => _busy;
void setBusy(bool value) {
_busy = value;
notifyListeners();
}
Future<bool> login(String userIdText) async {
setBusy(true);
var userId = int.tryParse(userIdText);
var success = await _authenticationService.login(userId);
setBusy(false);
return success;
}

Implementing a View

class LoginView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LoginViewModel>.value(
value: LoginViewModel(),
child: Consumer<LoginViewModel>(
builder: (context, model, child) => Scaffold(
backgroundColor: backgroundColor,
body: Center(child: Text('Login View'))),
),
);
}
}
ChangeNotifierProvider<LoginViewModel>.value(
value: LoginViewModel(
// Inject authentication service setup in the provider_setup
authenticationService: Provider.of(context)
,),
...)
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LoginViewModel>.value(
value: LoginViewModel(authenticationService: Provider.of(context)),
child: Consumer<LoginViewModel>(
builder: (context, model, child) => Scaffold(
backgroundColor: backgroundColor,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LoginHeader(controller: _controller),
model.busy
? CircularProgressIndicator()
: FlatButton(
color: Colors.white,
child: Text(
'Login',
style: TextStyle(color: Colors.black),
),
onPressed: () {},
)
],
),
),
),
);
}
final TextEditingController _controller = TextEditingController();
...
onPressed: () async {
var loginSuccess = await model.login(_controller.text);
if (loginSuccess) {
Navigator.pushNamed(context, RoutePaths.Home);
}
},
...

Further optimisation

Consumer<LoginViewModel>(
// Pass the login header as a prebuilt-static child
child: LoginHeader(controller: _controller),
builder: (context, model, child) => Scaffold(
...
body: Column (
children: [
// Put the child in place of where the LohinHeader was
child,
...
]
)

Sharing functionality and reducing boilerplate

BaseWidget implementation

import 'package:flutter/material.dart';class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
BaseWidget({Key key}) : super(key: key);
_BaseWidgetState<T> createState() => _BaseWidgetState<T>();
}
class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Widget Function(BuildContext context, T value, Widget child) builder;
final T model;
final Widget child;
BaseWidget({Key key, this.model, this.builder, this.child}) : super(key: key);
...
}
class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
// We want to store the instance of the model in the state
// that way it stays constant through rebuilds
T model;
@override
void initState() {
// assign the model once when state is initialised
model = widget.model;
super.initState();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>.value(
value: model,
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
);
}
}
BaseWidget<LoginViewModel>(
model: LoginViewModel(authenticationService: Provider.of(context)),
child: LoginHeader(controller: _controller),
builder: (context, model, child) => Scaffold(...),
);

BaseModel implementation

class BaseModel extends ChangeNotifier {
bool _busy = false;
bool get busy => _busy;
void setBusy(bool value) {
_busy = value;
notifyListeners();
}
}

Home View Implementation

Injecting UI consumable streams

List<SingleChildCloneableWidget> uiConsumableProviders = [
StreamProvider<User>(
builder: (context) =>
Provider.of<AuthenticationService>(context, listen: false).user,
),
];

Consuming the Stream in Home

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UIHelper.verticalSpaceLarge,
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
'Welcome ${Provider.of<User>(context).name}',
style: headerStyle,
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text('Here are all your posts', style: subHeaderStyle),
),
UIHelper.verticalSpaceSmall,
Expanded(child: Posts()),
],
),
);
}

Building a widget with a single responsibility

class Posts extends StatelessWidget {
const Posts({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BaseWidget<PostsModel>();
}
}
class PostsModel extends BaseModel {
Api _api;
PostsModel({
@required Api api,
}) : _api = api;
List<Post> posts; Future getPosts(int userId) async {
setBusy(true);
posts = await _api.getPostsForUser(userId);
setBusy(false);
}
}
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {final Function(T) onModelReady;
...
BaseWidget({
...
this.onModelReady,
});
...
}
...@override
void initState() {
model = widget.model;
if (widget.onModelReady != null) {
widget.onModelReady(model);
}
super.initState();
}
...
@override
Widget build(BuildContext context) {
return BaseWidget<PostsModel>(
model: PostsModel(api: Provider.of(context)),
onModelReady: (model) => model.getPosts(Provider.of<User>(context).id),
);
}
@override
Widget build(BuildContext context) {
return BaseWidget<PostsModel>(
...
builder: (context, model, child) => model.busy
? Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: model.posts.length,
itemBuilder: (context, index) => PostListItem(
post: model.posts[index],
onTap: () {
Navigator.pushNamed(
context,
RoutePaths.Post,
arguments: model.posts[index],
);
},
),
));
}
class PostView extends StatelessWidget {
final Post post;
PostView({@required this.post});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UIHelper.verticalSpaceLarge,
Text(post.title, style: headerStyle),
Text(
'by ${Provider.of<User>(context).name}',
style: TextStyle(fontSize: 9.0),
),
UIHelper.verticalSpaceMedium,
Text(post.body),
Comments(post.id)
],
),
),
);
}
}
class CommentsModel extends BaseModel {
Api _api;
CommentsModel({@required Api api}) : _api = api;
List<Comment> comments; Future fetchComments(int postId) async {
setBusy(true);
comments = await _api.getCommentsForPost(postId);
setBusy(false);
}
}
@override
Widget build(BuildContext context) {
return BaseWidget<CommentsModel>(
onModelReady: (model) => model.fetchComments(postId),
model: CommentsModel(api: Provider.of(context)),
builder: (context, model, child) => model.busy
? Center(
child: CircularProgressIndicator(),
)
: Expanded(
child: ListView.builder(
itemCount: model.comments.length,
itemBuilder: (context, index) =>
CommentItem(model.comments[index]),
),
));
}

Disposing

// base_widget.dart
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
builder: (context) => model,
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
);
}
@override
void dispose() {
print('I have been disposed!!');
super.dispose();
}

Flutter Community

Articles and Stories from the Flutter Community

Dane Mackier

Written by

A full stack software developer focused on building mobile products, its tools and architecture. Always reducing boiler plate code and experimenting.

Flutter Community

Articles and Stories from the Flutter Community