Article

Introducing Holden - A Certificate Generator App using Flutter

Last updated 
Dec 24, 2019
 min read

If you’re an event organizer, you know the pain of generating the certificates for participants. What if there was a mobile application that can do this tedious task for you?!

Introducing Holden, easy to use certificate generator app made using Flutter.

Overview

  • The user selects an xlsx file from the device.
  • The app parses xlsx file and displays the names (of participants) in a list.
  • Clicking on an individual name opens the certificate of that participant.
  • The user can select the table from the drop-down.
  • A Certificate can be downloaded, viewed and shared.
  • Multiple Certificates can be shared with others via other applications (Google Drive, WhatsApp, Gmail, etc.) or saved locally on the device.

Prerequisites

  • Flutter SDK
  • Android Studio / VSCode / Intellij Idea / Any Other (IDE of choice)
  • Dart & Flutter Plugins for IDE
  • Emulator or Physical Device
  • Create a new flutter project using the following command
flutter create certificate_generator

Directory Structure

Dependencies

  • Add the following dependencies in pubspec.yaml file and run the following command to retrieve them.
1dependencies:
2  flutter:
3    sdk: flutter
4  cupertino_icons: ^0.1.2
5  spreadsheet_decoder: # parsing xlsx files
6  path_provider: # reading/writing files in application document directory
7  file_picker: # using local files from device
8  pdf_viewer_plugin: # viewing pdf files
9  pdf: # generating pdf file
10  permission_handler: # handling runtime permissions
11  share_extend: # sharing files
12  provider: # state-management
13  printing: # using image assets in pdf generation
flutter packages get

Coding

  • There is a total of 3 screens in the app: HomeScreen, ResultScreen & ViewerScreen.

main.dart

  • Here we have set up routes, providers, runtime permission requests, and theme for the application.
1import 'package:flutter/material.dart';
2
3import 'package:provider/provider.dart';
4import 'package:permission_handler/permission_handler.dart';
5
6import 'package:certificate_generator/providers/home.dart';
7
8import 'package:certificate_generator/screens/home.dart';
9import 'package:certificate_generator/screens/result.dart';
10import 'package:certificate_generator/screens/viewer.dart';
11
12void main() async {
13  runApp(
14    MultiProvider(
15      providers: [
16        ChangeNotifierProvider<HomeProvider>(
17          create: (context) => HomeProvider(),
18        ),
19      ],
20      child: MyApp(),
21    ),
22  );
23  await PermissionHandler().checkPermissionStatus(PermissionGroup.storage);
24}
25
26class MyApp extends StatelessWidget {
27  @override
28  Widget build(BuildContext context) {
29    return MaterialApp(
30      debugShowCheckedModeBanner: false,
31      title: 'Certificate Generator',
32      theme: ThemeData(
33        primarySwatch: Colors.deepPurple,
34        accentColor: Colors.white,
35        appBarTheme: AppBarTheme(
36          iconTheme: IconThemeData(
37            color: Colors.black,
38          ),
39          color: Colors.white,
40          textTheme: TextTheme(
41            title: TextStyle(
42              color: Colors.black,
43              fontWeight: FontWeight.bold,
44              fontSize: 20,
45            ),
46          ),
47        ),
48      ),
49      routes: {
50        '/': (context) => HomeScreen(),
51        '/result': (context) => ResultScreen(),
52        '/viewer': (context) => ViewerScreen(),
53      },
54      initialRoute: '/',
55    );
56  }
57}

HomeScreen

  • Initially, suggestion text is shown to the user when no file is selected.
1import 'dart:io';
2
3import 'package:flutter/material.dart';
4
5import 'package:provider/provider.dart';
6import 'package:spreadsheet_decoder/spreadsheet_decoder.dart';
7import 'package:file_picker/file_picker.dart';
8
9import 'package:certificate_generator/providers/home.dart';
10
11class HomeScreen extends StatelessWidget {
12  @override
13  Widget build(BuildContext context) {
14    return Consumer<HomeProvider>(
15      builder: (_, homeProvider, __) => Scaffold(
16        body: Center(
17          child: homeProvider.xlsxFilePath != null
18              ? ListView(
19                // list of names
20              )
21              : Center(
22                  child: Text(
23                    'Click FAB to read xlsx',
24                    style: TextStyle(color: Colors.white),
25                  ),
26                ),
27        ),
28      ),
29    );
30  }
31}
  • Format of the xlsx should be like this:

  • HomeScreen has a floating action button, clicking on which file picker is shown where the user can select an xlsx file that has names of participants.
