Flutter: setState() called in constructor - flutter

I am novice in Flutter, and ran upon a problem trying to show a map and implement some tilelayers.
The app has a Drawer implemtation where I want to enable/disable and clear the tile cache.
I have fetched some examples where this was working well, so I know that the tiling works great, but here i ran upon a problem where I want to call member functions of the MyWorldMap stateful widget from the drawer widget, and to my spare knowledge I now are plagued by the setState() called in constructor error message.
Do you have any suggestions to help, or guide me on the correct path ?
Note !! Remember to add your own MAP API KEY according to: https://codelabs.developers.google.com/codelabs/google-maps-in-flutter?hl=en&continue=https%3A%2F%2Fcodelabs.developers.google.com%2F#3
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget{
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title:"My App test",
theme: ThemeData(primarySwatch: Colors.blue),
home: HomePage(title: "My World Map")
);
}
}
class HomePage extends StatefulWidget{
final String title;
HomePage({Key? key, required this.title}):super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
#override
void initState(){
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: MainDrawer(),
body: MyWorldMap(),
);
}
}
class MainDrawer extends StatefulWidget{
#override
State<StatefulWidget> createState() => MainDrawerState();
}
class MainDrawerState extends State<MainDrawer>{
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text("My World Map"),
),
ListTile(
title: const Text ("Add tile overlay"),
onTap: () => addTileOverlay(),
),
ListTile(
title: const Text ("Clear tile overlay cache"),
onTap: () => clearTileCache(),
),
ListTile(
title: const Text ("Remove tile overlay"),
onTap: () => removeTileOverlay(),
),
],
),
);
}
void addTileOverlay(){
print("Attempting to add tile overlay");
MyWorldMap().addTileOverlay();
}
void clearTileCache(){
print("Attempting clear tile cache");
MyWorldMap().clearTileCache();
}
void removeTileOverlay(){
print("Attempting removing tile overlay");
MyWorldMap().removeTileOverlay();
}
}
class MyWorldMap extends StatefulWidget{
const MyWorldMap({Key? key}) : super(key: key);
addTileOverlay() => createState()._addTileOverlay();
removeTileOverlay() => createState()._removeTileOverlay();
clearTileCache() => createState()._clearTileCache();
#override
_MyWorldMapState createState() => _MyWorldMapState();
}
class _MyWorldMapState extends State<MyWorldMap>
{
TileOverlay? _tileOverlay;
late GoogleMapController _mapController;
final LatLng _initialCameraPosition = const LatLng(61.9026,6.7003); //Change with your location
//You need to change maps API key in AndroidManifest.xml
#override
void initState(){
super.initState();
}
Future<void> _onMapCreated(GoogleMapController controller) async {
_mapController = controller;
setState(() {
//Do stuff ?
});
}
#override
Widget build(BuildContext context) {
Set<TileOverlay> overlays = <TileOverlay>{
if(_tileOverlay != null) _tileOverlay!,
};
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _initialCameraPosition,
zoom:15,
),
myLocationEnabled: false,
tileOverlays: overlays,
);
}
void _addTileOverlay()
{
final TileOverlay tileOverlay = TileOverlay(
tileOverlayId: TileOverlayId("My World Map Overlay"),
tileProvider: MyWorldMapTileProvider(),
);
setState((){ //The code fails here when pushing the 'Add tile overlay button' !!
_tileOverlay = tileOverlay;
});
}
void _clearTileCache()
{
if(_tileOverlay != null){
_mapController.clearTileCache(_tileOverlay!.tileOverlayId);
}
}
void _removeTileOverlay()
{
setState(() {
_tileOverlay = null;
});
}
}
class MyWorldMapTileProvider implements TileProvider {
#override
Future<Tile> getTile(int x, int y, int? zoom) async {
String path = 'https://maptiles1.finncdn.no/tileService/1.0.1/norortho/$zoom/$x/$y.png';
http.Response response = await http.get(
Uri.parse(path)
);
return Tile(x,y,response.bodyBytes);
}
}

