Using a StreamBuilder and a SliverLists In CustomScrollView - dart

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.

Related

Auto-Refresh UI when data change

I need to refresh my UI when data changes. I have a ListView to display Cards that contain my events, and these events are sorted with a datepicker. When I change the date with the datepicker I need to reload the page to display the correct pages.
I try to pass the datepicker data as a parameter of the ListView to sort the events in the ListView, I also tried to sort the data before ListView is built with a parameter containing the list of sorted data.
Widget of my HomePage class :
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: Image.asset('assets/logo2.PNG', fit: BoxFit.contain),
title: Text(widget.title,style: TextStyle(fontFamily: 'IndieFlower',fontSize: 30,fontWeight: FontWeight.bold),),
actions: <Widget>[ // Add 3 lines from here...
new IconButton(icon: const Icon(Icons.account_circle, color: Color(0xFFf50057)), onPressed: _pushSaved, iconSize: 35,),
], // ... to here.
centerTitle: true,
backgroundColor: new Color(0xFF263238),
),
body: FutureBuilder<List<Event>>(
future: fetchPosts(http.Client()),
builder: (context, snapshot) {
//print(convertIntoMap(snapshot.data));
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? ListViewEvents(posts: sortEvents(snapshot.data), pickerDate: '${dobKey.currentState.dobDate} ' +dobKey.currentState.dobStrMonth +' ${dobKey.currentState.dobYear}')
: Center(child: CircularProgressIndicator(backgroundColor: Color(0xFFf50057),));
},
),
bottomNavigationBar : BottomAppBar(
child: Container(height: 100.0,
alignment: Alignment.topCenter,
child:
DatePicker(
key: dobKey,
setDate: _setDateOfBirth,
customItemColor: Color(0xFFf50057),
customGradient:
LinearGradient(begin: Alignment(-0.5, 2.8), colors: [
Color(0xFFf50057),
Color(0xFFffcece),
Color(0xFFf50057),
]),
),
),
),
);
}
}
This is my map:
List<Event> sortEvents(List<Event> data) {
List<Event> eventsSelected = new List<Event>();
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
eventsSelected.add(data[index]);
}
}
return eventsSelected;
}
And this is how I render my cards:
class ListViewEvents extends StatefulWidget {
ListViewEvents({Key key, this.posts, this.pickerDate}) : super(key: key);
final posts;
final pickerDate;
#override
_ListViewEventsState createState() => _ListViewEventsState();
}
class _ListViewEventsState extends State<ListViewEvents> with
SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
if(widget.posts.isEmpty) {
return Center(
child: Text(
'No events for this date'
),
);
}
return ListView.builder(
itemCount: widget.posts.length,
padding: const EdgeInsets.all(15.0),
itemBuilder: (context, index) {
return Center(
child: Text(
'Title : ${widget.posts[index].title}'
),
);
},
);
}
}
I actually have a system to display my events's Cards that works but it's not in real-time, I would like to refresh the UI when the data of the datepicker changes.
You need to call setState() when list data changes.
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
setState(() { eventsSelected.add(data[index]); } ); <--- add it here.
}
}
you can use setState() for this problem so setState() call when anything change in the screen

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;
}

Show snackbar when item is tapped in bottom sheet

I want to show Snackbar when an item is clicked in the bottom sheet. I tried this.
#override
Widget build(BuildContext defaultContext) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () => showModalBottomSheet(
context: defaultContext,
builder: (BuildContext context) {
return Builder(
builder: (BuildContext builderContext) {
return ListTile(
title: Text("Click me"),
onTap: () {
Navigator.pop(builderContext); // hiding bottom sheet
Scaffold.of(builderContext).showSnackBar(SnackBar(content: Text("Hi")));
},
);
},
);
},
),
),
),
);
}
But I am having error
Scaffold.of() called with a context that does not contain a Scaffold
Note The question is not a duplicate of this
PS: I know I can use GlobalKey in Scaffold to show the Snackbar but I want to do it using Builder like the docs suggest to use Builder. I did use builder and it didn't work.
This worked out finally.
#override
Widget build(BuildContext defaultContext) {
return Scaffold(
body: Center(
child: Builder(builder: (builderContext) {
return RaisedButton(
onPressed: () => showModalBottomSheet(
context: defaultContext,
builder: (BuildContext context) {
return ListTile(
title: Text("Click me"),
onTap: () {
Navigator.pop(builderContext); // hiding bottom sheet
Scaffold.of(builderContext).showSnackBar(SnackBar(content: Text("Hi")));
},
);
},
),
);
},),
),
);
}
I need to move Builder up the tree. Don't know the reason why but it worked.

