Flutter cannot call setState to rebuild widget - flutter

I am very new to flutter and the millions of answers I have read online made me even more confusion. This is my page:
class SecondDegreePage extends StatefulWidget {
const SecondDegreePage({Key key}) : super(key: key);
#override
SecondDegreePageState createState() => SecondDegreePageState();
}
class SecondDegreePageState extends State<SecondDegreePage> {
final GlobalKey<FormState> _formKey = new GlobalKey();
final controllerParamA = TextEditingController();
final controllerParamB = TextEditingController();
final controllerParamC = TextEditingController();
Quadratic _solver = Quadratic([
(Random().nextInt(10) + 1) * 1.0,
(Random().nextInt(10) + 1) * 1.0,
(Random().nextInt(10) + 1) * 1.0
]);
void updateData() {
setState(() {
_solver = Quadratic([
(Random().nextInt(10) + 1) * 1.0,
(Random().nextInt(10) + 1) * 1.0,
(Random().nextInt(10) + 1) * 1.0
]);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalization.of(context).title),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
// Input form
Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
EquationInputField(
controller: controllerParamA,
textHint: 'a',
width: 70,
),
EquationInputField(
controller: controllerParamB,
textHint: 'b',
width: 70,
),
EquationInputField(
controller: controllerParamC,
textHint: 'c',
width: 70,
),
],
),
),
),
// Submit button
Padding(
padding: EdgeInsets.fromLTRB(0, 30, 0, 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => updateData(),
color: Colors.blue,
elevation: 6,
child: Text(
AppLocalization.of(context).solve,
style: TextStyle(
color: Colors.white
),
),
)
],
),
),
//Solutions
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: 10),
child: AlgebraicSolutions(
solver: _solver
),
),
),
],
),
);
}
#override
void dispose() {
controllerParamA.dispose();
controllerParamB.dispose();
controllerParamC.dispose();
super.dispose();
}
}
The type Quadratic is simply a class that does some math, nothing important. The problem is in this line:
RaisedButton(
onPressed: () => updateData()
}
Why nothing happens? I have read thet the call to setState calld the build method and the widget is re-built. For this reason I expect that
child: AlgebraicSolutions(
solver: _solver
),
here the _solver reference gets updated. AlgebraicSolutions is the following:
class AlgebraicSolutions extends StatefulWidget {
final Algebraic solver;
const AlgebraicSolutions({Key key, #required this.solver}) : super(key: key);
#override
AlgebraicSolutionsState createState() => AlgebraicSolutionsState();
}
class AlgebraicSolutionsState extends State<AlgebraicSolutions> {
//'Quadratic' is a subtype of Algebraic
Algebraic _solver;
#override
void initState() {
_solver = widget.solver;
super.initState();
}
#override
Widget build(BuildContext context) {
var solutions = _solver.solve();
return ListView.builder(
itemCount: solutions.length,
itemBuilder: (context, index) {
//...
}
);
}
}
Is that because I am using initState in AlgebraicSolutionsState that breaks something?
Please note that Quadratic type is subclass of Algebraic type.

Yep, saving the initial value in initState is what's giving you trouble. That lifecycle method is only called on first build but since the widget reconciles to the compatible type the same state is used. i.e. the properties change (widget instance is different) but the state is the same object. build gets called again but initState does not.
In this situation I would use a getter to give you the same convenience but always use the _solver from the current widget. Then you can discard the initState.
Algebraic get _solver => widget.solver;
The other option being didUpdateWidget but it really not necessary here for something so straightforward otherwise.

Related

Not getting values when querying list in search bar implementation Flutter

I need some help when implementing the search bar funcionality in Flutter.
I am implementing flappy_search_bar: https://pub.dev/packages/flappy_search_bar
However, it does return any value when I try to search something.
Is there anything I am missing? Seems trivial this kind of implementations, just query some list and include the results in other list but I cannot figure out the way to do it.
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
TextEditingController textController = TextEditingController();
Future<List<dynamic>> search(String search) async {
await Future.delayed(Duration(seconds: 2));
List<dynamic> dogs = BreedList.where((dog) => dog['breed'].contains(search)).toList();
return dogs;
// return List(search.length, (int index) {
// return DogClass(breed: "$search $index");
// });
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(children: [
Container(
margin: const EdgeInsets.only(top: 60, bottom: 15),
padding: const EdgeInsets.only(left: 20, right: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Center(
child: Container(
width: MediaQuery.of(context).size.width -
(MediaQuery.of(context).size.width / 3.5),
height: 80,
child: SearchBar<dynamic>(
searchBarStyle: SearchBarStyle(borderRadius: BorderRadius.circular(20)),
onSearch: search,
cancellationWidget: Text('Cancel'),
emptyWidget: SizedBox.shrink(),
shrinkWrap:true,
onItemFound: (dynamic dogs, int index) {
return Container(
child: ListTile(
title: Text(dogs.breed.toString())
),
);
}),
//child: const Icon(Icons.search, color: Colors.white),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(Dimensions.radius20),
color: Colors.white),
),
)
],
),
),
//wrapping with this widgets the scroll problem is solved in list
const Expanded(
child: SingleChildScrollView(
child: BookPageBody(),
)),
]));
}
}

