flutter_bloc subwidget closes the bloc of the parent widget - flutter

I am using the flutter_bloc package.
I have a page with a tabbar.
The page creates a bloc and provides it to the widgets, which are used as tab content.
class _MyScreenState extends State<MyScreen> {
MyBloc _myBloc;
...
Widget _buildTabBar() {
...
var children = [
_buildFirstTab()
_buildSecondTab(),
];
return WillPopScope(
onWillPop: _onWillPop,
child: DefaultTabController(
length: children.length,
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(75.0),
child: AppBar(
bottom: TabBar(isScrollable: true, tabs: tabs),
)),
body: TabBarView(
children: children,
),
),
),
);
}
..
}
Widget _buildSecondTab() {
if (_myBloc == null) {
_myBloc = MyBloc(<..initialdata..>);
}
_myBloc.add(LoadServerDataEvent());
return BlocProvider<MyBloc>(create: (_) => _myBloc, child: SecondTab());
}
The widget on the second tab gets the bloc from the BlocProvider and uses it.
class _SecondTabState extends State<SecondTab> {
MyBloc _myBloc;
#override
void initState() {
_myBloc = BlocProvider.of<MyBloc>(context);
super.initState();
}
My problem is that the block is automatically closed when I switch between the tabs.
This is kind of strange, because the block is still used on the page.

Well, you are doing a couple of things wrong:
if you want your bloc to be accessible in both first and second tab you should wrap the whole tab bar with it,
that's very odd that you keep an instance of a bloc in _MyScreenState and do some magic with lazy init singeltone if (_myBloc == null) _myBloc = MyBloc(),
The _myBloc.add(LoadServerDataEvent()); in most cases should be triggered in initState method of the _SecondTabState .
That's very unusal that a bloc gets initial data from the UI. If you want to build your architecture using blocs this initial data should come from another bloc.
But if you want to provide an existing bloc to the second tab and prevent it from being closed you should use:
BlocProvider.value(
value: _myBloc,
child: SecondTab(),
);
Instead:
BlocProvider<MyBloc>(create: (_) => _myBloc, child: SecondTab());

Related

Are states not passed to BlocBuilder widgets that are children of a BlocConsumer widget?

I am new to Flutter Bloc and must be missing how State changes are processed by the UI widgets. At the top level I have a BlocConsumer and under that I have nested BlocBuilder widgets with buildWhen methods to indicate when and how the Bloc widget should be rebuilt. Based on print statements,it looks like the Bloc state is consumed in the top level BlocConsumer widget and never makes it down to the lower level BlocBuilder widgets.
The code below should
Display circular progress bar on startup - this works ok
Call a bunch of APIs - This is happening
In the meantime display the initial screen with default text values in various widgets - this happens
As API returns and Bloc passes states on the stream, the appropriate UI widget should be rebuilt replacing default text with the data in the stream object. -- this doesn't happen.
Code snippets:
RaspDataStates issued by Bloc (Just showing for reference. Not showing all subclasses of RaspDataState):
#immutable
abstract class RaspDataState {}
class RaspInitialState extends RaspDataState {
#override
String toString() => "RaspInitialState";
}
class RaspForecastModels extends RaspDataState {
final List<String> modelNames;
final String selectedModelName;
RaspForecastModels(this.modelNames, this.selectedModelName);
}
...
Bloc just to show how initialized. Code all seems to work fine and isn't shown.
class RaspDataBloc extends Bloc<RaspDataEvent, RaspDataState> {
RaspDataBloc({required this.repository}) : super(RaspInitialState());
#override
RaspDataState get initialState => RaspInitialState();
...
Now to the UI widget.
class SoaringForecast extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<RaspDataBloc>(
create: (BuildContext context) =>
RaspDataBloc(repository: RepositoryProvider.of<Repository>(context)),
child: RaspScreen(repositoryContext: context),
);
}
}
class RaspScreen extends StatefulWidget {
final BuildContext repositoryContext;
RaspScreen({Key? key, required this.repositoryContext}) : super(key: key);
#override
_RaspScreenState createState() => _RaspScreenState();
}
class _RaspScreenState extends State<RaspScreen>
with SingleTickerProviderStateMixin, AfterLayoutMixin<RaspScreen> {
// Executed only when class created
#override
void initState() {
super.initState();
_firstLayoutComplete = false;
print('Calling series of APIs');
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
_mapController = MapController();
}
#override
void afterFirstLayout(BuildContext context) {
_firstLayoutComplete = true;
print(
"First layout complete. mapcontroller is set ${_mapController != null}");
_setMapLatLngBounds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: AppDrawer.getDrawer(context),
appBar: AppBar(
title: Text('RASP'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: null),
],
),
body: BlocConsumer<RaspDataBloc, RaspDataState>(
listener: (context, state) {
print('In forecastLayout State: $state'); << Can see all streamed states here
if (state is RaspDataLoadErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text(state.error),
),
);
}
}, builder: (context, state) {
print('state is $state'); << Only see last streamed state here
if (state is RaspInitialState || state is RaspDataLoadErrorState) {
print('returning CircularProgressIndicator');
return Center(
child: CircularProgressIndicator(),
);
}
print('creating main screen'); << Only see this when all streams complete
return Padding(
padding: EdgeInsets.all(8.0),
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: [
getForecastModelsAndDates(),
getForecastTypes(),
displayForecastTimes(),
returnMap()
]));
}));
}
Widget getForecastModelsAndDates() {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: forecastModelDropDownList(), // ForecastModelsWidget()
),
Expanded(
flex: 7,
child: Padding(
padding: EdgeInsets.only(left: 16.0),
child: forecastDatesDropDownList(),
)),
],
);
}
// Display GFS, NAM, ....
Widget forecastModelDropDownList() {
return BlocBuilder<RaspDataBloc, RaspDataState>(
buildWhen: (previous, current) {
return current is RaspInitialState || current is RaspForecastModels;
}, builder: (context, state) {
if (state is RaspInitialState || !(state is RaspForecastModels)) {
return Text("Getting Forecast Models");
}
var raspForecastModels = state;
print('Creating dropdown for models');
return DropdownButton<String>(
value: (raspForecastModels.selectedModelName),
isExpanded: true,
iconSize: 24,
elevation: 16,
onChanged: (String? newValue) {
BlocProvider.of<RaspDataBloc>(context)
.add(SelectedRaspModel(newValue!));
},
items: raspForecastModels.modelNames
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value.toUpperCase()),
);
}).toList(),
);
});
}
... more BlocBuilder child widgets similar to the one above
The print statements in the console are:
Calling series of APIs
state is RaspInitialState
returning CircularProgressIndicator
First layout complete. mapcontroller is set true
... (First of bunch of API output displays - all successful)
state is RaspInitialState << Not sure why this occurs again
returning CircularProgressIndicator
... (More API output displays - all successful)
streamed RaspForecastModels
In forecastLayout State: Instance of 'RaspForecastModels' << Doesn't cause widget to be rebuild
streamed RaspForecastDates << Other states being produced by Bloc
In forecastLayout State: Instance of 'RaspForecastDates'
streamed RaspForecasts
In forecastLayout State: Instance of 'RaspForecasts'
In forecastLayout State: Instance of 'RaspForecastTime'
streamed RaspMapLatLngBounds
In forecastLayout State: Instance of 'RaspMapLatLngBounds'
state is Instance of 'RaspMapLatLngBounds'
creating main screen
Any words of wisdom on the errors of my way would be appreciated.
I added this earlier as a comment but then found Stackoverflow didn't initially show my comment (I needed to click on show more). So here it is in better readable form.
Problem solved. I needed to move the line:
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
from the initState() method to afterFirstLayout().
All blocbuilders then executed and the UI was built appropriately . And to answer my title question, the bloc states are broadcast and can be picked up by different BlocBuilders.

