例如,我们使用简单的身份验证流程。当登录请求发起时,设置正在加载中的状态。
为简单起见,此流程由三种可能的状态组成:
图上的状态可以由如下状态机表示,其中包括加载状态和认证状态:
当登录的请求正在进行中,我们会禁用登录按钮并展示进度指示器。
此示例 app 展示了如何使用各种状态管理方案处理加载状态。
主要导航
登录页面的主要导航是通过一个小部件实现的,该小部件使用 Drawer 菜单在不同选项中进行选择。
代码如下:
代码语言:javascript复制class SignInPageNavigation extends StatelessWidget {
const SignInPageNavigation({Key key, this.option}) : super(key: key);
final ValueNotifier<Option> option;
Option get _option => option.value;
OptionData get _optionData => optionsData[_option];
void _onSelectOption(Option selectedOption) {
option.value = selectedOption;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_optionData.title),
),
drawer: MenuSwitcher(
options: optionsData,
selectedOption: _option,
onSelected: _onSelectOption,
),
body: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
switch (_option) {
case Option.vanilla:
return SignInPageVanilla();
case Option.setState:
return SignInPageSetState();
case Option.bloc:
return SignInPageBloc.create(context);
case Option.valueNotifier:
return SignInPageValueNotifier.create(context);
default:
return Container();
}
}
}
复制代码
这个 widget 展示了这样一个 Scaffold
:
AppBar
的标题是选中的项目名称- drawer 使用了自定义构造器
MenuSwitcher
- body 使用了一个 switch 语句来区分不同的页
参考流程(vanilla)
要启用登录,我们可以从没有加载状态的简易 vanilla 实现开始:
代码语言:javascript复制class SignInPageVanilla extends StatelessWidget {
Future<void> _signInAnonymously(BuildContext context) async {
try {
final auth = Provider.of<AuthService>(context);
await auth.signInAnonymously();
} on PlatformException catch (e) {
await PlatformExceptionAlertDialog(
title: '登录失败',
exception: e,
).show(context);
}
}
@override
Widget build(BuildContext context) {
return Center(
child: SignInButton(
text: '登录',
onPressed: () => _signInAnonymously(context),
),
);
}
}
复制代码
当点击 SignInButton
按钮,就调用 _signInAnonymously
方法。
这里使用了 Provider 来获取 AuthService
对象,并将它用于登录。
札记
AuthService
是一个对 Firebase Authentication 的简单封装。详情请见这篇文章。- 身份验证状态由一个祖先 widget 处理,该 widget 使用
onAuthStateChanged
来决定展示哪个页面。我在前一篇文章中介绍了这一点。
setState
加载状态可以经过以下流程,添加到刚刚的实现中:
- 将我们的 widget 转化为
StatefulWidget
- 定义一个局部 state 变量
- 将该 state 放进 build 方法中
- 在登录前和登录后更新它
以下是最终代码:
代码语言:javascript复制class SignInPageSetState extends StatefulWidget {
@override
_SignInPageSetStateState createState() => _SignInPageSetStateState();
}
class _SignInPageSetStateState extends State<SignInPageSetState> {
bool _isLoading = false;
Future<void> _signInAnonymously() async {
try {
setState(() => _isLoading = true);
final auth = Provider.of<AuthService>(context);
await auth.signInAnonymously();
} on PlatformException catch (e) {
await PlatformExceptionAlertDialog(
title: '登录失败',
exception: e,
).show(context);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Center(
child: SignInButton(
text: '登录',
loading: _isLoading,
onPressed: _isLoading ? null : () => _signInAnonymously(),
),
);
}
}
复制代码
重要提示:请注意我们如何使用 finally
闭包。无论是否抛出异常,这都可被用于执行某些代码。
BLoC
加载状态可以由 BLoC 中,stream 的值表示。
我们需要一些额外的示例代码来设置:
代码语言:javascript复制class SignInBloc {
final _loadingController = StreamController<bool>();
Stream<bool> get loadingStream => _loadingController.stream;
void setIsLoading(bool loading) => _loadingController.add(loading);
dispose() {
_loadingController.close();
}
}
class SignInPageBloc extends StatelessWidget {
const SignInPageBloc({Key key, @required this.bloc}) : super(key: key);
final SignInBloc bloc;
static Widget create(BuildContext context) {
return Provider<SignInBloc>(
builder: (_) => SignInBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: Consumer<SignInBloc>(
builder: (_, bloc, __) => SignInPageBloc(bloc: bloc),
),
);
}
Future<void> _signInAnonymously(BuildContext context) async {
try {
bloc.setIsLoading(true);
final auth = Provider.of<AuthService>(context);
await auth.signInAnonymously();
} on PlatformException catch (e) {
await PlatformExceptionAlertDialog(
title: '登录失败',
exception: e,
).show(context);
} finally {
bloc.setIsLoading(false);
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: bloc.loadingStream,
initialData: false,
builder: (context, snapshot) {
final isLoading = snapshot.data;
return Center(
child: SignInButton(
text: '登录',
loading: isLoading,
onPressed: isLoading ? null : () => _signInAnonymously(context),
),
);
},
);
}
}
复制代码
简而言之,这段代码:
- 使用
StreamController<bool>
添加一个SignInBloc
,用于处理加载状态。 - 通过静态
create
方法中的 Provider / Consumer,让SignInBloc
可以访问我们的 widget。 - 在
_signInAnonymously
方法中,通过调用bloc.setIsLoading(value)
来更新 stream。 - 通过
StreamBuilder
来检查加载状态,并使用它来设置登录按钮。
关于 RxDart 的注意事项
BehaviorSubject
是一种特殊的 stream 控制器,它允许我们同步地访问 stream 的最后一个值。
作为 BloC 的替代方案,我们可以使用 BehaviorSubject
来跟踪加载状态,并根据需要进行更新。
我会通过 GitHub 项目 来展示具体如何实现。
ValueNotifier
ValueNotifier
可以被用于持有一个值,并当它变化的时候通知它的监听者。
实现相同的流程代码如下:
代码语言:javascript复制class SignInPageValueNotifier extends StatelessWidget {
const SignInPageValueNotifier({Key key, this.loading}) : super(key: key);
final ValueNotifier<bool> loading;
static Widget create(BuildContext context) {
return ChangeNotifierProvider<ValueNotifier<bool>>(
builder: (_) => ValueNotifier<bool>(false),
child: Consumer<ValueNotifier<bool>>(
builder: (_, ValueNotifier<bool> isLoading, __) =>
SignInPageValueNotifier(
loading: isLoading,
),
),
);
}
Future<void> _signInAnonymously(BuildContext context) async {
try {
loading.value = true;
final auth = Provider.of<AuthService>(context);
await auth.signInAnonymously();
} on PlatformException catch (e) {
await PlatformExceptionAlertDialog(
title: '登录失败',
exception: e,
).show(context);
} finally {
loading.value = false;
}
}
@override
Widget build(BuildContext context) {
return Center(
child: SignInButton(
text: '登录',
loading: loading.value,
onPressed: loading.value ? null : () => _signInAnonymously(context),
),
);
}
}
复制代码
在 静态 create
方法中,我们使用了 ValueNotifier<bool>
的 ChangeNotifierProvider
和 Consumer
,这为我们提供了一种表示加载状态的方法,并在更改时重建 widget。
ValueNotifier vs ChangeNotifier
ValueNotifier
和 ChangeNotifier
密切相关。
实际上,ValueNotifier
就是实现了 ValueListenable<T>
的 ChangeNotifier
的子类。
这是 Flutter SDK 中 ValueNotifier
的实现:
/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);
/// The current value stored in this notifier.
///
/// When the value is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
复制代码
所以我们应该什么时候用 ValueNotifier
,什么时候用 ChangeNotifier
呢?
- 如果在简单值更改时需要重建 widget,请使用
ValueNotifier
。 - 如果你想在
notifyListeners()
调用时有更多掌控,请使用ChangeNotifier
。
关于 ScopedModel 的注意事项
ChangeNotifierProvider
非常类似于 ScopedModel。实际上,他们之间几乎相同:
ScopedModel
↔︎ChangeNotifierProvider
ScopedModelDescendant
↔︎Consumer
因此,如果你已经在使用 Provider,则不需要 ScopedModel,因为 ChangeNotifierProvider
提供了相同的功能。
最后的比较
上述三种实现(setState、BLoC、ValueNotifier)非常相似,只是处理加载状态的方式不同。
如下是他们的比较方式:
- setState ↔︎ 最精简的代码
- BLoC ↔︎ 最多的代码
- ValueNotifier ↔︎ 中等水平
所以 setState
方案最适合这个例子,因为我们需要处理单个小部件的各自的状态。
在构建自己的应用程序时,你可以根据具体情况来评估哪个方案更合适