Flutter: Prevent `build` being called when `Refresher` is used - flutter

I am using the pull_to_refresh package.
I am having a Stack() with two elements. One of them is the Refresher(). When I pull down on my screen, activating the refreshing animation, the build method is called constantly. The problem is that my second Widget in my Stack is quite complex to build and takes some time. I want to prevent having it build all the time when triggering the Refresher-Animation. Is this possible?
My simplified code would look like this:
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: Stack(children: <Widget>[
SafeArea(
child: Column(children: [
Expanded(
child: Container(
margin: EdgeInsets.all(0),
width: 100.w,
constraints: const BoxConstraints.expand(),
child: SizedBox(
width: 100.w,
child: Refresher( refresher stuff )
)
)
)
)
),
SecondItem()
)
)
}
Somehow the build method of SecondItem is called all the time. Not the build method of the whole scaffold.

If Your second Item dont want to get refresh, then add it as a separeate class like,
Expanded(
child: Container(
margin: EdgeInsets.all(0),
width: 100.w,
constraints: const BoxConstraints.expand(),
child: SizedBox(
width: 100.w,
child: Refresher( refresher stuff )
)
)
)
)
),
SecondItem()
)
class SecondItem extends StatefulWidget {
#override
_SecondItemState createState() => _SecondItemState();
}
class _SecondItemState extends State<SecondItem> {
int counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Text("Your Second Widget"),
);
}
}
Now your SecondItem() will not get refresh when you refresh your FirstItem()

Since I wasn't really able to replicate the problem, I build a working structure that implements refreshing.
First the main widget, in my case MyHomePage.
This widget implements the Scaffold and Stack with FirstWidget and SecondWidget as children.
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: const <Widget>[
FirstWidget(),
SecondWidget(),
],
),
);
}
}
FirstWidget is a statefull widget with a counter in the state.
It implements the refresher with a specific controller.
Once the refresh is triggered, it calls set state and updates the counter within his state.
That should trigger only his build again and not any other.
I implemented a Text to show the counter value increasing at each refresh, and a Print to expose the build.
class FirstWidget extends StatefulWidget {
const FirstWidget({
Key? key,
}) : super(key: key);
#override
State<FirstWidget> createState() => _FirstWidgetState();
}
class _FirstWidgetState extends State<FirstWidget> {
late int _counter;
late RefreshController _refreshController;
#override
void initState() {
_counter = 1;
_refreshController = RefreshController(initialRefresh: false);
super.initState();
}
#override
Widget build(BuildContext context) {
print('First widget built');
return SafeArea(
child: Column(
children: [
Container(
margin: const EdgeInsets.all(0),
width: double.infinity,
height: 500,
color: Colors.red,
child: SmartRefresher(
controller: _refreshController,
onRefresh: () async {
setState(() {
_counter++;
});
await Future.delayed(const Duration(milliseconds: 1000));
_refreshController.refreshCompleted();
},
),
),
Text("Counter: $_counter"),
],
),
);
}
}
Last we got the SecondWidget which is a another simple widget with a print statement.
In case of build it writes on the console.
When the FirstWidget refresh, the second doesn't build becouse his state has not changed.
class SecondWidget extends StatefulWidget {
const SecondWidget({
Key? key,
}) : super(key: key);
#override
State<SecondWidget> createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> {
#override
Widget build(BuildContext context) {
print('Second widget built');
return const Center(child: Text('Second here!'));
}
}
Possible cause of your problem.
It could be that when refreshing, you actually are updating the state of a parent widget that, on cascade, causes the re build of your second widget.
If state is handled correctly, and your second widget doesn't depends on your first widget state, the refresh should not rebuild the second.

Related

how to call child widget function from parent widget

All data is contains in Parent widget and showing in child widget
I want to call function in child widget when parent state change
child widget function which is stateFull widget
void changeSelectedJodi(i) {
_jodiScrollController.animateTo(50.0 * i,
duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
}
Parent widget
child: JodiDataWidget(
this._jodies, <= data is here
this.selectedJodi, <= call function changeSelectedJodi in child widget when change
),
how to achieve this method
You still don't want to access your child from your parent. Flutter control flow goes only one way and there are good reasons for that, which your should respect.
The question is, then, how can my child know that my parent has changed? For this, you have to update the child from the parent (as always) then use didUdpdateWidget in the child to catch a widget change and react to it.
Here is a small example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ParentWidget()));
}
class ParentWidget extends StatefulWidget {
#override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
/// The item of the list to display
///
/// This will be changed randomly by clicking the button
int selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Center(
child: ChildWidget(
selectedIndex: selectedIndex,
),
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: ElevatedButton(
onPressed: () => setState(() => selectedIndex = Random().nextInt(100)),
child: Center(
child: Text('Press my to move the list'),
),
),
),
],
),
);
}
}
class ChildWidget extends StatefulWidget {
/// The item of the list to display
///
/// Changed randomly by the parent
final int selectedIndex;
const ChildWidget({
Key? key,
required this.selectedIndex,
}) : super(key: key);
#override
State<ChildWidget> createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
/// The colors of the items in the list
final _itemsColors = List.generate(
100,
(index) => getRandomColor(),
);
static Color getRandomColor() =>
Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
final _controller = PageController();
void functionOfChildWidget() {
_controller.animateToPage(
widget.selectedIndex,
duration: Duration(milliseconds: 200),
curve: Curves.easeIn,
);
}
/// Here is the important part: When data is set from the parent,
/// move this widget
#override
void didUpdateWidget(covariant ChildWidget oldWidget) {
// If you want to react only to changes you could check
// oldWidget.selectedIndex != widget.selectedIndex
functionOfChildWidget();
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: PageView.builder(
controller: _controller,
padEnds: false,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.all(50),
color: _itemsColors[index],
width: 100,
);
},
itemCount: _itemsColors.length,
),
);
}
}
You can use something like this:
import 'package:flutter/widgets.dart';
class ChangeCallWidget extends StatefulWidget {
final dynamic value;
final VoidCallback onChange;
final Widget child;
const ChangeCallWidget({
super.key,
required this.value,
required this.onChange,
required this.child,
});
#override
State<ChangeCallWidget> createState() => _ChangeCallWidgetState();
}
class _ChangeCallWidgetState extends State<ChangeCallWidget> {
#override
void didUpdateWidget(oldWidget) {
if (oldWidget.value != widget.value) widget.onChange();
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) => widget.child;
}
And use it like this to monitor changes of the _jodies and trigger the onChange if it changes:
ChangeCallWidget(
value: _jodies,
onChange: selectedJodi,
)

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,
],
));
}
}