Not that I am a real professional with flutter, but I think the problem might reside in here:
addTileOverlay() => createState()._addTileOverlay();
removeTileOverlay() => createState()._removeTileOverlay();
clearTileCache() => createState()._clearTileCache();
You're creating a new state each time you invoke one of those methods in MyWorldMap widget, and I don't think that's the correct behaviour.
If you want to edit a Widget state from another Widget, you should try using keys: I think any stateful Widget can take a key argument in the constructor, that can be used in turn to change its state from other widgets. I'll try writing a simple example.
class Parent extends StatelessWidget {
final keyA = GlobalKey();
final keyB = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
ChildA(keyA),
ChildB(keyB, keyA),
]),
);
}
}
class ChildA extends StatefulWidget {
const ChildA(GlobalKey key) : super(key: key);
#override
State<StatefulWidget> createState() => ChildAState();
}
class ChildAState extends State<ChildA> {
int counter = 0;
#override
Widget build(BuildContext context) {
return Text("Child A count: $counter");
}
void increaseCounter(){
setState(() {
counter++;
});
}
}
class ChildB extends StatefulWidget {
final GlobalKey childAKey;
const ChildB(GlobalKey key, this.childAKey) : super(key: key);
#override
State<StatefulWidget> createState() => ChildBState();
}
class ChildBState extends State<ChildB> {
#override
Widget build(BuildContext context) {
return TextButton(
child: const Text("Press here"),
onPressed: () {
(widget.childAKey.currentState as ChildAState).increaseCounter();
},
);
}
}

