list length from StreamBuilder? - dart

In the header of my flutter app, I want to display a Chip with the number of items in the list displayed in the body. The build method of the body is using a StreamBuilder to create the list.
The problem is that the length of the list isn't known until after the AppBar is built and the StreamBuilder finishes building the list.
Since I can't call setState() from within the build function, how can I get the value in the AppBar to update after the StreamBuilder is finished building the list?
I'm working through this example. Here's the code snippet:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Streams'),
elevation: 1.0,
),
body: Container(
child: _buildContent(),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _createCounter,
),
);
}
Widget _buildContent() {
return StreamBuilder<List<Counter>>(
stream: stream,
builder: (context, snapshot) {
return ListItemsBuilder<Counter>(
items: snapshot.hasData ? snapshot.data : null,
itemBuilder: (context, counter) {
return CounterListTile(
key: Key('counter-${counter.id}'),
counter: counter,
onDecrement: _decrement,
onIncrement: _increment,
onDismissed: _delete,
);
},
);
},
);
}
I've found a way around it by creating a second StreamBuilder just to update the Chip in the AppBar. Is it a good practice to have multiple StreamBuilders watching the same stream?

Related

Flutter-Not able to load Widget from StreamBuilder

I am calling API using BLoC. On successful response, I need to call Widget named
_moveToHomeScreen()
.
Following is my code for that
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
//body: UserDetail(),
body: new Container(
padding: EdgeInsets.all(16.0),
child:StreamBuilder(
stream: bloc.validateUser,
builder: (BuildContext context, snapshot) {
if(snapshot.hasData){
_moveToHomeScreen();
}
return Column(
children: <Widget>[
_createInputFields(),
_createRegisterButton(),
],
);
}
),
);
}
AND
Widget _moveToHomeScreen () {
print('inside move to home screen');
return Center(
child: Opacity(
opacity: 0.5,
child: Text(
"Save a person to see them here!",
key: Key("Placeholder"),
),
),
);
}
Control goes into the Widget but I am not able to see desired output from Widget.
Your Streambuilder never returns _moveToHomeScreen();
override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
//body: UserDetail(),
body: new Container(
padding: EdgeInsets.all(16.0),
child:StreamBuilder(
stream: bloc.validateUser,
builder: (BuildContext context, snapshot) {
if(snapshot.hasData){
return _moveToHomeScreen();
}
return Column(
children: <Widget>[
_createInputFields(),
_createRegisterButton(),
],
);
}
),
);
}
Just added return before _moveToHomeScreen();

Using a StreamBuilder and a SliverLists In CustomScrollView

I am trying to use a StreamBuilder to fetch data and I want to display that data using a SliverList all inside a CustomScrollView so I can take advantage of the features that come with the CustomScrollView.
Any ideas on how I can achieve this?
Sure, it's easy, here you have a code sample:
class SampleStreamBuilder extends StatelessWidget {
Stream<List<String>> loadData() async* {
await Future.delayed(Duration(seconds: 3));
yield List.generate(10, (index) => "Index $index");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<String>>(
stream: loadData(),
builder: (context, snapshot) {
return snapshot.hasData
? CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return ListTile(
title: Text(snapshot.data[index]),
);
}, childCount: snapshot.data.length),
)
],
)
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}
In this case it is fine to rerender the whole CustomScrollView. However if you want to rerender just one Sliver in a CustomScrollView, do it like this:
CustomScrollView(
slivers: <Widget>[
StreamBuilder(
stream: stream,
builder: (ctx, snapshot) {
return SliverToBoxAdapter(
child: Text('sliver box'),
);
},
)
],
),
Remember to always return a Sliver inside the StreamBuilder.

Is there a way to update the state of TabBarView if it has a child StreamBuilder whose state changes upon listening to a stream?

