Flutter ListView doesn't change when setState() is called - flutter

I'm new to using Flutter/Dart and I'm having a hard time understanding why the following code doesn't update the ListView contents when _updateResults() calls setState(). I've even added a print statement to display the contents of _results and I can see that items are being added to the list but the display never updates.
class SearchHomePage extends StatefulWidget {
SearchHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
createState() => _SearchHomePageState();
}
class _SearchHomePageState extends State<SearchHomePage> {
List<Widget> _results = <Widget>[];
#override
void initState() {
super.initState();
_results.add(Text("testing"));
}
void _updateResults(String text) {
setState(() {
_results.add(Text(text));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(children: [
Container(
child: TextField(
onSubmitted: (String text) {
_updateResults(text);
},
),
),
Expanded(
child: ListView(children: _results)
),
]),
);
}
}
If I change the ListView portion to be like:
Expanded(
child: ListView(children: _results.toList())
)
Then it will work and I'm not sure why because it was already a list. What exactly is going on here and why doesn't it work with just ListView?

From StatefulWidget documentation:
StatefulWidget instances themselves are immutable and store their mutable state either in separate State objects that are created by the createState method, or in objects to which that State subscribes [...]
From this article:
By definition, immutable means that, once created, an object/variable can’t be changed. So, instead of changing a property of an object, you have to make a copy (or clone) of the entire object and in the process, change the property in question.
You created the ListView with the same array. You changed the content of the array, but you did not change the reference to that object.
That's why it works when you use _results.toList() because .toList() "creates a [List] containing the elements of this [Iterable]".
Another solution could be:
setState(() {
_results = List.from(_results)
..add(Text(text));
});

it doesn't work because you are adding a new item to the existing list and not creating a new list.
this will fix your problem:
class SearchHomePage extends StatefulWidget {
SearchHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
createState() => _SearchHomePageState();
}
class _SearchHomePageState extends State<SearchHomePage> {
List<Widget> _results = <Widget>[];
#override
void initState() {
super.initState();
_results.add(Text("testing"));
}
void _updateResults(String text) {
setState(() {
_results = [..._results, Text(text)];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(children: [
Container(
child: TextField(
onSubmitted: (String text) {
_updateResults(text);
},
),
),
Expanded(
child: ListView(children: _results)
),
]),
);
}
}
note: it's better to do the follwing rather than creating widgets and saving them in an array:
class SearchHomePage extends StatefulWidget {
SearchHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
createState() => _SearchHomePageState();
}
class _SearchHomePageState extends State<SearchHomePage> {
List<String> _results = <String>[];
#override
void initState() {
super.initState();
_results.add("testing");
}
void _updateResults(String text) {
setState(() {
_results = [..._results, text];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(children: [
Container(
child: TextField(
onSubmitted: (String text) {
_updateResults(text);
},
),
),
Expanded(
child: ListView(
children: _results.map((v) => Text(v)).toList(),
)),
]),
);
}
}
when you call .toList() it will create a new list instance (copy of your old list)

ListView(
shrinkWrap: true,
children: _results.map((one) => one).toList(),
),
ListView.builder(
shrinkWrap: true,
itemCount: _results.length,
itemBuilder: (context, index) => _results[index],
),
If You strive to getting your output as per your requirement then you have to change your code like above instand of below code:
Expanded(
child: ListView(children: _results.toList()))
Please put any one listview from both...

Related

Screen goes blank if I search for something in TextField that is out of my list

I have created a list in another directory which is a list of JSON data and I have showed it on screen and also added a TextField which works as a search bar. it works fine if I search for a letter(or combination of letters) that is inside my list but when I search for something out of list the whole page goes blank except for the Appbar.
here's my code:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
// Hide the debug banner
debugShowCheckedModeBanner: false,
title: 'Kindacode.com',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List _items = [];
List _itemsForDisplay = [];
// Fetch content from the json file
Future<void> readJson() async {
final String response = await rootBundle.loadString('assets/Symptoms.json');
final data = await json.decode(response);
setState(() {
_items = data["Symptoms"];
_itemsForDisplay = _items;
});
}
#override
void initState() {
// TODO: implement initState
readJson();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Diagnose',
),
),
body: Padding(
padding: const EdgeInsets.all(25),
child: Column(
children: [
// Display the data loaded from sample.json
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return index == 0 ? _searchBar() : _ListItem(index);
},
itemCount: _itemsForDisplay.length,
),
)
],
),
),
);
}
_searchBar() {
return Padding(
padding: const EdgeInsets.all(8),
child: TextField(
decoration: InputDecoration(hintText: 'Search'),
onChanged: (text) {
text = text.toLowerCase();
setState(() {
_itemsForDisplay = _items.where((item) {
var itemTitle = item.toLowerCase();
return itemTitle.contains(text);
}).toList();
});
},
),
);
}
_ListItem(index) {
return Card(
margin: const EdgeInsets.all(10),
child: ListTile(
leading: Text(_itemsForDisplay[index]),
),
);
}
}
Here's a picture before for searching for something that is an entity of my list
and this happens when I search for "." or anything that is not in my list:
In your item builder return only _ListItem(index) and put _searchBar outside of Expanded. In current code, you are skipping first element from list and showing search bar instead, which is wrong. That's why when length of list is 0 you don't see anything. Because builder doesn't get executed.
So, your built method should look like this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'Diagnose',
),
),
body: Padding(
padding: const EdgeInsets.all(25),
child: Column(
children: [
// Display the data loaded from sample.json
_searchBar(),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return _ListItem(index);
},
itemCount: _itemsForDisplay.length,
),
)
],
),
),
);
}