Update Scaffold-Properties in nested CupertinoPageScaffold in Flutter

How can I update some properties of the roots scaffold in a child widget(page).
Here is a snippet from my root scaffold.
CupertinoPageScaffold(
resizeToAvoidBottomInset:
state.resizeToAvoidBottomInsets, //update this here
child: CupertinoTabScaffold(
controller: _tabController,
tabBar: CupertinoTabBar(
onTap: onTap,
items: widget.items,
),
tabBuilder: (BuildContext context, index) {
return StatusBarPadding(child: _tabs[index]);
}),
),
The docs say, I should add a listener to avoid a nested scaffold (e.g. to update resizeToAvoidBottomInset).
However, this does only work for one page per tab. When I nest tabs, I can't access them directly anymore.
I tried two solutions which I will explain in the follow (+problems):
Solution 1: Provider
I used a Provider to keep track of a global Navbar State:
class NavbarState extends ChangeNotifier {
bool _resizeBottom;
NavbarState(this._resizeBottom);
get resizeBottom => _resizeBottom;
void setResizeBottom(bool state) {
_resizeBottom = state;
notifyListeners();
}
}
Then In my Pages I set the state in the initState-Method with BlocProvider.of<NavbarState>(context).setResizeBottom(val) (respective for dispose).
This has 2 problems:
Calling notifyListeners triggers a setState in the consumer and you can't call setState in the initState method.
I have to declare this in every initState and dispose method.
Solution 2: Bloc
Once again I have a global state, but it does not have to inherit from `ChangeNotifier`. I track the state with a `NavbarBloc`-class.
Then I can add an event in the onGenerateRoute method. This is more handy then the provider approach, because there is just one place where I manage this state.
However, there is still a big problem:
When I navigate back, the onGenerateRoute Method does not get called and hence the state is not getting updated.
What the easiest solution would be
At least from an app-developer perspective it would be nice if I could just ask for the the current widget which is sitting in the active navigator.
Example of a Navbar
Here is an illustration of 3 navigators for the given cupertinotabscaffold.
The middle "stack" is active and the topmost widget is seen on the screen. Thus, currently the resize param should be false. On navigating between the stacks (tapping navigation icon), the resize parameter should adjust. Furthermore, on navigating in between a single stack (push, pop) should also adjust the resize param (E.g. on a pop the param should be set to true).
I couldn't find anything like that. Thus I need your help.
For setting the state how about a setter as a callback to onTap?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var values = [
[true, false],
[false, true],
];
int stackIndex = 0;
bool _resizeToAvoidBottomInsets = true;
set _resizeToAvoidButtomInsets(bool value) => setState(() {
print("set _resizeToAvoidBottomInsets = $value");
_resizeToAvoidBottomInsets = value;
});
void handleTap(int i) {
print("tapped: $i");
_resizeToAvoidBottomInsets = values[stackIndex][i];
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
resizeToAvoidBottomInset: _resizeToAvoidBottomInsets,
child: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.ac_unit)),
BottomNavigationBarItem(icon: Icon(Icons.wb_sunny)),
],
onTap: handleTap,
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 1 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: Text(
'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
),
onPressed: () {
// set state and increment stack index before push
stackIndex++;
_resizeToAvoidButtomInsets = values[stackIndex][0];
Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 2 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: Text(
'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
),
onPressed: () {
// set state and decrement stack index before pop
stackIndex--;
_resizeToAvoidButtomInsets =
values[stackIndex][0];
Navigator.of(context).pop();
}),
),
);
},
),
);
},
),
),
);
},
);
},
),
);
}
}
So I implemented a solution for the given problem. I don't think this is very smooth, but at least one can abstract the scaffold-properties from each single page.
I use a bloc for that as already mentioned in the question-solution-try-2.
The bloc emits a state MyScaffoldState which contains a ScaffoldProperties attribute. For this toy example I implemented it as follows:
class ScaffoldProperties {
bool resizeBottom;
String title;
ScaffoldProperties({this.resizeBottom = false, required this.title});
ScaffoldProperties copyWith({ScaffoldProperties? state}) {
return ScaffoldProperties(
resizeBottom: state?.resizeBottom ?? resizeBottom,
title: state?.title ?? title,
);
}
}
You could add any scaffold-property to this class.
The main work happens in my MultiTabScaffold class. This class takes the parameters
List<Widget> pages;
List<BottomNavigationBarItem> items;
Route<dynamic>? Function(RouteSettings)? onGenerateRoute;
List<NavigatorObserver> navigatorObservers
And most importantly
Function<ScaffoldProperties> getScaffoldPropertiesFromRouteName(String? route);
The class wraps the necessary Scaffolds around the pages. Additionally, it uses a BlocBuilder and thus can use the ScaffoldProperties of the current state.
The getScaffoldPropertiesFromRouteName Method takes a route and returns a ScaffoldProperties-Instance. Thus all title, resizeProperties,... have to be collected here.
To update the current state (or emit events to the bloc), I had to modify a few points.
onGenerateRoute: After generating the route (pushing to navigator), the ScaffoldProperties of the current page have to be emitted (with usage of the getScaffoldPropertiesFromRouteName Function)
WillPopScope: Also after popping the page, I emit an event with the properties of the page below.
Initial Value: Initially, when you open the app, neither onGenerateRoute nor willpop will be called. Thus, you have to have a different handling for the initial route '/' of every Navigator.
I have a toy example fully implemented.
This feels like a lot of work for such a simple use case, but I couldn't find a simpler way to do this. If there is one, please let me know.

