Flutter Roman / Arab Number Converter

Nick747Nick747
7 min read

Hello everyone! Today, we will be creating our first Flutter application. The app's essence is to facilitate the conversion of Arabic numerals to Roman and vice versa.

The Logic

First of all, as always, let's think of the logic behind the app we're going to create. In our app, we'll have two pages, one for Arabic to Roman converter and one for vice versa. On each page, we'll build an input field, a button to convert, and a text to show the result.

To convert the values we'll have two different functions that we're going to call when the button is pressed.

Set our project

Okay, to start, let's organize our project. We'll build two different pages that will be accessed by a navbar at the bottom of the screen.

In our lib folder add a folder called views with two dart files inside: arab_conv.dart & rom_conv.dart.

Next, go into your pubspec.yaml file and add this library so that we can use Google fonts in our app. My current version is 3.0.1 but you can add the latest available:

dependencies:
  flutter:
    sdk: flutter
  google_fonts: ^3.0.1 # add this

main.dart

Let's now edit our main file. In our main file, we first have to import the files in the views folder and add the Google Fonts library:

import 'package:conv_roman/views/rom_conv.dart';
import 'package:conv_roman/views/arab_conv.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

Then, let's edit our main widget (MyApp) in this way:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Arab/Roman Converter',
      theme: ThemeData(
        primarySwatch: Colors.red,
        fontFamily: GoogleFonts.getFont('Poppins').fontFamily,
      ),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(title: 'Arab/Roman Converter'),
    );
  }
}

With this code, we simply configure our app and its style. In this case, we use the Google font Poppins as default.

For the Stateful component, we need to specify our navbar using the following code, which can be adapted for use in other projects by adjusting certain file names and text.:

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _currentIndex = 0;
  static List<Widget> pages = <Widget>[
    const RomConv(),
    const ArabConv(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Arab - Roman Converter'),
        centerTitle: true,
      ),

      body: pages[_currentIndex],

      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: _onItemTapped,
        selectedItemColor: Colors.red,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.circle),
            label: 'To Roman',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.circle),
            label: 'To Arab',
          ),
        ],
      ),
    );
  }
}

Let's analyze this code step by step:

  1. Define a current index variable that indicates the page we are viewing;

  2. Define the list of pages available;

  3. Create a function to update the _currentIndex every time an item is pressed;

  4. Build the default page:

    1. Add a default appBar with a title;

    2. Add the body of the page we are currently viewing

    3. Add the bottomNavbar.

rom_conv.dart

UI

Let's now build the page to convert an Arab number to a Roman one. First of all, add the packages for flutter material and google fonts.

Then create a new StatefulWidget called RomConv and add this code which we'll analyze later:

class RomConv extends StatefulWidget {
  const RomConv({super.key});

  @override
  State<RomConv> createState() => _RomConvState();
}

class _RomConvState extends State<RomConv> {
  TextEditingController arabNum = TextEditingController();
  var pressed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            const SizedBox(
              height: 20,
            ),
            TextField(
              controller: arabNum,
              decoration: InputDecoration(
                suffixIcon: IconButton(
                  onPressed: arabNum.clear,
                  icon: const Icon(Icons.clear),
                ),
                hintText: 'Arab Number',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(15.00),
                ),
              ),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              child: const Text('Convert'),
              onPressed: () {
                pressed = true;
                setState(() {});
              },
            ),
            const SizedBox(
              height: 100,
            ),
            Container(
              padding: const EdgeInsets.all(10.0),
              decoration: BoxDecoration(
                border: Border.all(
                  width: 3.5,
                  color: pressed ? Colors.red : Colors.white,
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                pressed
                    ? calcRoman(arabNum.text)
                    : 'Insert a number',
                style: TextStyle(
                    fontSize: 40,
                    fontFamily:
                        GoogleFonts.getFont('Playfair Display').fontFamily),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.help),
        onPressed: () {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: const Text('Help'),
                content: const Text('To convert an arab number to a roman one, insert a number in the text field first.'),
                actions: <Widget>[
                  ElevatedButton(
                    child: const Text('Close'),
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                  ),
                ],
              );
            },
          );
        },
      ),
    );
  }
}

Now, analyze!

  1. Create the StatefulWidget RomConv

  2. Create the TextEditingController for the arabNum input, which will store the value of the Arabic number that we enter.

  3. Create a variable for the state of the button (false/true);

  4. Create the UI of the page:

    1. In the main column create a TextField which will be the input for our Arabic number;

    2. After some whitespace create the button to convert the Arabic number to a Roman one. This button will call a function we'll define later in this post;

    3. Create a container to display the Roman numeral result. In case there is no number to display, it'll show the message 'Insert a number'.

    4. A simple FloatingActionButton to help people do the right things