Make PageView page scroll when child ListView reaches scroll limit - nested scroll?

Nested scrolling?
I have three vertical pages in a PageView that I want to be able to flip between.
The pages consists of scrollable ListViews.
When a page is in focus the displayed list should be vertically scrollable, but when the list is scrolled to either end I want the pageView scrolling to take over the scroll behaviour and handle the page flipping to next page (like a web page with scrollable elements).
Example with scrolling lists below.
If the list scrolling is disabled the page flipping works.
How can I make both work?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: VerticalPageView(),
);
}
}
class VerticalPageView extends StatelessWidget {
VerticalPageView({Key key}) : super(key: key);
final PageController pageController = PageController();
final ScrollController scrollController = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: PageView(
controller: pageController,
pageSnapping: true,
scrollDirection: Axis.vertical,
children: <Widget>[
Container(
color: Colors.pinkAccent,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 0 item $index');
},
),
),
Container(
color: Colors.lightBlue,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 1 item $index');
},
),
),
Container(
color: Colors.lightGreen,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 2 item $index');
},
),
),
],
),
),
);
}
}
I think what you are trying to achieve is not impossible but needs a lot of study and care.
I have been trying to use several NotificationListener<ScrollNotification>'s to adapt the reaction depending on the position of the scroll but did not get anywhere.
Give a look to the Animation sample home.dart file in the Gallery App. It is full of insight in this regard.
The problem with this approach is basically what you say. When you disable programmatically the scrolling when reaching the end of the list to enable page scrolling, then you can switch to the other page but can't scroll the list in the other direction any more.
So, either you scroll the list or you scroll the pages, but not both.
Maybe you could add a GestureDetector() above everything and check on every drag update what is your situation below to configure accordingly the different scrollers.
Anyway, in case it is helpful to you I let you here an alternate solution using a CustomScrollView and SliverList's.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: VerticalPageView(),
);
}
}
class VerticalPageView extends StatelessWidget {
final ScrollController _scrollController = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.pinkAccent,
child: Text('page 0 item $index'),
);
},
childCount: 100,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.lightBlue,
child: Text('page 1 item $index'),
);
},
childCount: 100,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.lightGreen,
child: Text('page 2 item $index'),
);
},
childCount: 100,
),
),
],
),
);
}
}

Flutter dart add list to children

I am new on dart, I just want to add a list of my data using map (store.state.availablePage.map) to the children of ListView, but I already have some item on the ListView.
Here the sample code I want :
class MgDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (context, store) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
Text("Title"),
store.state.availablePage.map((value) {
return ListTile(
title: Text("Title"),
onTap: () {},
);
}).toList(),
],
),
);
},
);
}
}
Please help me how I can achieve that?
Update: the code above is not compiled.
Updates Dart Lang version > 2.2.2, can be achieve using below code :
class MgDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (context, store) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
Text("Title"),
...store.state.availablePage.map((value) {
return ListTile(
title: Text("Title"),
onTap: () {},
);
}).toList(),
],
),
);
},
);
}
}
You ListView is having Text(Widget) and a Array of ListTile(Array of Widgets) which means ListView(children: [Widget, [Widget]]).
But it should be ListView(children: [Widget1, Widget2, Widget3,...]). Below code might help
class MgDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (context, store) {
List<Widget> list = store.state.availablePage.map((value) {
return ListTile(
title: Text("Title"),
onTap: () {},
);
}).toList();
list.insert(0, Text("Title"));
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: list,
),
);
},
);
}
}
If for some reason someone is unable to use "..." operator, there is also another solution. It is not as elegant as "..." operator, but it also works.
You can just use ..addAll(list) at the end of the list.
Here is how it would look like in given example:
class MgDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (context, store) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
Text("Title"),
]..addAll(store.state.availablePage.map((value) {
return ListTile(
title: Text("Title"),
onTap: () {},
);
}).toList(),),
),
);
},
);
}
}

Resources