flutter_bloc: ^8.0.0 state does not updating - flutter

I have a bloc that keeps user data -via firebase- on the state. It works as it is but when the user signs out and sign in again, I can't update the bloc with new user data.
Eg.
User_1 (name: one) signs out. Then User_2 (name: two) signs in. Bloc state still keeping data from User_1
user_state.dart
part of 'user_bloc.dart';
class UserState extends Equatable {
const UserState();
#override
List<Object?> get props => [];
}
class UserLoading extends UserState {
const UserLoading();
#override
List<Object> get props => [];
}
class UserLoaded extends UserState {
final UserFindine user;
UserLoaded({required this.user});
#override
List<Object> get props => [user];
}
user_event.dart
part of 'user_bloc.dart';
class UserEvent extends Equatable {
const UserEvent();
#override
List<Object> get props => [];
}
class LoadUser extends UserEvent {
const LoadUser();
#override
List<Object> get props => [];
}
class UpdateUser extends UserEvent {
final UserFindine user;
const UpdateUser(this.user);
#override
List<Object> get props => [user];
}
user_bloc.dart
class UserBloc extends Bloc<UserEvent, UserState> {
final DatabaseRepository _databaseRepository;
StreamSubscription? _databaseSubscription;
UserBloc({
required DatabaseRepository databaseRepository,
}) : _databaseRepository = databaseRepository,
super(UserLoading()) {
on<LoadUser>((event, emit) {
_databaseSubscription?.cancel();
_databaseSubscription = _databaseRepository.getUser().listen((user) {
add(UpdateUser(user));
});
});
on<UpdateUser>((event, emit) {
final data = event.user; // This data is right.
emit(UserLoaded(user: data));
});
}
}
I've tried to trigger LoadUser event when the user signs in. But still getting old user data. Am I missing something?
Thanks for any help 🔥
Edit:
In my main.dart file:
MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (context) => AuthBloc(
authRepository: context.read<AuthRepository>(),
),
),
BlocProvider(
create: (_) => UserBloc(databaseRepository: DatabaseRepository())..add(LoadUser()),
),
],
),
Also I use this after sign in event.
UserBloc(databaseRepository: DatabaseRepository())..add(LoadUser());

Similar happened to me when i had more than one instance of LoginBloc. In the case of normal pages, it's useful to recreate the Bloc, but for the login, you do not want to have more than one instance of it or re-create it when you navigate.
here is an example using AppRouter which contains a normal Bloc and a login Bloc, pls check the difference. i added some comments to the code.
you might also try to put a breakpoint to the MultiblocProvider in your code to see if it's called more than once.
class AppRouter {
late Repository repository;
LoginCubit? loginCubit; //declare global login Bloc
AppRouter() {
repository = Repository(apiClient: ApiClient());
loginCubit = LoginCubit(repository: repository); //create the Bloc once
}
Route? generateRoute(RouteSettings settings) {
switch (settings.name) {
case "/":
return MaterialPageRoute(
builder: (_) => BlocProvider.value( // use Blocprovider.value which will not create a new Bloc but use the one declared before
value: loginCubit!,
child: const LoginPage(key: null),
),
);
case "/main_page":
final args = settings.arguments as Map;
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) =>
OtherCubit( repository: repository),
child: OtherPage(),
),
);
}
}
}

Just add emit(UserLoading()); above the line of final data = event.user;
Because once UserLoaded state is emitted and again you want to emit the same state, it will not rebuild BlocBuilder so I prefer always emit two different states in one method.

Related

Provider to be initialized asynchronously from `initState()` but get `could not find the correct Provider`