The function

Let's now write the function to convert an Arabic number to a Roman one.

To begin with, we need to verify that the input is a numerical value. If not, we will display an error message. Next, we will create two lists - one for Arabic numerals and the other for Roman numerals. We will then iterate through the Arabic numeral list and assign the corresponding Roman numeral for each value.

Here's the code to do that:

String calcRoman(input) {

  if (int.tryParse(input) == null) {
    return 'Error';
  }

  int value = int.parse(input);

  List<int> iNum = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
  List<String> rNum = [
    'M',
    'CM',
    'D',
    'CD',
    'C',
    'XC',
    'L',
    'XL',
    'X',
    'IX',
    'V',
    'IV',
    'I'
  ];

  var nRoman = '';


  for (var i = 0; i < iNum.length; i++) {
    while (iNum[i] <= value) {
      nRoman += rNum[i];
      value -= iNum[i];
    }
  }

  return nRoman;
}

arab_conv.dart

UI

The UI is the same as the rom_conv.dart but with changes to variable names and texts so I am going to skip that part by passing only the code:

class ArabConv extends StatefulWidget {
  const ArabConv({super.key});

  @override
  State<ArabConv> createState() => _ArabConvState();
}

class _ArabConvState extends State<ArabConv> {

  final romanNum = TextEditingController();
  var pressed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            const SizedBox(
              height: 20,
            ),
            TextField(
              controller: romanNum,
              decoration: InputDecoration(
                suffixIcon: IconButton(
                  onPressed: romanNum.clear,
                  icon: const Icon(Icons.clear),
                ),
                hintText: 'Roman Number',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(15.00),
                ), 
              ),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              child: const Text('Convert'),
              onPressed: () {
                pressed = true;
                setState(() {});
              },
            ),
            const SizedBox(
              height: 100,
            ),
            Container(
              padding: const EdgeInsets.all(10.0),
              decoration: BoxDecoration(
                  border: Border.all(
                    width: 3.5,
                    color: pressed ? Colors.red : Colors.white,
                  ),
                  borderRadius: BorderRadius.circular(8),   
                ),
              child: Text(
                pressed ? calcArab(romanNum.text).toString() : 'Insert a number',
                style: TextStyle(
                    fontSize: 40,
                    fontFamily:
                        GoogleFonts.getFont('Playfair Display').fontFamily),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.help),
        onPressed: () {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: const Text('Help'),
                content: const Text('To convert a roman number to an arab one, insert a roman number with this letters: \n I - V - X - L - C - D - M.'),
                actions: <Widget>[
                  ElevatedButton(
                    child: const Text('Close'),
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                  ),
                ],
              );
            },
          );
        },
      ),
    );
  }
}

The function

In this scenario, the approach will be altered slightly. Firstly, we need to establish a Map (Dictionary) where each Roman letter is paired with its corresponding value. Next, we must address situations such as IV or IX, where we don't simply sum the value of each letter, but instead check if the subsequent value is greater and perform a subtraction:

int calcArab(String romanNumber) {
  var romanSymbols = {
    'I': 1,
    'V': 5,
    'X': 10,
    'L': 50,
    'C': 100,
    'D': 500,
    'M': 1000,
  };

  int result = 0;

  int lastValue = 0;
  bool error = false;

  for (int i = romanNumber.length - 1; i >= 0; i--) {

    if ('IVXLCDM'.contains(romanNumber[i]) && !error) {
      int currentValue = romanSymbols[romanNumber[i]]!;

      if (currentValue >= lastValue) {
        result += currentValue;
      }
      else {
        result -= currentValue;
      }

      lastValue = currentValue;
    }
    else {
      error = true;
    }
  }

  if (error) {
    return 0;
  } else {
    return result;
  }

}

Final Execution & Resources

We've finally arrived at the end. You should now see your amazing app that converts Arabic to Roman numbers and vice versa. If you get any errors, feel free to comment them on this post. I should respond right away.

You can find the entire project and some screenshots of the app on the GitHub repo at this link. Until a new post, you can read the old ones which are pretty cool too.

That's it for today,

Take care

0
Subscribe to my newsletter

Read articles from Nick747 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nick747
Nick747

Hello! I am a teenager and a web/app developer since 2018.