After #il_boga lead me to the answer (all credits to him), I'll post the working code here:
I moved the TileOverlay creation to initState of _MyWorldMapState class, and added a buffered 'layer' too so I could switch on/off the layer by setting _mapTileOverlay to null when removing and back to _bufferedMapTileOverlay when adding the overlay.
Further I have created two GlobalKeys (actually not knowing why i need drawerKey actually, since I never activily reference it anywhere..., mapKey is obvious)
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget{
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title:"My App test",
theme: ThemeData(primarySwatch: Colors.blue),
home: HomePage(title: "My World Map")
);
}
}
class HomePage extends StatefulWidget{
final String title;
const HomePage({Key? key, required this.title}):super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
final drawerKey = GlobalKey();
final mapKey = GlobalKey();
#override
void initState(){
print("_HomePageState(): initState");
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: MainDrawer(drawerKey: drawerKey, mapKey: mapKey,),
body: MyWorldMap(mapKey: mapKey,),
);
}
}
class MainDrawer extends StatefulWidget{
final GlobalKey mapKey;
const MainDrawer({required GlobalKey drawerKey, required this.mapKey}) : super(key: drawerKey);
#override
State<StatefulWidget> createState() => MainDrawerState();
}
class MainDrawerState extends State<MainDrawer>{
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text("My World Map"),
),
ListTile(
title: const Text ("Add tile overlay"),
onTap: () => addTileOverlay(),
),
ListTile(
title: const Text ("Clear tile overlay cache"),
onTap: () => clearTileCache(),
),
ListTile(
title: const Text ("Remove tile overlay"),
onTap: () => removeTileOverlay(),
),
],
),
);
}
void addTileOverlay(){
print("Attempting to add tile overlay");
//MyWorldMap().addTileOverlay();
(widget.mapKey.currentState as _MyWorldMapState)._addTileOverlay();
}
void clearTileCache(){
print("Attempting clear tile cache");
//MyWorldMap().clearTileCache();
(widget.mapKey.currentState as _MyWorldMapState)._clearTileCache();
}
void removeTileOverlay(){
print("Attempting removing tile overlay");
//MyWorldMap().removeTileOverlay();
(widget.mapKey.currentState as _MyWorldMapState)._removeTileOverlay();
}
}
class MyWorldMap extends StatefulWidget{
const MyWorldMap({required GlobalKey mapKey}) : super(key: mapKey);
//addTileOverlay() => createState()._addTileOverlay();
//removeTileOverlay() => createState()._removeTileOverlay();
//clearTileCache() => createState()._clearTileCache();
#override
_MyWorldMapState createState() => _MyWorldMapState();
}
class _MyWorldMapState extends State<MyWorldMap>
{
TileOverlay? _bufferedMapTileOverlay; //intermediate, which actually holds the overlay
TileOverlay? _mapTileOverlay; //value which connects to the map
late GoogleMapController _mapController;
final LatLng _initialCameraPosition = const LatLng(61.9026,6.7003); //Change with your location
//You need to change maps API key in AndroidManifest.xml
#override
void initState(){
print("_MyWordMapState(): initState");
super.initState();
final TileOverlay newMapTileOverlay = TileOverlay( //Inits the tileOverlay
tileOverlayId: const TileOverlayId("My World Map Overlay"),
tileProvider: MyWorldMapTileProvider(),
);
_bufferedMapTileOverlay = newMapTileOverlay;
}
Future<void> _onMapCreated(GoogleMapController controller) async {
_mapController = controller;
setState(() {
//Do stuff ?
});
}
#override
Widget build(BuildContext context) {
Set<TileOverlay> overlays = <TileOverlay>{ //connect a set of overlays (here just one)
if(_mapTileOverlay != null) _mapTileOverlay!,
};
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _initialCameraPosition,
zoom:15,
),
myLocationEnabled: false,
tileOverlays: overlays, //connect to the set of overlays (I have only one (see above))
);
}
void _addTileOverlay()
{
setState((){
_mapTileOverlay = _bufferedMapTileOverlay;
});
}
void _clearTileCache()
{
if(_mapTileOverlay != null){
print("Clearing tile cache");
_mapController.clearTileCache(_mapTileOverlay!.tileOverlayId);
}
}
void _removeTileOverlay()
{
setState(() {
_mapTileOverlay = null;
});
}
}
class MyWorldMapTileProvider implements TileProvider {
#override
Future<Tile> getTile(int x, int y, int? zoom) async {
String path = 'https://maptiles1.finncdn.no/tileService/1.0.1/norortho/$zoom/$x/$y.png';
http.Response response = await http.get(
Uri.parse(path)
);
return Tile(x,y,response.bodyBytes);
}
}

Seems like you are using setState before build method has finished building the widgets. I would suggest using setState after build has finished, this way :
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
// do stuff;
});
});

Related

Flutter lifting the state up through multiple dynamically added widgets

