I currently have an interesting issue related to displaying of a snackbar based on a user action.
Above sounds very trivial, but lets elaborate:
I have 2 screens:
The application uses the bloc pattern (with streams/rxdart).
Here is what I want:
I tried several ways of implementing this:
Add a new stream (employeeAdded), and when adding an employee to the employees stream, additionally push a boolean to employee added.
In the list of employees, add a new stream builder and in the builder logic add the snackbar.
This gives all sorts of problems, for example trying to display the snackbar before the page has been (re)build, and so on.
The question is twofold: What is good UX practice for this scenario, and what would be a good solution for this problem?
(will post code on request)
Thanks for the help!
you probably have a parent widget that contains Scaffold as well. Create a scaffoldKey for that and pass it to your child widget that have to show the Snakcbar . Note that you can't use Sanckbar without Scaffold widget.
But it fails with The method showSnackBar was called on null . What's wrong with this code? Scaffold. of() is deprecated for showing SnackBars, use ScaffoldMessenger instead.
Creating SnackBar In Flutter We can create a snackbar in flutter by calling its constructor. There is only one required property to create a snackbar which is content. Usually we will use a Text widget for content since we need to show some message to the user. We can use other widgets instead if we want.
There is ready recipe for showing Snackbar
in Bloc
.
Basically BlocListener
has been created for that purpose.
Please look on https://felangel.github.io/bloc/#/recipesfluttershowsnackbar
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dataBloc = BlocProvider.of<DataBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: BlocListener(
bloc: dataBloc,
listener: (BuildContext context, DataState state) {
if (state is Success) {
Scaffold.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text('Success'),
),
);
}
},
child: YourChild()
),
);
}
}
Wrap your body
(the child of your Scaffold
, in your case returned by _buildBody
) with a Builder
so you get to write some "free" code with the context
.
Now you can listen to the stream with a handler you provide (like you already did), but this time you register the listener only once per Scaffold
build (and not registering a listener at each event occurred in employeesAdded
stream).
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (BuildContext context) {
bloc.employeesAdded.listen(
(_) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Employee added'),
), // SnackBar
);
},
);
return _buildBody(bloc);
},
), // Builder
); // Scaffold
}
@joey I have found a solution but I'm not sure that it's the best. I'm changing my app to bloc pattern now, the same problem as yours occurred so I did this:
In my bloc file I put a controller for the scaffoldState:
ScaffoldState scaffold;
StreamController<ScaffoldState> _scaffoldController = StreamController<ScaffoldState>();
StreamSink<ScaffoldState> get setScaffold => _scaffoldController.sink;
In the constructor of the bloc file I listen to that stream:
LoginBloc(){
_scaffoldController.stream.listen((sc){ scaffold = sc;});
}
In the widget I sink the scaffoldState whenever I want to use it:
onPressed:() {
bloc.setScaffold.add(Scaffold.of(context));
}
so then, I can show snackbars as usual from the bloc:
scaffold.showSnackBar(SnackBar(content: Text(hi world'),),);
UPDATE
I think the best way would for separating the ui and the bloc would be to listen for the stream on the ui (either on initState or didChangeDependencies if you are using a blocprovider).
Although its not my favourite answer, I am also not sure if there is a better alternative; this is what I did as a temporary fix for future watchers:
Create a new stream (in my case employeeAdded), upon adding employees, also create an entry into the stream:
final _employees = BehaviorSubject<List<Employee>>(seedValue: List<Employee>());
final _employeeAdded = BehaviorSubject();
// streams (out)
Observable<List<Employee>> get employees => _employees.stream;
Observable<dynamic> get employeesAdded => _employeeAdded.stream;
addEmployee(Employee employee) async {
final newList = <Employee>[]..addAll(_employees.value)..add(employee);
await _employeeRepo.upsertEmployee(employee);
_employees.add(newList);
_employeeAdded.add(true);
//FIXME: Save to db
}
Note there is no seedValue in employeeAdded, this is to prevent the snackbar from showing on initial load.
In my screen / page I have a scaffold; it's body calls another method which should explain the rest of the code:
Widget _buildBody(EmployeeBloc bloc) {
return StreamBuilder(
stream: bloc.employees,
builder: (context, snapshot) {
if (!snapshot.hasData) {
bloc.employeesAdded.listen(
(_) => Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Employee added'),
),
),
);
bloc.seedEmployees();
return Center(
child: Text("No employees"),
);
}
return _buildList(bloc, snapshot.data);
},
);
}
Note the listen on the bloc with the hasData if.
This works for now, but would like to know if there is a more neat example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With