Flutter Tutorial: Icon Picker Functionality

This is the third part of Flutter tutorial series where we are building an Expenses Manager app. In this post we will refactor our existing work and also build an icon picker functionality

Introduction

In our previous post of the Flutter tutorial series, we created a static list of expense categories. We also setup an OfflineDbProvider for SQLite database engine and a service implementation for expense category.

In this post, we will re-factor our classes to make things cleaner. We will then add a screen to create a new category.

DecisionMentor app

Re-factoring Classes

So far we have been writing all of our code in the main.dart file. Not a clean approach right? Let’s move the classes to separate files.

Inside the lib folder create a folder “screens“. This folder will contain all the routes that we will create.

Move Out HomePage

Add a new file “home_page.dart” and move the content of HomePage and _HomePageState classes from main.dart file to this file. Your main.dart file should look clean like this:

import 'package:expense_manager/screens/home_page.dart';
import 'package:flutter/material.dart';
import 'package:expense_manager/db/offline_db_provider.dart';

void main() {
  OfflineDbProvider.provider.initDB();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: HomePage(),
    );
  }
}

Move Out Category

Now create another file “category.dart” inside the screens folder. Move the content for Category widget from home_page.dart to this file creating a Stateful Category widget . At the end, your HomePage should look like this:

import 'package:expense_manager/screens/category_page.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  TabController _tabController;

  List<String> _tabs = ["Home", "Category", "Report"];

  @override
  void initState() {
    super.initState();
    _tabController = new TabController(vsync: this, length: _tabs.length);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Expense Manager"),
          bottom: TabBar(
            controller: _tabController,
            tabs: [
              Tab(icon: Icon(Icons.home)),
              Tab(icon: Icon(Icons.category)),
              Tab(icon: Icon(Icons.report)),
            ],
          ),
        ),
        body: TabBarView(
          controller: _tabController,
          children: <Widget>[
            Center(
                child: Text(
              "Home",
              style: Theme.of(context).textTheme.display1,
            )),
            CategoryPage(),
            Center(
                child: Text(
              "Reports",
              style: Theme.of(context).textTheme.display1,
            ))
          ],
        ));
  }
}

And the Category widget:

import 'package:expense_manager/models/category_model.dart';
import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {

  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  List<CategoryModel> _lsCateogies = List<CategoryModel>();

  @override
  initState() {
    super.initState();
    _initCategories();
  }

  _initCategories() {
    var cat1 = CategoryModel().rebuild((b) => b
      ..id = 0
      ..title = "Home Utils"
      ..desc = "Home utility related expenses"
      ..iconCodePoint = Icons.home.codePoint);

    _lsCateogies.add(cat1);

    var cat2 = CategoryModel().rebuild((b) => b
      ..id = 0
      ..title = "Grocery"
      ..desc = "Grocery related expenses"
      ..iconCodePoint = Icons.local_grocery_store.codePoint);

    _lsCateogies.add(cat2);

    var cat3 = CategoryModel().rebuild((b) => b
      ..id = 0
      ..title = "Food"
      ..desc = "Food related expenses"
      ..iconCodePoint = Icons.fastfood.codePoint);

    _lsCateogies.add(cat3);

    var cat4 = CategoryModel().rebuild((b) => b
      ..id = 0
      ..title = "Auto"
      ..desc = "Car/Bike related expenses"
      ..iconCodePoint = Icons.directions_bike.codePoint);

    _lsCateogies.add(cat4);
  }

  @override
  Widget build(BuildContext context) {
    return _getCategoryTab();
  }

  Widget _getCategoryTab() {
    return ListView.builder(
      itemCount: _lsCateogies.length,
      itemBuilder: (BuildContext ctxt, int index) {
        var category = _lsCateogies[index];
        return Container(
          decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4.0),
              border: new Border.all(
                  width: 1.0, style: BorderStyle.solid, color: Colors.white)),
          margin: EdgeInsets.all(12.0),
          child: ListTile(
            onTap: () {},
            leading: Icon(
              IconData(category.iconCodePoint, fontFamily: 'MaterialIcons'),
              color: Theme.of(context).accentColor,
            ),
            title: Text(category.title, style: Theme.of(context).textTheme.body2.copyWith(
              color: Theme.of(context).accentColor
            ),),
            subtitle: Text(category.desc, ),
          ),
        );
      },
    );
  }
}

