Removing and Add new Cards in the List -Flutter - flutter

I'm working in a Dating App. So, it's like a Tinder, basically.
We have the cards layout here. So, it's a List in a Stack with Cards Widgets.
In this example that I was following https://mightytechno.com/flutter-tinder-swipe-cards/ he create a List and when Swiped it is removed from List. But, when I try to add new cards, the cards aren't added.
My SwipeArea:
List<Widget> cards = List();
#override
void initState() {
super.initState();
cards.add(
new ProfileCard());
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: Stack(
children: cards,
),
));
}
void callBack(Widget value) {
setState(() {
cards.remove(value);
cards.add(new ProfileCard());
_adCardCounter++;
});
}
When the ProfileCard (card) is swiped, callBack is called, then I remove the Widget from List and Add a new one.
But, I can't see the Card in the Display.
If have 2 cards in a list, and I remove 2 and add 2, it display none cards.. but the array have 3.
Someone knows why? And how is the best solution to work with it? I tried to reuse the cards, but I can't.

You can copy past run full code below
You can use package https://pub.dev/packages/flutter_tindercard
You can add new card in swipeCompleteCallback
code snippet
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
_addToStream();
...
void _addToStream() {
Random random = new Random();
int index = random.nextInt(3);
print("index $index");
welcomeImages.add('https://picsum.photos/250?image=$index');
welcomeImages.removeAt(0);
_streamController.add(welcomeImages);
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:flutter_tindercard/flutter_tindercard.dart';
import 'dart:async';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AsyncDataExampleHomePage(),
);
}
}
// support for asynchronous data events
class AsyncDataExampleHomePage extends StatefulWidget {
#override
_AsyncDataExampleHomePageState createState() =>
_AsyncDataExampleHomePageState();
}
class _AsyncDataExampleHomePageState extends State<AsyncDataExampleHomePage>
with TickerProviderStateMixin {
StreamController<List<String>> _streamController;
List<String> welcomeImages = [
"https://picsum.photos/250?image=9",
"https://picsum.photos/250?image=10",
"https://picsum.photos/250?image=11",
];
#override
initState() {
super.initState();
_streamController = StreamController<List<String>>();
}
void _addToStream() {
Random random = new Random();
int index = random.nextInt(3);
print("index $index");
welcomeImages.add('https://picsum.photos/250?image=$index');
welcomeImages.removeAt(0);
_streamController.add(welcomeImages);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("asynchronous data events test"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Added image appears on top:',
),
StreamBuilder<List<String>>(
stream: _streamController.stream,
initialData: welcomeImages,
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
print('snapshot.data.length: ${snapshot.data.length}');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Add image');
case ConnectionState.waiting:
//return Text('Awaiting images...');
case ConnectionState.active:
print("build active");
return _AsyncDataExample(context, snapshot.data);
case ConnectionState.done:
return Text('\$${snapshot.data} (closed)');
}
return null; // unreachable
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _addToStream,
tooltip: 'Add image',
child: Icon(Icons.add),
),
);
}
Widget _AsyncDataExample(BuildContext context, List<String> imageList) {
CardController controller; //Use this to trigger swap.
print(imageList.length);
return Center(
key: UniqueKey(),
child: Container(
height: MediaQuery.of(context).size.height * 0.6,
child: TinderSwapCard(
orientation: AmassOrientation.TOP,
totalNum: imageList.length,
stackNum: 3,
swipeEdge: 4.0,
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: MediaQuery.of(context).size.width * 0.9,
minWidth: MediaQuery.of(context).size.width * 0.8,
minHeight: MediaQuery.of(context).size.width * 0.8,
cardBuilder: (context, index) {
print("cardbuilder ${index}");
print("imageList length ${imageList.length}");
return Card(
color: Colors.blue,
child: Image.network('${imageList[index]}'),
);
},
cardController: controller = CardController(),
swipeUpdateCallback: (DragUpdateDetails details, Alignment align) {
/// Get swiping card's alignment
if (align.x < 0) {
//Card is LEFT swiping
} else if (align.x > 0) {
//Card is RIGHT swiping
}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
_addToStream();
/// Get orientation & index of swiped card!
},
),
),
);
}
}

Related

Resize ListView during scrolling in Flutter