1FloatingActionButton(
2  onPressed: () async {
3    homeProvider.xlsxFilePath = await FilePicker.getFilePath(
4      type: FileType.CUSTOM,
5      fileExtension: 'xlsx',
6    );
7  },
8  child: Icon(Icons.attach_file),
9)
  • HomeProvider exposes setter & getters to manipulate the path of user-selected xlsx file, names of tables from user-selected xlsx file, and the name of the currently selected table from user-selected xlsx file.
1import 'package:flutter/foundation.dart';
2
3///
4/// Manages state for screens/HomeScreen.dart
5///
6class HomeProvider with ChangeNotifier {
7  // holds the path of selected xlsx file path
8  String _xlsxFilePath;
9
10  // holds the names of tables of selected xlsx file path
11  Map<String, dynamic> _xlsxFileTables;
12
13  // holds the name of selected table from dropdown in screens/home.dart
14  String _xlsxFileSelectedTable;
15
16  String get xlsxFilePath => this._xlsxFilePath;
17
18  set xlsxFilePath(String _xlsxFilePath) {
19    this._xlsxFilePath = _xlsxFilePath;
20    notifyListeners();
21  }
22
23  Map<String, dynamic> get xlsxFileTables => this._xlsxFileTables;
24
25  set xlsxFileTables(Map<String, dynamic> _xlsxFileTables) {
26    this._xlsxFileTables = _xlsxFileTables;
27    this._xlsxFileSelectedTable = this._xlsxFileTables.keys.first;
28    notifyListeners();
29  }
30
31  String get xlsxFileSelectedTable => this._xlsxFileSelectedTable;
32
33  set xlsxFileSelectedTable(String _xlsxFileSelectedTable) {
34    this._xlsxFileSelectedTable = _xlsxFileSelectedTable;
35    notifyListeners();
36  }
37}
  • Once the file is selected, HomeProvider sets the path of that file using the setter & notifies the listeners. So, the body of HomeScreen now displays the list of names parsed using spreadsheet decoder from the selected xlsx file. We need to pass the result of File(filePath).readAsBytesSync() the inside decodeBytes()method.
  • Clicking on a name shows the certificate of that participant in ResultScreen.
1ListView(
2  padding: EdgeInsets.all(16),
3  children: <Widget>[
4    ...SpreadsheetDecoder.decodeBytes(
5            File(homeProvider.xlsxFilePath).readAsBytesSync())
6        .tables[homeProvider.xlsxFileSelectedTable]
7        .rows
8        .map(
9          (value) => Column(
10            children: <Widget>[
11              Container(
12                decoration: BoxDecoration(
13                  color: Colors.transparent.withOpacity(0.1),
14                  borderRadius: BorderRadius.circular(50),
15                ),
16                child: ListTile(
17                  contentPadding: EdgeInsets.all(16),
18                  onTap: () {
19                    Navigator.pushNamed(context, '/result',
20                        arguments: {'result': value});
21                  },
22                  leading: Container(
23                    width: 40,
24                    height: 40,
25                    decoration: BoxDecoration(
26                      shape: BoxShape.circle,
27                      color: Colors.deepPurple[200],
28                    ),
29                    child: Icon(
30                      Icons.person,
31                      color: Colors.deepPurple[100],
32                      size: 30,
33                    ),
34                  ),
35                  title: Text(
36                    value[0],
37                    style: TextStyle(
38                      color: Colors.white70,
39                      fontWeight: FontWeight.w700,
40                    ),
41                  ),
42                ),
43              ),
44              SizedBox(
45                height: 24,
46              ),
47            ],
48          ),
49        ),
50  ],
51)
  • Now, in the app-bar a drop-down gets visible. It displays names all tables from xlsx file and choosing an option displays names from that table.