How to refresh a widget on the outside?

There is a red customView and a button in the page:
I want to change the customView's color to green when I tap the button.
Require:
You must call customView's function changeColor to achieve it;
You can't call page's setState, it's stateless;
Do not use eventBus or provider.
Here is all my code, you can copy and test, input your code in CustomView's changeColor, I desire the easiest way.
import 'package:flutter/material.dart';
class RefreshOutsidePage extends StatelessWidget {
const RefreshOutsidePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
CustomView customView = CustomView();
return Scaffold(
appBar: AppBar(title: Text('refresh outside')),
body: Column(
children: [
customView,
SizedBox(height: 30),
RaisedButton(
child: Text('refresh outside'),
onPressed: () {
customView.changeColor();
},
),
],
),
);
}
}
class CustomView extends StatefulWidget {
CustomView({Key key}) : super(key: key);
#override
_CustomViewState createState() => _CustomViewState();
void changeColor() {
// input your code here
print('change');
}
}
class _CustomViewState extends State<CustomView> {
Color color = Colors.red;
void changeColor() {
setState(() {
color = Colors.green;
});
}
#override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: color,
);
}
}
You can change it through key:
Make CustomViewState public (remove _ at beginning)
Define key and call function changeColor:
class RefreshOutsidePage extends StatelessWidget {
const RefreshOutsidePage({Key key}) : super(key: key);
final _customViewKey = GlobalKey<CustomViewState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('refresh outside')),
body: Column(
children: [
CustomView(key: _customViewKey),
SizedBox(height: 30),
RaisedButton(
child: Text('refresh outside'),
onPressed: () {
_customViewKey.currentState.changeColor();
},
),
],
),
);
}
}
There are two main solutions.
Key solution as mentioned by Autocrab.
State solution where the parent widget becomes Stateful or implement any state management solution in Flutter to update the values of his child.
The child CustomView should be Stateless as it is now because you are not changing the state within widget. So you just require extra parameters received from the parent widget to properly update or get a reference to the widget with the GlobalKey to update it.
If you are using this project for learning or something not legacy I suggest you upgrade Flutter as RaisedButton is deprecated and on the long term you will also have to use null-safety. But that is outside the scope of this question.

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,
)
],
),
);
}
}

statfulWidget with key concept

i am studying key in flutter. and in explanation, when i want swap widget in statefulWidget i need to add key value. because when flutter check element structure if type, state are not same they don't response. this is how i understand.
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
#override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatefulColorfulTile(key: UniqueKey()), // Keys added here
StatefulColorfulTile(key: UniqueKey()),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key); // NEW CONSTRUCTOR
#override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
#override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
#override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
but i saw this code.
Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox()
: const Placeholder(),
GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox()
: const Placeholder(),
],
);
}
this code is also use statefulWidget. in this code when user taps Box it's changed but i think there're no key value and in element structure there are different type(one is SizedBox and the other is placeHolder) so i think there aren't changed. why they're changed? what i misunderstand?