I'm trying to build a screen where two vertically stacked ListViews cause themselves to grow and shrink as a result of being scrolled. Here is an illustration:
The initial state is that both lists take up 50% of the top and bottom of the screen respectively. When the user starts dragging the top list downward (to scroll up) it will initially cause the list to expand to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging upwards (to scroll down), then as they get to the bottom of the list it will cause the list to shrink back up to only taking up 50% of the screen (the initial state).
The bottom list would work similarly, dragging up would cause the list to expand upwards to take up 75% of the screen before the normal scrolling behavior starts; when the user changes direction, dragging downwards (to scroll up), then as they get to the top of the list it will shrink back to 50% of the screen.
Here is an animation of what it should look like:
https://share.cleanshot.com/mnZhJF8x
My question is, what is the best widget combination to implement this and how do I tie the scrolling events with resizing the ListViews?
So far, this is as far as I've gotten:
Column(
children: [
SizedBox(
height: availableHeight / 2,
child: ListView(...)
),
Expanded(child: ListView(...)),
],
),
In terms of similar behavior, it appears that the CustomScrollView and SliverAppBar have some of the elements in scrolling behaving I'm going after but it's not obvious to me how to convert that into the the two adjacent lists view I described above.
Any advice would be greatly appreciated, thank you!
hi Check this,
Column(
children: [
Expanded (
flex:7,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
),
Expanded (
flex:3,
child: Container(
child: ListView.builder(
itemCount:50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
),
],
),
edit: refactored and maybe better version:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeight = 150.0,
});
final double height;
final double minHeight;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
void incrementNotifier(ValueNotifier notifier, double increment) {
if (notifier.value + increment >= widget.height - widget.minHeight) return;
if (notifier.value + increment < widget.minHeight) return;
notifier.value += increment;
}
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null) {
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification) {
final delta = notification.scrollDelta ?? 0.0;
if (dragOnUpper) {
if (notification.metrics.extentAfter != 0) {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
} else {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
}
}
if (!dragOnUpper) {
if (notification.metrics.extentBefore != 0) {
incrementNotifier(upperSizeNotifier, -1 * delta.abs());
incrementNotifier(lowerSizeNotifier, delta.abs());
} else {
incrementNotifier(upperSizeNotifier, delta.abs());
incrementNotifier(lowerSizeNotifier, -1 * delta.abs());
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
here is the older post:
so, here's my shot on this. There might be a less complicated solution of course but I think it's somewhat understandable. At least I've tried to comment good enough.
Let me know if it works for you.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ExtentableTwoRowScrollable Demo',
home: Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ExtentableTwoRowScrollable(
height: constraints.maxHeight,
);
}),
),
);
}
}
// sorry for the name :)
class ExtentableTwoRowScrollable extends StatefulWidget {
const ExtentableTwoRowScrollable({
super.key,
required this.height,
this.minHeightUpper = 300.0,
this.minHeightLower = 300.0,
});
final double height;
final double minHeightUpper;
final double minHeightLower;
#override
State<ExtentableTwoRowScrollable> createState() =>
_ExtentableTwoRowScrollableState();
}
class _ExtentableTwoRowScrollableState extends State<ExtentableTwoRowScrollable>
with SingleTickerProviderStateMixin {
final upperSizeNotifier = ValueNotifier(0.0);
final lowerSizeNotifier = ValueNotifier(0.0);
var upperHeight = 0.0;
var dragOnUpper = true;
bool handleVerticalDrag(ScrollNotification notification) {
if (notification is ScrollStartNotification &&
notification.dragDetails != null)
// only act on ScrollStartNotification events with dragDetails
{
if (notification.dragDetails!.globalPosition.dy <
upperSizeNotifier.value) {
dragOnUpper = true;
} else {
dragOnUpper = false;
}
}
if (notification is ScrollUpdateNotification &&
notification.dragDetails != null)
// only act on ScrollUpdateNotification events with dragDetails
{
if (dragOnUpper) {
// dragging is going on, was started on upper ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (lowerSizeNotifier.value >= widget.minHeightLower)
// expand upper until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (notification.metrics.extentAfter == 0.0 &&
upperSizeNotifier.value > widget.minHeightUpper)
// when at the end of upper shrink it until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
if (!dragOnUpper) {
// dragging is going on, was started on lower ListView
if (notification.dragDetails!.delta.direction > 0)
// dragging backward/downwards
{
if (notification.metrics.extentBefore == 0.0 &&
lowerSizeNotifier.value > widget.minHeightLower)
// when at the top of lower shrink it until minHeightLower gets hit
{
lowerSizeNotifier.value -= notification.dragDetails!.delta.distance;
upperSizeNotifier.value += notification.dragDetails!.delta.distance;
}
} else
// dragging forward/upwards
{
if (upperSizeNotifier.value >= widget.minHeightUpper)
// expand lower until minHeightUpper gets hit
{
lowerSizeNotifier.value += notification.dragDetails!.delta.distance;
upperSizeNotifier.value -= notification.dragDetails!.delta.distance;
}
}
}
}
return true;
}
#override
Widget build(BuildContext context) {
// initialize ratio of lower and upper, f.e. here 50:50
upperSizeNotifier.value = widget.height / 2;
lowerSizeNotifier.value = widget.height / 2;
return NotificationListener(
onNotification: handleVerticalDrag,
child: Column(
children: [
ValueListenableBuilder<double>(
valueListenable: upperSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.greenAccent,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("upper ListView $index"));
},
),
);
},
),
ValueListenableBuilder<double>(
valueListenable: lowerSizeNotifier,
builder: (context, value, child) {
return Container(
color: Colors.blueGrey,
height: value,
child: ListView.builder(
shrinkWrap: true,
itemCount: 40,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
title: Text("lower ListView $index"));
},
),
);
},
),
],
),
);
}
}
I think it's working okayish so far but supporting the "fling" effect - I mean the acceleration when users shoot the scrollable until simulated physics slows it down again - would be really nice, too.
First, initialise two scroll controllers for two of your listviews. Then register a post-frame callback by using WidgetsBinding.instance.addPostFrameCallback to make sure that the scroll controller has been linked to a scroll view. Next, setup scroll listeners in that callback.
To listen to scrolling update you can use scrollController.addListener. Then use if-else cases to catch the position of the scroll, if scroll position equals to maxScrollExtent then the user scrolled bottom and its the other way round for minScrollExtent. Check my edited implementation below:
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController _scrollCtrl1 = ScrollController();
final ScrollController _scrollCtrl2 = ScrollController();
double height1 = 300;
double height2 = 300;
bool isLoading = true;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
isLoading = false;
height1 = SizeConfig.blockSizeVertical! * 50;
height2 = SizeConfig.blockSizeVertical! * 50;
});
_scrollCtrl1.addListener(() {
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl1.position.pixels == _scrollCtrl1.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
_scrollCtrl2.addListener(() {
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.maxScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 25;
height2 = SizeConfig.blockSizeVertical! * 75;
});
}
if (_scrollCtrl2.position.pixels == _scrollCtrl2.position.minScrollExtent) {
setState(() {
height1 = SizeConfig.blockSizeVertical! * 75;
height2 = SizeConfig.blockSizeVertical! * 25;
});
}
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: !isLoading ? Column(
children: [
AnimatedContainer(
color: Colors.blueGrey,
height: height1,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl1,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.list),
dense: true,
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("List item $index"));
}),
),
AnimatedContainer(
height: height2,
color: Colors.deepPurpleAccent,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: ListView.builder(
itemCount: 50,
padding: EdgeInsets.zero,
controller: _scrollCtrl2,
itemBuilder: (BuildContext context, int index) {
return ListTile(
dense: true,
leading: const Icon(Icons.list),
trailing: const Text(
"GFG",
style: TextStyle(color: Colors.green, fontSize: 15),
),
title: Text("aaaaaaaaa $index"));
}),
),
],
) : const Center(child: CircularProgressIndicator(),),
);
}
}
class SizeConfig {
static MediaQueryData? _mediaQueryData;
static double? screenWidth;
static double? screenHeight;
static double? blockSizeHorizontal;
static double? blockSizeVertical;
/// This class measures the screen height & width.
/// Remember: Always call the init method at the start of your application or in main
void init(BuildContext? context) {
_mediaQueryData = MediaQuery.of(context!);
screenWidth = _mediaQueryData?.size.width;
screenHeight = _mediaQueryData?.size.height;
blockSizeHorizontal = (screenWidth! / 100);
blockSizeVertical = (screenHeight! / 100);
}
}