I'm trying to build a parent widget that has a button, when clicked, it displays another widget with some text and a drop-down list. When the drop-down selection is changed, the text should change accordingly. I've included below a simplified code of what I'm trying to achieve which doesn't work. The state lifting up concept is something confusing for me as a newcomer to Flutter
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String text = "Empty";
void addWidget() {
setState(() {
widList.clear();
widList.add(MidWidget(
text: text,
setValue: selectValue,
));
});
}
void selectValue(String value) {
setState(() {
text = value;
});
}
List<Widget> widList = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
ElevatedButton(onPressed: addWidget, child: const Text("Add Widget")),
Column(
children: widList,
)
]),
),
);
}
}
class MidWidget extends StatelessWidget {
const MidWidget({super.key, required this.text, required this.setValue});
final String text;
final Function setValue;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(text),
LowestWidget(
dropDownValue: "First",
setValue: setValue,
),
],
);
}
}
////////////////////
///////////////////
///
class LowestWidget extends StatelessWidget {
LowestWidget(
{super.key, required this.dropDownValue, required this.setValue});
final List<String> items = ["First", "Second"];
final String dropDownValue;
final Function setValue;
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: dropDownValue,
icon: const Icon(Icons.arrow_downward),
onChanged: (String? value) {
setValue(value);
},
items: items.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
}
}
First of all, both MidWidget and LowestWidget need to be converted to StatefulWidget because we need state changes inside those widgets too.
Secondly, selectValue function should be in the MidWidget, not in the parent widget, because it attempts to change the state of text that has already been passed onto the MidWidget with its original value at the time of its instantiation. Any change in text via setState is not going to affect its value in MidWidget anymore.
Thirdly, I've introduced _value variable in both MidWidget and LowestWidget that takes its initial value from the respective parent widgets in initState and then gets value changes via setState that are then used to be displayed in Text widget in MidWidget and DropdownButton widget in LowestWidget.
Following is the revised code that is working as per your requirements. I've commented out the deletions so that you could relate it with the original code.
Hope it helps!
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String text = "Empty";
void addWidget() {
setState(() {
widList.clear();
widList.add(MidWidget(
text: text,
// setValue: selectValue,
));
});
}
// void selectValue(String value) {
// setState(() {
// text = value;
// });
// }
List<Widget> widList = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
ElevatedButton(onPressed: addWidget, child: const Text("Add Widget")),
Column(
children: widList,
)
]),
),
);
}
}
class MidWidget extends StatefulWidget {
const MidWidget({super.key, required this.text, /*required this.setValue*/});
final String text;
// final Function setValue;
#override
State<MidWidget> createState() => _MidWidgetState();
}
class _MidWidgetState extends State<MidWidget> {
String? _value;
void selectValue(String value) {
setState(() => _value = value);
}
#override
void initState() {
_value = widget.text;
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(_value!),
LowestWidget(
dropDownValue: "First",
setValue: selectValue,
),
],
);
}
}
////////////////////
///////////////////
///
class LowestWidget extends StatefulWidget {
LowestWidget(
{super.key, required this.dropDownValue, required this.setValue});
final String dropDownValue;
final Function setValue;
#override
State<LowestWidget> createState() => _LowestWidgetState();
}
class _LowestWidgetState extends State<LowestWidget> {
final List<String> items = ["First", "Second"];
String? _value;
#override
void initState() {
_value = widget.dropDownValue;
super.initState();
}
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: _value,
icon: const Icon(Icons.arrow_downward),
onChanged: (String? value) {
setState(() => _value = value);
widget.setValue(value);
},
items: items.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
}
}

While learning flutter using (https://github.com/afitz0/exploration_planner). How to implement the action on the LinearProgressIndicator()?

This code is part of online training of flutter by Google team. The original code can be accessed in https://github.com/afitz0/exploration_planner. I am new on flutter and I´ve got some dificulties to use statefull widget. I still do not have enough confidence. I made some modification on original code to add action to the indicator bar, it works fine but I dont think my solution is ideal...
My question is related to the right way to make a change in the state of the taskitem give an
update on the linearProgressIndicator ? Thanks in advance..
import 'package:flutter/material.dart';
double _percentual = 0; //variable to hold progress bar values from zero to 1 step 0.2
// first comes root run appp
void main() => runApp(MyApp()
//MaterialApp
//Scaffold
//AppBar
//Text
//body: Column
//text, text, text
//image
//Row
//text, text, bttom
//....
);
// second comes materialapp
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Exploration!',
theme: ThemeData(primarySwatch: Colors.blueGrey),
home: MyHomePage(),
);
}
}
//third comes home page describes visual of app
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController controller;
#override
void initState() {
controller = AnimationController(
vsync: this,
)..addListener(() {
setState(() {
controller.value = _percentual;
});
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Space Exploration planner'),
),
body: Column(
children: [
Progress(),
TaskList(),
],
),
);
}
}
class Progress extends StatefulWidget {
const Progress({super.key});
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
#override
Widget build(BuildContext context) {
return Column(
children: [
Text('You are this far away from exploring the whole universe'),
LinearProgressIndicator(
value: _percentual,
)
],
);
}
}
class TaskList extends StatelessWidget {
const TaskList({super.key});
#override
Widget build(BuildContext context) {
return Column(
children: [
TaskItem(label: "Load rocket with supplies"),
TaskItem(label: "Launch rocket"),
TaskItem(label: "Circle the home planet"),
TaskItem(label: "Head out to de first moon"),
TaskItem(label: "Launch moon lander #1"),
],
);
}
}
class TaskItem extends StatefulWidget {
final String label;
const TaskItem({Key? key, required this.label}) : super(key: key);
#override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
bool? _value = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
onChanged: (newValue) => setState(() => {
_value = newValue,
if (_value == true)
{
_percentual = double.parse(
(_percentual + 0.2).toStringAsPrecision(1)),
_ProgressState(),
}
else if (_value == false)
{
_percentual = double.parse(
(_percentual - 0.2).toStringAsPrecision(1)),
_ProgressState(),
},
main(), *//<-- worked like hot-reload but I dont think is the right way to do it.*
}),
value: _value,
),
Text(widget.label),
],
);
}
}