Navigate part of screen from drawer

let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.

Animating between provider stream changes

I have a pageview in flutter with 5 pages, each with its own scaffold. All states are managed through providers of streams or values. I have a stream that has it's own built in method which passes InternetConnected.connected or disconnected. When the internet connection is lost, I want to load a separate UI in the specific page which shows internet connection lost instead of the widgets that were previously present in the scaffold.
How I'm doing it now (pseudo code):
class ConnectionScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final connection = Provider.of<InternetStatus>(context);
detectConnection () {
if (connection.InternetConnection == InternetConnection.connected)
return Container(); // Returns UI when internet on
else
return Container(); // Returns disconnected UI
}
return Scaffold(body: detectConnection());
}
Two Questions:
I want to animate the transition between the two states ie. Connected and Disconnected with the disconnection screen flowing down from the top of the display and vice versa. What is the correct way to do that with provider state management? Right now it just rebuilds instantaneously, which is not very 'pretty'.
Since Provider.of<> does not allow granular rebuilds in case the stream value changes, how would I make it so that other properties 'provided' by provider are handled better? I know about Consumer and Selector, but those are rebuilding the UI too...
Thanks in advance
Animation
An AnimatedSwitcher widget (included in SDK, not related to Provider) might suffice to animate between your two widgets showing connected / disconnected states. (AnimatedContainer might work as well if you're just switching a color or something else in the constructor argument list of Container.)
A key is needed for the child of AnimatedSwitcher when the children are of the same class, but differ internally. If they're completely different Types, Flutter knows to animate between the two, but not if they're the same Type. (Has to do with how Flutter analyzes the widget tree looking for needed rebuilds.)
Rebuild Only Affected Widgets
In the example below, the YellowWidget isn't being rebuilt, and neither is its parent. Only the Consumer<InternetStatus> widget is being rebuilt when changing from Connected to Disconnected statuses in the example.
I'm not an expert on Provider and I find it easy to make mistakes in knowing which Provider / Consumer / Selector / watcher to use to avoid unnecessary rebuilds. You might be interested in other State Management solutions like Get, or RxDart+GetIt, etc. if Provider doesn't click for you.
Note: an extra Builder widget is used as parent to the child of ChangeNotifierProvider, to make everything underneath a child. This allows InheritedWidget to function as intended (the base upon which Provider is built). Otherwise, the child of ChangeNotifierProvider would actually share its context and be its sibling, not descendant.
i.e. They'd both get the context shown here:
class ProviderGranularPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
This is also a tricky nuance of Flutter. If you wrap your entire MaterialApp or MyApp widget in Provider, this extra Builder is obviously unnecessary.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InternetStatus extends ChangeNotifier {
bool connected = true;
void setConnect(bool _connected) {
connected = _connected;
notifyListeners();
}
}
/// Granular rebuilds using Provider package
class ProviderGranularPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<InternetStatus>(
create: (_) => InternetStatus(),
child: Builder(
builder: (context) {
print('Page (re)built');
return SafeArea(
child: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 3,
child: Consumer<InternetStatus>(
builder: (context, inetStatus, notUsed) {
print('status (re)built');
return AnimatedSwitcher(
duration: Duration(seconds: 1),
child: Container(
key: getStatusKey(context),
alignment: Alignment.center,
color: getStatusColor(inetStatus),
child: getStatusText(inetStatus.connected)
),
);
},
),
),
Expanded(
flex: 3,
child: YellowWidget(),
),
Expanded(
flex: 1,
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Connect'),
onPressed: () => setConnected(context, true),
),
RaisedButton(
child: Text('Disconnect'),
onPressed: () => setConnected(context, false),
)
],
),
),
)
],
),
),
);
},
),
);
}
/// Show other ways to access Provider State, using context & Provider.of
Key getStatusKey(BuildContext context) {
return ValueKey(context.watch<InternetStatus>().connected);
}
void setConnected(BuildContext context, bool connected) {
Provider.of<InternetStatus>(context, listen: false).setConnect(connected);
}
Color getStatusColor(InternetStatus status) {
return status.connected ? Colors.blue : Colors.red;
}
Widget getStatusText(bool connected) {
String _text = connected ? 'Connected' : 'Disconnected';
return Text(_text, style: TextStyle(fontSize: 25));
}
}
class YellowWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Yellow was (re)built');
return Container(
color: Colors.yellow,
child: Center(
child: Text('This should not rebuild'),
),
);
}
}

