Rebuild widget inside stack using bloc? - flutter

Is it possible to rebuild widget inside a stack using BlocBuilder? I tried a simple code below but it only rebuild the widget once. I want to rebuild the widget inside the stack which is the GoogleMapDetailsList and pass the search value. GoogleMapDetailsList also has different bloc that triggers in its InitState.
Column(
children: [
searchWidget(context),
Expanded(
child: Stack(
children: [
googleMap(),
BlocBuilder<GoogleMapSearchBloc, GoogleMapSearchState>(
builder: (context, state) {
if (state is GoogleMapSearchInProgressState) {
return GoogleMapDetailsList(
search: search,
goToPlaceCallback: _goToPlace,
);
} else if (state is GoogleMapSearchLoadInProgressState) {
return CircularProgressIndicator();
} else {
return Container();
}
},
),
],
),
),
],
),
Row searchWidget(BuildContext context) {
return Row(
children: [
ButtonTemplate(
textPaddingInset: EdgeInsets.zero,
text: 'Search',
buttonColor: Theme.of(context).accentColor,
onPressed: () {
setState(() {
searchResult = true;
});
BlocProvider.of<GoogleMapSearchBloc>(context)
.add(StartSearchEvent());
})
],
}
//BLOC
class GoogleMapSearchBloc
extends Bloc<GoogleMapSearchEvent, GoogleMapSearchState> {
GoogleMapSearchBloc() : super(GoogleMapSearchInitialState());
#override
Stream<GoogleMapSearchState> mapEventToState(
GoogleMapSearchEvent event,
) async* {
if (event is StartSearchEvent) {
yield GoogleMapSearchLoadInProgressState();
Future.delayed(const Duration(seconds: 5), () {});
yield GoogleMapSearchInProgressState();
}
}
}

I solved it by overriding didUpdateWidget() in GoogleMapDetailsList. I saw it in this answer. I also want to know if there is another way of doing this.
#override
void didUpdateWidget(GoogleMapDetailsList oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.search != widget.search) {
BlocProvider.of<GoogleMapPlaceSearchBloc>(context)
.add(RefreshGoogleMapPlaceSearchEvent());
}
}

The app gets notified only once in this case. To subscribe for further changes use listen:true.
BlocProvider.of<GoogleMapSearchBloc>(context, listen:true)
.add(StartSearchEvent());

Related

Issue in custom flutter search widget's search results selection - flutter web

I build a simple search widget for flutter web. Everything working fine but after I got the search results, I have to click twice on the result to select a specific search result. Please help me to figure out the problem. I tried for several day but no luck. I'm using flutter 2.5.2 version.
darpad link to run the code
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(MaterialApp(
home: SearchView(),
));
}
class SearchView extends StatefulWidget {
#override
State<SearchView> createState() => _SearchViewState();
}
class _SearchViewState extends State<SearchView> {
String searchResult = '';
final textController = TextEditingController();
final List<String> data = ['Result 1', 'Result 2', 'Result 3', 'Result 4'];
Future<List<String>> loadData() {
return Future.delayed(Duration(seconds: 1), () {
if (this.textController.text.trim().length != 0) {
return data;
} else {
return [];
}
});
}
#override
void initState() {
this.textController.addListener(this._onTextChanged);
super.initState();
}
void _onTextChanged() {
print('text cahnged');
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Brand'),
),
body: SingleChildScrollView(
child: Column(
children: [
TextFormField(
controller: this.textController,
),
FutureBuilder(
future: loadData(),
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
print("future build");
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (String result in snapshot.data)
InkWell(
onTap: () {
print('Clicked');
setState(() {
this.textController.clear();
this.searchResult = result;
});
},
child: Text(result),
),
],
);
} else {
return CircularProgressIndicator();
}
},
),
Text('Search result is ${searchResult}'),
],
),
),
);
}
}
Please help me to fix this issue. Thank you and have a nice day
This weird behavior happens because of a flutter issue. Before flutter version 2.5, Text change listeners only listen for text changes. But from version 2.5, listeners also listen for focus change events. Need to use onChanged method in TextFormField until flutter fix the issue.

How to get value from an object which in the state (flutter_bloc)