1AppBar(
2  title: Text('Holden'),
3  elevation: 0,
4  shape: RoundedRectangleBorder(
5    borderRadius: BorderRadius.only(
6      bottomRight: Radius.circular(50),
7      bottomLeft: Radius.circular(50),
8    ),
9  ),
10  centerTitle: true,
11  bottom: homeProvider.xlsxFilePath != null
12      ? PreferredSize(
13          preferredSize: Size.fromHeight(60),
14          child: Container(
15            margin: EdgeInsets.symmetric(horizontal: 60),
16            child: DropdownButtonFormField(
17              value: homeProvider.xlsxFileSelectedTable,
18              onChanged: (newXlsxFileSelectedTable) {
19                homeProvider.xlsxFileSelectedTable =
20                    newXlsxFileSelectedTable;
21              },
22              items: [
23                ...homeProvider.xlsxFileTables.keys.map(
24                  (table) => DropdownMenuItem(
25                    child: Text(table),
26                    value: table,
27                  ),
28                ),
29              ],
30            ),
31          ),
32        )
33      : PreferredSize(
34          child: Container(),
35          preferredSize: Size.fromHeight(0),
36        ),
37)
  • A second FAB with share icon gets visible, clicking on which shows the share dialog to share PDFs of all the participants from the selected table.
1FloatingActionButton(
2  heroTag: 'Share',
3  onPressed: () async {
4    final names = homeProvider
5        .xlsxFileTables[homeProvider.xlsxFileSelectedTable]
6        .rows
7        .map((name) => name
8            .toString()
9            .substring(1, name.toString().length - 1));
10    names.forEach((name) => pdfGenerator(name));
11    final String downloadPath =
12        await getApplicationDocumentsDirectoryPath();
13    final files = names
14        .map((name) => File('$downloadPath/$name.pdf').path);
15    await ShareExtend.shareMultiple([
16      ...files,
17    ], 'file');
18  },
19  child: Icon(Icons.share),
20)

HomeScreen Flow

ResultScreen

  • It displays the certificate of the individual participant.
1import 'dart:io';
2
3import 'package:flutter/material.dart';
4
5import 'package:share_extend/share_extend.dart';
6
7import 'package:certificate_generator/utils/commons.dart';
8
9class ResultScreen extends StatelessWidget {
10  @override
11  Widget build(BuildContext context) {
12    String name = (ModalRoute.of(context).settings.arguments
13        as Map<String, dynamic>)['result'][0];
14    return Scaffold(
15      backgroundColor: Colors.deepPurple[400],
16      appBar: AppBar(
17        shape: RoundedRectangleBorder(
18          borderRadius: BorderRadius.only(
19            bottomRight: Radius.circular(50),
20            bottomLeft: Radius.circular(50),
21          ),
22        ),
23        title: Container(
24          child: Text('Result'),
25        ),
26        centerTitle: true,
27        bottom: PreferredSize(
28          preferredSize: Size.fromHeight(64),
29          child: Column(
30            children: <Widget>[
31              Row(
32                mainAxisAlignment: MainAxisAlignment.center,
33                children: <Widget>[
34                  Container(
35                    width: 50,
36                    height: 50,
37                    decoration: BoxDecoration(
38                      shape: BoxShape.circle,
39                      color: Colors.deepPurple[200],
40                    ),
41                    child: Icon(
42                      Icons.person,
43                      color: Colors.deepPurple[100],
44                      size: 35,
45                    ),
46                  ),
47                  SizedBox(
48                    width: 20,
49                  ),
50                  Text(
51                    name,
52                    style: TextStyle(
53                      fontWeight: FontWeight.w700,
54                    ),
55                  )
56                ],
57              ),
58              SizedBox(
59                height: 10,
60              ),
61            ],
62          ),
63        ),
64      ),
65      body: Builder(
66        builder: (context) => SingleChildScrollView(
67          child: Container(
68            margin: EdgeInsets.all(16),
69            color: Colors.deepPurple[50],
70            child: Column(
71              crossAxisAlignment: CrossAxisAlignment.center,
72              children: <Widget>[
73                SizedBox(
74                  height: 50,
75                ),
76                Container(
77                  width: 150,
78                  height: 150,
79                  decoration: BoxDecoration(
80                    shape: BoxShape.circle,
81                    color: Colors.deepPurple[200],
82                  ),
83                  child: Icon(
84                    Icons.person,
85                    color: Colors.deepPurple[100],
86                    size: 100,
87                  ),
88                ),
89                SizedBox(
90                  height: 50,
91                ),
92                Text(
93                  'certificate of completion',
94                  style: TextStyle(
95                    fontSize: 22,
96                    color: Colors.grey[700],
97                  ),
98                ),
99                SizedBox(
100                  height: 20,
101                ),
102                Text(
103                  'presented to:',
104                  style: TextStyle(
105                    color: Colors.grey[600],
106                  ),
107                ),
108                SizedBox(
109                  height: 30,
110                ),
111                Text(
112                  name,
113                  style: TextStyle(
114                    fontSize: 24,
115                    fontWeight: FontWeight.bold,
116                    color: Colors.grey[800],
117                  ),
118                ),
119                SizedBox(
120                  height: 50,
121                ),
122                Row(
123                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
124                  children: <Widget>[
125                    // Actions to either download, view or share certificate goes here.
126                  ],
127                ),
128                SizedBox(
129                  height: 50,
130                )
131              ],
132            ),
133          ),
134        ),
135      ),
136    );
137  }
138}
  • The certificate has 3 actions: Download, View, and Share.
