everyone!
I'm learning Flutter / Dart and keep having issues with instantiation and initialization.
Currently I'm writing a reorderable list but for some reason, I can't populate the list because my instance has some issue... (I think the issue is at these lines below)
List<String> tasks = [];
MyTask t;
tasks = [ t.task = 'Buy ice cream', t.task = 'Learn Flutter', t.task = 'Read books' ];)
Can you check it and give me a clue?
Thanks in advance. Any tip to seek docs is welcome!
Exception:
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following NoSuchMethodError was thrown building _BodyBuilder:
The setter 'task=' was called on null.
Receiver: null
Tried calling: task="Buy ice cream"
Relevant code:
import 'package:flutter/material.dart';
import './bottomNavigationBar.dart';
import './ViewFeed.dart';
import './ViewNewTodo.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
//home: MyScaffold(),
initialRoute: '/',
routes: {
'/':(context) => MyFeed(),
'/toDo':(context) => MyScaffold(),
'/newToDo':(context) => MyNewTodo(),
},
);
}
}
class MyScaffold extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My ToDoS'),
),
body: MyReorderableTaskList(),
floatingActionButton: MyFloatingActionButtonNewTodo() ,
bottomNavigationBar: MyBottomNavigationBar(),
);
}
}
//Reorderable list elements
//FloatingActionButton
class MyFloatingActionButtonNewTodo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
tooltip: 'Idea',
onPressed: ()=>{ Navigator.pushNamed(context, '/newToDo') },
);
}
}
class MyTask{
String task;
MyTask(this.task);
}
//ReorderableListView implementation
class MyReorderableTaskList extends StatefulWidget {
#override
_MyReorderableTaskListState createState() => _MyReorderableTaskListState();
}
class _MyReorderableTaskListState extends State<MyReorderableTaskList> {
List<String> tasks = [];
MyTask t;
void initState(){
tasks = [ t.task = 'Buy ice cream', t.task = 'Learn Flutter', t.task = 'Read books' ];
super.initState();
}
#override
Widget build(BuildContext context) {
return ReorderableListView(
onReorder: _onReorder,
children: List.generate(
tasks.length,
(index){ return MyListView(index, Key('$index'), tasks ); }
),
);
}
void _onReorder(int oldIndex, int newIndex){
setState(() {
if(newIndex > oldIndex) { newIndex -= 1; }
final String item = tasks.removeAt(oldIndex);
tasks.insert(newIndex, item);
});
}
}
class MyListView extends StatefulWidget {
final int index;
final Key key;
final List<String> listTasks;
MyListView(this.index, this.key, this.listTasks);
#override
_MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(6),
child: InkWell(
splashColor: Colors.blue,
onTap: ()=>{ MaterialState.focused },
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text('Tarea: ${widget.listTasks[widget.index]} | ${widget.index}',),
),
],
),
),
);
}
}
MyTask t; is null. t isn't actually anything yet. You must create a new object MyTask() and assign it to t
Change it to
MyTask t = new MyTask("");
Edit: More thorough explanation
You can't just declare MyTask t; and then try to use t, because you have not yet said what t is. That's like declaring int a; and then trying to do print(a + 5). You can't do that, because you have not yet assigned a value to a. Similarly, MyTask t; means you have created a variable named t of type MyTask. In other words, you have declared a variable t. But you must still initialize it, in other words, assign t a value of type MyTask, otherwise the value of t will be null.
So to summarize,
MyTask t;
t.task = "Hello";
will not work, because it's like doing
int a;
int b = a + 5;
You can't add 5 to a, because although you have declared the variable a, you have not yet initialized it, so it does not yet have a value.
Likewise, you can't access the .task property of t, because you haven't said what t is. t is just null.
So you must initialize t by instantiating a new MyTask object-
MyTask t = new MyTask("");
The "" is required inside the parentheses because your MyTask class constructor requires a parameter.
class MyTask{
String task;
MyTask(this.task); // Parameter required to assign to the String task
}
This might help you.
Related
I am learning MVVM with the Provider package in Flutter.
I am creating a screen using StatelessWidget.
I have come to the point where I need to cache the widget, where should I store it?
StatelessWidget is immutable, so it cannot have variables in its properties.
MVVM's ViewModel cannot depend on a View, so it cannot have a Widget instance.
In the above question, I used Widget as an example, but there are other things I would like to store in View variables, such as GlobalKey.
The test code is as follows The purpose is to reference _needCacheWidget and _key later, but
I get a warning in the comment section
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<TestViewModel>(create: (context) => TestViewModel()),
],
child: S1(),
));
}
class TestViewModel extends ChangeNotifier {
var _count = 0;
int get count {
return _count;
}
void increment() {
this._count += 1;
notifyListeners();
}
}
/// This class (or a class that this class inherits from) is marked as '#immutable',
/// but one or more of its instance fields aren't final: S1._needCacheWidget, S1._key (Documentation)
class S1 extends StatelessWidget {
Widget? _needCacheWidget = null;
GlobalKey? _key = null;
Widget _getNeedCacheWidget() {
GlobalKey key = _key ?? GlobalKey();
return _needCacheWidget ?? Text("need cache widget", key: key);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("text"),
),
body: Column(
children: [
Consumer<TestViewModel>(
builder: (context, testViewModel, child) {
return Text("count-${testViewModel.count}");
},
),
_getNeedCacheWidget(),
],
),
floatingActionButton: Consumer<TestViewModel>(
builder: (context, testViewModel, child) {
return FloatingActionButton(
onPressed: () {
testViewModel.increment();
print("globalKey is $_key"); // I want to refer to GlobalKey of NeedCacheWidget here.
},
child: const Icon(Icons.add),
);
},
),
),
);
}
}
I have a Stateless-Provider widget along with its ChangeNotifier-model. Inside the Provider, there is a Stateful widget. When notifyListeners is called, all widgets in the stateless widget get updated, except the Stateful one. What am I missing here, and how do I go about it? Providing a simplified example here: Upon pressing the button, the expected result is First: The value is 1st, but the actual output is First: The value is 2nd
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Model extends ChangeNotifier {
final List<ListElement> elements;
Model({required this.elements});
void add() {
elements.insert(0, ListElement(name: "First", value: "1st"));
notifyListeners();
}
}
class ListElement {
final String name;
var value;
ListElement({required this.name, required this.value});
}
class ValueWidget extends StatefulWidget {
final String value;
ValueWidget({required this.value});
#override
State<StatefulWidget> createState() => _ValueWidget(value: value);
}
class _ValueWidget extends State<ValueWidget> {
String value;
_ValueWidget({required this.value});
#override
Widget build(BuildContext context) {
return Text("The value is ${value}.");
}
}
class StatelessPage extends StatelessWidget {
final model = Model(elements: [
ListElement(name: "Second", value: "2nd"),
ListElement(name: "Third", value: "3rd")]);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (context) => model,
child: ConsumerWidget())
);
}
}
class ConsumerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Model>(builder: (context, model, _) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.fromLTRB(10, 30, 10, 10000),
child: Column(
children: [Column(
children: model.elements.map((element) {
return Row(children: [
Text("${element.name}: "),
ValueWidget(value: element.value)
]);
}).toList(),
),
TextButton(onPressed: model.add,
child: Text("Add element to beginning")),
],
),
),
);
});
}
}
Please consider that this is simplified version of my production code, and changing the whole Provider class to a Stateful one would be difficult.
Edit: Thanks Aimen for showing the path. What finally worked was using only the index of the list elements in the Stateful wiget (ValueWidget). And fetch the data from the model. I think the reason for this is that if the Stateful-widget in independece is not affected, it will not rebuild. We need to affect the build part of the widget. Pasting the changed working code.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Model extends ChangeNotifier {
final List<ListElement> elements;
Model({required this.elements});
void add() {
elements.insert(0, ListElement(name: "First", value: "1st"));
notifyListeners();
}
}
class ListElement {
final String name;
var value;
ListElement({required this.name, required this.value});
}
class ValueWidget extends StatefulWidget {
final int ind;
final Model model;
ValueWidget({required this.ind, required this.model});
#override
State<StatefulWidget> createState() => _ValueWidget(
ind: ind, model: model);
}
class _ValueWidget extends State<ValueWidget> {
final int ind;
final Model model;
_ValueWidget({required this.ind, required this.model});
#override
Widget build(BuildContext context) {
// Can also use Provider like this so that it does not need to be passed
// final model = Provider.of<Model>(context, listen: true);
// This is the part because of which widget is getting rebuilt
final element = model.elements[ind];
return Text("The value is ${element.value}.");
}
}
class StatelessPage extends StatelessWidget {
final model = Model(
elements: [
ListElement(name: "Second", value: "2nd"),
ListElement(name: "Third", value: "3rd")]
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (context) => model,
child: ConsumerWidget())
);
}
}
class ConsumerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Model>(builder: (context, model, _) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.fromLTRB(10, 30, 10, 10000),
child: Column(
children: [Column(
children:
model.elements.asMap().entries.map((ele) {
return Row(children: [
Text("${ele.value.name}: "),
ValueWidget(ind: ele.key, model: model),
]);
}).toList(),
),
TextButton(onPressed: model.add,
child: Text("Add element to beginning")),
],
),
),
);
});
}
}
you are not implementing provider in the stateful widget you are just passing a value through a parameter you need to call a provider and set the listen to true
inside the statful widget
like
var model = Model.of(context, listen = true);
List elements = model.elements;
here the elements variable will change when the elements in the provider will have a new value
As shown in the image, I'm trying to have a list of dice where I can add or delete a die. I've tried StateProvider, ChangeNotifier, and StateNotifier. Each one doesn't seem to work as I expect it to. I'm trying to make a provider that contains a list of dieWidgets, but I can't figure out how to remove a specific die when I longpress on it. The image shows a popup menu to delete it, that's the long-term goal, but just a longpress delete would be good for now. Thoughts on how to approach this?
Code
main.dart
class DiceNotifier extends ChangeNotifier {
List<DieWidget> dice = [];
void add() {
dice.add(DieWidget());
notifyListeners();
}
void removeDie(int id) {
// FIXME: Unable to delete a die based on id
print(id);
notifyListeners();
}
}
final diceProvider = ChangeNotifierProvider((_) {
return DiceNotifier();
});
class MyHomePage extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final dice = watch(diceProvider).dice;
return Scaffold(
appBar: AppBar(
title: Text("Dice"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
...dice,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(diceProvider).add();
},
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
die_widget.dart
class DieWidget extends StatefulWidget {
#override
_DieWidgetState createState() => _DieWidgetState();
}
class _DieWidgetState extends State<DieWidget> {
int value = 0;
int id = 0;
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text(
'$value',
),
onPressed: () {
setState(() {
value++;
id++;
});
// context.read(dieProvider).increment();
},
onLongPress: () {
final dice = context.read(diceProvider);
dice.removeDie(id);
// print(this.value);
},
);
}
}
One solution would be to define a parameter value in the DiceWidget class:
class DiceWidget extends StatefulWidget {
const DiceWidget({ Key key, this.value }) : super(key: key);
int value;
#override
_DiceWidgetState createState() => _DiceWidgetState();
}
And access this data from the DiceWidget:
class DiceWidget extends StatefulWidget {
#override
_DiceWidgetState createState() => _DiceWidgetState();
}
class _DiceWidgetState extends State<DiceWidget> {
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text(
widget.value.toString() ?? '',
),
onLongPress: () {
final dice = context.read(diceProvider);
dice.removeDice(widget.value);
// print(widget.value);
},
);
}
}
In the DiceNotifier class, I'd recommend to implement the dices array as a List<int>:
List<int> dices = [];
Therefore, the addDice() and removeDice() functions will be, respectively:
class DiceNotifier extends ChangeNotifier {
List<int> dices = [];
void addDice() {
dices.add(dices.length);
notifyListeners();
}
void removeDice(int id) {
dices.remove(id);
print(id);
notifyListeners();
}
}
To make the example work, we need to modify the MyHomePage Column children as well, to build the list of DiceWidgets:
...dices.map((d) => DiceWidget(value: d)).toList(),
The whole example will then be:
main.dart:
class DiceNotifier extends ChangeNotifier {
List<int> dices = [];
void addDice() {
dices.add(dices.length);
notifyListeners();
}
void removeDice(int id) {
dices.remove(id);
print(id);
notifyListeners();
}
}
final diceProvider = ChangeNotifierProvider((_) {
return DiceNotifier();
});
class MyHomePage extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final dices = watch(diceProvider).dices;
return Scaffold(
appBar: AppBar(
title: Text("Dice"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
...dices.map((d) => DiceWidget(value: d)).toList(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(diceProvider).addDice();
},
child: Icon(Icons.add),
),
);
}
}
dice_widget.dart:
class DiceWidget extends StatefulWidget {
#override
_DiceWidgetState createState() => _DiceWidgetState();
}
class _DiceWidgetState extends State<DiceWidget> {
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text(
widget.value.toString() ?? '',
),
onLongPress: () {
final dice = context.read(diceProvider);
dice.removeDice(widget.value);
print(widget.value);
},
);
}
}
I have a StatefulWidget class 'FirstClass' which extends a State '_FirstClassState'. From a separate State class, called '_SecondClassState', I'm trying to access the value of a variable (called 'counter' in '_FirstClassState').
I've found a solution, but I'm not sure if this is the best solution to the problem. I've tried looking at other similar questions:
How to access Stateful widget variable inside State class outside the build method? (Declaring the variable in the StatefulWidget class and using 'widget.' in the State class does not allow me to get the value of 'counter' from '_SecondClassState')
I've also seen other sites that recommend using a GlobalKey. However, even though this might be a solution, I didn't explore further into this as I found other sites which recommend minimising the use of GlobalKeys
second_class.dart:
import 'package:brew_crew/question/first_class.dart';
import 'package:flutter/material.dart';
class SecondClass extends StatefulWidget {
#override
_SecondClassState createState() => _SecondClassState();
}
class _SecondClassState extends State<SecondClass> {
final FirstClass firstClass = FirstClass();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MyApp"),
centerTitle: true,
),
body: Column(
children: <Widget>[
firstClass, //Displays the button to increase 'counter'
FlatButton(
child: Text("Submit"),
onPressed: () {
print(firstClass
.getCounter()); //I need to get the value of 'counter' here to use elsewhere
}),
],
),
);
}
}
first_class.dart
import 'package:flutter/material.dart';
class FirstClass extends StatefulWidget {
final _FirstClassState firstClassState = _FirstClassState();
#override
_FirstClassState createState() => firstClassState;
int getCounter() {
return firstClassState.counter;
}
}
class _FirstClassState extends State<FirstClass> {
int counter = 0;
//Increases counter by 1
void _increaseCounter() {
setState(() {
counter++;
});
}
#override
Widget build(BuildContext context) {
//A button which increases counter when clicked
return FlatButton.icon(
icon: Icon(Icons.add),
label: Text("Counter: $counter"),
onPressed: () {
_increaseCounter();
},
);
}
}
As you can see, within 'FirstClass', I initialise a new instance of '_FirstClassState'. Using this in the 'getCounter()' function, I can get the 'counter' value.
Is there a better way (e.g. best practice, fewer lines of code, an easier to understand method, etc.) than this to achieve what I'm trying to do?
The use of GlobalKey is definitely the recommended approach if absolutely you have to access the state of a widget from outside. However, in this case, you shouldn't use either approach.
_SecondClassState should contain the counter, and you should pass it, along with the increaseCounter function, as parameters to FirstClass. If you want to increase the number, just call that function.
Something along these lines:
class SecondClass extends StatefulWidget {
#override
_SecondClassState createState() => _SecondClassState();
}
class _SecondClassState extends State<SecondClass> {
int counter = 0;
//Increases counter by 1
void _increaseCounter() {
setState(() {
counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MyApp"),
centerTitle: true,
),
body: Column(
children: <Widget>[
FirstClass(counter: counter, increaseCounter: _increaseCounter), //Displays the button to increase 'counter'
FlatButton(
child: Text("Submit"),
onPressed: () {
print(counter.toString()); //I need to get the value of 'counter' here to use elsewhere
}),
],
),
);
}
}
class FirstClass extends StatefulWidget {
final int counter;
final Function increaseCounter;
FirstClass({this.counter, this.increaseCounter});
final _FirstClassState firstClassState = _FirstClassState();
#override
_FirstClassState createState() => firstClassState;
}
class _FirstClassState extends State<FirstClass> {
#override
Widget build(BuildContext context) {
//A button which increases counter when clicked
return FlatButton.icon(
icon: Icon(Icons.add),
label: Text("Counter: ${widget.counter}"),
onPressed: () {
widget.increaseCounter();
},
);
}
}
I've got a model which I simplify for the purpose of this question
MyModel(a:0, b:0, c:0) //a b c are integers
Now I need to generate buttons from this model so I've created a new class which takes that model as a parameter and generates buttons
class Panel extends StatelessWidget {
final MyModel model;
const Panel({this.model});
List<Widget> _buildContent() {
final list = [
{'a': model.a},
{'b': model.b},
{'c': model.c}
];
List<Widget> l = [];
list.forEach((element) {
l.add(FlatButton(child: Text(element.keys.first), onPressed: () {}));
});
return l;
}
#override
Widget build(BuildContext context) {
return Container(
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildContent(),
));
}
}
and in my main class, I'm just calling it like this
Panel(model: myModel);
I'm trying to find the neatest way to handle presses of these buttons, each button should update its element at +1 on each press and send the updated model back to the main class
You need for your Panel to provide a callback when the model changes. Then you can change the model used in your application. Note the "onChange" callback added to the Panel that in the App calls setState to actually change the model.
Example:
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class Model {
final int a;
final int b;
final int c;
const Model({this.a, this.b, this.c});
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Model model = Model(a: 1, b: 2, c: 3);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('Current Model: a:${model.a}, b:${model.b}, c: ${model.c}')),
body: Center(
child: Panel(
model: model,
onChange: (changedModel) => setState(() => model = changedModel)),
),
),
);
}
}
class PanelButton {
final String text;
final Model Function() onClick;
const PanelButton(this.text, this.onClick);
}
class Panel extends StatelessWidget {
final Model model;
final void Function(Model) onChange;
const Panel({#required this.model, this.onChange});
List<Widget> _buildContent() {
final list = [
PanelButton('a', () => Model(a: model.a + 1, b: model.b, c: model.c)),
PanelButton('b', () => Model(a: model.a, b: model.b + 1, c: model.c)),
PanelButton('c', () => Model(a: model.a, b: model.b, c: model.c + 1)),
];
return list
.map((x) => FlatButton(
child: Text(x.text), onPressed: () => _raiseOnChange(x.onClick())))
.toList();
}
void _raiseOnChange(Model model) {
if (onChange != null) {
onChange(model);
}
}
#override
Widget build(BuildContext context) {
return Container(
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildContent(),
));
}
}
As far as my knowledge is concerned, what you can do is the following:
Rather than following model thing, you can make a Global Variable, which has a dictionary/hasmap. Make a file named as globals.dart and do this
/*
This is the primary util for maintaining
all the Global values/variables
*/
library globals;
//This is your variable
Map<String, dynamic> mydata = {'a':0, 'b':0, 'c':0};
Now, you can access global value anywhere in any file, by just importing the globals.dart file like import '{path}/globals.dart'
Now in your Panel, we can access it, and do the increment of the item as per your will
import '{path}/globals.dart' as globals
class Panel extends StatelessWidget {
List<Widget> _buildContent() {
List<Widget> l = [];
// No list declaration, direct items access
globals.mydata.forEach((key, value) {
l.add(
FlatButton(
child: Text(key),
onPressed: (key) {
//on pressed you just increment when it is hit for the key elem
globals.mydata[key] += 1;
}
));
});
return l;
}
#override
Widget build(BuildContext context) {
return Container(
height: 70,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildContent(),
));
}
}
Final step, now when you hit on the button, you get updated HasMap, which is globally accessible, so you can call the Panel(), no params required. And when you want to check the updated value just print it
import '{path}/globals.dart' as globals
Panel()
// This will give out the updated data if the button is hit else {'a': 0, 'b': 0, 'c': 0}
print(globals.mydata)
This is the cleanest way I can think for you in this case. I hope that would work out for you as well :) Let me know, I would be happy if that will work out :)