Menu Close

Time Tracker – Flutter

This example is about a time tracking app. The app deals with sql lite, bottom navigation bar and the integrated stopwatch from Flutter. The app is fairly simple and requires intermediate knowledge in the area of flutter and darts!

First of all in the pubspec.yaml, please update the following lines:

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  path: ^1.6.4
  sqflite: ^1.2.0
  path_provider: ^1.5.1

In the lib Folder we have 5 Files which should look like this:

lib/main.dart
lib/database_helpers.dart
lib/screens/tracker_list_screen.dart
lib/screens/tracker_main_screen.dart
lib/screens/tracker_timer_screen.dart

In the main.dart file we use routes to connect and get to the screens:

import 'package:flutter/material.dart';
import 'package:time_tracker/screens/tracker_list_screen.dart';
import 'package:time_tracker/screens/tracker_timer_screen.dart';
import 'screens/tracker_main_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: TrackerMain.id,
      routes: {
        TrackerMain.id: (context) => TrackerMain(),
        TrackerTimer.id: (context) => TrackerTimer(),
        TrackerList.id: (context) => TrackerList(),
      },
    );
  }
}

The core of the app is data processing with sql and the functions, in the database_helpers.dart file:

import 'dart:io';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';

// database table and column names
final String tableTimes = 'times';
final String columnId = '_id';
final String columnTime = 'time';
final String columnDate = 'date';

// data model class
class Time {
  int id;
  String trackedTime;
  String date;

  Time();

  // convenience constructor to create a Time object
  Time.fromMap(Map<String, dynamic> map) {
    id = map[columnId];
    trackedTime = map[columnTime];
    date = map[columnDate];
  }

  // convenience method to create a Map from this Time object
  Map<String, dynamic> toMap() {
    var map = <String, dynamic>{columnTime: trackedTime, columnDate: date};
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }
}

// singleton class to manage the database
class DatabaseHelper {
  // This is the actual database filename that is saved in the docs directory.
  static final _databaseName = "MyDatabase.db";
  // Increment this version when you need to change the schema.
  static final _databaseVersion = 1;

  // Make this a singleton class.
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // Only allow a single open connection to the database.
  static Database _database;
  Future<Database> get database async {
    if (_database != null) return _database;
    _database = await _initDatabase();
    return _database;
  }