I am trying to create a UX that looks like WhatsApp Dashboard in Flutter. I created a Scaffold with an AppBar and put the TabBar in the bottomNavigationBar slot instead of the bottom slot of the AppBar. Each of the TabBarView children is a StreamBuilder that listens to particular stream. The problem is that whenever the stream emits a value the StreamBuilder rebuilds (checked via logging build function) but the UI doesn't update until I switch tabs and come back to the tab.
I have tried creating a stateful widget that hosts the StreamBuilder and instantiating that as a child of the TabBarView. I also tried adding a listener to the stream and calling setState there but it didn't work either.
I expect the page to update the UI whenever a chat message is received but it doesn't update until I switch tabs.
body: TabBarView(
controller: _tabController,
children: <Widget>[
ChatListView(),
...
class ChatListView extends StatefulWidget {
#override
_ChatListViewState createState() => _ChatListViewState();
}
class _ChatListViewState extends State<ChatListView>
with AutomaticKeepAliveClientMixin {
List<ListTile> itemList = <ListTile>[];
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: chatsListBloc.chatList,
builder: (context, snapshot) {
print("rebuilt");
if (!snapshot.hasData) {
chatsListBloc.fetchChatList();
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'You have not started any chat yet. To start a chat, click on the Start Chat icon.',
textAlign: TextAlign.center,
),
),
);
} else {
List<ChatListItem> dataList = List<ChatListItem>.from(snapshot.data);
itemList.clear();
for (int i = 0; i < dataList.length; i++) {
itemList.add(ListTile(
onTap: () {
}
},
title: Text(dataList[i].displayName),
subtitle: dataList[i].lastMessage,
leading: CircleAvatar(
backgroundColor: Colors.blueGrey,
backgroundImage:MemoryImage(dataList[i].avatar),
child: Stack(
children: <Widget>[
Icon(
Icons.account_circle,
size: 40,
),
(dataList[i].type == ChatType.Incognito)
? Icon(Icons.lock,
color: Colors.blueGrey[700], size: 10)
: Container(),
],
),
),
trailing: StreamBuilder(
stream: Stream.periodic(Duration(seconds: 1),
(computationCount) => computationCount)
.asBroadcastStream(),
builder: (context, snapshot) => Text(timeLabel(
DateTime.fromMillisecondsSinceEpoch(
dataList[i].lastAccessed))),
)));
}
return ListView(
children: itemList,
);
}
}
});
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => false;
}

Flutter Switching to Tab Reloads Widgets and runs FutureBuilder

The issue:
I have 2 tabs using Default Tabs Controller, like so:
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
drawer: Menu(),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
Container(
padding: EdgeInsets.all(8.0),
child: Text('Deals'),
),
Container(
padding: EdgeInsets.all(8.0),
child: Text('Viewer'),
),
],
),
),
body: TabBarView(
children: <Widget>[
DealList(),
ViewersPage(),
],
),
),
);
}
}
The DealList() is a StatefulWidget which is built like this:
Widget build(BuildContext context) {
return FutureBuilder(
future: this.loadDeals(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
print('Has error: ${snapshot.hasError}');
print('Has data: ${snapshot.hasData}');
print('Snapshot data: ${snapshot.data}');
return snapshot.connectionState == ConnectionState.done
? RefreshIndicator(
onRefresh: showSomething,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
print('A Deal: ${deal}');
return _getDealItem(deal, context);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
);
}
}
With the above, here's what happens whenever I switch back to the DealList() tab: It reloads.
Is there a way to prevent re-run of the FutureBuilder when done once? (the plan is for user to use the RefreshIndicator to reload. So changing tabs should not trigger anything, unless explicitly done so by user.)
There are two issues here, the first:
When the TabController switches tabs, it unloads the old widget tree to save memory. If you want to change this behavior, you need to mixin AutomaticKeepAliveClientMixin to your tab widget's state.
class _DealListState extends State<DealList> with AutomaticKeepAliveClientMixin<DealList> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context); // need to call super method.
return /* ... */
}
}
The second issue is in your use of the FutureBuilder -
If you provide a new Future to a FutureBuilder, it can't tell that the results would be the same as the last time, so it has to rebuild. (Remember that Flutter may call your build method up to once a frame).
return FutureBuilder(
future: this.loadDeals(), // Creates a new future on every build invocation.
/* ... */
);
Instead, you want to assign the future to a member on your State class in initState, and then pass this value to the FutureBuilder. The ensures that the future is the same on subsequent rebuilds. If you want to force the State to reload the deals, you can always create a method which reassigns the _loadingDeals member and calls setState.
Future<...> _loadingDeals;
#override
void initState() {
_loadingDeals = loadDeals(); // only create the future once.
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // because we use the keep alive mixin.
return new FutureBuilder(future: _loadingDeals, /* ... */);
}

How can i make a list and click on item to navigate to a new route

I know this code only displays title and i want to make a onTap method to navigate to a new route, but this is how fare i made it, any help, hint, tip, even shaming me for how stupid i am would be very much appreciated.
Edit: I did posted the whole code because something is going wrong even after help that i got here. maybe is a syntax problem or maybe i am just too stupid
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView.builder(
itemCount: data == null ? 0 : 10,
itemBuilder: (BuildContext context, int index){
return new Card(
child: new ListTile(
onTap: _onTapped,
title : new Text(data[index]["title"]),
),
);
},
),
);
}
}
Just wrap your title in a GestureDecector to handle clicks.
Then call Navigator's pushNamed to redirect to a new route.
new GestureDetector(
onTap: () {
Navigator.pushNamed(context, "myRoute");
},
child: new Text("my Title"),
);
An easier approach I found is to just wrap the item inside the ListTile with a FlatButton (or some interactive widget). In your code, for example:
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView.builder(
itemCount: data == null ? 0 : 10,
itemBuilder: (BuildContext context, int index){
return new Card(
child: new ListTile(
title: FlatButton(
child: new Text(data[index]["title"]),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => YourPage()),
);
},
),
),
);
},
),
);

Resources