How to automatically scroll through all the ListTiles in the Listview.seperated in Flutter?

Scroll automatically (without any user interaction) through all the ListTiles in the Listview using a Timer in flutter. The below method makes only one ListTile to animate but I want to animate all the ListTiles from top to bottom one by one and again from bottom to top one by one.
The below is the Listview:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: FutureBuilder(
future: fetchNews(),
builder: (context, snap) {
if (snap.hasData) {
news = snap.data;
return ListView.separated(
//controller: _controller,
scrollDirection: scrollDirection,
controller: controller,
itemBuilder: (context, i) {
final NewsModel _item = news[i];
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: ListTile(
title: Text('${_item.title}'),
subtitle: Text(
'${_item.description}',
// maxLines: 1,
//overflow: TextOverflow.ellipsis,
),
),
);
},
separatorBuilder: (context, i) => Divider(),
itemCount: news.length,
);
} else if (snap.hasError) {
return Center(
child: Text(snap.error.toString()),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
}
This is the automatic scrolling i have tried:
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 2), (Timer t) async {
await controller.scrollToIndex(1,
preferPosition: AutoScrollPosition.begin);
});
Here is a solution assuming that all your items in the ListView have the same itemExtent.
In this solution, I highlight the current Item as selected. You could also want to stop autoscrolling as soon as you reach the bottom of the list.
Full source code
import 'dart:async';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '66455867.auto_scroll.freezed.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
Future<List<News>> _fetchNews() async => dummyData;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('News')),
body: FutureBuilder(
future: _fetchNews(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return NewsList(newsList: snapshot.data);
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
class NewsList extends StatefulWidget {
final List<News> newsList;
const NewsList({
Key key,
this.newsList,
}) : super(key: key);
#override
_NewsListState createState() => _NewsListState();
}
class _NewsListState extends State<NewsList> {
ScrollController _scrollController = ScrollController();
Timer _timer;
double _itemExtent = 100.0;
Duration _scrollDuration = Duration(milliseconds: 300);
Curve _scrollCurve = Curves.easeInOut;
int _autoScrollIncrement = 1;
int _currentScrollIndex = 0;
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
_autoScrollIncrement = _currentScrollIndex == 0
? 1
: _currentScrollIndex == widget.newsList.length - 1
? -1
: _autoScrollIncrement;
_currentScrollIndex += _autoScrollIncrement;
_animateToIndex(_currentScrollIndex);
setState(() {});
});
}
void _animateToIndex(int index) {
_scrollController.animateTo(
index * _itemExtent,
duration: _scrollDuration,
curve: _scrollCurve,
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _scrollController,
itemExtent: _itemExtent,
children: widget.newsList
.map((news) => ListTile(
title: Text(news.title),
subtitle: Text(
news.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: widget.newsList[_currentScrollIndex].id == news.id,
selectedTileColor: Colors.amber.shade100,
))
.toList(),
);
}
}
#freezed
abstract class News with _$News {
const factory News({int id, String title, String description}) = _News;
}
final faker = Faker();
final dummyData = List.generate(
10,
(index) => News(
id: faker.randomGenerator.integer(99999999),
title: faker.sport.name(),
description: faker.lorem.sentence(),
),
);
Packages used in the solution:
freeze for the News Domain Class
build_runner to generate the freezed code
faker to generate the list of random news
UPDATE : Scroll only once
To stop the autoscrolling at the bottom of the listview, you just need to modify the initState method:
int _currentScrollIndex;
News _selectedNews;
#override
void initState() {
super.initState();
_currentScrollIndex = -1;
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
setState(() {
if (_currentScrollIndex == widget.newsList.length - 1) {
_timer.cancel();
_selectedNews = null;
} else {
_selectedNews = widget.newsList[++_currentScrollIndex];
_animateToIndex(_currentScrollIndex);
}
});
});
}
We don't need the scroll direction defined as _autoScrollIncrement. However, I would introduce a new _selectedNews to easily unselect the last News item when we arrive at the bottom of the list. The selected flag of our ListTile would then become:
#override
Widget build(BuildContext context) {
return ListView(
[...]
children: widget.newsList
.map((news) => ListTile(
[...]
selected: _selectedNews?.id == news.id,
[...]
))
.toList(),
);
}