Update Text with Dissmissble setState

I want to update my Text() value whenever I dismiss an item from the screen .
This is the MainScreen() :
Text.rich(
TextSpan(
text: total().toString() + " DT",
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold),
),
The function total() is located in Product Class like this :
class Product {
final int? id;
final String? nameProd;
final String? image;
final double? price;
Product({this.id, this.nameProd, this.image, this.price});
}
List<Product> ListProduitss = [
Product(
price: 100, nameProd: 'Produit1', image: 'assets/images/freedomlogo.png')
];
double total() {
double total = 0;
for (var i = 0; i < ListProduitss.length; i++) {
total += ListProduitss[i].price!;
}
print(total);
return total;
}
I have this in the main screen .
After I remove the item from list , I want to reupdate the Text() because the function is printing a new value in console everytime I dismiss a product :
This is from statefulWidget CartItem() that I render inside MainScreen() :
ListView.builder(
itemCount: ListProduitss.length,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Dismissible(
key: Key(ListProduitss.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
setState(() {
ListProduitss.removeAt(index);
total();
// What to add here to update Text() value everytime
});
},
I tried to refresh the main screen but It didn't work .
onDismissed: (direction) {
setState(() {
ListProduitss.removeAt(index);
MainScreen();
});
},
One way is to declare a local string variable to use within the text. Then initialise the variable using total() within initState(). Then in setState do the same process.
However, it may be beneficial for you to look into a state management pattern such as BLoC pattern. https://bloclibrary.dev/#/
late String text;
void initState() {
super.initState();
text = Product.total();
}
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
appBar: AppBar(),
body: Column(
children: [
Text(text),
ElevatedButton(child: Text("Update"), onPressed:() => setState(() {
text = Product.total();
}),)
],
)
);
}
I am going to add another example as there was confusion to the above example. Below is an example of updated a text field with the length of the list. It is updated every time an item is removed.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
List<int> items = List<int>.generate(100, (int index) => index);
late String text;
#override
void initState() {
text = items.length.toString(); // << this is total;
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(text),
Expanded(
child: ListView.builder(
itemCount: items.length,
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (BuildContext context, int index) {
return Dismissible(
background: Container(
color: Colors.green,
),
key: ValueKey<int>(items[index]),
onDismissed: (DismissDirection direction) {
setState(() {
items.removeAt(index);
text = items.length.toString(); // < this is total()
});
},
child: ListTile(
title: Text(
'Item ${items[index]}',
),
),
);
},
),
),
],
);
}
}

Flutter - How to connect void to build widget?