Flutter: Use AbsorbPointer without rebuilding entire widget tree

I have a Stateful home page which has a list of Stateful widget children. When I click on a child, I'm gonna call its setState() to add a CircularProgressIndicator to it. That's all fine and dandy; clicking on a child only rebuilds that child.
However, I also have my home page wrapped inside an AbsorbPointer, and I want to set absorbing = true when I click on a child widget. The goal is to stop the user from clicking around while the app is doing some async work in the background. The problem now is that if I call setState() in the home page to set "absorbing" to true, it will rebuild all of the child widgets.
I could pass some parameters into the child widgets so that only the one I clicked on will have a CircularProgressIndicator, but even then all the other children will still be rebuilt.
I guess this boils down to the fact that I can't call setState() on a parent widget without rebuilding all the children, even though the parameter I pass to that setState() (absorbing) has nothing to do with those children.
Is there a workaround for this?
Thanks!
// home_screen.dart
class HomeScreen extends StatefulWidget {
static const String routeName = "homeScreen";
final MyUser? user;
const HomeScreen({Key? key, required this.user}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
final MyDatabase _db = MyDatabase();
MyUser? _me;
int _currentPage = -1;
bool _isLoading = false;
...
#override
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: _isLoading,
child: Container(
decoration: BoxDecoration(
// color: Color(0xFF0d0717),
image: DecorationImage(
image: Image.asset(
'assets/background.png',
).image,
fit: BoxFit.cover,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: ...,
bottomNavigationBar: ...,
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StreamBuilder<QuerySnapshot>(
stream: _db.getLiveChannels(),
builder: (_, snapshot) {
if (!snapshot.hasData) {
// print("Has no data");
return Center(
child: CircularProgressIndicator(),
);
}
_channels.addAll(List.generate(
snapshot.data!.docs.length,
(index) => Channel.fromSnapshot(
snapshot.data!.docs[index])));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Now Playing',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.w700,
),
),
SizedBox(width: 8.0),
LiveIndicator(),
],
),
SizedBox(height: 8.0),
Container(
height: 250,
child: PageView.builder(
physics: PageScrollPhysics(),
scrollDirection: Axis.horizontal,
controller: PageController(
viewportFraction: .9,
),
itemCount: _channels.length,
itemBuilder: (context, index) {
Channel channel =
_channels[_channels.length - 1 - index];
return ChildWidget(
callback: _callback;
loading: (_isLoading && _currentPage == index),
key: UniqueKey(),
);
},
),
),
...,
],
);
},
),
...,
],
),
),
),
),
),
);
}
Future<void> _callback(params) async {
if (_isLoading == false) {
setState(() {
_isLoading = true;
_currentPage = index;
});
}
someAsyncMethod().then((_) => setState(() {
_isLoading = false;
_currentPage = -1;
}));
}
}
// child_widget.dart
class ChildWidget extends StatefulWidget {
final Future<void> Function(params) callback;
final bool loading;
const ChildWidget({
Key? key,
required this.callback,
required this.loading,
}) : super(key: key);
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
late Future<void> Function(params) callback;
late bool loading;
#override
void initState() {
super.initState();
callback = widget.callback;
loading = widget.loading;
}
#override
Widget build(BuildContext context) {
return Padding(
child: Column(
children: [
Expanded(
child: CustomClickableWidget(
onPressed: callback,
child: Expanded(
child: Container(
child: Stack(
children: [
...,
if (loading) ...[
Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
],
],
),
),
),
),
),
...,
],
),
);
}
}
Screenshot
The SetState function triggers the Build() function, so all the code present in the Build() function will be executed again. I don't quite see why this is a problem for you ?
On the other hand in your code I see that for your child you have defined a key: UniqueKey (). When the build function will run after SetState (), it will create a new child without keeping the state of the previous child. You shouldn't define the UniqueKey () in your function but rather as an instance variable of your state
ChildWidget(callback: _callback;
loading: (_isLoading && _currentPage == index),
key: UniqueKey(),
)
You should define you key here
class _ChildWidgetState extends State<ChildWidget> {
UniqueKey myKey = UniqueKey();
and you function
ChildWidget(callback: _callback;
loading: (_isLoading && _currentPage == index),
key: myKey,
)

GetX: Save order of elements of fetching new data

I am using GetX. I need to display list from JSON.
class JobModelListView extends StatelessWidget {
final modelController = Get.put(JobModelList());
#override
build(BuildContext context) {
return Obx(() =>
ListView.builder(
itemBuilder: (ctx, idx) => JobModelView(modelController.jobList[idx], key: UniqueKey()),
itemCount: modelController.jobList.length
));
}
}
Single Element Item:
class JobModelView extends StatelessWidget {
final JobModel jobModel;
JobModelView(this.jobModel, {Key? key}) : super(key: key);
final modelController = Get.put(JobModelList());
#override
build(BuildContext context) {
return Row(
children: [
Container(
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
child:
CustomPaint(
size: Size(50, 100),
painter: JobModelPainter(jobModel.parserStatus),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("""PID: ${jobModel.jobPid}"""),
Text("""Queue: ${jobModel.unprocessedFileCount}"""),
Text("""Jobs: ${jobModel.jobNumbers}"""),
Text("""Section: ${jobModel.currentSection}"""),
Text("""LastPingDate: ${jobModel.lastPingDate == null ? '' : timeago.format(jobModel.lastPingDate!, locale: 'en_short')} ago"""),
Text("""ParserStatus: ${jobModel.parserStatus.toString().split(".").last}"""),
Text("""isSomeUnknownTags: ${jobModel.isSomeUnknownTags}"""),
Text("""Region: ${jobModel.region}"""),
Text("""Msg: ${jobModel.msg}"""),
])),
SizedBox(
height: 45,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.red, // background
onPrimary: Colors.white, // foreground
),
child: Text("KILL"),
onPressed: () async {
await modelController.removeByPid(jobModel.jobPid!);
await modelController.fetchModelList();
},
))
],
);
}
}
I need to save order of elements on every redraw (getting updated dates from JSON). But they are randomly change order.
Sometimes pid: 4712 on top, sometimes another pid.
Hot to fix it?