Is there a way I can have my HookWidget rebuild when the text of the TextEditingController changes?

I have a test edit field. When there is no text, I want a button disabled. When there is text in the TextField, I want the button enabled.
I am using flutter_hooks to reduce boiler plate code for controllers.
In the following example, when I enter test into the text field, the button never enables, because build is not triggered? How can I trigger a build when using a text editing controller with flutter hooks?
class MyHomePage extends HookWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var ctrl = useTextEditingController();
VoidCallback? onPressed;
if (ctrl.text.isNotEmpty) {
onPressed = () => print("Pressed!");
}
return Scaffold(
body: Column(
children: [
TextField(
controller: ctrl,
),
ElevatedButton(onPressed: onPressed, child: Text("Button")),
],
)
);
}
}
You can Achieve this using useState and useTextEditingController
var istextchanged = useState<bool>(false);
ctrl.addListener(() {
if (ctrl.text.isEmpty) {
istextchanged.value = false;
} else {
istextchanged.value = true;
}
});
Yourwidget
class MyHomePages2 extends HookWidget {
const MyHomePages2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var ctrl = useTextEditingController();
var istextchanged = useState<bool>(false);
ctrl.addListener(() {
if (ctrl.text.isEmpty) {
istextchanged.value = false;
} else {
istextchanged.value = true;
}
});
VoidCallback? onPressed = () {
print("change");
};
if (ctrl.text.isNotEmpty) {
onPressed = () => print("Pressed!");
}
return Scaffold(
body: Column(
children: [
TextField(
controller: ctrl,
onChanged: (v) {},
),
ElevatedButton(
onPressed: istextchanged.value ? onPressed : null,
child: Text("Button"))
],
));
}
}
Package used flutter_hooks: ^0.18.2+1
pub.dev/flutter_hooks
SampleCode
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
int myvalue = 0;
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
void initState() {
// functions().then((int value) {
// setState(() {
// myvalue = value;
// });
// future is completed you can perform your task
// });
}
Future<int> functions() async {
// do something here
return Future.value();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: MyHomePages2(),
);
}
}
class MyHomePages2 extends HookWidget {
const MyHomePages2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var ctrl = useTextEditingController();
var istextchanged = useState<bool>(false);
ctrl.addListener(() {
if (ctrl.text.isEmpty) {
istextchanged.value = false;
} else {
istextchanged.value = true;
}
});
VoidCallback? onPressed = () {
print("change");
};
if (ctrl.text.isNotEmpty) {
onPressed = () => print("Pressed!");
}
return Scaffold(
body: Column(
children: [
TextField(
controller: ctrl,
onChanged: (v) {},
),
ElevatedButton(
onPressed: istextchanged.value ? onPressed : null,
child: Text("Button"))
],
));
}
}