I am trying to get a new historyTile() to be called to the Scaffold() each second. I am unsure how to make the void function connect.
Any advice and feedback is appreciated!
Code:
class activityTab extends StatefulWidget {
const activityTab({Key? key}) : super(key: key);
#override
State<activityTab> createState() => _activityTabState();
}
class _activityTabState extends State<activityTab> {
#override
void historyTile() {
final now = DateTime.now();
String tileTime = DateFormat.yMMMMd().add_jm().format(now);
ListView.builder(
shrinkWrap: true,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('${tileTime}'),
tileColor: Colors.greenAccent,
);
}
);
}
#override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (Timer t) => historyTile());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:
Container(
child: SingleChildScrollView(
child: historyTile(); // ERROR HERE
),
),
);
}
}
You can try creating periodic streams with a Stream Builder widget. If not, the simplest method is to put your widget in scaffold and try calling the setState function periodically with a 1-second timer.
In the StreamBuilder example you should change the widget a bit. Sending the parameter you want to update to the widget from outside will add a little more flexibility to you.
return Scaffold(
body: StreamBuilder<String>(
stream: Stream.periodic(const Duration(seconds: 1), (x) {
// Your Action Here
final now = DateTime.now();
return DateFormat.yMMMMd().add_jm().format(now);
}),
builder: (context, snapshot) {
String param = "";
if (snapshot.hasData) param = snapshot.data!;
return _historyTile(txt = param);
}
),
);
Or you could use your widget in Scaffold Body and periodically set the widgets state in timer callback.
class _activityTabState extends State<activityTab> {
String tileTime = "";
...
Timer.periodic(Duration(seconds: 1), () {
setState(() {
final now = DateTime.now();
tileTime = DateFormat.yMMMMd().add_jm().format(now);
});
};
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SingleChildScrollView(
child: historyTile(tileName);
),
),
);
}
or just
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SingleChildScrollView(
child: ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('$tileTime'),
tileColor: Colors.greenAccent,
),
),
),
);
}
Create your historyTile widget as a custom widget
class HistoryTile extends StatefulWidget {
const HistoryTile({Key? key, required this.txt}) : super(key: key);
final String txt;
#override
State<HistoryTile> createState() => _HistoryTileState();
}
class _HistoryTileState extends State<HistoryTile> {
#override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text(widget.txt),
tileColor: Colors.greenAccent,
);
}
}
there is some issues in you ListView.Builder. You do not put itemCount there. And you need to use setState in timer. So codes are below. Please check.
class activityTab extends StatefulWidget {
const activityTab({Key? key}) : super(key: key);
#override
State<activityTab> createState() => _activityTabState();
}
class _activityTabState extends State<activityTab> {
String _now;
Timer _everySecond;
#override
historyTile() {
final now = DateTime.now();
String tileTime = DateFormat.yMMMMd().add_jms().format(now);
return ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('${tileTime}'),
tileColor: Colors.greenAccent,
);
});
}
void _timer() {
Future.delayed(Duration(seconds: 1)).then((_) {
setState(() {
_timer();
});
});
}
#override
void initState() {
super.initState();
_timer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: 500,
child: SingleChildScrollView(
child: historyTile(),
),
),
);
}
}

Force rebuild of a stateful child widget in flutter

Let's suppose that I have a Main screen (stateful widget) where there is a variable count as state. In this Main screen there is a button and another stateful widget (let's call this MyListWidget. MyListWidget initialize it's own widgets in the initState depending by the value of the count variable. Obviously if you change the value of count and call SetState, nothing will happen in MyListWidget because it create the values in the initState. How can I force the rebuilding of MyListWidget?
I know that in this example we can just move what we do in the initState in the build method. But in my real problem I can't move what I do in the initState in the build method.
Here's the complete code example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
});
},
),
),
MyListWidget(count),
],
));
}
}
class MyListWidget extends StatefulWidget {
final int count;
const MyListWidget(this.count, {Key? key}) : super(key: key);
#override
_MyListWidgetState createState() => _MyListWidgetState();
}
class _MyListWidgetState extends State<MyListWidget> {
late List<int> displayList;
#override
void initState() {
super.initState();
displayList = List.generate(widget.count, (int index) => index);
}
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text(displayList[index].toString()),
),
itemCount: displayList.length,
),
);
}
}
I don't think the accepted answer is accurate, Flutter will retain the state of MyListWidget because it is of the same type and in the same position in the widget tree as before.
Instead, force a widget rebuild by changing its key:
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
});
},
),
),
MyListWidget(count, key: ValueKey(count)),
],
),
);
}
}
Using a ValueKey in this example means the state will only be recreated if count is actually different.
Alternatively, you can listen to widget changes in State.didUpdateWidget, where you can compare the current this.widget with the passed in oldWidget and update the state if necessary.
USE THIS:
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
MyListWidget myListWidget = MyListWidget(5);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
myListWidget = MyListWidget(count);
});
},
),
),
myListWidget,
],
));
}
}

Accessing Widget State instances in Flutter

