In this post we will see how to handle user interactions like click/tap events and respond to them
https://flutter.dev/docs/development/ui/interactive
The current blog is extended from above, go through it once.
User interactions are divided into two parts broadly in flutter
a) Widgets which provide interactions like onTap, pressed etc i.e all such events
b) State-full widgets that we make ourselves which allow us to update widget presentation based on changes.
Statefull widgets
Till now all the widgets we have developed have been “Stateless” widgetes which means they won’t render. Once their presentation is calculated they don’t render at all. Statefull widgets on the other call their “build” function again when state changes. Let’s see this in more detail
This is a bit complicated topic to understand at first so i will try to explain this step by step. To explain this we will take example of a timer app.
class TimerWidget extends StatefulWidget {
@override
_TimerState createState() => _TimerState();
}
In this we create an empty “StatefulWidget”.
class _TimerState extends State<TimerWidget> {
@override
Widget build(BuildContext context) {
//...
}
}
For any code which requires re-render or interactive behaviour flutter requires us to create one “StatefulWidget” and “State” class. The reason for this is
You might wonder why StatefulWidget
and State
are separate objects. In Flutter, these two types of objects have different life cycles. Widgets
are temporary objects, used to construct a presentation of the application in its current state. State
objects, on the other hand, are persistent between calls to build()
, allowing them to remember information.
Now the “State” class has function “setState” which when called will call the “build()” function again and re-render the widget on UI.
class _TimerState extends State<TimerWidget> {
int _timer = 0;
void _increaseTimer(Timer timer) {
setState(() {
print(_timer);
_timer++;
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(
"Timer $_timer",
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.black38, fontSize: 15),
),
);
}
}
Ok, above we set a container with a Text where we are using the variable $_timer and also function _increaseTimer() which changes the state. Next we need to find a way to call the function _increaseTimer(). This can be done either via user interaction like button or maybe a timer.
In the current example lets see timer https://api.dartlang.org/stable/2.5.0/dart-async/Timer/Timer.periodic.html
https://stackoverflow.com/questions/49072957/flutter-dart-open-a-view-after-a-delay
class _TimerState extends State<TimerWidget> {
static const timeout = const Duration(seconds: 3);
static const ms = const Duration(milliseconds: 1);
int _timer = 0;
Timer _timerObj;
_TimerState() {
_timerObj = startTimeout();
}
startTimeout([int milliseconds]) {
var duration = milliseconds == null ? timeout : ms * milliseconds;
return new Timer.periodic(duration, _increaseTimer);
}
void _increaseTimer(Timer timer) {
setState(() {
print(_timer);
_timer++;
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(
"Timer $_timer",
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.black38, fontSize: 15),
),
);
}
}
If you use this widget in your app it will show a counter increasing every second. Also notice the “print” function which we used to display debug information.
Now, lets add a button to start/stop this timer. First we create a button widget using “GestureDetector”
class MyButton extends StatelessWidget {
MyButton({this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 36.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: Center(
child: Text(
'Start',
textDirection: TextDirection.ltr,
),
),
),
);
}
}
P.S We are still not using MaterialUI Components. Using material ui will things make easier/prettier. We will transition to it soon!
class _TimerState extends State<TimerWidget> {
static const timeout = const Duration(seconds: 3);
static const ms = const Duration(milliseconds: 1);
int _timer = 0;
Timer _timerObj;
_TimerState() {
_timerObj = startTimeout();
}
startTimeout([int milliseconds]) {
var duration = milliseconds == null ? timeout : ms * milliseconds;
return new Timer.periodic(duration, _increaseTimer);
}
void _increaseTimer(Timer timer) {
setState(() {
print(_timer);
_timer++;
});
}
void _stopTimer(){
print("stop timer");
_timerObj.cancel();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Text(
"Timer $_timer",
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.black38, fontSize: 15),
),
MyButton(onTap:_stopTimer)
],
),
);
}
}
We use the “MyButton” widget in our app to make it stop the timer.
Next let’s add a “start” button as well. We will also make “MyButton” dynamic i.e we will have a single button but with different arguments for text. See the MyButton constructor on how to do this.
class MyButton extends StatelessWidget {
MyButton({this.buttonText = "", this.onTap});
final VoidCallback onTap;
final String buttonText;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 36.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: Center(
child: Text(
buttonText,
textDirection: TextDirection.ltr,
),
),
),
);
}
}
class TimerWidget extends StatefulWidget {
@override
_TimerState createState() => _TimerState();
}
class _TimerState extends State<TimerWidget> {
static const timeout = const Duration(seconds: 3);
static const ms = const Duration(milliseconds: 1);
int _timer = 0;
Timer _timerObj;
_TimerState() {
_timerObj = startTimeout();
}
startTimeout([int milliseconds]) {
var duration = milliseconds == null ? timeout : ms * milliseconds;
return new Timer.periodic(duration, _increaseTimer);
}
void _increaseTimer(Timer timer) {
setState(() {
print(_timer);
_timer++;
});
}
void _stopTimer() {
print("stop timer");
_timerObj.cancel();
}
void _startTimer() {
print("start timer");
if (!_timerObj.isActive) {
startTimeout();
}
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Text(
"Timer $_timer",
textDirection: TextDirection.ltr,
style: TextStyle(color: Colors.black38, fontSize: 15),
),
MyButton(buttonText: "Start", onTap: _startTimer),
MyButton(buttonText: "Stop", onTap: _stopTimer),
],
),
);
}
}
Next take this a level further i.e when timer is running only “Stop” button will show and when is stopped only “start button will show”
This is how our current app code looks now
https://gist.github.com/excellencetechnologies/b07d28f1d6c9f436c04a62b1aae96745
Also, at this stage it’s better to start using MaterialApp now! 🙂
So i will update my full source to MaterialApp