1Row(
2  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
3  children: <Widget>[
4    FlatButton(
5      onPressed: () {
6        // Download
7      },
8      child: Icon(Icons.file_download),
9    ),
10    FlatButton(
11      onPressed: () {
12        // View
13      },
14      child: Icon(Icons.open_in_new),
15    ),
16    FlatButton(
17      onPressed: () {
18        // Share
19      },
20      child: Icon(Icons.share),
21    ),
22  ],
23)
  • When the user clicks on the download button PDF is generated with the help of pdf package. Snackbar is shown when the certificate is generated which also contains an action to quickly view the certificate.
1FlatButton(
2  onPressed: () {
3    pdfGenerator(name);
4    Scaffold.of(context).showSnackBar(
5      SnackBar(
6        content: Text('$name.pdf downloaded'),
7        action: SnackBarAction(
8          label: 'View',
9          onPressed: () async {
10            String downloadPath =
11                await getApplicationDocumentsDirectoryPath();
12            Navigator.pushNamed(context, '/viewer',
13                arguments: {
14                  'view': '$downloadPath/$name.pdf'
15                });
16          },
17        ),
18      ),
19    );
20  },
21  child: Icon(Icons.file_download),
22)
  • The code for PDF generation is extracted into a separate method. Once the pdf is generated it is saved on the user’s device inside the application documents directory. PDF package provides widgets to build UI for PDF and its widget system is very similar to Flutter’s widget system. So, it’s easy to replicate certificate UI for PDF.
1import 'dart:io';
2
3import 'package:flutter/material.dart';
4
5import 'package:pdf/pdf.dart';
6import 'package:pdf/widgets.dart' as pdf;
7import 'package:printing/printing.dart';
8import 'package:path_provider/path_provider.dart';
9
10Future<void> pdfGenerator(name) async {
11  final _pdf = pdf.Document();
12  final _assetImage = await pdfImageFromImageProvider(
13    pdf: _pdf.document,
14    image: AssetImage(
15      'assets/images/account.png',
16    ),
17  );
18  _pdf.addPage(
19    pdf.Page(
20      pageFormat: PdfPageFormat.a4,
21      build: (context) => pdf.Center(
22        child: pdf.Container(
23          margin: pdf.EdgeInsets.all(16),
24          width: double.maxFinite,
25          color: PdfColors.deepPurple50,
26          child: pdf.Column(
27            mainAxisAlignment: pdf.MainAxisAlignment.center,
28            crossAxisAlignment: pdf.CrossAxisAlignment.center,
29            children: [
30              pdf.SizedBox(
31                height: 50,
32              ),
33              pdf.Container(
34                width: 160,
35                height: 160,
36                decoration: pdf.BoxDecoration(
37                  shape: pdf.BoxShape.circle,
38                  color: PdfColors.deepPurple200,
39                ),
40                child: pdf.Image(_assetImage),
41              ),
42              pdf.SizedBox(
43                height: 50,
44              ),
45              pdf.Text(
46                'certificate of completion',
47                style: pdf.TextStyle(
48                  fontSize: 22,
49                  color: PdfColors.grey700,
50                ),
51              ),
52              pdf.SizedBox(
53                height: 20,
54              ),
55              pdf.Text(
56                'presented to:',
57                style: pdf.TextStyle(
58                  color: PdfColors.grey600,
59                ),
60              ),
61              pdf.SizedBox(
62                height: 30,
63              ),
64              pdf.Text(
65                name,
66                style: pdf.TextStyle(
67                  fontSize: 24,
68                  fontWeight: pdf.FontWeight.bold,
69                  color: PdfColors.grey800,
70                ),
71              ),
72            ],
73          ),
74        ),
75      ),
76    ),
77  );
78  var path = await getApplicationDocumentsDirectory();
79  File('${path.path}/$name.pdf').writeAsBytesSync(_pdf.save());
80}
81
82Future<String> getApplicationDocumentsDirectoryPath() async {
83  final Directory applicationDocumentsDirectory =
84      await getApplicationDocumentsDirectory();
85  return applicationDocumentsDirectory.path;
86}
  • When the user clicks on the view button, the downloaded certificate is opened in ViewerScreen. If the certificate doesn’t exist then the “Not Found” message is shown in SnackBar with download action to quickly download the certificate.