I develop an ad app, with a message button on the detailed view.
When the user tap on it, the chats view (stateful widget) is pushed to the screen.
The initState() is there to call the asyncInitMessages() which asynchronously fetches the chats and related message from the distant database. The asyncInitMessages() belongs to the Chats class which extends ChangeNotifier.
/// A chat conversation
class Chats extends ChangeNotifier {
/// Internal, private state of the chat.
void asyncInitMessages(
{required ClassifiedAd ad,
required String watchingUserId,
required bool isOwner}) async {
// blah blah
}
}
The ClassifiedAdMessagesViewstateful widget class implementation is as follows (snipet):
#override
void initState() {
// == Fetch conversation and messages
asyncInitMessages();
}
void asyncInitMessages() async {
// === Update all messages
try {
Provider.of<Chats>(context, listen: false).asyncInitMessages(
ad: widget.ad,
watchingUserId: widget.watchingUser!.uid,
isOwner: _isOwner);
} catch (e) {
if (mounted) {
setState(() {
_error = "$e";
_ready = true;
});
}
}
}
#override
Widget build(BuildContext context) {
// <<<<<<<<<<< The exception fires at the Consumer line right below
return Consumer<Chats>(builder: (context, chats, child) {
return Scaffold(
// ... blah blah
Finally, when running ll that, I got the exception in the build at the Consumer line:
could not find the correct Provider<chats>
Help greatly appreciated.
[UPDATED]
Here is the main (very far up from the messages screen)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// } else {
// Firebase.app(); // if already initialized, use that one
// }
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(RootRestorationScope(
restorationId: 'root',
child: ChangeNotifierProvider(
create: (context) => StateModel(),
child: const App())));
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return PersistedAppState(
storage: const JsonFileStorage(),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ThemeModel>.value(value: _themeModel),
//ChangeNotifierProvider<AuthModel>.value(value: _auth),
],
child: Consumer<ThemeModel>(
builder: (context, themeModel, child) => MaterialApp(
// blah blah
}
}
}
And the component just on top of the
/// Classified ad detail view
class ClassifiedAdDetailView extends StatefulWidget {
final User? watchingUser;
final ClassifiedAd ad;
const ClassifiedAdDetailView(
{Key? key, required this.watchingUser, required this.ad})
: super(key: key);
#override
State<ClassifiedAdDetailView> createState() => _ClassifiedAdDetailViewState();
}
class _ClassifiedAdDetailViewState extends State<ClassifiedAdDetailView>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Chats(),
builder: ((context, child) => Scaffold(
// blah blah
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ClassifiedAdMessagesView(
ad: ad,
watchingUser: widget.watchingUser)));
}),
Providers must be located in the widget tree above the widget where you want to use them with Consumer or Provider.of. When you push a new route with Navigator, it won't be add the pushed route below the widget from where you push, it will add it at the same level where home of MaterialApp is located.
(I think the error message you get also states that you can't access the providers between routes.)
In general the tree will look like this if you push some routes (check it with the Flutter Widget Inspector):
MaterialApp
home
widget1
widget2
widget21
widget22
page1
widget1
widget2
page2
page3
In your code you create the provider in ClassifiedAdDetailView and then push
ClassifiedAdMessagesView from this in the onPressed method. You won't be access this provider from ClassifiedAdMessagesView because the tree will be like (simplified):
MaterialApp
home
ClassifiedAdDetailView
ClassifiedAdMessagesView
The solution is to "lift the state up" and place the provider above every widget from where you need to access it. It can be a part of your existing Multiprovider above MaterialApp but if it is too far, you need to find a proper place that is above both ClassifiedAdDetailView and ClassifiedAdMessagesView.

Is there a way to trigger a BlocListener just after its initialization?

I'm working with Bloc and Hydrated Bloc and at some point in my app I want to store a boolean variable "firstTime" in a Hydrated Bloc to know if it's the first time my user is using the app. If it is the case, I redirect the user to a on-boarding page (called IntroPage), and if not, the login screen is displayed.
I use a BlocListener to listen to the changes of "firstTime", so once my user has finished navigating the on-boarding page, it redirects to the login screen.
#override
Widget build(BuildContext context) {
return MaterialApp(
...
builder: (context, child) {
return BlocListener<UserPreferencesBloc, UserPreferencesState>(
listener: (context, state) {
if (state.firstTime) {
_navigator.pushAndRemoveUntil<void>(
IntroPage.route(),
(route) => false,
);
}
},
child: child,
);
},
onGenerateRoute: (_) => SplashPage.route(),
);
}
The main problem is that if there's no change in the state of the Bloc, it does not fire the BlocListener part. The user never access the IntroPage.
Is there a way to make it so I can get into that listener just after its initialization, even without any change in the state of the Bloc ? Or is there another way to do that (that doesn't involve the use of Shared Preferences or other packages) ?
Edit : Here is the code for the Bloc :
class UserPreferencesBloc
extends HydratedBloc<UserPreferencesEvent, UserPreferencesState> {
UserPreferencesBloc() : super(const UserPreferencesState()) {
on<UserPreferencesFirstTimed>(_onFirstTime);
}
void _onFirstTime(
UserPreferencesFirstTimed event,
Emitter<UserPreferencesState> emit,
) async {
emit(state.copyWith(firstTime: event.firstTime));
}
#override
UserPreferencesState? fromJson(Map<String, dynamic> json) {
return UserPreferencesState(firstTime: json['firstTime'] as bool);
}
#override
Map<String, dynamic>? toJson(UserPreferencesState state) => {
'firstTime': state.firstTime,
};
}
And here is the state :
part of 'user_preferences_bloc.dart';
class UserPreferencesState extends Equatable {
const UserPreferencesState({
this.firstTime = true,
});
final bool firstTime;
UserPreferencesState copyWith({
bool? firstTime,
}) {
return UserPreferencesState(
firstTime: firstTime ?? this.firstTime,
);
}
#override
List<Object> get props => [firstTime];
}
And the Bloc is initialized in the app.dart file, at the start of the application :
class App extends StatelessWidget {
const App({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: ... //not shown in this piece of code
child: MultiBlocProvider(
providers: [
...
BlocProvider(create: (_) => UserPreferencesBloc())
],
child: AppView(),
),
);
}
}
It is by design so that BlocListener is only triggered once per state change.
But there are of course ways to do what you are after. If you'd show how you provide/create the bloc and also the definition of the state it could help...
But you could for instance let firstTime be nullable and use the cascade notion operator (..) when creating the bloc to immediately call a method in the bloc that sets the value of firstTime to true/false after initialization.
Edit:
Obviously hard from here to write all the changes you'd have to make, but here is the main idea:
Change: final bool firstTime; to bool? firstTime; and handle the null cases where applicable.
On creation, change:
BlocProvider(create: (_) => UserPreferencesBloc())
to:
BlocProvider(create: (_) => UserPreferencesBloc()..onFirstTime())
Write the method onFirstTime() something like this:
void onFirstTime() async {
emit(state.copyWith(firstTime: state.firstTime ?? true));
}
And remove the on<UserPreferencesFirstTimed>(_onFirstTime); part as well as this.firstTime = true,

Could not find the correct Provider<Movies> above this MyApp Widget

So, I'm using BLoC and Provider packages in one app.
In my 'moviesprovider.dart' I am fetching some data from my API which returns a json, when app is opening first time. How can I get access to Provider.of(context) from main.dart in MultiProvider? Basically, I want to get access to the same instance of List movies, but don't know how.
The error I'm getting:
Error: Could not find the correct Provider above this MyApp Widget
This happens because you used a BuildContext that does not include the provider
of your choice.
Code:
Main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Movies(),
),
Provider<SwipeBloc>(create: (_) {
SwipeBloc()
..add(
LoadMoviesEvent(
movies: context.read<Movies>().movies,
),
);
}),
ChangeNotifierProvider.value(
value: User(),
),
ChangeNotifierProvider.value(
value: Auth(),
),
],
child: ...
}
}
movies_provider.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:movies_recomendations/constants.dart';
import 'package:http/http.dart' as http;
import './single_movie_provider.dart';
class Movies with ChangeNotifier {
String plotText = "";
List<Movie> _movies = [];
List<Movie> get movies {
return <Movie>[..._movies];
}
.....
Future<void> fetchAndSetMovies() async {
const url = 'http://192.168.1.142:8000/Desktop/textData.json';
try {
final response = await http.get(
Uri.parse(url),
);
String source = Utf8Decoder().convert(response.bodyBytes);
final extractedData =
List<Map<String, dynamic>>.from(json.decode(source));
final List<Movie> loadedMovies = [];
extractedData.forEach(
((movieInfo) => {
loadedMovies.add(Movie(
id: movieInfo['id'],
age: 12,
countries: List<String>.from(movieInfo['country']),
description: movieInfo['descriprion'],
frames: movieInfo['frames'],
genre: movieInfo['genre'],
poster: movieInfo['poster'],
premiereWorld: movieInfo['date'].toString(),
ratingIMDb: movieInfo['ratingIMDb'],
ratingKinopoisk: movieInfo['ratingKinopoisk'],
title: movieInfo['title'][1],
ifSeries: movieInfo['ifSeries'],
dateTo: movieInfo['dateTo'].toString(),
isFavourite: true,
seasons: movieInfo['seasons'],
)),
}),
);
_movies = loadedMovies;
notifyListeners();
} on Exception catch (e) {
print('error');
print(e.toString());
}
}
}
Swipe_event.dart
part of 'swipe_block.dart';
abstract class SwipeEvent extends Equatable {
const SwipeEvent();
#override
List<Object> get props => [];
}
class LoadMoviesEvent extends SwipeEvent {
final List<Movie> movies ;
LoadMoviesEvent({
required this.movies,
});
#override
List<Object> get props => [movies];
}
class SwipeLeftEvent extends SwipeEvent {
final Movie movie;
SwipeLeftEvent({
required this.movie,
});
#override
List<Object> get props => [movie];
}
class SwipeRightEvent extends SwipeEvent {
final Movie movie;
SwipeRightEvent({
required this.movie,
});
#override
List<Object> get props => [movie];
}
You probably need to move the code calling Provider.of(context) into its own widget. As the error implies you can't use Provider to retrieve dependencies within the same BuildContext you used to set the Provider scope. Creating a new widget will also generate a new BuildContext.
If you really need to use Provider.of(context) in the same class you define MultiProvider you could use the Builder widget to generate a new context.
So, to solve this problem you should NOT use BlocProvider in main.dart. You should use it in that direct widget where BLoC Provider will be implemented. So I use it in one screen - recomendations, so I write it there like this
class RecomendationsScreen extends StatelessWidget {
static const routeName = '/recomendations';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kBackgroundColor,
body: BlocProvider(
create: (_) => SwipeBloc()
..add(
LoadMoviesEvent(
movies: Provider.of<Movies>(context).movies,
),
),
child: RecomendationsBody(),
),
);
}
}

