In this tutorial, you will learn to create your app with Flutter, which can help you calculate tips (when you buy chips, ha-ha). In this process, you will explore a few interesting things from the Flutter stack:
Ready? Let’s start!
Widget is an entity, which can have some state. Also, this entity is a way to declare and construct the UI of your app. As the Flutter team says:
All is Widget
The Widget can be small and plain - only one word in the text can be a separated Widget. But, on the other hand, Widget can be huge with much complex logic.
Well, to start, you need to have installed Flutter and Dart. I hope you have done this. Next - create a Flutter application from the template with the command: flutter create.
The 'tip app' will consist of only one screen - in which you can see two simple text fields - one for the overall amount of your order and the second input for the tip percentage. The screen also has two text lines with a tip in dollars and a total amount of charge, which equals the overall amount + tip amount.
import 'package:anitex/anitex.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:tipsalc/order/view/money_row.dart';
import '../../constants/input.dart';
import '../../constants/padding.dart';
import '../../constants/text_style.dart';
import '../../locale/locale.dart';
import '../../utils/formatters.dart';
import '../../utils/utils.dart';
import '../../widgets_helpers/horizontal_divider.dart';
import '../../widgets_helpers/vertical_divider.dart';
import '../state/order.state.dart';
class OrderView extends StatefulWidget {
const OrderView({
Key? key,
}) : super(key: key);
@override
_OrderViewState createState() => _OrderViewState();
}
class _OrderViewState extends State<OrderView> {
/// We will use Flutter 2.0 with Dart 2.12
/// which offer you null-safety
late OrderState orderState;
@override
void initState() {
super.initState();
/// For state management we will
/// use simple [setState] method
/// But to prevent placing business logic
/// in UI layer - we will take out it in
/// separate class [OrderState]
orderState = OrderState()
..registerHook(() {
setState(() {});
})
..initState();
}
@override
void dispose() {
orderState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final infoStyle = TextStyl.bold(context);
return Padding(
padding: const EdgeInsets.all(Pad.l1),
child: Column(
children: [
/// This is first field with
/// amount of just order
TextFormField(
decoration: InputDec.outline8.copyWith(
labelText: Locale.billTotalLabel,
),
keyboardType: TextInputType.number,
controller: orderState.billTotalController,
inputFormatters: [
/// We will using formatter
/// for prevent input non-digit symbols
/// in our input
NumberFormatter(),
],
),
const VDivider(),
/// Second field with amount of tips
/// in percents
TextFormField(
decoration: InputDec.outline8.copyWith(
labelText: Locale.tipPercentLabel,
),
keyboardType: TextInputType.number,
controller: orderState.tipAmountController,
inputFormatters: [
NumberFormatter(int: true),
],
),
const VDivider(level: DividerLevel.l2),
/// This is two ours text lines with
/// total amount of tips in dollars
Row(
children: [
Text(
Locale.tipAmountTitle,
style: infoStyle,
),
AnimatedText(
Utils.formatMoney(orderState.order.tipAmount),
style: infoStyle.copyWith(fontWeight: FontWeight.bold),
useOpacity: false,
),
],
),
const VDivider(),
/// And the second row with total amount
/// of all order (just order + tips in dollars)
MoneyRow(
title: Locale.totalAmountTitle,
money: orderState.order.totalWithTipAmount,
),
/// Well, there we have a visual helper to manipulate
/// tips quantity - you can simple increase or reduce
/// the amount of tips with step in 5%
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(Locale.changeTipHint, style: TextStyl.bold(context)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add),
onPressed: orderState.increaseTip,
color: Colors.green,
highlightColor: Colors.transparent,
splashColor: Colors.green.withOpacity(0.15),
),
const HDivider(),
IconButton(
icon: const Icon(Icons.remove),
onPressed: orderState.reduceTip,
color: Colors.red,
highlightColor: Colors.transparent,
splashColor: Colors.red.withOpacity(0.15),
),
],
),
const VDivider(level: DividerLevel.l5),
],
),
),
)
],
),
);
}
}
From this sample, you can see that we use a
StatefulWidget
for our single screen because it has its own state, where you can place some data which you can rule. We place the data in a separate class, OrderState.
The logic is placed there too, but the UI layer's renewal is with
_OrderViewState
- state of OrderView
- StatefulWidget
, which can refresh UI by calling setState
method.
@override
void initState() {
super.initState();
/// For state management we will
/// use simple [setState] method
/// But to prevent placing business logic
/// in UI layer - we will take out it in
/// separate class [OrderState]
orderState = OrderState()
..registerHook(() {
/// We pass method [setState] to our
/// OrdersState class, where this method
/// will called after any data updates
setState(() {});
})
..initState();
}
The second type of
Widget
that you can see in this example is a StatelessWidget
. In opposite to StatefulWidget
- that type hasn’t its own state - the only UI without and logic. But you also can use methods and data, which you must pass to your StatelessWidget
from its parents.For example, our
OrderView
widget places two identical widgets with the same goal - show the user two similar rows with text. Here they are:
Row(
children: [
Text(
Locale.tipAmountTitle,
style: infoStyle,
),
AnimatedText(
Utils.formatMoney(orderState.order.tipAmount),
style: infoStyle.copyWith(fontWeight: FontWeight.bold),
useOpacity: false,
),
],
),
You can also simplify the code as follows:
MoneyRow(
title: Locale.tipAmountTitle,
money: orderState.order.tipAmount,
),
You create your own
StatelessWidget
.
import 'package:anitex/anitex.dart';
import 'package:flutter/widgets.dart';
import '../../constants/text_style.dart';
import '../../utils/utils.dart';
class MoneyRow extends StatelessWidget {
const MoneyRow({
required this.title,
required this.money,
Key? key,
}) : super(key: key);
final String title;
final num money;
/// We move all UI logic from
/// old place (OrderView) to here
/// and in OrderView you can use simple
/// and small widget MoneyRow instead
/// this several widgets
///
/// And also you can reuse this in any place
/// of your app
@override
Widget build(BuildContext context) {
final infoStyle = TextStyl.bold(context);
return Row(
children: [
Text(
title,
style: infoStyle,
),
AnimatedText(
Utils.formatMoney(money),
style: infoStyle.copyWith(fontWeight: FontWeight.bold),
useOpacity: false,
),
],
);
}
}
As you can see - we moved widgets from
OrderView
to separate widget MoneyRow
. However, we placed them in a special method build
, which must return a widget too. What does this mean? All Widgets must have a build method and that will be called by the parent of the widget. As Flutter docs say:Build method describes the part of the user interface represented by this widget.
The framework calls this method when the widget is inserted into the tree in a given BuildContext and when the dependencies of this widget change (e.g., an InheritedWidget referenced by this widget changes). This method can potentially be called in every frame and should not have any side effects beyond building a widget.
Hot Reload. How nice to hear these words! That feature saves your time as a developer time. You can ask - How? Well - because you can change the code and see the result of your actions simultaneously. You can change the theme of all your app, and after part of one second - you will see your app in new colors. You can cut a big part of your widgets and replace them with another and see changes after splits of seconds. But you must know that you can make some changes and don’t see any difference in-app.
For example - you add new logic in some method which called only once when your app loading or in
initState
method - in that situations, you must using the Hot Restart feature - it is like Hot Reload, but it reloads all Flutter layer of your app without saving of state. So in some situations, you must use Hot Restart instead of Hot Reload. You will understand when to use what.To start creating apps with Flutter you should know how widgets work. You can use
StatefulWidgets
in small applications for simple state management. You can also use StatelessWidgets
to separate complex UI logic into simple different widgets, from which you will assemble the interface like a constructor.
You can see this application in real-time here.