使用 copywith 时 Flutter Bloc State 不更新
Flutter Bloc State not updating when using copywith
大纲:
- 导航到个人资料页面
- 传入用户 ID 以从 firestore 获取文档
- 将检索到的数据传递给state.copyWith(data:data)
- 产生成功状态并呈现 ui
我的问题是,当我使用 state.copyWith(data:data) 时,状态没有更新,即使数据 100% 存在,因为我可以在控制台中打印它。
代码:
UI:
class ProfileView extends StatelessWidget {
final String uid;
final UserRepository userRepository = UserRepository();
ProfileView({required this.uid});
@override
Widget build(BuildContext context) {
// Init repository
return BlocProvider<ProfileBloc>(
create: (context) => ProfileBloc(
// Should be able to pull user repo from context
userRepository: UserRepository(),
isCurrentUser: UserRepository().isCurrentUser(uid))
..add(InitializeProfile(uid: uid)),
// Get User Doc
child: _profileView(context));
}
Widget _profileView(context) {
return BlocListener<ProfileBloc, ProfileState>(
listener: (context, state) {
if (state.imageSourceActionSheetIsVisible) {
_showImageSourceActionSheet(context);
}
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingFailed) {
showSnackBar(context, state.loadingStatus.exception.toString());
}
if (loadingStatus is LoadingInProgress) {
LoadingView();
}
if (loadingStatus is LoadingFailed) {
LoadingFailedView(exception: loadingStatus.exception.toString());
}
},
child: Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
),
);
}
BLoC:
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final bool isCurrentUser;
final UserRepository userRepository;
final _imagePicker = ImagePicker();
ProfileBloc({
required this.isCurrentUser,
required this.userRepository,
}) : super(ProfileState(
isCurrentUser: isCurrentUser, loadingStatus: LoadingInProgress()));
@override
Stream<ProfileState> mapEventToState(
ProfileEvent event,
) async* {
if (event is InitializeProfile) {
yield* _initProfile(uid: event.uid);
}
}
Stream<ProfileState> _initProfile({required String uid}) async* {
// Loading View
yield state.copyWith(loadingStatus: LoadingInProgress());
// Fetch profile data
try {
final snapshot = await userRepository.getUserDoc(uid);
if (snapshot.exists) {
final data = snapshot.data();
print(data['fullName']);
yield state.copyWith(
fullName: data["fullName"].toString(),
);
print(state.fullName);
yield state.copyWith(loadingStatus: LoadingSuccess());
}
else {
yield state.copyWith(
loadingStatus: LoadingFailed("Profile Data Not present :("));
}
} catch (e) {
print(e);
}
}
State:
part of 'profile_bloc.dart';
class ProfileState {
final bool isCurrentUser;
final String? fullName;
final LoadingStatus loadingStatus;
bool imageSourceActionSheetIsVisible;
ProfileState({
required bool isCurrentUser,
this.fullName,
this.loadingStatus = const InitialLoadingStatus(),
imageSourceActionSheetIsVisible = false,
}) : this.isCurrentUser = isCurrentUser,
this.imageSourceActionSheetIsVisible = imageSourceActionSheetIsVisible;
ProfileState copyWith({
bool? isCurrentUser,
String? fullName,
LoadingStatus? loadingStatus,
bool? imageSourceActionSheetIsVisible,
}) {
print('State $fullName');
print('State ${this.fullName}');
return ProfileState(
isCurrentUser: this.isCurrentUser,
fullName: fullName ?? this.fullName,
loadingStatus: loadingStatus ?? this.loadingStatus,
imageSourceActionSheetIsVisible: imageSourceActionSheetIsVisible ??
this.imageSourceActionSheetIsVisible,
);
}
}
新更新的代码实现了 Bloc builder 而不是建议的监听器
Widget _profileView(context) {
return BlocBuilder<ProfileBloc, ProfileState>(builder: (context, state) {
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingInProgress) {
return LoadingView();
}
if (loadingStatus is LoadingFailed) {
return LoadingFailedView(exception: loadingStatus.exception.toString());
}
if (loadingStatus is LoadingSuccess) {
return Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
);
}
return LoadingView();
});
}
这段代码仍然停留在加载屏幕上,当我打印出各种状态时,它被注册为一个事件,但是在使用
复制后状态打印出 null
感谢帮助
看来您在这段代码中搞砸了概念。
您的 Bloc 逻辑很好,问题出在 UI 层。
我的观察:
BlocListener
不会更新您的 UI(它不是为此而设计的,而是为单次操作而设计的,例如 navigation/dialog 显示)。考虑改用 BlocBuilder
。
- 如果您真的需要
BlocListener
来触发您的 UI 状态更改,请考虑将您的 Widget 转换为 StatefulWidget
并像这样使用 setState
:
class MyWidget extends StatefulWidget {
...
}
class MyWidgetState extends State<MyWidget> {
Widget _currentWidget; // use it on your view hierarchy, remember to initialize with default Widget!
...
@override
Widget build(BuildContext context) {
// Init repository
return BlocProvider<ProfileBloc>(
create: (context) => ProfileBloc(
// Should be able to pull user repo from context
userRepository: UserRepository(),
isCurrentUser: UserRepository().isCurrentUser(uid))
..add(InitializeProfile(uid: uid)),
// Get User Doc
child: _profileView(context));
}
Widget _profileView(context) {
return BlocListener<ProfileBloc, ProfileState>(
listener: (context, state) {
if (state.imageSourceActionSheetIsVisible) {
_showImageSourceActionSheet(context);
}
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingFailed) {
showSnackBar(context, state.loadingStatus.exception.toString());
}
if (loadingStatus is LoadingInProgress) {
/// HERE'S THE CHANGE:
setState(() {
_currentWidget = LoadingWidget();
});
}
if (loadingStatus is LoadingFailed) {
/// HERE'S THE CHANGE:
setState(() {
_currentWidget = LoadingFailedView(exception: loadingStatus.exception.toString());
});
}
},
child: Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
),
);
}
这是因为您需要添加一个运算符来比较您的 ProfileState 对象。实际上,您的事件映射器会生成 ProfileState 对象,但所有这些对象都被视为相等,因此 UI 不会更新。
一个好的做法是让你的状态实现 Equatable,所以你只需要定义将用于比较对象的道具。
根据 bloc 文档,我认为这是我的错误所在。
当我们在私有 mapEventToState 处理程序中产生一个状态时,我们总是产生一个新状态而不是改变状态。这是因为每次我们 yield 时,bloc 都会将状态与 nextState 进行比较,并且只有在两个状态不相等时才会触发状态更改(转换)。如果我们只是改变并产生相同的状态实例,那么 state == nextState 将评估为 true 并且不会发生状态变化。
大纲:
- 导航到个人资料页面
- 传入用户 ID 以从 firestore 获取文档
- 将检索到的数据传递给state.copyWith(data:data)
- 产生成功状态并呈现 ui
我的问题是,当我使用 state.copyWith(data:data) 时,状态没有更新,即使数据 100% 存在,因为我可以在控制台中打印它。
代码:
UI:
class ProfileView extends StatelessWidget {
final String uid;
final UserRepository userRepository = UserRepository();
ProfileView({required this.uid});
@override
Widget build(BuildContext context) {
// Init repository
return BlocProvider<ProfileBloc>(
create: (context) => ProfileBloc(
// Should be able to pull user repo from context
userRepository: UserRepository(),
isCurrentUser: UserRepository().isCurrentUser(uid))
..add(InitializeProfile(uid: uid)),
// Get User Doc
child: _profileView(context));
}
Widget _profileView(context) {
return BlocListener<ProfileBloc, ProfileState>(
listener: (context, state) {
if (state.imageSourceActionSheetIsVisible) {
_showImageSourceActionSheet(context);
}
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingFailed) {
showSnackBar(context, state.loadingStatus.exception.toString());
}
if (loadingStatus is LoadingInProgress) {
LoadingView();
}
if (loadingStatus is LoadingFailed) {
LoadingFailedView(exception: loadingStatus.exception.toString());
}
},
child: Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
),
);
}
BLoC:
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final bool isCurrentUser;
final UserRepository userRepository;
final _imagePicker = ImagePicker();
ProfileBloc({
required this.isCurrentUser,
required this.userRepository,
}) : super(ProfileState(
isCurrentUser: isCurrentUser, loadingStatus: LoadingInProgress()));
@override
Stream<ProfileState> mapEventToState(
ProfileEvent event,
) async* {
if (event is InitializeProfile) {
yield* _initProfile(uid: event.uid);
}
}
Stream<ProfileState> _initProfile({required String uid}) async* {
// Loading View
yield state.copyWith(loadingStatus: LoadingInProgress());
// Fetch profile data
try {
final snapshot = await userRepository.getUserDoc(uid);
if (snapshot.exists) {
final data = snapshot.data();
print(data['fullName']);
yield state.copyWith(
fullName: data["fullName"].toString(),
);
print(state.fullName);
yield state.copyWith(loadingStatus: LoadingSuccess());
}
else {
yield state.copyWith(
loadingStatus: LoadingFailed("Profile Data Not present :("));
}
} catch (e) {
print(e);
}
}
State:
part of 'profile_bloc.dart';
class ProfileState {
final bool isCurrentUser;
final String? fullName;
final LoadingStatus loadingStatus;
bool imageSourceActionSheetIsVisible;
ProfileState({
required bool isCurrentUser,
this.fullName,
this.loadingStatus = const InitialLoadingStatus(),
imageSourceActionSheetIsVisible = false,
}) : this.isCurrentUser = isCurrentUser,
this.imageSourceActionSheetIsVisible = imageSourceActionSheetIsVisible;
ProfileState copyWith({
bool? isCurrentUser,
String? fullName,
LoadingStatus? loadingStatus,
bool? imageSourceActionSheetIsVisible,
}) {
print('State $fullName');
print('State ${this.fullName}');
return ProfileState(
isCurrentUser: this.isCurrentUser,
fullName: fullName ?? this.fullName,
loadingStatus: loadingStatus ?? this.loadingStatus,
imageSourceActionSheetIsVisible: imageSourceActionSheetIsVisible ??
this.imageSourceActionSheetIsVisible,
);
}
}
新更新的代码实现了 Bloc builder 而不是建议的监听器
Widget _profileView(context) {
return BlocBuilder<ProfileBloc, ProfileState>(builder: (context, state) {
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingInProgress) {
return LoadingView();
}
if (loadingStatus is LoadingFailed) {
return LoadingFailedView(exception: loadingStatus.exception.toString());
}
if (loadingStatus is LoadingSuccess) {
return Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
);
}
return LoadingView();
});
}
这段代码仍然停留在加载屏幕上,当我打印出各种状态时,它被注册为一个事件,但是在使用
复制后状态打印出 null感谢帮助
看来您在这段代码中搞砸了概念。 您的 Bloc 逻辑很好,问题出在 UI 层。
我的观察:
BlocListener
不会更新您的 UI(它不是为此而设计的,而是为单次操作而设计的,例如 navigation/dialog 显示)。考虑改用BlocBuilder
。- 如果您真的需要
BlocListener
来触发您的 UI 状态更改,请考虑将您的 Widget 转换为StatefulWidget
并像这样使用setState
:
class MyWidget extends StatefulWidget {
...
}
class MyWidgetState extends State<MyWidget> {
Widget _currentWidget; // use it on your view hierarchy, remember to initialize with default Widget!
...
@override
Widget build(BuildContext context) {
// Init repository
return BlocProvider<ProfileBloc>(
create: (context) => ProfileBloc(
// Should be able to pull user repo from context
userRepository: UserRepository(),
isCurrentUser: UserRepository().isCurrentUser(uid))
..add(InitializeProfile(uid: uid)),
// Get User Doc
child: _profileView(context));
}
Widget _profileView(context) {
return BlocListener<ProfileBloc, ProfileState>(
listener: (context, state) {
if (state.imageSourceActionSheetIsVisible) {
_showImageSourceActionSheet(context);
}
final loadingStatus = state.loadingStatus;
if (loadingStatus is LoadingFailed) {
showSnackBar(context, state.loadingStatus.exception.toString());
}
if (loadingStatus is LoadingInProgress) {
/// HERE'S THE CHANGE:
setState(() {
_currentWidget = LoadingWidget();
});
}
if (loadingStatus is LoadingFailed) {
/// HERE'S THE CHANGE:
setState(() {
_currentWidget = LoadingFailedView(exception: loadingStatus.exception.toString());
});
}
},
child: Scaffold(
appBar: _appBar(),
body: _profilePage(),
bottomNavigationBar: bottomNavbar(context),
),
);
}
这是因为您需要添加一个运算符来比较您的 ProfileState 对象。实际上,您的事件映射器会生成 ProfileState 对象,但所有这些对象都被视为相等,因此 UI 不会更新。 一个好的做法是让你的状态实现 Equatable,所以你只需要定义将用于比较对象的道具。
根据 bloc 文档,我认为这是我的错误所在。
当我们在私有 mapEventToState 处理程序中产生一个状态时,我们总是产生一个新状态而不是改变状态。这是因为每次我们 yield 时,bloc 都会将状态与 nextState 进行比较,并且只有在两个状态不相等时才会触发状态更改(转换)。如果我们只是改变并产生相同的状态实例,那么 state == nextState 将评估为 true 并且不会发生状态变化。