I'm using the Provider package to manage my apps business logic but I've encountered a problem where my entire ListView is rebuilding instead of an individual ListTile. Here's the UI to give you a better understanding:
Currently if I scroll to the bottom of the list, tap the checkbox of the last item, I see no animation for the checkbox toggle and the scroll jumps to the top of the screen because the entire widget has rebuilt. How do I use Provider so that only the single ListTile rebuilds and not every item in the List?
Here's some of the relevant code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Checklist',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo[500],
accentColor: Colors.amber[500],
),
home: ChecklistHomeScreen(),
),
providers: [
ChangeNotifierProvider(
create: (ctx) => ChecklistsProvider(),
),
],
);
}
}
class ChecklistHomeScreen extends StatefulWidget {
#override
_ChecklistHomeScreenState createState() => _ChecklistHomeScreenState();
}
class _ChecklistHomeScreenState extends State<ChecklistHomeScreen> {
void createList(BuildContext context, String listName) {
if (listName.isNotEmpty) {
Provider.of<ChecklistsProvider>(context).addChecklist(listName);
}
}
#override
Widget build(BuildContext context) {
final _checklists = Provider.of<ChecklistsProvider>(context).checklists;
final _scaffoldKey = GlobalKey<ScaffoldState>();
ScrollController _scrollController =
PrimaryScrollController.of(context) ?? ScrollController();
return Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: false,
title: Text('Your Lists'),
centerTitle: true,
actions: <Widget>[
PopupMenuButton(
itemBuilder: (ctx) => null,
),
],
),
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(ctx, i) => _buildListItem(_checklists[i], i),
childCount: _checklists.length,
),
onReorder: (int oldIndex, int newIndex) {
setState(() {
final checklist = _checklists.removeAt(oldIndex);
_checklists.insert(newIndex, checklist);
});
},
),
],
),
drawer: Drawer(
child: null,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: null,
),
);
}
Widget _buildListItem(Checklist list, int listIndex) {
return Dismissible(
key: ObjectKey(list.id),
direction: DismissDirection.endToStart,
background: Card(
elevation: 0,
child: Container(
alignment: AlignmentDirectional.centerEnd,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
child: Card(
child: ListTile(
onTap: null,
title: Text(list.name),
leading: Checkbox(
value: list.completed,
onChanged: (value) {
Provider.of<ChecklistsProvider>(context)
.toggleCompletedStatus(list.id, list.completed);
},
),
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
),
),
),
onDismissed: (direction) {
_onDeleteList(list, listIndex);
},
);
}
void _onDeleteList(Checklist list, int listIndex) {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<ChecklistsProvider>(context)
.undoDeleteChecklist(list, listIndex);
},
),
content: Text(
'List deleted',
style: TextStyle(color: Theme.of(context).accentColor),
),
),
);
}
}
class ChecklistsProvider with ChangeNotifier {
final ChecklistRepository _repository = ChecklistRepository(); //singleton
UnmodifiableListView<Checklist> get checklists => UnmodifiableListView(_repository.getChecklists());
void addChecklist(String name) {
_repository.addChecklist(name);
notifyListeners();
}
void deleteChecklist(int id) {
_repository.deleteChecklist(id);
notifyListeners();
}
void toggleCompletedStatus(int id, bool completed) {
final list = checklists.firstWhere((c) => c.id == id);
if(list != null) {
list.completed = completed;
_repository.updateChecklist(list);
notifyListeners();
}
}
}
I should say I understand why this is the current behavior, I'm just not sure of the correct approach to ensure only the list item I want to update gets rebuilt instead of the whole screen.
I've also read about Consumer but I'm not sure how I'd fit it into my implementation.
A Consumer will essentially allow you to consume any changes made to your change notifier. It's best practice to embed the Consumer as deep down as possible in your build method. This way only the wrapped widget will get re-built. This document explains it well: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
Try wrapping your CheckBox widget in a Consumer widget. Only the checkbox should be rebuilt.
Consumer<ChecklistsProvider>(
builder: (context, provider, _) {
return Checkbox(
value: list.completed,
onChanged: (value) {
provider.toggleCompletedStatus(list.id, list.completed);
},
);
},
),
If you'd rather have the ListTile AND the CheckBox be re-built, just wrap the ListTile in the Consumer instead
Related
I have a drawer where the user can add items to it which are links to a different page. It works, but if I add an item and then close the drawer, when I open the drawer again that item is gone (other than the default one I have in a list). I have the code below
class SkillDrawer extends StatefulWidget {
#override
State<SkillDrawer> createState() => SkillDrawerState();
}
class SkillDrawerState extends State<SkillDrawer> {
List<Skill> _skills = [
Skill(title: "Test Title 1", pointCap: 500, id: DateTime.now().toString())
];
void _addNewSkill(String sklTitle, double sklPoints) {
final newSkill = Skill(
title: sklTitle,
pointCap: sklPoints,
id: DateTime.now().toString(),
);
setState(() {
_skills.add(newSkill);
});
}
void _startAddNewSkill(BuildContext ctx) {
showModalBottomSheet(
context: ctx,
builder: (bCtx) {
return NewSkill(_addNewSkill);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.blue,
),
child: Text('Subjects'),
),
// ## the container listing added items to drawer
Container(
height: double.maxFinite,
child: ListView.builder(
itemBuilder: (ctx, index) {
return ListTile(
title: Text(_skills[index].title),
onTap: () =>
Navigator.of(ctx).pushNamed('/skills', arguments: {
'id': _skills[index].id,
'title': _skills[index].title,
}));
},
itemCount: _skills.length,
))
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: (() => _startAddNewSkill(context)),
),
);
}
}
I want to make it so that the items are not cleared when the drawer is closed, until the app is closed of course.
I have an app that makes a post request to an API with data about drivers and orders. The initial page displays a list of drivers in individual list tiles. The list tile has a drop down option. Clicking on that option brings you to a new page with a list view of orders for that driver. Clicking on an individual order brings you to a form. On submitting and validating this form, I want to change the color of that orders text from red to green. Each Order has a submitted flag, and when it submits I would want to change that to true and then have the color change. When all the orders are green within an List View, I want the color of that driver to turn green. I've been going over riverpod tutorials and documentation but can't quite figure out how to get this done. Can someone point me in the right direction?
main.dart
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: DriversPage(),
);
}
}
drivers.dart - This is where the drivers are displayed
class DriversPage extends StatelessWidget {
final HttpService httpService = HttpService();
var colorChoice = Colors.red;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFAFAFA),
appBar: AppBar(
title: Text("Drivers")
),
body: Container(
child: FutureBuilder(
future: httpService.getOrders(),
builder: (BuildContext context, AsyncSnapshot<List<Order>> snapshot) {
if (snapshot.hasData) {
List<Order> orders = snapshot.data;
return ListView(
children: orders.map((Order order) => Card(child: ExpansionTile(
title: Text(order.driver, style: TextStyle(color: colorChoice),),
children: <Widget>[
Container(
alignment: Alignment.center,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.0),
border: Border.all(color: Colors.black26)
),
child: ListTile(title: Text("Orders"),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => OrdersState(driverName: order.driver, driverOrders: order.orders))),),
),
],
))).toList(),
);
}
return Center(child: CircularProgressIndicator());
}),
));
}
}
orders.dart - This is where the orders for a driver are displayed. I originally had it as a stateful widget but turned it into a Consumer Widget and took an attempt at making a provider but was lost on how to handle it in a listview like this. As you can see here I am using the ternary operator for the text color based on item.submitted
final driverListProvider = StateNotifierProvider((ref) => new DriverListTest());
class OrdersState extends ConsumerWidget {
final String driverName;
final List<OrderElement> driverOrders;
const OrdersState({Key key, this.driverName, this.driverOrders}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return Scaffold(
appBar: AppBar(
title: Text(driverName),
),
body: ListView.builder(
itemCount: driverOrders.length,
itemBuilder: (context, index){
final item = driverOrders[index];
return Card(
key: UniqueKey(),
child: ListTile(title: Text(item.order, style: TextStyle(color: item.submitted? Colors.green : Colors.red),),
subtitle: Text('${item.company}\n${item.address}'),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => OrderForm(orderTitle: item.order,))),));
}),
);
}
}
orderform.dart - Only showing one field for the form, figured the rest was not neccessary, just need to show what happens on submit.
class OrderForm extends StatefulWidget {
final String orderTitle;
const OrderForm({this.orderTitle});
#override
_OrderFormState createState() => _OrderFormState();
}
class _OrderFormState extends State<OrderForm> {
#override
final _formKey = GlobalKey<FormState>();
final _orderModel = Order();
List<String> _pickerNames = ['Loader 1', 'Loader 2', 'Loader 3', 'Loader 4'];
String _selectedPickedBy;
String _selectedCheckedBy;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.blueGrey,title: Center(
child: Text(widget.orderTitle),
),),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.delete
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
scrollable: true,
title: Text('Login'),
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'Reason',
icon: Icon(Icons.account_box),
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Reason 1',
icon: Icon(Icons.email),
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Reason 2',
icon: Icon(Icons.message),
),
),
],
),
),
),
actions: [
RaisedButton(
child: Text("Submit"),
onPressed: () {
})
],
);
});
}
),
body: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
child: Builder(
builder: (context) => Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField(
isExpanded: true,
hint: Text('Picked By'),
value: _selectedPickedBy,
onChanged: (newValue){
setState(() {
_selectedPickedBy = newValue;
});
},
validator: (value) => value == null
? 'Picked By Required' : null,
items: _pickerNames.map((picker) {
return DropdownMenuItem(
child: new Text(picker),
value: picker,
);
}).toList(),
onSaved: (value) => setState(() => _orderModel.pickedBy = value) ,
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 16.0, horizontal: 16.0
),
child: RaisedButton(
onPressed: (){
final form = _formKey.currentState;
if (form.validate()){
form.save();
Navigator.pop(context,);
}
},
child: Text("Submit"),
),
)
],
)),
),
)
);
}
}
ordermodel.dart - This is the model for the drivers and orders when making http requests to my api. At the bottom you can see where I attempt at making a statenotifier and what I'm trying to with accepting a list of OrderElement(The list of orders).
List<Order> orderFromJson(String str) => List<Order>.from(json.decode(str).map((x) => Order.fromJson(x)));
String orderToJson(List<Order> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class Order {
Order({
this.driver,
this.orders,
});
String driver;
List<OrderElement> orders;
factory Order.fromJson(Map<String, dynamic> json) => Order(
driver: json["Driver"],
orders: List<OrderElement>.from(json["Orders"].map((x) => OrderElement.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"Driver": driver,
"Orders": List<dynamic>.from(orders.map((x) => x.toJson())),
};
}
class OrderElement {
OrderElement({
this.order,
this.company,
this.address,
this.submitted,
this.index,
});
String order;
String company;
String address;
bool submitted;
num index;
factory OrderElement.fromJson(Map<String, dynamic> json) => OrderElement(
order: json["Order"],
company: json["Company"],
address: json["Address"],
submitted: json["submitted"],
index: json["index"]
);
Map<String, dynamic> toJson() => {
"Order": order,
"Company": company,
"Address": address,
};
}
class DriverListTest extends StateNotifier<List<OrderElement>> {
DriverListTest([List<OrderElement> drivers1]) : super(drivers1 ?? []);
void onSubmit(num index) {
state = [
for(final currentOrder in state)
if (currentOrder.index == index)
OrderElement(
order: currentOrder.order,
company: currentOrder.company,
address: currentOrder.address,
submitted: !currentOrder.submitted,
index: currentOrder.index,
)
else
currentOrder,
];
}
}
Don't know if my Http class is necessary but let me know if it is. I tried following https://www.refactord.com/guides/riverpod-state-management-explained and How to set the state of a widget at an index in a listview.builder in flutter how to handle individual widgets but again I just got lost. Any help would be greatly appreciated! Thanks in advance.
I want to display a SnackBar in my Flutter app. I have read the docs and copyed it:
The body of my scaffold:
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Osztályok"),
leading: Padding(
padding: const EdgeInsets.only(left: 5.0),
child: IconButton(
icon: Icon(Icons.exit_to_app, color: Colors.white70),
onPressed: () {
authService.signOut();
authService.loggedIn = false;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GoogleSignUp()));
})),
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 5.0),
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.add_circle_outline,
color: Colors.white70),
onPressed: () {
createPopup(context);
}),
// IconButton(
// icon: Icon(Icons.search, color: Colors.black38),
// onPressed: null),
],
)),
],
),
The SnackBarPage class:
class SnackBarPage extends StatelessWidget {
void jelszopress(TextEditingController jelszoController, BuildContext context) async{
var jelszo;
DocumentReference docRef =
Firestore.instance.collection('classrooms').document(globals.getid());
await docRef.get().then((value) => jelszo= (value.data['Jelszo']) );
if (jelszo == jelszoController.text.toString()){
Navigator.push(context,
MaterialPageRoute(builder: (context) => InClassRoom()));
}
else{
Navigator.pop(context);
final snackBar = SnackBar(content: Text('Yay! A SnackBar!'));
Scaffold.of(context).showSnackBar(snackBar);
}
}
Future<String> jelszoba(BuildContext context) {
TextEditingController jelszoController = TextEditingController();
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Add meg a jelszót'),
content: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: TextField(
controller: jelszoController,
decoration: InputDecoration(hintText: "Jelszó")
)
),
actions: <Widget>[
MaterialButton(
elevation: 5.0,
child: Text('Mehet'),
onPressed: () {
jelszopress(jelszoController, context);
},
)]);
}
);
}
var nevek;
var IDS;
SnackBarPage(this.nevek, this.IDS);
#override
Widget build(BuildContext context){
return ListView.builder(
itemCount: nevek.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {
globals.setid(IDS[index]);
jelszoba(context);
},
title: Text(nevek[index]),
),
);
},
) ;
}
}
But my cody doesn't display the SnackBar. I tried the solution of this question: How to properly display a Snackbar in Flutter? but adding a Builder widget didn't help.
"Scaffold.of(context)" has been deprecated, will return null. Now use "ScaffoldMessenger.of(context)". As per Flutter documentation.
#override
Widget build(BuildContext context) {
// here, Scaffold.of(context) returns null
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: const Text('snack'),
duration: const Duration(seconds: 1),
action: SnackBarAction(
label: 'ACTION',
onPressed: () { },
),
));
},
child: const Text('SHOW SNACK'),
),
),
);
}
NOTE: Make sure your main.dart overrided build() function should return "MaterialApp" as a widget, such as:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Must be MaterialApp widget for ScaffoldMessenger support.
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyDashboard(),
);
}
}
So based on the error, it would seem that the context passed in Snackbar.of() is not the correct context. This would make sense based on 1 & 2; and summary copied below:
Each widget has its own BuildContext, which becomes the parent of the widget returned by the StatelessWidget.build or State.build function. (And similarly, the parent of any children for RenderObjectWidgets.)
In particular, this means that within a build method, the build context of the widget of the build method is not the same as the build context of the widgets returned by that build method.
So this means that the build context you are passing in jelszoba(context) function is not the build context you need and is actually the build context of the widget that is instantiating the Scaffold.
So How to Fix:
To fix this wrap your Card widget in your SnackbarPage in a Builder widget and pass the context from it, to the jelszoba(context) method.
An example from 1 I post below:
#override
Widget build(BuildContext context) {
// here, Scaffold.of(context) returns null
return Scaffold(
appBar: AppBar(title: Text('Demo')),
body: Builder(
builder: (BuildContext context) {
return FlatButton(
child: Text('BUTTON'),
onPressed: () {
// here, Scaffold.of(context) returns the locally created Scaffold
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('Hello.')
));
}
);
}
)
);
}
You can normally use snack bar in the Bottom Navigation bar in this way. However, if you want to show it in the body, then just copy the code from Builder and paste it in the body of the scaffold.
Scaffold(bottomNavigationBar: Builder(builder: (context) => Container(child: Row(children: <Widget>[
Icon(Icons.add_alarm), Icon(Icons.map), IconButton(icon: Icon(Icons.bookmark),
onPressed:() {
Scaffold.of(context).showSnackBar(mySnackBar);
final mySnackBar = SnackBar(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.white, duration: Duration(seconds: 1),
content: Text(
'Article has been removed from bookmarks',
),);
}
),
],
),
),
),
);
Note: In the behaviour property of SnackBar, you can just leave it empty. But the problem with that is "If you have Curved Navigation Bar or you have a floating action button above the bottom navigation bar, then the snackbar will lift these icons (or FAB ) and will affect the UI". That's why SnackBar.floating is more preferred as it is more capatible with the UI.
But you can check and see on your own which suits you the best.
I get this error when I run the app. I have followed a youtube tutorial but in my case, the error A non-null String must be provided to a Text widget always comes up. I've already tried multiple things but nothing happened. I don't know what else I can write... but I can't post the question without giving more detailed information.
How can I solve this problem?
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() => runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
accentColor: Colors.orange
),
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List todos = List();
String input = "";
createTodos() {
DocumentReference documentReference =
Firestore.instance.collection("MyTodos").document(input);
//Map
Map<String, String> todos = {"todoTitle": input};
documentReference.setData(todos).whenComplete(() {
print("$input created");
});
}
deleteTodos() {
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My ToDos'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Add ToDo', style: TextStyle(fontWeight: FontWeight.bold),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)
),
content: TextField(
onChanged: (String value) {
input = value;
},
),
actions: <Widget>[
FlatButton(
onPressed: () {
createTodos();
Navigator.of(context).pop();
},
child: Text('Add'))
],
);
});
},
child: Icon(
Icons.add,
color: Colors.white,
),
),
body: StreamBuilder(
stream: Firestore.instance.collection("MyTodos").snapshots(),
builder: (context, snapshots){
if(snapshots.data == null) return CircularProgressIndicator();
return ListView.builder(
shrinkWrap: true,
itemCount: snapshots.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot documentSnapshot = snapshots.data.documents[index];
return Dismissible(
key: Key(index.toString()),
child: Card(
elevation: 4,
margin: EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
child: ListTile(
title: Text(documentSnapshot["todoTitle"]),
trailing: IconButton(
icon: Icon(
Icons.delete),
color: Colors.red,
onPressed: (){
setState(() {
todos.removeAt(index);
});
} ),
),
));
});
}),
);
}
}
Your documentSnapshot["todoTitle"] is returning null and you shouldn't provide any null value to Text widget. So, a solution would be to use something like
Text(documentSnapshot["todoTitle"] ?? "No title found")
I'm using the Flutter BLoC package for manage the state of my app, but since last package update (v0.22.1) I have a strange behavior.
In simple I have a StreamSubscription listener on PositionBloc's state in EventsBloc but when I add an event from PositionBloc the listener not work.
The strangest thing is that the first time I add the event from EventsBloc the listener work, but after seems to be completely ignored.
This is my build tree:
main.dart: Here I show home page or the login page based on authentication. AuthenticationBloc and MoviesBloc aren't important or LocalsBloc the focus is on PositionBloc.
class App extends StatelessWidget {
...
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) => MoviesBloc()..add(LoadMovies()),
child: MaterialApp(
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
if (state is Unauthenticated) {
return Login(
authenticationRepository: _authenticationRepository,
);
}
if (state is Authenticated) {
return MultiBlocProvider(
providers: [
BlocProvider<LocalsBloc>(
builder: (context) => LocalsBloc()..add(LoadLocals()),
),
BlocProvider<PositionBloc>(
builder: (context) => PositionBloc(
localsBloc: BlocProvider.of<LocalsBloc>(context),
),
)
],
child: Home(),
);
}
return Splash();
},
),
),
);
}
}
home.dart: This is a StatefulWidget for manage TabBarView and here I provide EventsBloc to the widget EventsCarousel. In SearchBar widget I have the trigger for dispatch event and so rebuild the EventsCarousel based on selected position.
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
...
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: TabBarView(
children: <Widget>[
Stack(
alignment: Alignment.topCenter,
children: <Widget>[
BlocProvider<EventsBloc>(
builder: (context) => EventsBloc(),
child: EventsCarousel(),
),
Positioned(
child: SearchBar(),
),
],
),
MoviesList(),
Settings(),
],
controller: _tabController,
),
),
bottomNavigationBar: Material(
child: TabBar(
indicatorColor: Colors.deepOrange,
tabs: <Tab>[
Tab(
icon: Icon(
FontAwesomeIcons.home
),
),
Tab(
icon: Icon(
FontAwesomeIcons.bullhorn,
),
),
Tab(
icon: Icon(
FontAwesomeIcons.cogs,
),
),
],
controller: _tabController,
),
),
);
}
search_bar.dart: This is a StatefulWidget for manage _textController and _focusNode. Here I have my app state changer. I use Flutter TypeAhead package for build the search bar and the entries of my search bar is provided from LocalsBloc that we see in main.dart. When I tap on an entry I add the event UpdatePosition(local) to PositionBloc.
class _SearchBarState extends State<SearchBar> {
...
#override
Widget build(BuildContext context) {
return BlocBuilder<PositionBloc, PositionState>(
builder: (context, state) {
if (state is PositionUpdated) {
if (!_focusNode.hasFocus) {
_textController.text = state.position.local.description;
}
return Container(
decoration: BoxDecoration(
color: Colors.white,
),
width: MediaQuery.of(context).size.width * 0.75,
child: TypeAheadField<Local>(
getImmediateSuggestions: true,
textFieldConfiguration: TextFieldConfiguration(
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0),
),
),
hintText: "Inserisci una posizione",
contentPadding: EdgeInsets.all(15),
),
focusNode: _focusNode,
controller: _textController,
),
suggestionsCallback: (String pattern) {
return (BlocProvider.of<LocalsBloc>(context).state as LocalsLoaded)
.locals
.where(
(local) =>
local.description
.toLowerCase()
.contains(pattern.toLowerCase()),
)
.toList();
},
noItemsFoundBuilder: (context) {
return Text('Nessun risultato trovato');
},
itemBuilder: (context, Local suggestion) {
return Card(
child: ListTile(
title: Text(suggestion.description),
subtitle: Text(suggestion.town),
trailing: Icon(Icons.gps_not_fixed),
),
);
},
onSuggestionSelected: (Local suggestionSelected) {
BlocProvider.of<PositionBloc>(context).add(
UpdatePosition(suggestionSelected),
);
}),
);
} else {
return Container();
}
},
);
}
I have standards Bloc boilerplate for events and states and in particular for EventsBloc as I said I have this subscription:
EventsBloc({#required PositionBloc positionBloc})
: assert(PositionBloc != null),
_positionBloc = positionBloc {
positionSubscription = positionBloc.listen((state) {
if (state is PositionUpdated) {
add(LoadEvents(state.local));
}
});
This listener not work after the first widgets tree build, but when I add event UpdatePosition from selection of an entry in the search bar the state of PositionBloc change and the event is correctly mapped to PositionUpdated. This is a very strange situation, can help me?
I just noticed that I had missed a super-call in the CTORs initializer for my PositionUpdated state so the listener was not invoked. This is my mistake and I had been following it for three days in debug.