How to add new items in the Flutter grid view builder from the api when the scroll view reaches the middle

I am trying to update the list of the models when the scroll view reaches the end, while I was able to do so, I am unable to do it smoothly. I want to load the new images from the API into the List of models whenever the user reaches the middle of the scroll so that the scrolling feels smooth as in the current scenario, there is a second or two of pause when the user hits the end after which the new elements will be added so the user won't know if the new elements are added.
Also, my current approach is putting too much load on the main thread and is crashing the app.
here's the code below:
home.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:wallpaper_app/data/data.dart';
import 'package:wallpaper_app/model/categories.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';
import 'package:wallpaper_app/network/network.dart';
import 'package:wallpaper_app/widgets/branding.dart';
import 'package:wallpaper_app/widgets/list_tile.dart';
import 'package:wallpaper_app/widgets/wallpaper_tile.dart';
var pageIndex=1;
Data data = Data();
List<CategoriesModel> categories;
List<WallpaperModel> wallpapers =[];
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
// TODO: implement initState
super.initState();
_controller = ScrollController();
_controller.addListener(() {
if (_controller.position.atEdge) {
if (_controller.position.pixels == 0)
print('top');
// you are at top position
else{
print('end');
incrementIndex();
getWallpapers(pageIndex);
}
// you are at bottom position
}
});
categories = data.getCategories();
getWallpapers(pageIndex);
}
ScrollController _controller;
void incrementIndex(){
setState(() {
pageIndex++;
});
}
void getWallpapers(var index) async {
try {
Network network = Network();
Map<String, dynamic> jsonData = await network.getCuratedWallpapers(
index, 80);
jsonData["photos"].forEach((element) {
WallpaperModel wallpaperModel = WallpaperModel.fromMap(element);
setState(() {
wallpapers.add(wallpaperModel);
});
// for (var wallpaper in wallpapers){
// print(wallpaper.src.small);
// }
});
}
catch(e){
print(e);
}
}
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Color(0xff999999), // navigation bar color
));
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
title: Brand(),
),
body: SingleChildScrollView(
controller: _controller,
child: Container(
child: Column(
children: [
Container(
height: 100,
child: ListView.builder(
// controller: _controller,
padding: EdgeInsets.all(10),
itemCount: categories.length,
// shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return CategoriesTile(
imageUrl: categories[index].imageUrl,
title: categories[index].categoriesName,
);
}),
),
SizedBox(
height: 20,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: GridView.builder(
physics: ScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 6.0,
mainAxisSpacing: 6.0,
childAspectRatio: 0.6),
itemBuilder: (context,index){
// if(index==50){
//// incrementIndex();
// getWallpapers(pageIndex+1);
// }
return WallpaperTile(small: wallpapers[index].src.portrait,);
},
itemCount: wallpapers.length,
),
)
],
),
),
),
);
}
}
wallpaper_model.dart
class WallpaperModel {
String photographer;
String photographerUrl;
int photographerId;
SrcModel src;
WallpaperModel(
{this.photographer, this.photographerUrl, this.photographerId, this.src});
factory WallpaperModel.fromMap(Map<String,dynamic> jsonData){
return WallpaperModel(
photographer: jsonData["photographer"],
photographerId: jsonData["photographer_id"],
photographerUrl: jsonData["photographer_url"],
src: SrcModel.fromMap(jsonData["src"])
);
}
}
class SrcModel {
String original;
String small;
String portrait;
SrcModel({this.original, this.small, this.portrait});
factory SrcModel.fromMap(Map<String, dynamic> jsonData) {
return SrcModel(
original: jsonData["original"],
small: jsonData["small"],
portrait: jsonData["portrait"]);
}
}
wallpaper_tile.dart
import 'package:flutter/material.dart';
class WallpaperTile extends StatelessWidget {
final String small;
WallpaperTile({this.small});
#override
Widget build(BuildContext context) {
return Container(
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child:
Image.network(
small,
fit: BoxFit.cover,
)),
);
}
}
network.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
const apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
class Network{
Future<dynamic> getCuratedWallpapers(var pageNo, var maxWallpapers) async{
var response = await http.get("https://api.pexels.com/v1/curated/?page=$pageNo&per_page=$maxWallpapers",
headers: {
"Authorization":apiKey
});
return jsonDecode(response.body);
}
}
the top list view is just for categories and can be ignored.
it's the grid view.builder that has the core functionality
While I couldn't solve the main issue(loading every time the scroll reached the mid of the screen so that the user doesn't have to wait and see the progress bar) but I was able to solve all the other issues besides that.
too many images(>3000) was causing the app to crash without a stack trace.
Solution-> Instead of using a grid view inside a singleChildScrollView, I added it in an Expanded widget which solved that crashing and sluggish scroll behaviour.
after a certain number of pages I have started to clear ImageCache because from what I was searching I found that after compressing the images, they can still stay in memory(I still don't know how true that is as that is internal flutter process).
I also have used Provider alongside ChangeNotifier instead of setState to avoid extra rebuilding of the widgets.
Result:- Though the UI isn't super smooth, scrolling is happening seamlessly, images are cleared automatically when they are not in view. There is a second of progress bar after which the next page from the API call is loaded into the list. This is my implementation of lazy loading in a way. I am not calling an async call when another is running by using a boolean.
I also added cacheheight and cachewidth to each image to just load compressed image sizes in the grid view and not load full sized images in it.
Also, I used the scroll controller to load more elements into the list every time scroll reaches the bottom of the page.
Here are the main changes.
main.dart
import 'package:flutter/material.dart';
import 'package:wallpaper_app/data/provider_data.dart';
import 'package:wallpaper_app/screens/home.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context)=>ProviderData(),
child: MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: Colors.black,
),
title: 'Wallpaper App',
home: Home()
),
);
}
}
home.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:wallpaper_app/data/data.dart';
import 'package:wallpaper_app/data/provider_data.dart';
import 'package:wallpaper_app/model/categories.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';
import 'package:wallpaper_app/network/network.dart';
import 'package:wallpaper_app/widgets/branding.dart';
import 'package:wallpaper_app/widgets/list_tile.dart';
import 'package:wallpaper_app/widgets/wallpaper_tile.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pagewise/flutter_pagewise.dart';
Data data = Data();
List<CategoriesModel> categories;
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
// TODO: implement initState
super.initState();
categories = data.getCategories();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_controller = ScrollController();
_controller.addListener(() {
if (_controller.position.atEdge) {
if (_controller.position.pixels == 0)
print('top');
// you are at top position
else{
print('end');
// incrementIndex();
if(Provider.of<ProviderData>(context,listen: false).pageIndex>5){
print('image cache: ${imageCache.currentSize}');
imageCache.clear();
}
Provider.of<ProviderData>(context,listen: false).incrementPage();
getWallpapers(Provider.of<ProviderData>(context,listen: false).pageIndex);
}
// you are at bottom position
}
});
getWallpapers(Provider.of<ProviderData>(context,listen: false).pageIndex);
});
}
ScrollController _controller;
void getWallpapers(var index) async {
if(!Provider.of<ProviderData>(context,listen: false).isPerformingRequest){
Provider.of<ProviderData>(context,listen: false).togglePerformingRequest();
try {
Network network = Network();
Map<String, dynamic> jsonData = await network.getCuratedWallpapers(
index, 80);
jsonData["photos"].forEach((element) {
WallpaperModel wallpaperModel = WallpaperModel.fromMap(element);
setState(() {
Provider.of<ProviderData>(context,listen: false).addWallpapers(wallpaperModel);
});
});
}
catch(e){
print(e);
}
Provider.of<ProviderData>(context,listen: false).togglePerformingRequest();
}
}
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return Scaffold(
// appBar: AppBar(
// backgroundColor: Colors.black,
// elevation: 0,
//// backgroundColor: Colors.white,
// title: Brand(),
// ),
body:
// SingleChildScrollView(
// controller: _controller,
// child:
Container(
child: Column(
children: [
// Container(
// height: 100,
// child: ListView.builder(
//// controller: _controller,
// padding: EdgeInsets.all(10),
// itemCount: categories.length,
// shrinkWrap: true,
// scrollDirection: Axis.horizontal,
// itemBuilder: (context, index) {
// return CategoriesTile(
// imageUrl: categories[index].imageUrl,
// title: categories[index].categoriesName,
// );
// }),
// ),
// SizedBox(
// height: 20,
// ),
Expanded(
// padding: EdgeInsets.symmetric(horizontal: 10),
child: GridView.builder(
controller: _controller,
physics: ScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 6.0,
mainAxisSpacing: 6.0,
childAspectRatio: 0.6),
addAutomaticKeepAlives: false,
itemBuilder: (context,index){
if(index==(Provider.of<ProviderData>(context,listen: false).wallpapers!=null?Provider.of<ProviderData>(context,listen: false).wallpapers.length+0:80)){
return _buildProgressIndicator();
}
else
return WallpaperTile(small: Provider.of<ProviderData>(context,listen: false).wallpapers[index].src.medium,);
},
itemCount: Provider.of<ProviderData>(context,listen: false).wallpapers!=null?Provider.of<ProviderData>(context,listen: false).wallpapers.length+1:81,
),
)
],
),
),
// ),
);
}
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: 1,
child: new CircularProgressIndicator(),
),
),
);
}
wallpaper_model.dart
class WallpaperModel {
String photographer;
String photographerUrl;
int photographerId;
SrcModel src;
WallpaperModel(
{this.photographer, this.photographerUrl, this.photographerId, this.src});
factory WallpaperModel.fromMap(Map<String,dynamic> jsonData){
return WallpaperModel(
photographer: jsonData["photographer"],
photographerId: jsonData["photographer_id"],
photographerUrl: jsonData["photographer_url"],
src: SrcModel.fromMap(jsonData["src"])
);
}
}
class SrcModel {
String original;
String small;
String portrait;
String medium;
SrcModel({this.original, this.small, this.portrait,this.medium});
factory SrcModel.fromMap(Map<String, dynamic> jsonData) {
return SrcModel(
original: jsonData["original"],
small: jsonData["small"],
medium: jsonData["medium"],
portrait: jsonData["portrait"]);
}
}
network.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
const apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
class Network{
Future<dynamic> getCuratedWallpapers(var pageNo, var maxWallpapers) async{
var response = await http.get("https://api.pexels.com/v1/curated/?page=$pageNo&per_page=$maxWallpapers",
headers: {
"Authorization":apiKey,
});
print(response.statusCode);
// print(response.body);
return jsonDecode(response.body);
}
}
wallpaper_tile.dart
import 'package:flutter/material.dart';
class WallpaperTile extends StatelessWidget {
final String small;
WallpaperTile({this.small});
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Colors.grey
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child:
Image.network(
small,
fit: BoxFit.cover,
cacheHeight: 400,
cacheWidth: 225,
)
),
);
}
}
provider_data.dart
import 'package:flutter/foundation.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';
class ProviderData extends ChangeNotifier{
List<WallpaperModel> wallpapers =[];
bool isPerformingRequest = false;
var pageIndex=1;
void togglePerformingRequest(){
isPerformingRequest = !isPerformingRequest;
notifyListeners();
}
void incrementPage(){
pageIndex++;
print(pageIndex);
notifyListeners();
}
void addWallpapers(WallpaperModel wallpaperModel){
wallpapers.add(wallpaperModel);
notifyListeners();
}
}
You need to use NotificationListener with ScrollNotification. Modify this with your requirements.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int present = 0;
int perPage = 15;
final originalItems = List<String>.generate(10000, (i) => "Item $i");
var items = List<String>();
#override
void initState() {
super.initState();
setState(() {
items.addAll(originalItems.getRange(present, present + perPage));
present = present + perPage;
});
}
void loadMore() {
setState(() {
if((present + perPage )> originalItems.length) {
items.addAll(
originalItems.getRange(present, originalItems.length));
} else {
items.addAll(
originalItems.getRange(present, present + perPage));
}
present = present + perPage;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels ==
scrollInfo.metrics.maxScrollExtent / 2) {
loadMore();
}
},
child: ListView.builder(
itemCount: (present <= originalItems.length) ? items.length + 1 : items.length,
itemBuilder: (context, index) {
return (index == items.length ) ?
Container(
color: Colors.greenAccent,
child: FlatButton(
child: Text("Load More"),
onPressed: () {
loadMore();
},
),
)
:
ListTile(
title: Text('${items[index]}'),
);
},
),
),
);
}
}
Read more.
try SliverGrid instead of GridView.builder in your home.dart.
because when you will using GridView.builder it will load it's all possible result at once instead of this when you use SliverGrid it will load only those data which is currently present in screen.
SliverGrid(delegate: SliverChildBuilderDelegate((context, int index) {
final _wallpaper = wallpaper.photos ? [index];
return Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10.0),), height: MediaQuery
.of(context)
.size
.height / 2, child: GestureDetector(onTap: () {
//Wallpaper Preview
},
child: Hero(tag: '${_wallpaper?.id}',
child: ClipRRect(borderRadius: BorderRadius.circular(10.0),
child: CachedNetworkImage(fit: BoxFit.cover,
imageUrl: _wallpaper!.src!.large!,
filterQuality: FilterQuality.high,
progressIndicatorBuilder: (context, url, progress) => Center(child: CircularProgressIndicator()),),),),),);
}, childCount: wallpaper.photos?.length),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.5, crossAxisSpacing: 5.0 mainAxisSpacing: 5.0,),),
note: Remove extra that you don't required.