How to Change AppBar 'title:' Value without Re-running FirebaseAnimatedList

I am using FirebaseAnimatedList in a Flutter/Dart chat app. The simplified build() method is as follows:
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text(_onLineStatus), // <-- This text value changed using setState()
),
body: Column(
children: <Widget>[
Flexible(
child: FirebaseAnimatedList(
query: reference,
sort: (a, b) => b.key.compareTo(a.key),
padding: EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (BuildContext context, DataSnapshot snapshot,
Animation<double> animation, int index) {
return ChatMessage(snapshot: snapshot, animation: animation);
},
),
),
Divider(height: 1.0),
Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
)
],
));
}
I want to change the value of _onLineStatus on line 5 based on an event value returned by a listener, basically to indicate if the other chat participant in on or off line. I want any change to the status to reflect immediately. The obvious way to do this is to use setState() but of course this triggers a full rerun of the build() method which therefore reruns the FirebaseAnimatedList query, downloading the same data once again. I want to avoid this.
All examples of the use of FirebaseAnimatedList show it as part of the build() method yet we are recommended to avoid putting database calls into build() to avoid these side-effects, because build() can be run multiple times.
My questions therefore are:
How can I move the call to FirebaseAnimatedList outside of the build() method so that I can use setState() to update the value of the AppBar title: property WITHOUT it rerunning FirebaseAnimatedList?
OR...
How can I update the value of the AppBar title: property without rerunning the build() method ie. without calling setState()?
Create a StateFullWidget containing your app bar.
Something like this:
Widget build(BuildContext context) {
return new Scaffold(
appBar: CustomAppBar(),
body: Column(
...
And then your new appbar widget:
class CustomAppBar extends StatefulWidget {
#override
_CustomAppBarState createState() => _CustomAppBarState();
}
class _CustomAppBarState extends State<CustomAppBar> {
String _onLineStatus = "online";
#override
Widget build(BuildContext context) {
return AppBar(
title: Text(_onLineStatus),
);
}
}
in this way you can independently rebuild the appbar from the list. You need to call setState in your new widget.