How To Create a Tip Calculator with Flutter

Written by alphamikle | Published 2021/05/16
Tech Story Tags: flutter | mobile | app | tutorial | howto | mobile-application | dart | software-development

TLDR In this tutorial, you will learn to create your app with Flutter, which can help you calculate tips. 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. You can also use the Hot Restart feature - it is like Hot Reload, but it reloads all Flutter layer of your app without saving state. You will understand when to start creating apps in Flutter and how to use Hot Reload - one of the most exciting features of Flutter.via the TL;DR App

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:
  • What is a widget?
  • What build method does?
  • Hot Reload - one of the most exciting features of Flutter
Ready? Let’s start!

A Little Theory

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. 

Source Code


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),
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

StatefulWidget

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();
}

StatelessWidget

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,
        ),
      ],
    );
  }
}

Build Method

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.

Super Exciting Hot Reload

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.

Conclusion

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.

Written by alphamikle | Lead software engineer
Published by HackerNoon on 2021/05/16