Preserve Widget State in PageView while enabling Navigation

I have a rather complex situation in a Flutter App.
I have a Home screen that is a swipable PageView,that displays 3 child Widgets : Calendar, Messages, Profile.
My issue at the moment is with the Calendar Widget. It is populated dynamically from the initState() method.
I managed to fix a first issue that came from swiping from one page to another that caused rebuilding the Calendar Widget every time.
My issue now is when I tap an item in the Calendar list, I open the detail view. Then, when I close it… all is still OK. However, when I swipe again the initState() method is called once more and the List view is rebuilt. I would like to prevent that and preserve it's state. any suggestions ?
Here is the Home code.
class HomeStack extends StatefulWidget {
final pages = <HomePages> [
CalendarScreen(),
MessagesScreen(),
ProfileScreen(),
];
#override
_HomeStackState createState() => _HomeStackState();
}
class _HomeStackState extends State<HomeStack> with AutomaticKeepAliveClientMixin<HomeStack> {
User user;
#override
bool get wantKeepAlive{
return true;
}
#override
void initState() {
print("Init home");
_getUser();
super.initState();
}
void _getUser() async {
User _user = await HomeBloc.getUserProfile();
setState(() {
user = _user;
});
}
final PageController _pageController = PageController();
int _selectedIndex = 0;
void _onPageChanged(int index) {
_selectedIndex = index;
}
void _navigationTapped(int index) {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
GestureDetector _navBarItem({int pageIndex, IconData iconName, String title}) {
return GestureDetector(
child: HomeAppBarTitleItem(
index: pageIndex,
icon: iconName,
title: title,
controller: _pageController
),
onTap: () => _navigationTapped(pageIndex),
);
}
Widget _buildWidget() {
if (user == null) {
return Center(
child: ProgressHud(imageSize: 70.0, progressSize: 70.0, strokeWidth: 5.0),
);
} else {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_navBarItem(
pageIndex: 0,
iconName: Icons.calendar_today,
title: AppLocalizations.of(context).calendarViewTitle,
),
_navBarItem(
pageIndex: 1,
iconName: Icons.message,
title: AppLocalizations.of(context).messagesViewTitle,
),
_navBarItem(
pageIndex: 2,
iconName: Icons.face,
title: AppLocalizations.of(context).profileViewTitle,
),
],
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
backgroundColor: Colors.transparent,
body: PageView(
onPageChanged: (index) => _onPageChanged(index),
controller: _pageController,
children: widget.pages,
),
floatingActionButton: widget.pages.elementAt(_selectedIndex).fabButton,
);
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Stack(
children: <Widget>[
BackgroundGradient(),
_buildWidget(),
],
),
onWillPop: () async {
return true;
},
);
}
}
And the Calendar code.
class CalendarScreen extends StatelessWidget implements HomePages {
/// TODO: Prevent reloading
/// when :
/// 1) push detail view
/// 2) swipe pageView
/// 3) come back to calendar it reloads
static const String routeName = "/calendar";
static Color borderColor(EventPresence status) {
switch (status) {
case EventPresence.present:
return CompanyColors.grass;
case EventPresence.absent:
return CompanyColors.asher;
case EventPresence.pending:
return CompanyColors.asher;
default:
return CompanyColors.asher;
}
}
final FloatingActionButton fabButton = FloatingActionButton(
onPressed: () {}, /// TODO: Add action to action button
backgroundColor: CompanyColors.sky,
foregroundColor: CompanyColors.snow,
child: Icon(Icons.add),
);
#override
Widget build(BuildContext context) {
return CalendarProvider(
child: CalendarList(),
);
}
}
class CalendarList extends StatefulWidget {
#override
_CalendarListState createState() => _CalendarListState();
}
class _CalendarListState extends State<CalendarList> with AutomaticKeepAliveClientMixin<CalendarList> {
Events events;
void _getEvents() async {
Events _events = await CalendarBloc.getAllEvents();
setState(() {
events = _events;
});
}
#override
void initState() {
_getEvents();
super.initState();
}
#override
bool get wantKeepAlive{
return true;
}
Widget _displayBody() {
if (events == null) {
return ProgressHud(imageSize: 30.0, progressSize: 40.0, strokeWidth: 3.0);
} else if(events.future.length == 0 && events.past.length == 0) return _emptyStateView();
return EventsListView(events: events);
}
#override
Widget build(BuildContext context) {
return _displayBody();
}
Widget _emptyStateView() {
return Center(
child: Text("No data"),
);
}
}
class EventsListView extends StatefulWidget {
final Events events;
EventsListView({this.events});
#override
_EventsListViewState createState() => _EventsListViewState();
}
class _EventsListViewState extends State<EventsListView> {
GlobalKey _pastEventsScrollViewKey = GlobalKey();
GlobalKey _scrollViewKey = GlobalKey();
double _opacity = 0.0;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderSliverList renderSliver = _pastEventsScrollViewKey.currentContext.findRenderObject();
setState(() {
CustomScrollView scrollView = _scrollViewKey.currentContext.widget;
scrollView.controller.jumpTo(renderSliver.geometry.scrollExtent);
_opacity = 1.0;
});
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: AnimatedOpacity(
opacity: _opacity,
duration: Duration(milliseconds: 300),
child: CustomScrollView(
key: _scrollViewKey,
controller: ScrollController(
//initialScrollOffset: initialScrollOffset,
keepScrollOffset: true,
),
slivers: <Widget>[
SliverList(
key: _pastEventsScrollViewKey,
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.past[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.past.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
return Padding(
padding: EdgeInsets.only(top: 32.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Text(
DateFormat.MMMMEEEEd().format(DateTime.now()),
style: Theme.of(context).textTheme.body2.copyWith(
color: CompanyColors.snow,
),
),
);
},
childCount: 1,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.future[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.future.length,
),
),
],
),
),
);
}
}
From the documentation on AutomaticKeepAliveClientMixin:
/// A mixin with convenience methods for clients of
[AutomaticKeepAlive]. Used with [State] subclasses.
/// Subclasses must implement [wantKeepAlive], and their [build]
methods must call super.build (the return value will always return
null, and should be ignored).
So in your code, before you return the Scaffold just call super.build:
Widget build(BuildContext context) {
super.build(context);
return Scaffold(...);
}