I'm having trouble accessing instances of objects (or States) in Flutter, from other classes. I've tried a lot of fiddling using similar questions on the web, and am currently using 'GlobalKey', but I just can't get it working.
I'm trying to make a simple Flutter app where the State of a Widget gets accessed from another class, on button press:
import 'viewer.dart' as viewer;
(...)
onPressed: () {
//Works
print("Doing something");
//Doesn't work
viewer.key.currentState.nextPage();
},
My viewer.dart file looks contains a PageController, and a class containing that controller:
final key = new GlobalKey<_RegistryState>();
final PageController _controller = PageController(
initialPage: 0,
);
class Registry extends StatefulWidget {
Registry({ Key key }) : super(key: key);
#override
_RegistryState createState() => _RegistryState();
}
class _RegistryState extends State<Registry> {
void next() {
print("Doing something!");
_controller.nextPage();
}
#override
Widget build(BuildContext context) {
return PageView(
//physics: NeverScrollableScrollPhysics(), //Disable user manually scrolling
controller: _controller,
children: [
registry_screens.ScreenSplash(),
registry_screens.ScreenName(),
Text("Bye"),
],
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
The idea is that whenever the button gets pressed, the PageController moves to the next page (which is already there, I can scroll to it manually by swiping on the screen).
The app compiles fine, but when pressing the button I get the error 'NoSuchMethodError: invalid member on null: 'next''.
Am I using the correct approach for accessing instances of Widgets (or States)?
Flutter is a declarative framework. In this kind of environment, everytime that you want to change the view (or interface) you need to rebuild it. And if you rebuild what is holding the state, you would loose it. That's why it should not be responsible of holding the state of the program.
State management in Flutter is a broad subject with lots of options. As #DrSatan1 mentioned in the comments, in Flutter.dev you can find good documentation about state management using Provider, but you have lots of options with BLoC, ReduX, MobX, etc.
In your specific scenario, since it is simpler, you could accomplish that using a global object or Inherited Widget.
Global Object
globals.dart
currentPage=0;
In the Widget
import 'globals.dart' as global;
(...)
onPressed: () {
setState((){
globals.currentPage++;
});
},
viewer.dart
#override
Widget build(BuildContext context) {
return PageView(
//physics: NeverScrollableScrollPhysics(), //Disable user manually scrolling
currentPage: globals.currentPage, //instead of using PageController
children: [
registry_screens.ScreenSplash(),
registry_screens.ScreenName(),
Text("Bye"),
],
);
}
You could use the PageController as your global object. In that case you could pass the PageController down the widget tree. In this case, it would be better to use InheritedWidget instead.
InheritedWidget
As per docs, InheritedWidget is
Base class for widgets that efficiently propagate information down the
tree.
You can pass your PageController to all the widgets below the tree. Your viewer.dart would be:
(...)
#override
Widget build(BuildContext context) {
return MyInheritedWidget (
pageController: _controller,
child: PageView(
//physics: NeverScrollableScrollPhysics(), //Disable user manually scrolling
//controller: _controller, // Don't pass controller here
children: [
registry_screens.ScreenSplash(),
registry_screens.ScreenName(),
Text("Bye"),
],
);
);
}
(...)
// create the inherited widget wrapper. It could be done with [Builder][7] too, instead of a different Widget.
class MyInheritedWidget extends InheritedWidget {
final PageController pageController;
MyInheritedWidget({
Key key,
#required Widget child,
#required this.pageController,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
}
(...)
After that you can access pageController in PageView or any Widget under it.
(...)
onPressed: () {
//Works
print("Doing something");
// Find closest InheritedWidget
MyInheritedWidget myInheritedWidget =
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
// Get pageController from it
PageController controller = myInheritedWidget.pageController
// call nextPage()
nextPage();
},
(...)
Although both methods works in your specific scenario, you should check Flutter Docs about state management. Maybe you don't need the PageController at all.
It's generally a bad idea for state to be accessed externally. Instead, external classes should only interact with Widgets through the methods they expose.
I just made a video walking through the exact same onboarding setup you have using a PageView, which you can see here -- as I go through it step-by-step: https://www.youtube.com/watch?v=ji__FEKSnMw
In essence, it looks like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MainPage(),
debugShowCheckedModeBanner: false,
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
PageController pageController = new PageController(initialPage: 0);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
child: PageView(
controller: pageController,
physics: NeverScrollableScrollPhysics(),
children: [
Slide(
hero: Image.asset("./assets/hero-1.png"),
title: "Boost your traffic",
subtitle:
"Outreach to many social networks to improve your statistics",
onNext: nextPage),
Slide(
hero: Image.asset("./assets/hero-2.png"),
title: "Give the best solution",
subtitle:
"We will give best solution for your business isues",
onNext: nextPage),
Slide(
hero: Image.asset("./assets/hero-3.png"),
title: "Reach the target",
subtitle:
"With our help, it will be easier to achieve your goals",
onNext: nextPage),
Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text(
'Be kind to yourself',
style: kTitleStyle,
),
),
)
])),
),
);
}
void nextPage() {
pageController.nextPage(
duration: const Duration(milliseconds: 200), curve: Curves.ease);
}
}
class Slide extends StatelessWidget {
final Widget hero;
final String title;
final String subtitle;
final VoidCallback onNext;
const Slide({Key key, this.hero, this.title, this.subtitle, this.onNext})
: super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: hero),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
title,
style: kTitleStyle,
),
SizedBox(
height: 20,
),
Text(
subtitle,
style: kSubtitleStyle,
textAlign: TextAlign.center,
),
SizedBox(
height: 35,
),
],
),
),
GestureDetector(
onTap: onNext,
child: Text(
"Skip",
style: kSubtitleStyle,
),
),
SizedBox(
height: 4,
)
],
),
);
}
}