I am learning Android app development using Dart/Flutter and I am trying to understand the general process of developing a custom widget for an app. For example, if I need a widget that has a TextField, an Image, and a checkbox, how do I test that widget individually?
I mean, there is no app to put that widget in as of now, so how do I "execute" it to see if that widget is getting laid out correctly and working properly as expected?
In the Java Swing world, I would just put a main method in my component class. In that main method, I would create a frame or something and add that component to that frame. Then run that class directly. That way, I can basically fine tune the component without worrying about running the actual application, and without having to go through the whole app flow to reach that component and check how it looks.
Is it similar in Flutter app as well? Create a dummy app with the widget as the only "screen" and then execute that dummy app?
There is an excellent online tool called "Dart Pad", the link here should get you started on basic boilerplate code that is the beginnings of your app.
You can then proceed to create a custom widget, I'll try and replicate your example widget below:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue;
}
return Colors.red;
}
return Container(
child: Column(
children: [
Image.network("https://icatcare.org/app/uploads/2018/07/Thinking-of-getting-a-cat.png"),
TextField(decoration: InputDecoration(hintText: "Type something")),
Checkbox(
checkColor: Colors.white,
fillColor: MaterialStateProperty.resolveWith(getColor),
value: isChecked,
onChanged: (bool? value) {
setState(
() {
isChecked = value!;
},
);
},
),
],
),
);
}
}
Related
I'm using Provider and a ChangeNotifier to access some data across multiple screens in my app. I'm now wanting to implement restoration, so that even if that app dies in the background the user will come back to the same page with the same data. I have managed to make the app go back to the same page, but I can't figure out how to implement a restorable version of my ChangeNotifier. I found a RestorableChangeNotifier class on the Flutter API but am struggling to use it. I've put a simple code below that demonstrates the issue that I'm trying to achieve - any help would be appreciated!
Super simplified example
Class that uses ChangeNotifier, that stores the data:
class ItemClass extends ChangeNotifier {
final List<String> _itemNames = [];
List<String> get itemNames => _itemNames;
void addItem(String newItem) {
_itemNames.add(newItem);
notifyListeners();
}
}
Top level widget with ChangeNotifierProvider:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ItemClass(),
child: const MaterialApp(
home: HomeScreen(),
restorationScopeId: 'root',
),
);
}
}
The screen I want to be at, showing the info from the ItemClass:
class Screen1 extends StatefulWidget {
const Screen1({Key? key}) : super(key: key);
#override
State<Screen1> createState() => _Screen1State();
}
class _Screen1State extends State<Screen1> {
final TextEditingController _textEditingController = TextEditingController();
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
ItemClass itemClass = context.watch<ItemClass>();
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
Text('Items: ${itemClass.itemNames}'),
TextField(
controller: _textEditingController,
),
TextButton(
child: const Text('Add item'),
onPressed: () =>
itemClass.addItem(_textEditingController.text)),
],
)));
}
}
I don't know where or how to implement the restoration for this ItemClass.
I have tried to learn this concept for over a month and I just can't seem to figure out what exactly is GlobalKeys and various types of keys in simpler terms, Is it just for form validator or does it serve more purpose,There aren't clear basic example of this in youtube too or perhaps may be there is a book somewhere ?
Here in basic flutter app
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const 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();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Here there are lines like
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
\\\\
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
What does Key /super keyword have to do with it or how do we use it?
I found this very helpful: https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d
I found two uses for GlobalKeys (I'm sure there's more):
First one: this is what the article above discusses: how to link a state object to a widget. I'm going to oversimplify few things here.
Normally, flutter will not use Global Keys. When you create a stateful widget, two object get created: a widget, and it's state. The idea is that the widget itself will be destroyed at the end of the build (or after it is painted on the screen). Once you initiate the build again (through setState() for exmaple) - a widget will be recreated.
Of course - you want your state object to persist between the changes - this is where you store your data after all. And you want Flutter to link back your state object to newly created Widget object instance.
Most of the time, this is an easy thing for Flutter - it will just find the same widget in the same position, link it to it's state object and it's done.
In case your widget moves around in the next build - this will not work, and new state object will be created and you will lose your state.
Take a look at this modified Flutter Counter app - I moved the counter into a separate Widget, but also linked it back to the the original _MyHomePageState. Each time we rebuild the main widget we will flip the order of the button and text box in the column - and you will see that the count never changes: it always show zero.
Since the widget changed the position, it's state is dropped and new one created.
You can run this in dartpad quickly:
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _key=GlobalKey();
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_counter.isOdd) Counter(onPressed: _incrementCounter),
const Text(
'You have pushed the button this many times:',
),
if (_counter.isEven) Counter(onPressed: _incrementCounter),
],
),
),
);
}
}
class Counter extends StatefulWidget {
final VoidCallback onPressed;
const Counter({Key? key, required this.onPressed}) : super(key: key);
#override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
widget.onPressed();
}
#override
Widget build(BuildContext context) {
return Column(children: [
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
ElevatedButton(
style:
ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: _incrementCounter,
child: const Text('Increment'),
)
]);
}
}
Now simply by adding a GlobalKey to Counter widget (in both calls), we help flutter link the state object. Note that we created the _key value once in the state object - since we want to pass the same key each time (otherwise what's the point...)
Counter(onPressed: _incrementCounter, key: _key),
And after this change, even when Widget changes the place and order on screen - your state is kept.
Now you would ask: why wouldn't Flutter do this by default for all the Widgets? Turns out - GlobalKeys are very expensive to maintain, and if you want to render 60 frames per second (16ms each frame) - you want to optimize every single step. By 'no keys by default' approach, everything is optimized, and works in large majority of the cases - but in few situations when you need global keys - you need to learn a little bit about it, and it is very simple to use.
Second use I found for it: you need global key to find the your exact Widget position on screen after it renders. Widget itself will not know where it will end up, you only know this after the rendering is done. You need this if you want to do custom animation or something like that.
This is a simple function that will tell you where your widget is:
Rect getRectFromKey(GlobalKey key) {
RenderBox renderBox = (key.currentContext!.findRenderObject())! as RenderBox;
var targetPosition = renderBox.localToGlobal(Offset.zero);
var targetSize = renderBox.size;
// A Rect can be created with one its constructors or from an Offset and a Size using the & operator:
Rect rect = targetPosition & targetSize;
return rect;
}
Multiple widgets of the same type and at the same level in a widget tree may not update as expected unless they have unique keys, given that these widgets hold some state.
Explicitly setting a key to a widget helps Flutter understand which widget it needs to update when state changes.
keys also store and restore the current scroll position in a list of widgets.
Global key usually use to change parent widget from any portion of app considering state unchanged. Gobal keys are broad than keys in short.
Global keys are for Form validations. Other than global keys there is value,object,and unique key in Flutter. You get clear idea about all this through below link.
Keys in Flutter
I was following flutter tutorials for managing state of a widget from its parent on this link [https://flutter.dev/docs/development/ui/interactive#parent-managed][1]
and i cant figure out how would call the widget in this case
it is very simple once you get the logic.
In practice, the parent (the "true" widget that you call), i.e.
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
is the one that you call wherever and whenever you want in the rest of the code.
Since this is a Stateful widget, it means that it has stated (to keep it simple, it will manage any changes on the UI). Any change will occur, It will be changing its state and so, this code:
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
#override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
Anyhow, once you use a Stateful widget, you change its state whenever you want to call the function
setState(() {
oldValue= newValue;
});
It will rebuild the entire widget changing the stuff you want (such as texts, images, widgets, and so on).
In a non-proper way, consider it as a particular widget that can change its UI during the time.
if you want to call it in MyApp's build method you will have to make MyApp a stateful widget so that it can manage the state of the said widget
void main() => runApp(MyApp());
//we make MyApp to be a stateful widget
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
//we define the state which will be used in the widget here
var myState = "something";
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: Center(
child: Container(
//the data used by MyWidget is managed by MyApp which is a statefull widget.
child: MyWidget(state: myState),
),
),
),
);
}
}
Or rather wrap your widget with another stateful widget which you will use in MyApp's build method
//we create a widget which will manage the state of its children class MyStateManagingWidget extends StatefulWidget { #override
_MyStateManagingWidgetState createState() => _MyStateManagingWidgetState(); }
class _MyStateManagingWidgetState extends State<MyStateManagingWidget> { var myState = "some state"; #override Widget build(BuildContext context) {
//we put our widget who's state is to be managed here
return MyWidget(); } }
class MyApp extends StatelessWidget { #override Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: Center(
child: Container(
//we now use the state managing widget here
child: MyStateManagingWidget()),
),
),
); } }
I am new to flutter and building a sample app to learn it. In the above screenshot, I have created multiple widgets. My main widget contains the following widget.
Boy Girl Selector
Common Card
CounterButton (Plus or Minus)
Calculate Button
My main widget has two counter - age & weight.
CommonCard has below property :
incrementFunction() : I am setting this value from MainWidget as below.
decrementFunction()
ageIncrement() {
setState(() {
age++;
});
}
ageDecrement() {
setState(() {
age--;
});
}
value : age declared in main widget is passed to this value.
CounterButton has below property.
onPressed: increment or decrement function from parent widget is passed here through card widget.
If I keep whole code in main widget then it is working properly. But if I create multiple widget and pass increment and decrement function as argument in child widget onPressed on plus and minus is not working propely. Please share your thoughts. I am missing some fundamental of communication between child and parent widget.
There are different ways to achieve what you like as there are a couple of different state management techniques such as Dependency Injection, ChangeNotifier, BLoC, and so on (search for Flutter State Management for more details).
Here's an example of how you can achieve this on the famous counter example. This example is using dependency injection (we are passing the increment function to the a child widget as callback function). You can copy the code and past it on DartPad to quickly test it and see how it works:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
SizedBox(height: 50),
MySecondButton(secondButtonIncrement: incrementCounter),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class MySecondButton extends StatelessWidget {
MySecondButton({Key key, this.secondButtonIncrement}) : super(key: key);
final VoidCallback secondButtonIncrement;
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text("Second Button"),
onPressed: () {
secondButtonIncrement();
},
color: Colors.blue);
}
}
I hope that helps.
In Emily Fortuna's article (and video) she mentions:
GlobalKeys have two uses: they allow widgets to change parents
anywhere in your app without losing state, or they can be used to
access information about another widget in a completely different part
of the widget tree. An example of the first scenario might if you
wanted to show the same widget on two different screens, but holding
all the same state, you’d want to use a GlobalKey.
Her article includes a gif demo of an app called "Using GlobalKey to ReuseWidget" but does not provide source code (probably because it's too trivial). You can also see a quick video demo here, starting at 8:30 mark: https://youtu.be/kn0EOS-ZiIc?t=510
How do I implement her demo? Where do I define the GlobalKey variable and how/where do I use it? Basically for example, I want to display a counter that counts up every second, and have it on many different screens. Is that something GlobalKey can help me with?
The most common use-case of using GlobalKey to move a widget around the tree is when conditionally wrapping a "child" into another widget like so:
Widget build(context) {
if (foo) {
return Foo(child: child);
}
return child;
}
With such code, you'll quickly notice that if child is stateful, toggling foo will make child lose its state, which is usually unexpected.
To solve this, we'd make our widget stateful, create a GlobalKey, and wrap child into a KeyedSubtree.
Here's an example:
class Example extends StatefulWidget {
const Example({Key key, this.foo, this.child}) : super(key: key);
final Widget child;
final bool foo;
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
final key = GlobalKey();
#override
Widget build(BuildContext context) {
final child = KeyedSubtree(key: key, child: widget.child);
if (widget.foo) {
return Foo(child: child);
}
return child;
}
}
I would not recommend using GlobalKey for this task.
You should pass the data around, not the widget, not the widget state. For example, if you want a Switch and a Slider like in the demo, you are better off just pass the actual boolean and double behind those two widgets. For more complex data, you should look into Provider, InheritedWidget or alike.
Things have changed since that video was released. Saed's answer (which I rewarded 50 bounty points) might be how it was done in the video, but it no longer works in recent Flutter versions. Basically right now there is no good way to easily implement the demo using GlobalKey.
But...
If you can guarantee that, the two widgets will never be on the screen at the same time, or more precisely, they will never be simultaneously inserted into the widget tree on the same frame, then you could try to use GlobalKey to have the same widget on different parts of the layout.
Note this is a very strict limitation. For example, when swiping to another screen, there is usually a transition animation where both screens are rendered at the same time. That is not okay. So for this demo, I inserted a "blank page" to prevent that when swiping.
How to:
So, if you want the same widget, appearing on very different screens (that hopefully are far from each other), you can use a GlobalKey to do that, with basically 3 lines of code.
First, declare a variable that you can access from both screens:
final _key = GlobalKey();
Then, in your widget, have a constructor that takes in a key and pass it to the parent class:
Foo(key) : super(key: key);
Lastly, whenever you use the widget, pass the same key variable to it:
return Container(
color: Colors.green[100],
child: Foo(_key),
);
Full Source:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
final _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Global Key Demo")),
body: PageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
color: Colors.green[100],
child: Foo(_key),
);
break;
case 1:
return Container(
color: Colors.blue[100],
child: Text("Blank Page"),
);
break;
case 2:
return Container(
color: Colors.red[100],
child: Foo(_key),
);
break;
default:
throw "404";
}
},
),
);
}
}
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
Foo(key) : super(key: key);
}
class _FooState extends State<Foo> {
bool _switchValue = false;
double _sliderValue = 0.5;
#override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
value: _switchValue,
onChanged: (v) {
setState(() => _switchValue = v);
},
),
Slider(
value: _sliderValue,
onChanged: (v) {
setState(() => _sliderValue = v);
},
)
],
);
}
}
Update: this was an old approach to tackle the state management and not recommended anymore,please see my comments on this answer and also check user1032613's answer below
Global keys can be used to access the state of a statefull widget from anywhere in the widget tree
import 'package:flutter/material.dart';
main() {
runApp(MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: App(),
));
}
class App extends StatefulWidget {
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
GlobalKey<_CounterState> _counterState;
#override
void initState() {
super.initState();
_counterState = GlobalKey();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
Counter(
key: _counterState,
),
],
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigate_next),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return Page1(_counterState);
}),
);
},
),
);
}
}
class Counter extends StatefulWidget {
const Counter({
Key key,
}) : super(key: key);
#override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count;
#override
void initState() {
super.initState();
count = 0;
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
count++;
});
},
),
Text(count.toString()),
],
);
}
}
class Page1 extends StatefulWidget {
final GlobalKey<_CounterState> counterKey;
Page1( this.counterKey);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
widget.counterKey.currentState.count++;
print(widget.counterKey.currentState.count);
});
},
),
Text(
widget.counterKey.currentState.count.toString(),
style: TextStyle(fontSize: 50),
),
],
),
),
);
}
}