How to update stateful widget in Navigation drawer while keeping same class like fragments in Android?

I want to update stateful widget of my class while returning same class after getting data from server, from navigation drawer. I am having issue that class loads data only one time and remain the same if I navigate to another item of my navigation drawer. Because the state is created only once.
Here is my code:
class CategoryFilter extends StatefulWidget {
int productIndex;
String category_name;
CategoryFilter(this.productIndex, this.category_name)
{
print("CategoryFilter");
print(productIndex);
print(category_name);
new _CategoryFilterState(productIndex, category_name);
}
#override
_CategoryFilterState createState() => new
_CategoryFilterState(productIndex, category_name);
}
class _CategoryFilterState extends State<CategoryFilter> {
int productIndex;
List<ResponseDataProducts> productList;
List data;
String category_name;
_CategoryFilterState(this.productIndex, this.category_name)
{
print("CategoryFilter");
print(productIndex);
print(category_name);
}
#override
void initState(){
super.initState();
Future<String> status = getData(productIndex);
status.then((onValue){
if(onValue.toString() == "Success")
{
Navigator.pop(context);
}
});
// this.getData();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
color: Colors.white30,
child: new ListView.builder(
itemCount: productList == null ? 0 : productList.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
margin: const EdgeInsets.only( bottom: 10.0),
constraints: new BoxConstraints.expand(
height: 200.0
),
alignment: Alignment.bottomLeft,
decoration: new BoxDecoration(
image: new DecorationImage(image:
new NetworkImage
("http://myurl.com/"+productList[index].thumbnail),
fit: BoxFit.cover)
),
child:new Container(
child: new Text(
productList[index].name,
style: new TextStyle(color: Colors.white, fontSize: 30.0),
),
color: Colors.black54,
alignment: new FractionalOffset(0.5, 0.0),
height: 35.0,
// margin: const EdgeInsets.symmetric(vertical: 30.0),
),
);
})
),
) ;
}
void _onLoading()
{
showDialog(context: context,
barrierDismissible: false,
child: progress);
new Future.delayed(new Duration(seconds: 2), (){
// Navigator.pop(context);
});
}
Future<String> getData(int productIndex) async {
productList = new List<ResponseDataProducts>();
_onLoading();
http.Response response = await http.get(
Uri.encodeFull(CommonMethods.base_url + 'product/$productIndex'),
headers: {"Accept": "application/json"});
print(response.body);
setState(() {
var convertDataToJson = JSON.decode(response.body);
data = convertDataToJson["responseData"];
for(int i=0; i<data.length; i++)
{
ResponseDataProducts responseData = new ResponseDataProducts(
data[i]["id"],
data[i]["name"], data[i]["description"],
data[i]["title"], data[i]["thumbnail"]);
productList.add(responseData);
}
//Navigator.pop(context);
});
return "Success";
}
}
Here is how I am calling this categoryFilter class from Navigation Drawer:
_getDraserItemWidget(int pos)
{
switch(pos)
{
case 0:
return new Home(bar_id);
case 1:
return new CategoryFilter(categoryList[pos-1].id, categoryList[pos-1].name);
case 2:
return new CategoryFilter(categoryList[pos-1].id, categoryList[pos-1].name);
case 3:
return new CategoryFilter(categoryList[pos-1].id, categoryList[pos-1].name);
case 4:
return new OpeningTime();
case 5:
break;
}
}
I would suggest that instead of calling the method to load data within the initState method of your class, that you use a FutureBuilder widget. If you return a new FutureBuilder from your Navigation Drawer, that should call your service each time a new one is created, and is generally a better way of performing asynchronous requests anyways.
Here's a very simple example. It doesn't do the drawer very well (or a few other things - there's only so much time to spend on things like this), but it should illustrate the concept.
Note that rather than 'updating the widget' it simply creates a new widget. Because of the way flutter does things, this should be relatively performant, especially because you're not doing it all the time but rather only when the user selects something from the navigation menu.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new TextPage(text: "Home!"),
);
}
}
Map<int, int> _nums = Map();
class TextPage extends StatelessWidget {
final String text;
const TextPage({Key key, #required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new PreferredSize(
child: new Container(),
preferredSize: Size.fromHeight(10.0),
),
body: new Center(
child: new Text(text),
),
drawer: new Builder(
builder: (context) => Material(
child: new SafeArea(
child: Column(
children: <Widget>[
new FlatButton(
onPressed: () {
Navigator.pushReplacement(
context, new MaterialPageRoute(builder: (context) => _getDrawerItemWidget(1)));
},
child: Text("First item"),
),
new FlatButton(
onPressed: () {
Navigator.pushReplacement(
context, new MaterialPageRoute(builder: (context) => _getDrawerItemWidget(2)));
},
child: Text("Second item"),
),
],
),
),
),
),
);
}
_getDrawerItemWidget(int i) {
return new FutureBuilder<String>(
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.data != null) {
return new TextPage(text: snapshot.data);
} else {
return new TextPage(text: "Loading.");
}
},
future: () async {
var num = _nums.putIfAbsent(i, () => 0);
_nums[i] = num + 1;
String toReturn = "You retrieved number $i for the $num time";
return await Future.delayed<String>(Duration(seconds: 1), () => toReturn);
}(),
);
}
}
You could theoretically do something different with keeping GlobalKey references and using those to call a method on the child widget if it matches the current selection to have it update, but that's generally a bad idea in flutter - best practices encourage you to pass data downwards in the widget tree rather than call functions downwards. If you have to use GlobalKeys, you can generally refactor to do something better.