Create New Category

Now that we have organized our code a little, let’s start development again. Add a button to create a new category.

Widget _getCategoryTab() {
    return Column(
      children: <Widget>[
        Container(
          padding: EdgeInsets.all(12.0),
          width: 200.0,
          child: RaisedButton(
            child: Text("Add New"),
            onPressed: () {
              //todo: implement navigation
            },
          ),
        ),
        Expanded(
          child: ListView.builder(
...

We have updated the _getCategoryTab so that it now contains a button which is wrapped alongside the ListView in a Column.

The ListView is wrapped inside the Expanded widget so that it can take up the remaining space in the column.

The “Add New” button should redirect to create category page. To create this page, add a new folder “routes” inside the lib folder and add a file “add_category.dart“.

import 'package:flutter/material.dart';

class AddCategory extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Add New Category"),
      ),
      body: Container(
        padding: EdgeInsets.all(12.0),
        child: Column(
          children: <Widget>[
            TextField(
              decoration: InputDecoration(labelText: "Title"),
            ),
            TextField(
              decoration: InputDecoration(labelText: "Description"),
              maxLines: 2,
            ),
            
            //todo: icon picker
          ],
        ),
      ),
    );
  }
}

The AddCategory widget has a basic layout for now. We will make more changes here as we go along. But first, add a navigation to this page from Category screen.

child: RaisedButton(
            child: Text("Add New"),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => AddCategory()),
              );
            },

In Flutter, there are many options for using navigation and routing. You can learn more about navigation in our Introduction To Navigation And Routing.

Icon Picker Functionality

In Flutter, an icon is created out of IconData. Since we want users to be able to set icons for each category label, we need to create a list of icons to pick from. A user can select icons from the list and assign it to the category.

_showIconGrid() {
    var ls = [
      Icons.web_asset,
      Icons.weekend,
      Icons.whatshot,
      Icons.widgets,
      Icons.wifi,
      Icons.wifi_lock,
      Icons.wifi_tethering,
      Icons.work,
      Icons.wrap_text,
      Icons.youtube_searched_for,
      Icons.zoom_in,
      Icons.zoom_out,
      Icons.zoom_out_map,
      Icons.restaurant_menu,
      Icons.restore,
      Icons.restore_from_trash,
      Icons.restore_page,
      Icons.ring_volume,
      Icons.room,
      Icons.exposure_zero,
      Icons.extension,
      Icons.face,
      Icons.fast_forward,
      Icons.fast_rewind,
      Icons.fastfood,
      Icons.favorite,
      Icons.favorite_border,
    ];

    return GridView.count(
      crossAxisCount: 8,
      children: List.generate(ls.length, (index) {
        var iconData = ls[index];
        return IconButton(
            onPressed: () {
            },
            icon: Icon(
              iconData,
            ));
      }),
    );
  }

Here, we have created a list of random icons to work with. The icons will be shown in a GridView which can arrange the icons in a tile. Since, we want the users to be able to select icon, we have used IconButton.

Add _showIconGrid inside the column:

...
child: Column(
          children: <Widget>[
            TextField(
              decoration: InputDecoration(labelText: "Title"),
            ),
            TextField(
              decoration: InputDecoration(labelText: "Description"),
              maxLines: 2,
            ),
            Expanded(
                child: Container(
                    padding: EdgeInsets.symmetric(vertical: 12.0),
                    child: _showIconGrid()))
          ],
...
Add Category 1
Add Category 1

Wrapping Up

In this part of our Flutter tutorial series, we didn’t do a whole lot. We organized the classes to separate files and folders. Then we created a “Add New Category” screen and implemented an icon picker functionality.

Now in the next part we shall look into making this page functional.