in builder method I reach the value of state like
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
bool account = state.userSettings.publicAccount
}
But I need to get the values from initState. I need to set the values of the widget. I tried something like this but I got error
#override
void initState() {
super.initState();
UsersOwnProfileState state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
}
Can anyone show me how to get state value in initState?
class UserSettingPage extends StatefulWidget {
final UsersOwnProfileBloc bloc;
const UserSettingPage({Key key, this.bloc}) : super(key: key);
#override
_UserSettingPageState createState() => _UserSettingPageState();
}
class _UserSettingPageState extends State<UserSettingPage> {
bool newListingAlert;
bool listingForSearchAlert;
bool searchForListingAlert;
bool followAlert;
bool publicAccount;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});
}
#override
Widget build(BuildContext context) {
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(25.h),
child: ListingEditAppBar(
onCancel: () {
widget.bloc.add(FetchUserEvent(userId: CurrentUser.currentUser.id));
Navigator.pop(context);
},
),
),
body: Column(
children: [
PageTitle(title: "Preferences"),
Expanded(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Profilim herkese açık"),
Switch(
onChanged: (value) {
setState(() {
publicAccount = value;
});
},
value: publicAccount,
)
],
),
)
],
),
)
],
),
);
}
return MyProgressIndicator();
},
);
}
}
I have added the whole code. I am getting the following error.
Failed assertion: boolean expression must not be null
The relevant error-causing widget was
Switch
If you would like to access the state within initState you will need to use WidgetsBinding to access this. However, using this ensures that your widget is built and then triggers the method to get the value. It will be faster to just use the BlocBuilder, Watch, or Select to get the value you are looking for.
But to answer your question, you can do the following
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});

How can I update the ListView after getting a certain data?

I have faced this issue,
I read files using this _openFile function. After the reading, I want to update the ListView using that extracted data. But I couldn't find a way to reset the state of the ListView after the execution of _openFile.
Any help?
class ListTest extends StatefulWidget {
#override
_ListTestState createState() => _ListTestState();
}
class _ListTestState extends State<ListTest> {
List<XFile> _files = [];
Future <List<XFile>> _openFile(BuildContext context) async {
final XTypeGroup pngTypeGroup = XTypeGroup(
label: 'PNGs',
extensions: ['png'],
);
_files = await openFiles(acceptedTypeGroups: [
pngTypeGroup,
]);
}
#override
Widget build(BuildContext context) {
return Column(
children: [
RaisedButton(
color: Colors.blue,
textColor: Colors.white,
child: Text('Press to open images (png)'),
onPressed: () {
_openFile(context);
);
}
),
Expanded(
child: ListView.builder(
itemCount: _files.length,
itemBuilder: (context, index) {
return Text(_files[index].path);
},
),
),
],
);
}
}
You need to call setState in order to rebuild the widget. I recommend you to read this.
You can try something like:
var _newFiles = await openFiles(acceptedTypeGroups: [
pngTypeGroup,
]);
setState(() {
_files = _newFiles;
});

Future builder not firing

I can't seem to get my future builder to update. The api response is working fine I can see it in my logs. (model.getSuburbs). but it doesn't seem like my my future in the FutureBuilder suburbs is doing anything.. Am I missing something obvious (The onSubmitis trigger when I enter the last number and triggers the api)
class PostcodePage extends StatefulWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) => PostcodePage(),
);
}
#override
_PostcodeScreenState createState() => _PostcodeScreenState();
}
class _PostcodeScreenState extends State<PostcodePage> {
PostcodeViewmodel model = serviceLocator<PostcodeViewmodel>();
Future<List<Suburb>> suburbs;
String postCode;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(32),
child: Column(children: [
SizedBox(height: 200),
PinEntryField(
onSubmit: (input) => getSub(pc: input),
),
FutureBuilder<List<Suburb>>(
future: suburbs,
builder: (context, snapshot) {
if (snapshot.connectionState==ConnectionState.active) {
return Text('Would Like something here...');
} else
return Text('But always end up here...');
},
),
// (postCode != null) Text(postCode),
SizedBox(
height: 300,
),
SizedBox(
width: double.maxFinite,
child: OnBoardingButton(
text: 'Begin',
onPressed: () {},
color: Color(0xff00E6B9),
),
),
]),
),
);
}
getSub({String pc}) {
setState(() {
suburbs = model.getSuburbs(country: 'au', postcode: pc);
});
}
}
Try to change your condition inside the builder.
This code snapshot.connectionState==ConnectionState.active could be really really short depending on the suburbs future.
Please try this inside the builder.
if (snapshot.hasData) {
return Text('Would Like something here...');
} else {
return Text('But always end up here...');
}

Flutter BlocListener executed only once even after event gets re-fired