  // open the database
  _initDatabase() async {
    // The path_provider plugin gets the right directory for Android or iOS.
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    // Open the database. Can also add an onUpdate callback parameter.
    return await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

  // SQL string to create the database
  Future _onCreate(Database db, int version) async {
    await db.execute('''
              CREATE TABLE $tableTimes (
                $columnId INTEGER PRIMARY KEY,
                $columnTime TEXT NOT NULL,
                $columnDate TEXT NOT NULL
              )
              ''');
  }

  // Database helper methods:

  Future<int> insert(Time time) async {
    Database db = await database;
    int id = await db.insert(tableTimes, time.toMap());
    return id;
  }

  Future<Time> queryTime(int id) async {
    Database db = await database;
    List<Map> maps = await db.query(tableTimes,
        columns: [columnId, columnTime, columnDate],
        where: '$columnId = ?',
        whereArgs: [id]);
    if (maps.length > 0) {
      return Time.fromMap(maps.first);
    }
    return null;
  }

  Future<List> queryAllTimes(String dbTable) async {
    Database db = await database;

    var result = await db.rawQuery("SELECT * FROM $dbTable");

    return result.toList();
  }

  Future<List> deleteTime(dynamic id) async {
    Database db = await this.database;
    db.delete(tableTimes, where: '$columnId = ?', whereArgs: [id]);
    return null;
  }
}

In the tracker_main_screen.dart file we connect the two others screens with a BottomNavigationBar:

import 'package:flutter/material.dart';
import 'package:time_tracker/screens/tracker_list_screen.dart';
import 'package:time_tracker/screens/tracker_timer_screen.dart';

class TrackerMain extends StatefulWidget {
  static const String id = 'trackermain_screen';
  @override
  _TrackerMainState createState() => _TrackerMainState();
}

class _TrackerMainState extends State<TrackerMain> {
  int _selectedPage = 0;
  final _pageOptions = [
    TrackerTimer(),
    TrackerList(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pageOptions[_selectedPage],
      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.green,
        currentIndex: _selectedPage,
        onTap: (int index) {
          setState(() {
            _selectedPage = index;
          });
        },
        items: [
          BottomNavigationBarItem(
            icon: Icon(
              Icons.access_alarm,
              color: Colors.white,
            ),
            title: Text(
              'Time Tracker',
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          BottomNavigationBarItem(
            icon: Icon(
              Icons.list,
              color: Colors.white,
            ),
            title: Text(
              'Your Times',
              style: TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
          )
        ],
      ),
    );
  }
}

Then we go on with the tracker_timer_screen.dart file and insert the stopwatch and a button that transfers the stopped times to the SQL database. We use various functions to transfer the stopped times in the desired format:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:time_tracker/database_helpers.dart';

class TrackerTimer extends StatefulWidget {
  static const String id = 'trackertimer_screen';
  @override
  _TrackerTimerState createState() => _TrackerTimerState();
}

class _TrackerTimerState extends State<TrackerTimer> {
  Stopwatch stopwatch = Stopwatch();

  Time time = Time();

  void resetButtonPressed() {
    setState(() {
      stopwatch.reset();
    });
  }

  void playButtonPressed() {
    setState(() {
      stopwatch.start();
    });
  }

  void stopButtonPressed() {
    setState(() {
      if (stopwatch.isRunning) {
        stopwatch.stop();
      } else {
        stopwatch.start();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.green,
        title: Text('Time Tracking'),
        automaticallyImplyLeading: false,
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: Center(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.only(bottom: 30.0),
                    child: TimerText(
                      stopwatch: stopwatch,
                    ),
                  ),
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      CircleButton(
                          onTap: () {
                            resetButtonPressed();
                          },
                          iconData: Icons.autorenew),
                      SizedBox(
                        width: 30.0,
                      ),
                      CircleButton(
                          onTap: () {
                            stopButtonPressed();
                          },
                          iconData: Icons.pause),
                      SizedBox(
                        width: 30.0,
                      ),
                      CircleButton(
                          onTap: () {
                            playButtonPressed();
                          },
                          iconData: Icons.play_arrow),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 35.0),
                    child: FlatButton(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(15.0),
                      ),
                      child: Padding(
                        padding: const EdgeInsets.all(10.0),
                        child: Text(
                          'Transmit Time',
                          style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                            fontSize: 18.0,
                          ),
                        ),
                      ),
                      color: Colors.green,
                      onPressed: () async {
                        time.trackedTime = TimerTextFormatter.format(
                                stopwatch.elapsedMilliseconds)
                            .toString();
                        time.date = DateTime.now().toString();
                        DatabaseHelper helper = DatabaseHelper.instance;
                        int id = await helper.insert(time);
                        print('inserted row: $id');
                      },
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class CircleButton extends StatelessWidget {
  final GestureTapCallback onTap;
  final IconData iconData;

  const CircleButton({Key key, this.onTap, this.iconData}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    double size = 50.0;

    return InkResponse(
      onTap: onTap,
      child: Container(
        width: size,
        height: size,
        decoration: BoxDecoration(
          color: Colors.green,
          shape: BoxShape.circle,
        ),
        child: Icon(
          iconData,
          color: Colors.white,
        ),
      ),
    );
  }
}

class TimerTextFormatter {
  static String format(int milliseconds) {
    int hundreds = (milliseconds / 10).truncate();
    int seconds = (hundreds / 100).truncate();
    int minutes = (seconds / 60).truncate();
    int hours = (minutes / 60).truncate();

    String hourStr = (hours % 24).toString().padLeft(2, '0');
    String minutesStr = (minutes % 60).toString().padLeft(2, '0');
    String secondsStr = (seconds % 60).toString().padLeft(2, '0');
    String hundredsStr = (hundreds % 100).toString().padLeft(2, '0');

    return "$hourStr:$minutesStr:$secondsStr,$hundredsStr";
  }
}

class TimerText extends StatefulWidget {
  TimerText({this.stopwatch});
  final Stopwatch stopwatch;

  TimerTextState createState() => TimerTextState(stopwatch: stopwatch);
}

class TimerTextState extends State<TimerText> {
  Timer timer;
  final Stopwatch stopwatch;

  TimerTextState({this.stopwatch}) {
    timer = Timer.periodic(Duration(milliseconds: 30), callback);
  }

  void callback(Timer timer) {
    if (stopwatch.isRunning) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    final TextStyle timerTextStyle = const TextStyle(
      fontSize: 50.0,
      fontFamily: "Open Sans",
      color: Colors.green,
      fontWeight: FontWeight.bold,
    );
    String formattedTime =
        TimerTextFormatter.format(stopwatch.elapsedMilliseconds);
    return Text(formattedTime, style: timerTextStyle);
  }
}

Last but not least, we call the stopped times with the time stamp in a ListView and also a button to delete a time. There for we have a tracker_list_screen.dart file:

import 'package:flutter/material.dart';
import 'package:time_tracker/database_helpers.dart';

class TrackerList extends StatefulWidget {
  static const String id = 'trackerlist_screen';
  @override
  _TrackerListState createState() => _TrackerListState();
}

class _TrackerListState extends State<TrackerList> {
  DatabaseHelper helper = DatabaseHelper.instance;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.green,
        title: Text('Your Times'),
        automaticallyImplyLeading: false,
      ),
      body: FutureBuilder<dynamic>(
        future: helper.queryAllTimes('times'),
        initialData: List(),
        builder: (context, snapshot) {
          return snapshot.hasData
              ? ListView.builder(
                  itemCount: snapshot.data.length,
                  itemBuilder: (_, int position) {
                    final item = snapshot.data[position];

                    return Padding(
                      padding: const EdgeInsets.fromLTRB(15, 20, 15, 0),
                      child: Material(
                        borderRadius: BorderRadius.only(
                          topRight: Radius.circular(20),
                          topLeft: Radius.circular(20),
                          bottomLeft: Radius.circular(20.0),
                          bottomRight: Radius.circular(20.0),
                        ),
                        elevation: 5.0,
                        color: Colors.green,
                        child: Padding(
                          padding: EdgeInsets.symmetric(
                            vertical: 10.0,
                            horizontal: 20.0,
                          ),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Row(
                                    children: <Widget>[
                                      Icon(
                                        Icons.timer,
                                        color: Colors.white,
                                        size: 22.0,
                                      ),
                                      SizedBox(width: 10.0),
                                      Text(
                                        item.row[1] ?? ' ',
                                        style: TextStyle(
                                            fontSize: 22.0,
                                            color: Colors.white,
                                            fontWeight: FontWeight.bold),
                                      ),
                                    ],
                                  ),
                                  SizedBox(
                                    height: 10.0,
                                  ),
                                  Row(
                                    children: <Widget>[
                                      Icon(
                                        Icons.today,
                                        color: Colors.white,
                                        size: 16.0,
                                      ),
                                      SizedBox(width: 10.0),
                                      Text(
                                        item.row[2] ?? ' ',
                                        style: TextStyle(
                                          fontSize: 14.0,
                                          color: Colors.white,
                                        ),
                                      ),
                                    ],
                                  ),
                                ],
                              ),
                              IconButton(
                                icon: Icon(Icons.cancel),
                                color: Colors.white,
                                onPressed: () {
                                  setState(() {
                                    helper.deleteTime(item.row[0]);
                                  });
                                },
                              )
                            ],
                          ),
                        ),
                      ),
                    );
                  })
              : Center(
                  child: CircularProgressIndicator(),
                );
        },
      ),
    );
  }
}

You can download the project at Github:

https://github.com/DKoenig82/time_tracker_flutter