Why state change error occurs on flutter_riverpod during initialization

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dataProvider = StateNotifierProvider<DataNotifier, List<int>>((ref) {
return DataNotifier();
});
class DataNotifier extends StateNotifier<List<int>> {
DataNotifier() : super([]);
Future<void> getData() async {
state = [];
await Future.delayed(const Duration(seconds: 2));
state = [1, 2];
}
}
void main() => runApp(ProviderScope(child: App()));
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SecondPage()),
);
},
child: const Text('Next page'),
),
),
),
);
}
}
class SecondPage extends ConsumerStatefulWidget {
const SecondPage({Key? key}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends ConsumerState<SecondPage> {
#override
void initState() {
super.initState();
ref.read(dataProvider.notifier).getData();
}
#override
Widget build(BuildContext context) {
final numbers = ref.watch(dataProvider);
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (_, index) {
return Text('data: $index');
},
itemCount: numbers.length,
),
);
}
}
I am new to riverpod and I noticed this error while changing state.
In the above code when I tap the "next page" button at the fresh start for the first time it works as expected but when I go back and again tap the "next page" button, an error shown below is thrown:
StateNotifierListenerError (At least listener of the StateNotifier Instance of 'DataNotifier' threw an exception
when the notifier tried to update its state.
Does anyone know why this occurs and how can I prevent it.
You can solve the issue using autoDispose
final dataProvider = StateNotifierProvider.autoDispose<DataNotifier, List<int>>(
(ref) => DataNotifier(),
);
For Future I prefer using FutureProvider.
More about riverpod

How to pass a drawer with a string inside to next screen

I have the next block of code where I'm getting the AppVersion using a library and after that I'm passing the AppVersion to a drawer. That drawer I send it to next screen but when I open the drawer on the next screen is showing the AppVersion as NULL. What can be the issue ?
I will provide below the full code source and maybe somebody can help me to figure out where is the bug.
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
void main() {
runApp(FirstPage());
}
class FirstPage extends StatefulWidget {
final String title;
FirstPage({Key key, this.title}) : super(key: key);
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
String packageAppVersion = '';
#override
void initState() {
super.initState();
versionCheck();
}
Future<void> versionCheck() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
packageAppVersion = packageInfo.version;
});
}
Widget buildDrawerForSecondPage(BuildContext context) {
return new Drawer(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: new Column(
children: [
Flexible(
child: new ListView(
children: <Widget>[],
),
),
Flexible(
flex: 0,
child: Text("App version: $packageAppVersion"),
)
],
),
),
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Drawer Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SecondPage(
title: 'Second Page',
drawer: buildDrawerForSecondPage(context),
),
);
}
}
class SecondPage extends StatefulWidget {
final String title;
final Drawer drawer;
SecondPage({Key key, this.title, this.drawer}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState(drawer);
}
class _SecondPageState extends State<SecondPage> {
String packageAppVersion = '';
final Drawer drawer;
_SecondPageState(this.drawer);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
endDrawer: drawer,
body: Container(),
);
}
}
Thanks in advance.
That's because you are using a .then() syntax, the AppVersion actually gets updated but a bit later hence the null value. You could await the version before the run() method and then pass it down to MaterialApp, or you could try using a setState after the print inside then(). Let me know if this fixes your issue.
Initially, the value of packageAppVersion is null, that is what it is being shown in the UI. So to update the UI you need to use setState. Check the below code for a better understanding:
#override
void initState() {
super.initState();
versionCheck();
}
Future<void> versionCheck() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState((){
packageAppVersion = packageInfo.version;
});
print('App version received: $packageAppVersion');
}
I found the fix for above code, but I don't understand why is working only like this (removed the drawer property and the constructor from the _SecondPageState) :
class SecondPage extends StatefulWidget {
final String title;
final Drawer drawer;
SecondPage({Key key, this.title, this.drawer}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
String packageAppVersion = '';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
endDrawer: widget.drawer,
body: Container(),
);
}
}