1FlatButton(
2  onPressed: () async {
3    String downloadPath =
4        await getApplicationDocumentsDirectoryPath();
5    if (File('$downloadPath/$name.pdf').existsSync()) {
6      Navigator.pushNamed(context, '/viewer',
7          arguments: {'view': '$downloadPath/$name.pdf'});
8    } else {
9      Scaffold.of(context).showSnackBar(
10        SnackBar(
11          content: Text('$name.pdf File Not Found'),
12          action: SnackBarAction(
13            label: 'Download',
14            onPressed: () {
15              pdfGenerator(name);
16              Scaffold.of(context).showSnackBar(
17                SnackBar(
18                  content: Text('$name.pdf downloaded'),
19                  action: SnackBarAction(
20                    label: 'View',
21                    onPressed: () async {
22                      String downloadPath =
23                          await getApplicationDocumentsDirectoryPath();
24                      Navigator.pushNamed(
25                          context, '/viewer', arguments: {
26                        'view': '$downloadPath/$name.pdf'
27                      });
28                    },
29                  ),
30                ),
31              );
32            },
33          ),
34        ),
35      );
36    }
37  },
38  child: Icon(Icons.open_in_new),
39)
  • When the user clicks on the share button then share dialog is shown with the help of share_extend plugin to the user so the certificate can be shared with others via other apps. If the certificate doesn’t exist then the “Not Found” message is shown in SnackBar with download action to quickly download the certificate.
1FlatButton(
2  onPressed: () async {
3    String downloadPath =
4        await getApplicationDocumentsDirectoryPath();
5    if (File('$downloadPath/$name.pdf').existsSync()) {
6      ShareExtend.share(
7          File('$downloadPath/$name.pdf').path, 'file');
8    } else {
9      Scaffold.of(context).showSnackBar(
10        SnackBar(
11          content: Text('$name.pdf File Not Found'),
12          action: SnackBarAction(
13            label: 'Download',
14            onPressed: () {
15              pdfGenerator(name);
16              Scaffold.of(context).showSnackBar(
17                SnackBar(
18                  content: Text('$name.pdf downloaded'),
19                  action: SnackBarAction(
20                    label: 'View',
21                    onPressed: () async {
22                      String downloadPath =
23                          await getApplicationDocumentsDirectoryPath();
24                      Navigator.pushNamed(
25                          context, '/viewer', arguments: {
26                        'view': '$downloadPath/$name.pdf'
27                      });
28                    },
29                  ),
30                ),
31              );
32            },
33          ),
34        ),
35      );
36    }
37  },
38  child: Icon(Icons.share),
39)

ResultScreen Flow

ViewerScreen

1import 'package:flutter/material.dart';
2
3import 'package:pdf_viewer_plugin/pdf_viewer_plugin.dart';
4
5class ViewerScreen extends StatelessWidget {
6  @override
7  Widget build(BuildContext context) {
8    Map<String, dynamic> view = ModalRoute.of(context).settings.arguments;
9    return Scaffold(
10      appBar: AppBar(
11        title: Text('Certificate'),
12      ),
13      body: PdfViewer(
14        filePath: view['view'],
15      ),
16    );
17  }
18}

ViewerScreen Flow

This is a WIP project. We’re planning to add support for more file formats, the ability to use a URL of spreadsheets for simplicity, and certificate designer for customization.

You can check out the source code of the app here. Feel free to contribute to the project by creating issues or sending pull-requests.

That’s it for this one. Thank you for reading this

Add this to the end - Also Read: Creating a Flutter-based Code Screenshot Generator (Platypus)

Authors

Tirth Patel

Software Engineer
I'm a Flutter App Developer at Aubergine Solutions Pvt. Ltd. I'm passionate about mobile application development &amp; open-source technologies. My expertise includes Dart, Flutter, Python, Git, Firebase, SQLite, Java. This year I was nominated as a recommended candidate to attend Google I/O 2019 conference from the Google India Scholars community. (Google India Challenge Scholarship 2018) I've delivered several talks in local communities on topics ranging from Functional Programming in Dart to Getting Started with Flutter.

Tags

No items found.

Have a project in mind?

Read