I am implementing Reso Coder's clean architecture in flutter. I followed his guides in dividing the project to layers and using dependency injection. In one of the cases I want to have the following scenario: An administrator user logs in, sees data on their home screen, edits it and by pressing a button, saves the data to the local db (sqflite). Upon saving the data I want to show a Snackbar with some sort of text "Settings saved!" for example. Here's my code (parts):
class AdministratorPage extends StatefulWidget {
#override
_AdministratorPageState createState() => _AdministratorPageState();
}
class _AdministratorPageState extends State<AdministratorPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).backgroundColor,
centerTitle: true,
leading: Container(),
title: Text(AppLocalizations.of(context).translate('adminHomeScreen')),
),
body: SingleChildScrollView(
child: buildBody(context),
),
);
}
BlocProvider<SettingsBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => serviceLocator<SettingsBloc>(),
child: BlocListener<SettingsBloc, SettingsState>(
listener: (context, state) {
if (state is SettingsUpdatedState) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).translate('settingsUpdated')),
backgroundColor: Colors.blue,
),
);
}
},
child: Column(
children: <Widget>[
SizedBox(
height: 20.0,
),
AdministratorInput(),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('LOG OUT'),
onPressed: () {
serviceLocator<AuthenticationBloc>().add(LoggedOutEvent());
Routes.sailor(Routes.loginScreen);
},
),
),
],
),
),
);
}
}
Here's the AdministratorInput widget:
class AdministratorInput extends StatefulWidget {
#override
_AdministratorInputState createState() => _AdministratorInputState();
}
class _AdministratorInputState extends State<AdministratorInput> {
String serverAddress;
String daysBack;
final serverAddressController = TextEditingController();
final daysBackController = TextEditingController();
#override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
if (state is SettingsInitialState) {
BlocProvider.of<SettingsBloc>(context)
.add(SettingsPageLoadedEvent());
} else if (state is SettingsFetchedState) {
serverAddressController.text =
serverAddress = state.settings.serverAddress;
daysBackController.text =
daysBack = state.settings.daysBack.toString();
}
return Column(
children: <Widget>[
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context)
.translate('serverAddress')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: serverAddressController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
serverAddress = value;
},
),
),
SizedBox(
height: 5.0,
),
// Days Back Text Field
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context).translate('daysBack')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: daysBackController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
daysBack = value;
},
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('SAVE CHANGES'),
onPressed: updatePressed,
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('REFRESH'),
onPressed: refreshPressed,
),
),
],
);
},
),
),
);
}
void updatePressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsUpdateButtonPressedEvent(
settings: SettingsAggregate(
serverAddress: serverAddress,
daysBack: int.parse(daysBack),
),
),
);
}
void refreshPressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsRefreshButtonPressedEvent(),
);
}
}
The SettingsBloc is a standard bloc pattern with events and states and a mapper method. It is being injected using get_it package. Here's how is instantiated:
serviceLocator.registerFactory(
() => SettingsBloc(
pullUsersFromServerCommand: serviceLocator(),
getSettingsQuery: serviceLocator(),
updateSettingsCommand: serviceLocator(),
),
);
All instances of the commands and query for the constructor of the bloc are instantiated properly the same way.
Here's the bloc:
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final PullUsersFromServerCommand pullUsersFromServerCommand;
final UpdateSettingsCommand updateSettingsCommand;
final GetSettingsQuery getSettingsQuery;
SettingsBloc({
#required PullUsersFromServerCommand pullUsersFromServerCommand,
#required UpdateSettingsCommand updateSettingsCommand,
#required GetSettingsQuery getSettingsQuery,
}) : assert(pullUsersFromServerCommand != null),
assert(updateSettingsCommand != null),
assert(getSettingsQuery != null),
pullUsersFromServerCommand = pullUsersFromServerCommand,
updateSettingsCommand = updateSettingsCommand,
getSettingsQuery = getSettingsQuery;
#override
SettingsState get initialState => SettingsInitialState();
#override
Stream<SettingsState> mapEventToState(SettingsEvent event) async* {
if (event is SettingsPageLoadedEvent) {
final getSettingsEither = await getSettingsQuery(NoQueryParams());
yield* getSettingsEither.fold((failure) async* {
yield SettingsFetchedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsFetchedState(settings: result);
} else {
yield SettingsFetchedFailureState(
error: "settingsFetchFromDatabaseError");
}
});
} else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
} else if (event is SettingsRefreshButtonPressedEvent) {
final pullUsersFromServerEither =
await pullUsersFromServerCommand(NoCommandParams());
yield* pullUsersFromServerEither.fold((failure) async* {
yield SettingsRefreshedFailureState(
error: "settingsRefreshDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsRefreshedFailureState(error: "settingsRefreshedError");
}
});
}
}
}
The first time I enter this screen everything works perfect. The data is fetched from the database, loaded on screen and if I change it and press SAVE, it shows the snackbar. My problem is if I want to edit the data again while staying on that screen. I edit it again, therefore fire the changing event, the bloc gets it, calls the proper command below and the data is saved in the database. Then the state of the bloc is changed in attempt to tell the UI, "hey, I have a new state, get use of it". But the BlocListener never gets called again.
How should I achieve the behavior I desire?
EDIT:
I am adding another bloc I am using earlier in the App where I log in users. The Login Page utilizes that bloc and upon wrong username or password, I am showing a snackbar, clearing the input fields and leaving the page ready for more. If I try again with wrong credentials, I can see the snackbar again.
Here is the LoginBloc:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticateUserCommand authenticateUserCommand;
final AuthenticationBloc authenticationBloc;
LoginBloc({
#required AuthenticateUserCommand authenticateUserCommand,
#required AuthenticationBloc authenticationBloc,
}) : assert(authenticateUserCommand != null),
assert(authenticationBloc != null),
authenticateUserCommand = authenticateUserCommand,
authenticationBloc = authenticationBloc;
#override
LoginState get initialState => LoginInitialState();
#override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressedEvent) {
yield LoginLoadingState();
final authenticateUserEither = await authenticateUserCommand(
AuthenticateUserParams(
username: event.username, password: event.password));
yield* authenticateUserEither.fold((failure) async* {
yield LoginFailureState(error: "loginDatabaseError");
}, (result) async* {
if (result != null) {
authenticationBloc.add(LoggedInEvent(token: result));
yield LoginLoggedInState(result);
} else {
yield LoginFailureState(error: "loginUsernamePasswordError");
}
});
}
}
}
The Event and State classes here extend Equatable. And since it was working according to the expectations, I did it the same way in the Settings Page (where it failed). From the UI I raise the LoginButtonPressedEvent as many times as I want and the BlocListener gets called respectively.
else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
//
// this part is the problem.
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
In general, you should use Equatable if you want to optimize your code to reduce the number of rebuilds. You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
The source: when-to-use-equatable
How it works with flutter_bloc is you can't yield the same state. Yes, the above function before yield the state is working fine when you emit the event, but the yield itself doesn't get called.
So basically what happens with your bloc is,
Current state is SettingsFetchedState(settings: result)
You emit SettingsUpdateButtonPressedEvent()
Bloc yield SettingsUpdatedState()
State changes from SettingsFetchedState(settings: result) to SettingsUpdatedState()
Current state is SettingsUpdatedState()
BlocListener listens to state changes from SettingsFetchedState(settings: result) to SettingsUpdatedState()
You emit SettingsUpdateButtonPressedEvent()
Bloc doesn't yield SettingsUpdatedState(), it is ignored because the equality comparison returns true)
BlocListener does nothing because there is no state changes.
How to fix this? I am not confident enough to give suggestion based on my current knowledge, so maybe try what the quote says You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
EDIT :
LoginBloc works simply because it yield different state for each event. I think you don't notice but it yield LoginLoadingState() before yield either LoginLoggedInState(result) or LoginFailureState(error: "loginUsernamePasswordError")
Current state is LoginInitialState()
Emit event
Yield LoginLoadingState()
State changes from LoginInitialState() to LoginLoadingState()
Yield either LoginLoggedInState() or LoginFailurestate()
State changes from LoginLoadingState() to either LoginLoggedInState() or LoginFailurestate()
Back to step 2 for every event
#Federick Jonathan already given enough explain about the problem but I would like to do addon in this.
First things:
It is the standard behaviour of Equatable, Event listeners got called when state changes occurred. If you yield the same state every time then nothing going to happen.
Let's discussed all possible solutions.
Remove Equatable from bloc then every event trigger when state change.
Define start and end state for the state. For example, Create first state as StartDataUpdate and second as EndDataUpdate.
Refer below code
yield StartDataUpdate();
//Here... Please specified data changes related to operation.
yield EndDataUpdate();
Stream<ReportsState> setupState({required ReportsState state}) async* {
yield StartReportsState();
yield state;
yield EndReportsState();
}
Using:
yield* setupState( state: NavigationState() );