I have a widget which calls a function to fetch data from an API; after the fetch function finishes, it calls another function build a table for it's widget. Here's the code:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() => runApp(MeTube());
// class
class MeTube extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MeTubeState();
}
}
// state, component
class MeTubeState extends State<MeTube> {
bool _loading = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
// primarySwatch: Colors(212121),
),
home: Scaffold(
appBar: AppBar(
title: Text('MeTube'),
centerTitle: false,
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {_fetch();}, // call the fetch function (refresh)
)
],
),
body: Center(
child: _loading ? CircularProgressIndicator() : null /* I could load the ListView here
* and use the state to render each row
* but if there are 1,000+ rows, that
* would degrade the performance.
*/
),
)
);
}
// fetch data for the app
_fetch() async {
setState(() {_loading = true;}); // start the progress indicator
final String url = 'api.letsbuildthatapp.com/youtube/home_feed';
final res = await http.get('https://$url');
if (res.statusCode == 200) { // if successful, decode the json
final map = json.decode(res.body);
_build(map['videos'].length, map['videos']); // pass the # videos, and the videos
}
}
// build the data
_build(int rows, data) { /* MAKE EACH ROW */
MeTube().createElement().build( // create a ListView in the Class/Widget
ListView.builder(
itemCount: rows, /* number of rows to render, no need to setState() since this
* function (build) gets called, and the data is passed into it
*/
itemBuilder: (context, index) { // make each column for this class
Column(children: <Widget>[
Text(data[index]['name']), // render some Text and a Divider in each row
Divider()
]);
},
)
);
setState(() {_loading = false;}); // stop the progress indicator
}
}
The current code in the build() function is pretty janky and displays errors. How would I programmatically insert a ListView and Rows into the Widget instead of pushing all the videos to the state, then running code to render a row from all those values in the state?
ListView.builder constructor will create items as they are scrolled onto the screen like on demand. I guess you don't need to worry about performance. Do it like how you commented on the code.
Related
so I'm currently working on an application that has a listview on the first screen (implemented on main.dart).
The listview fetches it's data from internet (async).
The problem is that, the listview does not get updated when the data is changed.
(I can implement this functionality simply by designing a 'reload' button and pressing it every time I want the new data. But that's not what I want right now).
In other words, how can I update the listview automatically?
EDIT1: ADDING SOME CODE
code might be messy; see the description at the end.
class RssFeed extends StatelessWidget {
String title;
String pubDate;
RssFeed(this.title, this.pubDate);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Align(
alignment: Alignment.topRight,
child: Text(title),
),
Text(pubDate)
],
),
);
}
}
class FeedsList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _FeedsListState();
}
}
class _FeedsListState extends State<FeedsList> {
List<Widget> list1 = new List<Widget>();
#override
void initState() {
super.initState();
ls();
}
Future ls() async {
list1.clear();
list.clear();
sites1.clear();
RSS_reader rss_reader = new RSS_reader();
for (var i in saver.list.items) {
sites1.add(
site(siteAdress: i.siteAdress, siteDescription: i.siteDescription));
}
var res = await rss_reader.Get_items(sites1);
for (var val in res) {
list.add(InkWell(
onTap: () => _launchURL(val.item.link),
child: Container(
height: 50,
color: Colors.amber[100],
child: Center(
child: new RssFeed(val.item.title, val.item.pubDate.toString()),
),
)));
}
print(list.length);
setState(() {
list1 = list;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
}));
}
}
DESCRIPTION:
As you can guess, this is a RSS reader.
So, I have a class RSSFeed; which makes one of the tiles of Listview.
then in the FeedsList class (stateful widget), I make the listview.
I have a class called RSS_reader and a method Get_items, which gets a bunch of sites as input and puts those sites' newest feeds in a list ('res' in the above code).
Then, I put the items in a list of 'Container's and then build the listview.
Then, in the main function, I create a container like below:
Container(
height: 500,
width: 580,
child: FeedsList(),
)
and there appears the problem; the FeedsList class does not get updated automatically. although if I put a button and navigate to FeedsList class through that button, the list is refreshed and OK.
Thanks for reading and help.
If you just want to fetch data once from your external source use a FutureBuilder, if you want to fetch data multiple times take a look to StreamBuilder. Both widgets will have the behavior you are looking for, with no refresh button.
Simple example of how to use a FutureBuilder:
Future<List<String>> _fetchData() {
return // fetch data from source
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _fetchData,
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
// This widget will be built when data is fetched
const List<String> list = snapshot.data;
return ListView(
children: list.map(
(element) => ListTile(
title: Text(element),
),
).asList(),
);
} else {
// This widget will be built while you are waiting for your data to be fetched
return Container(
child: Center(
child: Text("Loading data..."),
),
);
}
},
);
}
You have to stream data and ListView will update automatically.
In the button that you say you can re call your ls() functions, your list should update on tap button
sample:
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: () => ls(),
),
);
I'm trying to display a comment stream from Reddit API. I"m using Streambuilder to stream contents as it arrives and displays it as list view thing is I can only view present stream content and this will disappear as new stream contents appear replacing the old ones. If I don't mention item count inside listview.builder prints contents infinitely still new stream appears.
is there a way to display contents along with previous contents in a scrollable fashion and automatically scroll down as a new stream message appears??
Assuming that the comment stream returns individual (and preferably unique) comments one at a time rather than as a list, what you need to do is store the incoming comments in a state object such as a list. When a new comment comes through the stream, you add it to the list and then trigger a widget rebuild.
What you are doing right now is replacing state with each new stream element rather than accumulating them. Using the code you provided, I have edited it to behave as an accumulator instead. Notice the List<Comment> comments = <Comment>[] object added to state. I have also removed the StreamBuilder since that isn't helpful for this use case.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:draw/draw.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: RedditFlutter(),
debugShowCheckedModeBanner: false,
);
}
}
class RedditFlutter extends StatefulWidget {
RedditFlutter({Key key}) : super(key: key);
#override
_RedditFlutterState createState() => _RedditFlutterState();
}
class _RedditFlutterState extends State<RedditFlutter> {
var comments;
ScrollController _scrollController =
new ScrollController(initialScrollOffset: 50.0);
List<Comment> comments = <Comment>[];
StreamSubscription<Comment>? sub;
var msg = '';
Future<void> redditmain() async {
// Create the `Reddit` instance and authenticated
Reddit reddit = await Reddit.createScriptInstance(
clientId: 'clientid',
clientSecret: 'clientsecret',
userAgent: 'useragent',
username: 'username',
password: 'password', // Fake
);
// Listen to comment stream and accumulate new comments into comments list
sub = reddit.subreddit('cricket').stream.comments().listen((comment) {
if (comment != null) {
// Rebuild from state when a new comment is accumulated
setState(() {
comments.add(comment);
})
}
});
}
#override
void initState() {
// TODO: implement initState
redditmain();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Reddit"),
centerTitle: true,
),
body: Center(
child: Container(
child: ListView.builder(
controller: _scrollController,
itemCount: comments.length,
itemBuilder: (context, index) {
final Comment comment = comments[index];
return Card(
child: ListTile(
leading: Image.asset('assets/criclogo.png'),
title: Text(comment.body),
trailing: Icon(Icons.more_vert),
),
);
},
),
);
),
);
}
#override
void dispose() {
sub?.cancel();
super.dispose();
}
}
Note that I have not tested this code so there may be (trivial) bugs. Still, conceptually, it should be fine.
I have an app that allows users to view river gauge data from an API that lists rivers from the entire US. Users can save gauges as favorites which are then displayed in a favorites view. The favorites are stored in user defaults by ID. The favorites view is populated through a favorites view model which is constructed using the user defaults. The favorites are displayed in a list view and when one is tapped the app loads a details view. In the details view, the user can opt to delete the favorite. When returning to the favorites view using the back button, the favorites view should reflect the change, but it does not. I am using Flutter Provider and this is my first implementation of it.
I am defining the provider in main since I will need access to it throughout the app.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
setupServiceLocator();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) {
runApp(
ChangeNotifierProvider(
create: (context) => FavoritesViewModel(),
child: MyApp(),
)
);
});
}
My ListView
class _FavoritesView extends State<FavoritesView> {
FavoritesViewModel viewModel = FavoritesViewModel(); // serviceLocator<FavoritesViewModel>();
#override
void initState() {
viewModel.loadFavorites();
super.initState();
}
#override
Widget build(BuildContext context) {
return Consumer<FavoritesViewModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: Text('=Favorites='),
),
body: ListView.builder(
itemCount: model.favorites.length,
itemBuilder: (context, index) {
return FavoriteCard(model.favorites[index], Key(model.favorites[index]));
},
),
),
);
}
Favorites View Model
class FavoritesViewModel extends ChangeNotifier {
List<String> favorites;
FavoritesViewModel() {
loadFavorites();
}
void deleteFavorite(String id) {
if (favorites.contains(id)) {
this.favorites.remove(id);
}
Storage.initializeList(kFavoritesKey, favorites);
notifyListeners();
}
In the Details view (one place where users can opt to remove a favorite) I am getting a reference to the favorites view model
FavoritesViewModel favesVM = FavoritesViewModel(); // serviceLocator<FavoritesViewModel>();
and calling its deleteFavorite function from within a dialog:
var approveRemovalButton = FlatButton(
child: Text('Remove',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue),
),
onPressed: () {
animationDuration = 0;
favesVM.deleteFavorite(widget.gaugeId);
Navigator.pop(context);
},
);
When I tap a button and invoke the deleteFavorite function on the view model, the item is removed from user preferences, and is also removed from the view model, however, upon returning to the favorites list view, the item is still present in the UI. It's as if the Favorites list view is not registered as a listener and doesn't respond to notifyListeners. Can anyone see what might be causing this? Thanks!
Your issue that you have two view models running at the same time, let me explain how :
By calling FavoritesViewModel viewModel = FavoritesViewModel(); // serviceLocator<FavoritesViewModel>();
This will create an instance from the view model.
And also , by doing this
ChangeNotifierProvider(
create: (context) => FavoritesViewModel(),
child: MyApp(),
)
You are creating another view model
so the tricky part is : you have two view models that both contains the same data,
You are using the view model from the consumer (not the locator) to show the data into your listview and then you are deleting the data from the locator , so the delete will not be reflected.
As a solution you can wrap your FavoriteCard by Consumer<FavoritesViewModel> and use model.deleteFavorite()
Update your State<FavoritesView> like bellow:
class _FavoritesView extends State<FavoritesView> {
FavoritesViewModel viewModel;
#override
void initState() {
super.initState();
viewModel = Provider.of<FavoritesViewModel>(context, listen: false);
viewModel.loadFavorites();
}
#override
Widget build(BuildContext context) {
return Consumer<FavoritesViewModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: Text('=Favorites='),
),
body: ListView.builder(
itemCount: model.favorites.length,
itemBuilder: (context, index) {
return FavoriteCard(model.favorites[index], Key(model.favorites[index]));
},
),
),
);
}
I think you are explicitly creating multiple provides and you loaded data in one provider and when you are notifying the changes, the parent notifier is getting notified and changes are not getting reflected. Don't create separate FavoritesViewModel as you should use the same object. You have already created one object in runApp()
Please replace
FavoritesViewModel viewModel = FavoritesViewModel();
with
FavoritesViewModel viewModel = Provider.of<FavoritesViewModel>(context);
I'm using ListView widget to show items as a list. In a window three, items viewing must the middle item place in the middle.
So how can I detect position of ListView when scrolling stop?
How to detect ListView Scrolling stopped?
I used NotificationListener that is a widget that listens for notifications bubbling up the tree. Then use ScrollEndNotification, which indicates that scrolling has stopped.
For scroll position I used _scrollController that type is ScrollController.
NotificationListener(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (t) {
if (t is ScrollEndNotification) {
print(_scrollController.position.pixels);
}
//How many pixels scrolled from pervious frame
print(t.scrollDelta);
//List scroll position
print(t.metrics.pixels);
},
),
majidfathi69's answer is good, but you don't need to add a controller to the list:
(Change ScrollUpdateNotification to ScrollEndNotification when you only want to be notified when scroll ends.)
NotificationListener<ScrollUpdateNotification>(
child: ListView(
children: ...
),
onNotification: (notification) {
//How many pixels scrolled from pervious frame
print(notification.scrollDelta);
//List scroll position
print(notification.metrics.pixels);
},
),
You can also achieve this functionality with the following steps
import 'package:flutter/material.dart';
class YourPage extends StatefulWidget {
YourPage({Key key}) : super(key: key);
#override
_YourPageState createState() => _YourPageState();
}
class _YourPageState extends State<YourPage> {
ScrollController _scrollController;
double _scrollPosition;
_scrollListener() {
setState(() {
_scrollPosition = _scrollController.position.pixels;
});
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Position $_scrollPosition pixels'),
),
body: Container(
child: ListView.builder(
controller: _scrollController,
itemCount: 200,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.mood),
title: Text('Item: $index'),
);
},
),
),
);
}
}
The NotificationListener now accepts a type argument which makes the code shorter :)
NotificationListener<ScrollEndNotification>(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (notification) {
print(_scrollController.position.pixels);
// Return true to cancel the notification bubbling. Return false (or null) to
// allow the notification to continue to be dispatched to further ancestors.
return true;
},
),
If you want to detect the scroll position of your ListView, you can simply use this;
Scrollable.of(context).position.pixels
In addition to #seddiq-sorush answer, you can compare the current position to _scrollController.position.maxScrollExtent and see if the list is at the bottom
https://coflutter.com/flutter-check-if-the-listview-reaches-the-top-or-the-bottom/ Source
If some want to Detect the bottom of a listview then use this way
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.metrics.atEdge) {
if (notification.metrics.pixels == 0) {
print('At top');
} else {
print('At bottom');
}
}
return true;
},
child: ListView.builder(itemBuilder: (BuildContext context) {
return YourItemWidget;
})
)
I would say You can easily detect Scroll Position by
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(() {
var _currectScrollPosition = _scrollController.position.pixels;//this is the line
});
}
but If you are going to call setState in addListener() ; It is okay, but this will cause to rebuild your entire build(context). This is not a good practice for Animations specially.
What I would recommand is to seprate your scrolling widget into a seprate StatefulWidget , Then get the sate of that widget by calling
_yourScrollableWidgetKey.currentState?.controller.addListener(() {
//code.......
setState(() {});
});
Note: Set a GlobalKey, and assign to your StatFulWidget.
final _yourScrollableWidgetKey = GlobalKey<_YourScrollAbleWidget>();
StackedPositionedAnimated(
key: _yourScrollableWidgetKey,
),
So I want to set the children of my GridView dynamically so I can set it when I want. Currently this is the whole class.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/CustomColors.dart';
import 'package:flutter_app/quote/Quote.dart';
import 'package:flutter_app/quote/QuoteView.dart';
import 'package:flutter_app/section/Section.dart';
import 'package:flutter_app/quote/QuoteData.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
static List<QuoteData> data = [];
_readJson() async {
data.clear();
var url = 'https://www.dropbox.com/s/7k280ca5dktlhoo/quotes.json?dl=1';
var httpClient = new HttpClient();
httpClient.getUrl(Uri.parse(url)).then((HttpClientRequest request) {
return request.close();
}).then((HttpClientResponse response) {
response.transform(utf8.decoder).listen((contents) {
List<Map> decoded = JSON.decode(contents);
decoded.forEach((m) {
String url = m["url"];
String title = m["title"];
String sectionString = m["section"];
Section section;
for (Section element in Section.values) {
if (element.toString() == "Section." + sectionString) {
section = element;
}
}
List<Quote> quotes = m["quotes"];
data.add(new QuoteData(url, title, section, quotes));
});
});
});
}
#override
Widget build(BuildContext context) {
_readJson();
return new MaterialApp(
title: 'Quotes',
theme: new ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or press Run > Flutter Hot Reload in IntelliJ). Notice that the
// counter didn't reset back to zero; the application is not restarted.
primarySwatch: CustomColors.black,
),
home: new MyHomePage(title: 'Quotes'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static Section currentSection = Section.movies;
void _onTileClicked(QuoteData quote) {
Navigator.push(context,
new MaterialPageRoute(builder: (context) => new QuoteView(quote)));
}
List<Widget> _getTiles(Section section) {
final List<Widget> tiles = <Widget>[];
for (var i in MyApp.data) {
if (i.section != section) {
continue;
}
tiles.add(new GridTile(
child: new InkResponse(
enableFeedback: true,
child: new Image.network(
i.url,
fit: BoxFit.cover,
),
onTap: () => _onTileClicked(i),
)));
}
return tiles;
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height - kToolbarHeight - 24) / 2;
final double itemWidth = size.width / 2;
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return new Scaffold(
appBar: new 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: new Text(currentSection
.toString()
.replaceAll("Section.", "")
.substring(0, 1)
.toUpperCase() +
currentSection.toString().replaceAll("Section.", "").substring(1)),
),
body: new Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: new GridView.count(
crossAxisCount: 2,
childAspectRatio: (itemWidth / itemHeight),
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
children: _getTiles(currentSection)),
),
);
}
}
So right now on startup, it launches the application but MyApp.data is empty because the json has to be read so the GridView will be empty, when I refresh the application, the GridView won't be empty because MyApp.data won't be empty anymore. I want to be able to set the children after the json gets read, also I need to be able to change it dynamically because I will add a function for switching sections.
The same goes for the title, I also need to be able to change it dynamically when switching sections.
You can create a loading widget and wait for the request to finish when the request is finished hide the loading and show the grid than rebuild the state. but you need to have a statefullWidget not statelessWidget.
You can see how i use it on the code below I added my comments to the code
class _ChildrenPageState extends State<ChildrenPage> {
//declare the _load to true so it will show the loading when the page is loaded
bool _load = true;
ChildrenService _childrenService = new ChildrenService();
List children = [];
//I call the _rebuild function to rebuild the widgets and show the data
void _rebuild() {
setState(() {
});
}
#override
Widget build(BuildContext context) {
//I call the request to get my data when it is finished i put _load to false so the loading will hide and call the _rebuild function to rebuild the widgets and i have put if so when the widget is built it will not rebuild it a second time.
_childrenService.getChildren(children).then((data){
if(data && _load){
_load = false;
_rebuild();
}
});
//the below widget show the loading container if _load is true else it will show the dataTable in your app you should add the grid and you can pass the data that you got from the request
Widget loadingIndicator = _load ? new Container(
color: Colors.grey[300],
width: 70.0,
height: 70.0,
child: new Padding(
padding: const EdgeInsets.all(5.0),
child: new Center(
child: new CircularProgressIndicator()
)
),
) : new JLDataTable(
data: children,
);
return new Scaffold(
drawer: new Drawer(
child: MenuList.menuList,
),
appBar: new AppBar(title: const Text('Data tables')),
body: new ListView(
padding: const EdgeInsets.all(20.0),
children: <Widget>[
new Align(
child: loadingIndicator,
alignment: FractionalOffset.center
)
]
)
);
}
}