I have a page in my app with a ListView. I'm using a StreamBuilder to receive data. When there is data I build the listview children and the listview is wrapped inside a RefreshIndicator. This is working fine.
Now I want to handle the situation when there is no data aka EmptyState. Be able to display a Container with some content, while still be able to support pull to refresh to force the page/Listview to check for data.
This is how I'm currently doing it
return RefreshIndicator(
key: upcomingRefreshIndicatorKey,
onRefresh: handleRefresh,
child: snapshot.hasData ? list : Stack(children: <Widget>[list, emptystate],) ,
);
I'm not sure this is the way to handle EmptyState with ListView, but so far this is the only way I can see to still support refresh when the list is empty. So my main question is how you best handle empty state with LIstView and still supporting PullToRefresh?
To support the RefreshIndicator inside an empty ListView you can set physics: AlwaysScrollableScrollPhysics(). This makes the ListView always scrolalble. Therefore, we can always trigger the RefreshIndicator.
The Container you want to display when the ListView is empty can be accomplished by two methods:
If your data is empty (or null), replace the ListView with a SingleChildScrollView with physics set to AlwaysScrollableScrollPhysics(). Then you can place your Container as the child of the SingleChildScrollView.
Instead of replacing the ListView you can simply replace the children with your Container, given your data is empty (or null).
In the follwing standalone example, I took the second approach:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// dummy stream
StreamController<List<String>> yourStream = StreamController<List<String>>();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: RefreshIndicator(
onRefresh: refresh,
child: StreamBuilder<List<String>>(
stream: yourStream.stream,
builder: (_, snapshot) => ListView(
physics: AlwaysScrollableScrollPhysics(), // This makes it always scrollable!
children: snapshot.data == null
? [Text('Nothing here :(')]
: snapshot.data
.map((text) => ListTile(
title: Text(text),
))
.toList()),
),
),
),
),
);
}
Future<void> refresh() {
// dummy code
yourStream.add(['Quick', 'Brown', 'Fox']);
return Future.value();
}
}
Related
I have the following simple full code ..
import 'package:flutter/material.dart';
class Profile extends StatefulWidget {
const Profile({ Key? key, }) : super(key: key);
#override
ProfileState createState() => ProfileState();
}
class ProfileState extends State<Profile>{
#override
Widget build(BuildContext context) {
return SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context,value){
return[
const SliverAppBar(
expandedHeight: 400,
)
];
},
body: ListView.builder(
itemCount: 200,
itemBuilder: (BuildContext context, int index) {
return Center(child: Text(index.toString()));
},
)
),
);
}
}
in the previous code, everything is ok and it shifted the scroll in a smooth way BUT when I provide ScrollController into my ListView.builder the scroll is no longer smooth anymore.
so Please How could I keep the first result (with no providing ScrollController) the same as (with providing ScrollController)? .
I recreated your requirements using CustomScrollView, the API is "harder" to use but it allows you implement more complex features (like nested scrollviews as you are doing) because we have direct access to the Slivers API.
You can see that almost any Flutter scrollable widget is a derivation of either CustomScrollView or ScrollView which makes use of Slivers.
NestedScrollView is a subclass of CustomScrollView.
ListView and GridView widgets are subclasses of ScrollView.
Although seems complicated a Sliver is just a portion of a scrollable area:
CustomScrollView(
controller: _scrollController,
slivers: [
const SliverAppBar(expandedHeight: 400),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Center(child: Text('item $index.'));
},
),
),
],
)
So, instead of creating multiple scrollviews and trying to mix them together (which lead to buggy behaviors), just declare multiple scrollable areas and put them together inside a CustomScrollView, that's it.
See the working example at: https://dartpad.dev/?id=60cb0fa073975f3c80660815ae88af4e.
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'),
),
);
}
}
I have an AppBar button where I want to read a childData list and do something with it:
class _ParentState extends State<Parent> {
....
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.add),
onPressed: () async {
//Need to access childData data here
print(childData); //currently prints null
},
),
],
),
body: SafeArea(
child: Column(
children: <Widget>[
ChildWidget(),
],
The ChildWidget uses a StreamBuilder and which loops through the snapshots and does something else before appending the snapshot to the list:
class ChildWidgetState extends State<ChildWidget> {
....
Widget build(BuildContext context) {
return StreamBuilder<List<DocumentSnapshot>>(
stream: stream,
builder: (context, snapshots) {
if (snapshots.connectionState == ConnectionState.active &&
snapshots.hasData) {
List<DocumentSnapshot> childData = [];
for (var x in snapshots.data) {
//do something else with snapshot data
childData.add(x);
}
I have tried to use a callback function where I notify the parent widget to refresh, but this gives a setState() or markNeedsBuild() called during build error as it setStates during the build method of the ChildWidget. I definitely have data within my streambuilder as this is used elsewhere with no issues currently.
A state management package like Provider only seems to provide data to child widgets, not pass them back up, and I've read that using global variables is frowned upon. Would it also be bad practice to add the childData list to a database package like Hive and then read that in my ParentWidget? I don't necessarily want to rebuild the parent widget - just read the data within the child widget when I click my AppBar button.
Thanks.
You can use provider to get data in the parent widget
Provider(
create: (_) => MyModel(),
child: ParentWidget()
)
on ChildWidget use
data=Provider.of<MyModel>(context);
and MyModel() can be data class containing data you want to get in your parent appBar.
Now in steam builder you can use
data.childData=newData
And after that using
data=Provider.of<MyModel>(context);
In parent widget and
data.childData
Will have newdata you want.
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.
I have a sliver list in a custom scroll view.
Each child in the sliver list has a dynamic height. I want to trigger a function call in my viewmodel whenever one of the child widgets is crossing the center point of the screen view.
As shown below, I am using MVVM (FilledStack's architecture) which uses the ViewModelProvider to link the view to a viewmodel. This is generating a list of "Post Cards" views.
class HomeView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ViewModelProvider<HomeViewModel>.withConsumer(
viewModel: HomeViewModel(),
onModelReady: (model) => model.initialise(),
builder: (BuildContext context, HomeViewModel model, Widget child) =>
SafeArea(
child: Scaffold(
bottomNavigationBar: _buildBottomAppBar(model),
body: CustomScrollView(
controller: model.scrollController,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int postIndex) => PostCard(
postIndex: postIndex,
),
childCount: model.posts?.length,
),
),
],
);
),
),
);
}
I tried global key inside of the PostCard:
GlobalKey _postKey = GlobalKey();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
This calls a _afterLayout function after the item is rendered. This required me to change it to a stateful widget for the init state and it works to print the location but it feels very much like a hack.
Is there a cleaner way to get the position and size of each child of a sliver list?
I solved this by creating my own CustomSliverList and CustomRenderSliverList.
In the CustomRenderSliverList's performLayout() function you can use the constraints.scrollOffset and constraints.remainingPaintExtent to get the current view port and then using the child RenderBox object to figure out position and size of each child.
From that you can use a callback function to perform any action when it enters the view port.