Stateful widget doesn't change state

In this code, when I change page (I'm using PageView as is it in code below) flutter doesn't trigger rebuild, so condition if(_page == 1) will take effect after I press "hot reload". Any tips for solution? I calling this class in main.dart (HomePage) which is Stateless widget. Could it be the problem?
Thanks for any help!
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
int _page = 0;
class Guide extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new GuideState();
}
}
class GuideState extends State<Guide> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(children: [
if (_page == 1)
Padding(
padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
child: Image(
image: AssetImage('graphics/Logo.png'),
height: MediaQuery.of(context).size.height * 0.1)),
SizedBox(height: 500, child: Page()),
]))));
}
}
class Page extends StatefulWidget {
PageState createState() => PageState();
}
class PageState extends State<Page> {
final controller = PageController(
initialPage: 0,
);
#override
Widget build(BuildContext context) {
return Scaffold(
//appBar: AppBar(title: Text('PageView Widget in Flutter')),
body: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.95,
height: MediaQuery.of(context).size.height * 0.6,
child: PageView(
controller: controller,
onPageChanged: (page) {
setState(() {
if (page == 1) {
_page = 1;
}
});
},
pageSnapping: true,
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
//color: Colors.pink,
//child: Center(
child: Text(
'1. Tento text bude nahrán z databáze.',
style: TextStyle(fontSize: 25, color: Colors.black),
)),
Container(
//color: Colors.green,
child: Text(
'2. Tento text bude nahrán z databáze',
style: TextStyle(fontSize: 25, color: Colors.black),
)),
Container(
// color: Colors.lightBlue,
child: Text(
'3. Tento text bude nahrán z databáze',
style: TextStyle(fontSize: 25, color: Colors.black),
)),
],
),
)));
}
}
The variable _page is set as global, it has to be part of as state inorder to trigger changes, but in your case you want to change a widget base on action in another child widget, this can be done in several ways depending on your choice the easies in you case is to have a function as a parameter for your child widget Page :
class Page extends StatefulWidget {
final Function(int) onChange;
const Page({Key key, this.onChange}) : super(key: key);
PageState createState() => PageState();
}
and then call it when the page change
onPageChanged: (page) {
widget.onChange(page);
},
so with this you can handle the change in you parent widget and trigger state change
class GuideState extends State<Guide> {
int _page = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
children:[
if (_page == 1)
Padding(
padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
child: Image(
image: AssetImage('graphics/Logo.png'),
height: MediaQuery.of(context).size.height * 0.1,
),
),
SizedBox(
height: 500,
child: Page(
onChange: (page) {
setState(() => _page = page);
},
),
),
],
),
),
),
);
}
}
int _page = 0; is not part of the state of your Guide widget. Place it here:
class GuideState extends State<Guide> {
int _page = 0;
...

Is it possible to create links to sections in the same page in flutter web?

I want to create a website using flutter web but I'm unable to navigate to sections in the same page. Here's an example of what I want to achieve using flutter.
P.S. Navigator is not working:
I created an example with PageView
class MyHomePage extends StatelessWidget {
var list = ["Home","Services", "Work", "About"];
var colors = [Colors.orange, Colors.blue, Colors.red, Colors.green];
PageController controller = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 50,
height: 50,
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10)
),
),
Spacer(),
Row(
children: List.generate(3, (index){
return GestureDetector(
onTap: (){
_scrollToIndex(index);
},
child: Container(
margin: EdgeInsets.all(8),
child: Text(
list[index+1]
),
),
);
}),
)
],
),
Expanded(
child : PageView(
scrollDirection: Axis.vertical,
pageSnapping: false,
controller: controller,
children: List.generate(list.length, (index){
return Container(
width: MediaQuery.of(context).size.width,
height: double.maxFinite,
color: colors[index],
child: Center(
child: Text(
list[index],
style: TextStyle(
color: Colors.white,
fontSize: 50
),
),
),
);
})
),
),
],
)
),
);
}
void _scrollToIndex(int index) {
controller.animateToPage(index + 1, duration: Duration(seconds: 2), curve: Curves.fastLinearToSlowEaseIn);
}
}
The output:
ScrollController is the thing you are looking for.
Add a new one to your ScrolView and you can set where you want it to scroll to.
Josteve mentioned a way of doing it. But I'd like to show the other way which provides more features as one would expect in the gif example you have put.
You can see the demo here: https://mohith7548.github.io/portfolio/
My project has 3 sections called About, Blog & Projects. It also has another top section called Home. So the order of screens is Home, About, Blog & Projects. Each section takes full-screen height & width. So the starting offset for these pages are [0 * screenHeight, 1 * screenHeight, 2 * screenHeight, 3 * screenHeight] respectively. screenHeight can be accessed by MediaQuery.of(context).size.height inside build method.
class Portfolio extends StatefulWidget {
#override
_PortfolioState createState() => _PortfolioState();
}
class _PortfolioState extends State<Portfolio> {
ScrollController _scrollController;
String _curNavItem;
static double offsetHome = 0;
static double offsetAbout = SizeConfig.screenHeight;
static double offsetBlog = 2 * SizeConfig.screenHeight;
static double offsetProjects = 3 * SizeConfig.screenHeight;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
}
#override
void dispose() {
super.dispose();
_scrollController.dispose();
}
void scrollTo(String title) {
double offset = 0;
switch (title) {
case Constants.HOME:
offset = offsetHome;
break;
case Constants.ABOUT:
offset = offsetAbout;
break;
case Constants.BLOG:
offset = offsetBlog;
break;
case Constants.PROJECTS:
offset = offsetProjects;
break;
}
setState(() {
_curNavItem = title;
});
// animate to the pag
_scrollController.animateTo(
offset,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutQuart,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: PageScrollPhysics(), // use NeverScrollableScrollPhysics() to block user scrolling
controller: _scrollController,
slivers: <Widget>[
// This is just SliverAppBar wrapped in InterheritedWidget called NavState
// You can use just SliverAppBar
NavState(
curNavItem: _curNavItem,
scrollTo: scrollTo,
child: AppBanner(key: _appBannerKey), // SliverAppBar in another file
),
SliverList(
delegate: SliverChildListDelegate([
About(),
Blog(),
Projects(),
]),
)
],
),
);
}
}
You can do this in different ways:
TabBarView https://stackoverflow.com/a/60624536/10976088
PageView https://stackoverflow.com/a/60778791/10976088
NavigationRail https://api.flutter.dev/flutter/material/NavigationRail-class.html
My method: Using a state management way to keep name or index of content pages and change visible page. I do it with the Riverpod package here:
Suppose you want to have a fixed SidebarView and HeaderView in all pages and also you have a ContentPage that will be changed.
So you can have a RootPage including these 3 sections and change ContentPage by the riverpod, so that only ContentPage will be changed.
class RootPage extends StatelessWidget {
const RootPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: SidebarView(),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isDesktop(context))
const Expanded(
flex: 1,
child: SidebarView(),
),
Expanded(
flex: 5,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
HeaderView(),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Consumer(
builder: (context, ref, _) {
var watch = ref.watch(pageVisibleStateProvider);
return contentPageSelection(watch.state);
},
),
),
),
],
),
),
),
],
),
);
}
}
simply change content page:
Widget contentPageSelection(String pageName){
switch(pageName){
case "page1":
return Page1();
case "page2":
return Page2();
case "page3":
return Page3();
default:
return DefaultPage();
}
}
where:
final pageVisibleStateProvider = StateProvider<String>((_) => "defaultPage");
and:
class SidebarView extends StatelessWidget {
const SidebarView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("sidebar content"),
);
}
}
class HeaderView extends StatelessWidget {
const HeaderView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("HeaderView content"),
);
}
}
Now you can change content page. for example you want to show Page2:
ElevatedButton(
onPressed: (){
ref.read(pageVisibleStateProvider.notifier).state = "page2";
},
child: Text("go to page 2"),
)
where page2 and other content pages only includes content not sidebar or header:
class Page2 extends StatelessWidget {
const Page2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text("page2 content");
}
}