Una semplice calcolatrice in Flutter

Nell'articolo di oggi vedremo come realizzare una calcolatrice con funzioni elementari. In particolare vedremo come utilizzare una griglia per disporre i tasti e come usare un package da aggiungere alle dipendenze del progetto per valutare le espressioni.


Il progetto base e la tastiera

Creiamo un nuovo progetto e sostituiamo il codice standard nel file main.dart con questo.


import 'package:flutter/material.dart';
import './pages/calculator.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter calculator',
debugShowCheckedModeBanner: false,
      home: Calculator(),
    );
  }
}


Creiamo quindi una sottocartella pages e dentro questa, il file calculator.dart con il seguente codice:


import 'package:flutter/material.dart';

List buttonNames = [
  "7", "8", "9", "÷",
  "4", "5", "6", "×",
  "1", "2", "3", "−",
  "0", ".", "C", "+",
  "±", "(", ")", "="
];

class Calculator extends StatelessWidget {
  const Calculator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: _buildButtons(),
      ),
    );
  }

  // Grid of buttons
  Widget _buildButtons() {
    return Material(
      color: const Color(0xFFF2F2F2),
      child: GridView.count(
          crossAxisCount: 4, // 5x4 grid
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          padding: const EdgeInsets.all(8),
          children: buttonNames.map<Widget>((caption) {
            return _button(caption);
          }).toList()),
    );
  }

  // Buttons
  Widget _button(text) {
    return Padding(
        padding: const EdgeInsets.all(4),
        child: InkWell(
          onTap: () {
            // event on press
          },
          child: Container(
            alignment: Alignment.center,
            child: Text(
              text,
              style: const TextStyle(
                color: Colors.black54,
                fontSize: 30.0,
                fontWeight: FontWeight.w300,
              ),
            ),
          ),
        ));
  }
}

Il codice descrive una applicazione il cui corpo contiene un unico widget  _buildButtons costituito da una GridView su 4 colonne in cui sono disposti i 20 pulsanti (quindi su  5 righe). I pulsanti a loro volta sono generati utilizzando la funzione .map sull'array di stringhe che contiene i simboli (disposti in un ordine particolare in modo che la disposizione in griglia è quella desiderata) e restituendo per ogni simbolo un widget _button che consiste sostanzialmente in un oggetto InkWell a cui assoceremo in seguito un funzione per gestire il click. 

L'aspetto finale, dovrebbe essere qualcosa del genere


Gestiamo il tap sui tasti

Prima di associare una funzione ai tasti, per gestire lo stato della nostra calcolatrice, ovvero per poter poi visualizzare l'espressione numerica generata, occorre trasformare il nostro widget StatelessWidget in uno StatefulWidget. Con un refactoring sulla  classe Calculator otteniamo questo

...
class Calculator extends StatefulWidget {
  const Calculator({Key? key}) : super(key: key);

  @override
  State<Calculator> createState() => _CalculatorState();
}

class _CalculatorState extends State<Calculator> {
  @override
  Widget build(BuildContext context) {
...

Aggiungiamo alle variabili una nuova stringa che conterrà l'espressione numerica da valutare:


String expression = "0";

Ora possiamo scrivere la funzione da associare ad ogni pulsante:


  void _buttonPressed(String text) {
    setState(() {
      if (text == "=") {
        // calculate

      } else if (text == "C") {
        // Backspace
        expression = expression.substring(0, expression.length - 1);
        if (expression == "") expression = "0";
      } else if (text == "±") {
        // Plus/Minus
        if (expression != "0") {
          if (expression.substring(0, 1) != "-") {
            expression = "-$expression";
          } else {
            expression = expression.substring(1);
          }
        }
      } else {
        // Default
        if (expression == "0" && text != ".") expression = "";
        expression += text;
      }
    });

  }

Questa funzione (che aggiorna lo stato dell'applicazione) fa le seguenti operazioni:

  • se si preme il tasto = elabora l'espressione (ancora da definire)
  • se si preme il tasto C cancella l'ultimo carattere dell'espressione
  • se si preme il tasto ± cambia il segno all'espressione
  • negli altri casi aggiunge il carattere all'espressione

Per associare la funzione all'evento dei pulsanti onTap dei pulsanti:


...
        child: InkWell(
          onTap: () {
            _buttonPressed(text);
          },
          child: Container(
...

Visualizziamo l'espressione

Modifichiamo il layout dell'applicazione per inserire un riquadro in cui visualizzare l'espressione da valutare e, vedremo dopo, anche il risultato delle operazioni.

Modifichiamo la funzione build in questo modo:

 
Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: Column(children: <Widget>[
        Expanded(
          flex: 1,
          child: Container(
              decoration: BoxDecoration(
                 border: Border.all(
                  color: Colors.green,
                  width: 1,
                ), //Border.all
              ),
              alignment: Alignment.topLeft,
              child: Container(
                padding: const EdgeInsets.fromLTRB(14, 8, 14, 0),
                child: Text(
                  expression,
                  style: const TextStyle(
                      color: Color.fromRGBO(227, 227, 227, 1),
                      fontSize: 48,
                      fontWeight: FontWeight.w400),
                ),
              )),
        ),
        Expanded(flex: 3, child: _buildButtons()),
      ]),

    ));
  }

Abbiamo introdotto un layout a colonne, composto da due elementi, quello superiore che visualizzerà l'espressione numerica e quello inferiore con la tastiera. I due elementi hanno un rapporto di proporzione di 1 a 3 (proprietà flex del widget Expanded).   


Valutiamo l'espressione e gestiamo il risultato

Per valutare l'espressione numerica possiamo utilizzare il package math_expressions. Aggiungiamolo alle dipendenze del progetto modificando il file pubspec.yaml. In particolare, sotto la riga relativa a cupertino_icons aggiungiamo il riferimento necessario, facendo attenzione a rispettare la stessa indentatura: 


  cupertino_icons: ^1.0.2
  math_expressions: ^2.3.1


fatto questo, possiamo aggiungere la riga 


import 'package:math_expressions/math_expressions.dart';

nel file calculator.dart

La funzione che valuta il  risultato dell'espressione è questa:


  String _evaluateEquation(String formula) {
    try {
      Expression exp = (Parser()).parse(formula);

      double res = double.parse(
          exp.evaluate(EvaluationType.REAL, ContextModel()).toString());

      return double.parse(res.toString()) == int.parse(res.toStringAsFixed(0))
          ? res.toStringAsFixed(0)
          : res.toStringAsFixed(4);
    } catch (e) {
      return "Errore";
    }
  }

che, passata una espressione numerica, prova a valutarla come numero reale. Se l'esito è positivo, aggiunge eventuali cifre decimali, altrimenti restituisce un errore.

Per completare, utilizziamo questa nuova funzione nella _buttonPressed già definita, passandogli la variabile expression quando si preme il tasto =.


...
    setState(() {
      if (text == "=") {
        expression = _evaluateEquation(expression);
      } else if (text == "C") {
...

Come per gli articoli precedenti potete trovare questo codice tra le mie repo GitHub: https://github.com/luigimicco/flutter_calculator

Commenti