How to navigate one page to another without changing state in Flutter using flutter_bloc

I am facing one issue in which when I am going from one page to another page using flitter BLoC, my first page rebuild before reaching to second. I am able to restrict the rebuild of the page using buildWhen in BlocBuilder, but the problem is when I come back to the first page again by back press then the page can not show the previous state widgets. I don't know how to manage navigation between pages without rebuild the page again, I am using flutter_bloc 6.1.1 below is my code.
FirstPage
class FirstPage extends StatefulWidget {
final MyData dataObj;
FirstPage({this.dataObj});
#override
_MyFirstPageState createState() => _MyFirstPageState();
}
class _MyFirstPageState extends State<FirstPage> {
FirstPageBloc _bloc = FirstPageBloc();
String _userAddress='';
#override
void initState() {
super.initState();
_bloc.add(UserInfoEvent(dataObj:widget.dataObj));
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(StringConstants.APP_TITLE_HEADING),
),
body: BlocListener<FirstPageBloc, FirstPageState>(
cubit: _bloc,
listenWhen: (previousState, state) {
// return true/false to determine whether or not
// to call listener with state
return true;
},
listener: (context, state) async{
if (state is LoadingState) {
print('Loading ...');
}
if (state is DataInfoState) {
_userAddress=state.userAddress;
}
if(state is ConfirmationState){
Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage(dataObj: widget.dataObj)));
}
},
child: BlocBuilder<FirstPageBloc, FirstPageState>(
//bloc: _bloc,
cubit: _bloc,
buildWhen: (previousState, state) {
// return true/false to determine whether or not
// to rebuild the widget with state
if(state is ConfirmationState){
return false;
}
return true;
},
builder: (context, state) {
if (state is LoadingState) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(valueColor:
AlwaysStoppedAnimation<Color>(ColorConstants.Primary),),
Text(StringConstants.PLEASE_WAIT)
],),
);
}
return _mainWidget();
}),
),
);
}
}
BLoC
class FirstPageBloc extends Bloc<FirstPageEvent, FirstPageState>{
FirstPageBloc() : super(InitialState());
#override
Stream<FirstPageState> mapEventToState(FirstPageEvent event) async*{
// TODO: implement mapEventToState
if(event is DataInfoEvent){
yield* _getUserData(event.dataObj);
}
if(event is ConfirmationEvent){
yield* _confirmTaskData(event.dataObj);
}
}
Stream<DelConfirmState> _confirmTaskData(MyData dataObj) async* {
yield LoadingState();
//Performing some SQLite DB operations
yield ConfirmationState();
}
Stream<DelConfirmState> _getUserData(MyData dataObj) async* {
yield LoadingState();
String userAddress='ABDC001, PIN- 0091910, 5th Main USA';
//Fetching User data from SQLite database and passing to UI
yield DataInfoState(userAddress:userAddress);
}
}
State
abstract class FirstPageState extends Equatable {}
///This is our initial state
class InitialState extends FirstPageState {
#override
List<Object> get props => null;
}
//This state will call for loading the progress var
class LoadingState extends FirstPageState {
#override
List<Object> get props => [];
}
//This state will call for loading the progress var
class ErrorState extends FirstPageState {
final String errorMessage;
ErrorState({#required this.errorMessage});
#override
List<Object> get props => [];
}
//This state will retun the userdata
class DataInfoState extends FirstPageState {
final String userAddress;
DataInfoState({#required this.userAddress});
#override
// TODO: implement props
List<Object> get props => [];
}
class TaskConfirmationState extends FirstPageState {
ConfirmationState({});
#override
// TODO: implement props
List<Object> get props => [];
}
Event
abstract class FirstPageEvent extends Equatable {}
class GetUserInfoEvent extends FirstPageEvent {
final MyData dataObj;
GetUserInfoEvent({this.taskObj});
#override
List<Object> get props => [];
}
class ConfirmationEvent extends FirstPageEvent {
final MyData dataObj
ConfirmationEvent({this.dataObj});
#override
List<Object> get props => [];
}
Please advise
Thank You
You need to provide your bloc at a higher level widget, then you need to get it from the context. In this way the state will persist even through navigation.
You can do that wrapping your widget like this:
BlocProvider(
create: (context) => FirstPageBloc(),
child: FirstPage(),
)
and then inside of initState you can get it like this:
_bloc = BlocProvider.of<FirstPageBloc>(context);

Should I use final in models with equatable and flutter_bloc to distinguish 2 states?

I'm creating an app where you login and go to a page where you have a list of your restaurants, you have also a form where you can add a new restaurant.
This part works.
The problem is that when i click add the restaurant is added in firestore correctly, but the list doesn't refresh. I usually yield 2 states, a LoadingState and a LoadedRestaurantsListState, but with the last version of flutter_bloc this trick doesn't work, seems like just the last state yielded is received, but the previous was LoadedRestaurantsListState, so they are equals and the blocbuilder ignores the second one. So I've to use the equatable's props to distinguish the 2 states, but in the equatable documentation is written: "Note: Equatable is designed to only work with immutable objects so all member variables must be final".
So I've to make all the model's fields final, but if I do it how can i modify just one o two fields when I need it to?
What is the best practice?
If someone has examples, or videos, etc it would be very appreciated.
Thanks in advance
Without props
FirebaseBloc.dart
Stream<FirebaseState> mapEventToState(
FirebaseEvent event,
) async* {
print("event firebase ${event.runtimeType.toString()}");
if (event is CreateRestaurantFirebaseEvent) {
yield LoadingState();
await _databaseService.createRestaurant(event.restaurant, event.user);
List<Restaurant> restaurantsList = await _databaseService
.loadRestaurantsList(event.user.restaurantsIDsList);
yield LoadedRestaurantsListState(restaurantsList);
}
if (event is LoadRestaurantsListEvent) {
List<Restaurant> restaurantsList =
await _databaseService.loadRestaurantsList(event.restaurantsIDs);
yield LoadedRestaurantsListState(restaurantsList);
}
FirebaseState.dart
class LoadingState extends FirebaseState {
#override
List<Object> get props => [];
}
class LoadedRestaurantsListState extends FirebaseState {
List<Restaurant> restaurantsList;
LoadedRestaurantsListState(this.restaurantsList);
#override
List<Object> get props => [];
}
view.dart
class RestaurantSelectionScreen extends StatefulWidget {
final User user;
RestaurantSelectionScreen({
#required this.user,
});
#override
_RestaurantSelectionScreenState createState() =>
_RestaurantSelectionScreenState();
}
class _RestaurantSelectionScreenState extends State<RestaurantSelectionScreen> {
FirebaseBloc _firebaseBloc;
#override
void initState() {
super.initState();
_firebaseBloc = FirebaseBloc();
_firebaseBloc.add(LoadRestaurantsListEvent(widget.user.restaurantsIDsList));
}
#override
Widget build(BuildContext context) {
return BlocProvider<FirebaseBloc>(
create: (context) => _firebaseBloc,
child: Scaffold(
body: SingleChildScrollView(
child: Center(
child: BlocBuilder(
cubit: _firebaseBloc,
builder: (context, state) {
print("state ${state.runtimeType.toString()}");
if (state is InitialFirebaseState) {
return CircularProgressIndicator();
} else if (state is LoadedRestaurantsListState) {
return buildUI(state);
} else if (state is LoadingState) {
return CircularProgressIndicator();
} else {
return _CreateRestaurantFormWidget(widget.user);
}
},
),
),
),
),
);
}