Implementing a lotto retrospective app

Implementing a lotto retrospective app

Sometimes I wonder, if I would have won a lot of money if I had played lotto. Being a rational being I have never done that. But the curious side of me wants to know, just in case, if it was a mistake to make such a decision based on mathematical probability.

In this tutorial, we are going to implement an app that lets the user choose his lucky numbers and tells him whether he would have won anything if he had played with these numbers every week in the UK lotto.

We are going to do that in four steps:

  • Build the UI
  • Implement the BLoC
  • Implement the service that encapsulates the data
  • Gather the lotto data

By using the BLoC pattern, we can clearly separate the steps, which also makes an explanation much easier. Since there is no dependency between the UI and the BLoC, we could change the order of the steps the way we wanted.

Build the UI

The central element of our UI is the one the user can interact with: that’s the “lottery ticket” on which the user is able to cross the numbers which he wants to test regarding the prize he would have won.

 1import 'package:flutter/material.dart';
 2import 'package:flutter/widgets.dart';
 3
 4
 5class LottoInput extends StatelessWidget {
 6  @override
 7  Widget build(BuildContext context) {
 8    return GridView.builder(
 9      padding: EdgeInsets.all(2),
10      physics: NeverScrollableScrollPhysics(),
11      shrinkWrap: true,
12      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
13        crossAxisCount: 9,
14        crossAxisSpacing: 4.0,
15        mainAxisSpacing: 4.0,
16      ),
17      itemCount: 59,
18      itemBuilder: (context, index) {
19        return Container(
20          decoration: BoxDecoration(
21            border: Border.all(
22              color: Colors.green,
23              width: 2
24            ),
25            shape: BoxShape.circle,
26          ),
27          child: Center(
28              child: Text(
29                (index + 1).toString(),
30                style: TextStyle(
31                    fontSize: 16,
32                    color: Colors.green,
33                    fontWeight: FontWeight.bold
34                ),
35              )
36          ),
37        );
38      },
39    );
40  }
41}

In the UK, there is the 6/59 format which means that you cross 6 numbers of a set of numbers from 1 to 59. These numbers are compared with the current winning numbers. Depending on what’s the intersection of both sets, you get a different prize (or none).

So using a GridView, we build this number block. We want it to have a fixed amount of rows (9 in this case, defined by crossAxisCount) and spacing of 4 pixels in between. We achieve this by using a SliverGridDelegateWithFixedCrossAxisCount as gridDelegate.

The index starts with 0, that’s why we display (index + 1).toString().

Flutter lotto static GridView
A GridView displaying the possible numbers

While that’s a great start, we have no interactivity yet. That’s okay, because we are going to implement that once the BLoC is ready. But what we can do already, is preparing the UI for the time when there is a state that the view receives from the BLoC.

1class LottoInput extends StatelessWidget {
2  LottoInput({
3    this.crossed = const []
4  });
5
6  final List<int> crossed;
7  ...
8}

First we add a constructor parameter determining the numbers that were chosen. We call this array crossed.

 1class LottoInput extends StatelessWidget {
 2  ...
 3  Stack _getNumber(int index) {
 4    return Stack(
 5      children: [
 6        Center(
 7          child: Text(
 8            '${index + 1}',
 9            style: TextStyle(
10              fontSize: 16,
11              color: Colors.green,
12              fontWeight: FontWeight.bold
13            ),
14          )
15        ),
16        Center(
17          child: CustomPaint(
18            painter: CrossPainter(),
19            size: crossed.contains(index + 1) ? Size.infinite : Size.zero
20          )
21        )
22      ]
23    );
24  }
25}
26
27class CrossPainter extends CustomPainter {
28  CrossPainter({
29    this.strokeWidth = 4,
30    this.color = Colors.blueGrey
31  });
32
33  int strokeWidth;
34  Color color;
35
36  @override
37  void paint(Canvas canvas, Size size) {
38    _drawCrosshair(canvas, size);
39  }
40
41  void _drawCrosshair(Canvas canvas, Size size) {
42    Paint crossPaint = Paint()
43      ..strokeWidth = strokeWidth / 2
44      ..color = color;
45
46    double crossSize = size.longestSide / 1.8;
47    canvas.drawLine(
48      size.center(Offset(-crossSize, -crossSize)),
49      size.center(Offset(crossSize, crossSize)),
50      crossPaint
51    );
52
53    canvas.drawLine(
54      size.center(Offset(crossSize, -crossSize)),
55      size.center(Offset(-crossSize, crossSize)),
56      crossPaint
57    );
58  }
59
60  @override
61  bool shouldRepaint(covariant CustomPainter oldDelegate) {
62    return true;
63  }
64}

Then we use this list to decide whether a cross should be display on top of the respective number. The cross itself is drawn by using a CustomPainter. We use the center of the drawn number to calculate the measurements of the cross and overdraw it a bit.

To make it only appear when the number is provided, we set the size of the CustomPaint to 0 when the crossed list does not contain the current number.

If we now provide a crossed list that actually contains some numbers to the widget, we get something like this:

Flutter lotto static GridView crossed
Our GridView with crossed numbers

Now it’s time to embed this input widget into another widget that looks better and contains some instructions for the user:

 1import 'package:flutter_lotto_retrospection/views/lotto_details.dart';
 2import 'package:flutter/material.dart';
 3
 4import 'lotto_input.dart';
 5
 6class Home extends StatefulWidget {
 7  @override
 8  _HomeState createState() => _HomeState();
 9}
10
11class _HomeState extends State<Home> {
12  @override
13  Widget build(BuildContext context) {
14    return Scaffold(
15        backgroundColor: Colors.green,
16        body: SingleChildScrollView(
17          padding: EdgeInsets.all(24),
18          child: Column(
19            mainAxisSize: MainAxisSize.max,
20            children: <Widget>[
21              Image.asset('assets/clover_white.png', width: 80),
22              SizedBox(height: 24),
23              Text(
24                'Lotto retrospective',
25                style: TextStyle(
26                  color: Colors.white,
27                  shadows: [
28                    Shadow(
29                      color: Colors.black26,
30                      offset: Offset(1.5, 1.5),
31                      blurRadius: 10
32                    )
33                  ],
34                  fontSize: 26
35                )
36              ),
37              SizedBox(height: 24),
38              Text(
39                'How would your numbers have performed in 2020?',
40                style: TextStyle(
41                    fontSize: 18
42                ),
43                textAlign: TextAlign.center,
44              ),
45              SizedBox(height: 24),
46              _getLottoNumbersInput(),
47            ],
48          )
49        )
50    );
51  }
52
53  Container _getLoadingIndicator() {
54    return Container(
55      child: Center(
56        child: Text('Loading ...')
57      ),
58    );
59  }
60
61  Container _getLottoNumbersInput() {
62    return Container(
63      decoration: BoxDecoration(
64        color: Colors.white,
65        borderRadius: BorderRadius.all(Radius.circular(32)),
66        boxShadow: [
67          BoxShadow(
68            offset: Offset(2,2),
69            blurRadius: 6,
70            spreadRadius: 1,
71            color: Colors.black26
72          )
73        ]
74      ),
75      padding: EdgeInsets.all(24),
76      alignment: Alignment.center,
77      child: Column(
78        children: [
79          Text(
80            'Choose your lucky numbers:',
81            style: TextStyle(
82              fontSize: 18,
83              color: Colors.black87
84            )
85          ),
86          SizedBox(height: 16),
87          SizedBox(
88            width: 300,
89            child: LottoInput()
90          )
91        ],
92      ),
93    );
94  }
95}

To make it look appealing, we choose a green background and add a clover as a symbol for luck. We also add some spacings and paddings as well as an explanatory text.

To make every text appear in white without further configuration, we set these values in the MaterialApp‘s ThemeData, too:

 1class LottoRetrospective extends StatelessWidget {
 2  @override
 3  Widget build(BuildContext context) {
 4    return MaterialApp(
 5      debugShowCheckedModeBanner: false,
 6      title: 'Flutter Demo',
 7      theme: ThemeData(
 8        primarySwatch: Colors.green,
 9        visualDensity: VisualDensity.adaptivePlatformDensity,
10        textTheme: TextTheme(
11          bodyText1: TextStyle(
12            color: Colors.white
13          ),
14          bodyText2: TextStyle(
15            color: Colors.white
16          ),
17          caption: TextStyle(
18            color: Colors.white
19          ),
20        ),
21        accentColor: Colors.white,
22        backgroundColor: Colors.green
23      ),
24      home: Scaffold(
25        body: SafeArea(
26          child: Home(),
27        ),
28      ),
29    );
30  }
31}

This way, we don’t always have to set the color explicitly to white when we add a text. Also, the borders of OutlineButtons are automatically in the correct color.

Flutter lotto static GridView embedded
The lotto input embedded into a view with instructions

Okay, now we have an app in which we have the numbers from 1 to 59. We’re still lacking actual interactivity, though. We’re still working with a static list of crossed numbers. That’s where the BLoC pattern comes into play.

Implement the BLoC

We’ll be using the BLoC from the flutter bloc library. This is going to save us a lot of work. The concept of this pattern itself is not that complicated. But we don’t need to reinvent the wheel. Actually, there are even plugins for the IDEs to automatically create the bloc class, events and states.

There are three parts we need to implement:

  • The BLoC: That’s where the actual business logic resides
  • The events: They represent an action triggered by the user inside of the view (e. g. tapping a button)
  • The states: This is what the BLoC sends back to the UI after it has processed an event

Let’s start with a BLoC that contains no logic:

 1import 'dart:async';
 2
 3import 'package:meta/meta.dart';
 4import 'package:bloc/bloc.dart';
 5import 'package:equatable/equatable.dart';
 6
 7part 'lotto_event.dart';
 8part 'lotto_state.dart';
 9
10class LottoBloc extends Bloc<LottoEvent, LottoState> {
11  @override
12  Stream<LottoState> mapEventToState(
13    LottoEvent event,
14  ) async* {}
15}

Okay, like it was mentioned above, the BLoC receives events and turns them into states. Let’s continue implementing the events.

What kind of events do we have? Actually, there are only two user interactions that trigger some business logic:

  • Starting the app
  • Tapping a number

Starting the app would load the historical lotto data whilst displaying a loading indicator. Tapping a number would select or deselect a number which makes it part of the “lucky numbers” or removes it from the selection.

 1part of 'lotto_bloc.dart';
 2
 3abstract class LottoEvent extends Equatable {
 4  const LottoEvent();
 5}
 6
 7class Initialize extends LottoEvent {
 8  @override
 9  List<Object> get props => [];
10}
11
12class CrossNumber extends LottoEvent {
13  CrossNumber({
14    this.number
15  });
16
17  final int number;
18
19  @override
20  List<Object> get props => [number];
21}

The Initialize event has no properties, while the CrossNumber event takes the number that was just tapped.

Last but not least, we’re creating the LottoState:

 1part of 'lotto_bloc.dart';
 2
 3class LottoState extends Equatable {
 4  LottoState({
 5    @required this.currentNumbers,
 6    @required this.searchResult,
 7    @required this.initialized
 8  });
 9
10  final List<int> currentNumbers;
11  final LottoHistorySearchResult searchResult;
12  final bool initialized;
13
14  LottoState copyWith(List<int> currentNumbers, LottoHistorySearchResult searchResult, bool initialized) {
15    return new LottoState(
16      currentNumbers: currentNumbers,
17      searchResult: searchResult,
18      initialized: initialized
19    );
20  }
21
22  @override
23  List<Object> get props => [currentNumbers, searchResult, initialized];
24}
25
26class LottoInitial extends LottoState {
27  LottoInitial(): super(
28    currentNumbers: [],
29    searchResult: LottoHistorySearchResult(),
30    initialized: false
31  );
32}
33
34class LottoHistorySearchResult extends Equatable {
35  final int match6count;
36  final int match5withBonusCount;
37  final int match5count;
38  final int match4count;
39  final int match3count;
40  final int match2count;
41  final int prizeSum;
42  final int costs;
43  final int gameCount;
44
45  LottoHistorySearchResult({
46    this.match6count = 0,
47    this.match5withBonusCount = 0,
48    this.match5count = 0,
49    this.match4count = 0,
50    this.match3count = 0,
51    this.match2count = 0,
52    this.prizeSum = 0,
53    this.costs = 0,
54    this.gameCount
55  });
56
57  @override
58  List<Object> get props => [
59    match6count,
60    match5withBonusCount,
61    match5count,
62    match4count,
63    match3count,
64    match2count,
65    prizeSum,
66    gameCount
67  ];
68}

The lotto state holds currentNumbers which is the list of numbers the user has chose in the UI, initialized, which is initially false and becomes true when the Initialize event was processed and emitted a state and searchResult being a model that holds all the information that is relevant for the user after choosing the numbers.

What information do we want to display for the user?

The most important is prizeSum. This is the total sum of prizes he won with the chosen numbers. But the user may wants to see further details like: “How do my prizes add up?”. That’s why we also want to show the amount of different matches (6, 5, 5 with bonus, 4, 3, 2). Also, the total numbers of games played could be interesting.

Implementing a service

For the BLoC to actually perform some business logic, we better extract the main business functionality into a separate service. That’s the service that fetches the historical lotto data and returns well-formed data. Later on, this will be real data, but yet we can just return some mock data.

 1import 'package:meta/meta.dart';
 2
 3class LottoHistoryService {
 4  List<LottoResult> _cachedList;
 5
 6  Future<List<LottoResult>> getHistoricalResults() async {
 7    return [
 8      LottoResult(
 9        prizes: Prizes(
10          match6: Prize(amount: 7000000),
11          match5plusBonus: Prize(amount: 1000000),
12          match5: Prize(amount: 1000000),
13          match4: Prize(amount: 1750),
14          match3: Prize(amount: 140),
15          match2: Prize(amount: 0),
16        ),
17        dateTime: DateTime.parse('2020-10-14'),
18        correctNumbers: CorrectNumbers(
19          numbers: [1, 14, 33, 44, 47, 56],
20          bonus: 6
21        )
22      )
23    ];
24  }
25}
26
27class LottoResult {
28  Prizes prizes;
29  DateTime dateTime;
30  CorrectNumbers correctNumbers;
31
32  LottoResult({
33    @required this.prizes,
34    @required this.dateTime,
35    @required this.correctNumbers
36  });
37}
38
39class Prizes {
40  Prize match6;
41  Prize match5plusBonus;
42  Prize match5;
43  Prize match4;
44  Prize match3;
45  Prize match2;
46
47  Prizes({
48    @required this.match6,
49    @required this.match5plusBonus,
50    @required this.match5,
51    @required this.match4,
52    @required this.match3,
53    @required this.match2,
54  });
55}
56
57class Prize {
58  int amount;
59  int winners;
60
61  Prize({
62    @required this.amount,
63    this.winners = 0
64  });
65}
66
67class CorrectNumbers {
68  @required List<int> numbers;
69  @required int bonus;
70
71  CorrectNumbers({
72    this.numbers,
73    this.bonus
74  });
75}

We have created models for all of our data. This is much cleaner than working with Maps and Lists because we can directly access the properties and ensure they are set. A LottoResult represents one lotto game. It has a date, a list of prizes and of course the correct numbers. Our only public method getHistoricalResults() returns a List of these.

Putting things together

Now the BLoC needs to have this service injected and call it during initialization.

 1class LottoBloc extends Bloc<LottoEvent, LottoState> {
 2  LottoBloc({
 3    @required this.lottoHistoryService
 4  })  : super(LottoInitial());
 5
 6  final LottoHistoryService lottoHistoryService;
 7  List<LottoResult> _lottoHistoryResults = [];
 8
 9  @override
10  Stream<LottoState> mapEventToState(
11    LottoEvent event,
12  ) async* {
13    if (event is Initialize) {
14      yield await _mapInitializeToState();
15    }
16  }
17
18  Future<LottoState> _mapInitializeToState() async {
19    _lottoHistoryResults = await lottoHistoryService.getHistoricalResults();
20
21    return LottoState(
22      currentNumbers: state.currentNumbers,
23      searchResult: state.searchResult,
24      initialized: true
25    );
26  }
27}

So once the Initialize event is received, the historic lotto results are fetched from the service. Its execution is awaited and afterwards a state is emitted with initialized set to true. The purpose is that we can display a loading indicator as long as the initialization takes place which we dismiss once there is a LottoState with initialized set to true.

Now we can properly react on the NumberCrossed event because we now have data we can work on:

 1Stream<LottoState> _mapCrossNumberToState(CrossNumber event) async* {
 2    List<int> newNumbersList = List.from(state.currentNumbers);
 3
 4    if (state.currentNumbers.contains(event.number)) {
 5      newNumbersList.remove(event.number);
 6
 7      yield state.copyWith(newNumbersList, LottoHistorySearchResult(), true);
 8      return;
 9    }
10
11    if (newNumbersList.length >= 6) {
12      return;
13    }
14
15    newNumbersList.add(event.number);
16
17    yield state.copyWith(newNumbersList, state.searchResult, true);
18
19    LottoHistorySearchResult newResult = _calculateSearchResult(newNumbersList, _lottoHistoryResults/*await lottoHistoryService.getHistoricalResults()*/);
20
21    yield state.copyWith(newNumbersList, newResult, true);
22  }
23
24  LottoHistorySearchResult _calculateSearchResult(List<int> numbers, List<LottoResult> results) {
25    Set<int> numbersAsSet = Set.of(numbers);
26    int match6count = 0;
27    int match5withBonusCount = 0;
28    int match5count = 0;
29    int match4count = 0;
30    int match3count = 0;
31    int match2count = 0;
32    int prizeSum = 0;
33    int gameCount = results.length;
34
35    for(LottoResult result in results) {
36      Set winningNumbers = Set.of(result.correctNumbers.numbers);
37      int correctNumbersCount;
38
39      correctNumbersCount = numbersAsSet.intersection(winningNumbers).length;
40
41      if (correctNumbersCount == 6) {
42        prizeSum += result.prizes.match6.amount;
43
44        match6count += 1;
45      }
46
47      if (correctNumbersCount == 5) {
48        if (state.currentNumbers.contains(result.correctNumbers.bonus)) {
49          match5withBonusCount += 1;
50        }
51        else {
52          prizeSum += result.prizes.match5.amount;
53          match5count += 1;
54        }
55      }
56
57      else if (correctNumbersCount == 4) {
58        prizeSum += result.prizes.match4.amount;
59        match4count += 1;
60      }
61
62      else if (correctNumbersCount == 3) {
63        prizeSum += result.prizes.match3.amount;
64        match3count += 1;
65      }
66
67      else if (correctNumbersCount == 2) {
68        prizeSum += result.prizes.match2.amount;
69        match2count += 1;
70      }
71    }
72
73    return LottoHistorySearchResult(
74      match6count: match6count,
75      match5withBonusCount: match5withBonusCount,
76      match5count: match5count,
77      match4count: match4count,
78      match3count: match3count,
79      match2count: match2count,
80      prizeSum: prizeSum,
81      costs: results.length * 2,
82      gameCount: gameCount
83    );
84  }

When the number that is attached to the NumberPressed event, is already present in the currentNumbers of the current state, it will be removed. That’s because we want the user to be able toe deselect numbers when tapping a number that is selected.

If there are already 6 numbers selected, nothing should happen.

Otherwise, the new LottoHistorySearchResult should be calculated. In the body of this method we just count the matches and prize sum. We calculate the costs by multiplyling the total number of games by 2, assuming a lottery ticket is 2 £.

Okay what do we have so far?

  • A UI displaying static content
  • A BLoC that is capable of handling events from the UI and emitting states
  • A service that provides (yet static) lotto data

Next step is to make the UI actually use the bloc, send events to it and react to state changes:

 1home: BlocProvider(
 2        create: (BuildContext context) {
 3          return LottoBloc(
 4            lottoHistoryService: LottoHistoryService()
 5          );
 6        },
 7        child: Scaffold(
 8          body: SafeArea(
 9            child: Home(),
10          ),
11        )
12      ),

First, we wrap the Home widget with a BlocProvider. That’s necessary for us to be able to use context.bloc<LottoBloc>() to access the bloc from within our widget.

 1class _HomeState extends State<Home> {
 2  LottoBloc bloc;
 3  final numberFormat = new NumberFormat("#,##0", "en_GB");
 4
 5  @override
 6  void didChangeDependencies() {
 7    bloc = context.read();
 8    super.didChangeDependencies();
 9  }
10
11  @override
12  void initState() {
13    WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
14      bloc.add(Initialize());
15    });
16
17    super.initState();
18  }
19
20  @override
21  void dispose() {
22    bloc.close();
23    super.dispose();
24  }
25
26  @override
27  Widget build(BuildContext context) {
28    return BlocBuilder<LottoBloc, LottoState>(
29      builder: (BuildContext context, LottoState state) {
30        return Scaffold(
31          backgroundColor: Colors.green,
32          body: _getBody(state)
33        );
34      },
35    );
36  }
37
38  Widget _getBody(LottoState state) {
39    if (!state.initialized) {
40      return _getLoadingIndicator();
41    }
42
43    return _getMainWidget(state);
44  }

Next we make the LottoBloc a member variable and assign it in the didChangeDependencies() method. After the first rendered frame, we add the Initialize event to our bloc.

In our widget hierarchy, we react on the state being initialized. If it’s not, we show a loading indicator, otherwise we show the usual content.

1Container _getLoadingIndicator() {
2    return Container(
3      child: Center(
4        child: Text('Loading ...')
5      ),
6    );
7  }

The loading indicator is just a centered text showing “Loading …”.

Everything else stays the same like before, expect for two details:

  • We pass state.currentNumbers to our LottoInput widget
  • We show the results to the user

The results are the costs and the prize presented in text form in a Column:

 1Widget _getResultTexts(LottoState state) {
 2  if (state.searchResult == null) {
 3    return Container();
 4  }
 5  return Column(
 6    children: [
 7      SizedBox(height: 24),
 8      Text(
 9        'Costs: ${numberFormat.format(state.searchResult.costs)} £',
10        style: TextStyle(
11          fontSize: 20,
12          color: Colors.white
13        ),
14        textAlign: TextAlign.center,
15      ),
16      SizedBox(height: 8),
17      Container(
18        padding: EdgeInsets.all(8),
19        child: Text(
20          'Prize: ${numberFormat.format(state.searchResult.prizeSum)} £ (and ${state.searchResult.match2count} free tickets)',
21          style: TextStyle(
22            fontSize: 20,
23          ),
24          textAlign: TextAlign.center,
25        )
26      )
27    ]
28  );
29}

Now if we start the app and input the static numbers we defined in the service, we get information about our costs (in this case 2 £ because our static result only contains one game) and a prize of 7,000,000 £ because we have 6 matches:

Flutter lotto static GridView interactive
An interactive UI showing the costs and the prize once we choose 6 numbers

We enhance the view by a “details” button that gives the user more insight about the game results:

 1OutlineButton(
 2  shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(32.0)),
 3  onPressed: () {
 4    setState(() {
 5      showDialog(
 6        context: context,
 7        builder: (BuildContext context) {
 8          return LottoDetails(state: state);
 9        }
10      );
11    });
12  },
13  borderSide: BorderSide(
14    color: Colors.white,
15    width: 2
16  ),
17  child: Text(
18    'DETAILS',
19    style: TextStyle(
20      color: Colors.white
21    ),
22  )
23)

We use the showDialog() method to show the details in an overview. This is encapsulated in its own widget:

 1import 'package:flutter/material.dart';
 2import 'package:flutter_lotto_retrospection/bloc/lotto_bloc.dart';
 3
 4class LottoDetails extends StatelessWidget{
 5  LottoDetails({
 6    this.state
 7  });
 8
 9  final LottoState state;
10
11  @override
12  Widget build(BuildContext context) {
13    if (state == null) {
14      return Container();
15    }
16
17    return AlertDialog(
18        backgroundColor: Colors.green,
19        shape: RoundedRectangleBorder(
20            borderRadius: BorderRadius.all(
21                Radius.circular(8.0)
22            )
23        ),
24        content: Column(
25            mainAxisSize: MainAxisSize.min,
26            crossAxisAlignment: CrossAxisAlignment.center,
27            children: [
28              Text(
29                'Game details',
30                style: TextStyle(
31                  fontSize: 24,
32                  color: Colors.white,
33                )
34              ),
35              SizedBox(height: 24),
36              _getTextRow('Games played:\n${state.searchResult.gameCount}'),
37              _getTextRow('Games with 2 matches:\n${state.searchResult.match2count}'),
38              _getTextRow('Games with 3 matches:\n${state.searchResult.match3count}'),
39              _getTextRow('Games with 4 matches:\n${state.searchResult.match4count}'),
40              _getTextRow('Games with 5 matches:\n${state.searchResult.match5count}'),
41              _getTextRow('Games with 5 matches and bonus:\n${state.searchResult.match5withBonusCount}'),
42              _getTextRow('Games with 6 matches:\n${state.searchResult.match6count}'),
43              SizedBox(height: 24),
44              OutlineButton(
45                onPressed: () {
46                  Navigator.of(context).pop();
47                },
48                shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(32.0)),
49                borderSide: BorderSide(
50                    color: Colors.white,
51                    width: 2
52                ),
53                child: Text(
54                  'OK',
55                  style: TextStyle(
56                      color: Colors.white
57                  ),
58                )
59              ),
60            ]
61        )
62    );
63  }
64
65  Text _getTextRow(String text) {
66    return Text(
67      text,
68      style: TextStyle(
69          color: Colors.white
70      ),
71      textAlign: TextAlign.center,
72    );
73  }
74}
Flutter lotto static GridView details
The details the user can view about the current numbers

Displaying actual lottery data

Yet, we only display made up data, which makes the whole app completely useless. Let’s fetch some actual lottery data instead.

The lottery website of the UK has an archive of lottery data.

With a simple Javascript we put all the data from the DOM in a useful JSON format:

 1function getMonthFromString(mon){
 2   var month = new Date(Date.parse(mon +" 1, 2012")).getMonth()+1;
 3
 4   return month <= 9 ? '0' + month : month;
 5}
 6
 7function getDayFromString(day){
 8   return day <= 9 ? '0' + day : day;
 9}
10
11results = [];
12
13document.querySelectorAll('#siteContainer > div.main > table > tbody > tr').forEach(function(element) {
14
15    let matches = element.innerText.match(/(.+) (\d{1,2})\w{1,2} (.+) (.+)\s+(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)\s+£(.+)/);
16
17    let data = {
18        date: matches[4] + '-' + getMonthFromString(matches[3]) + '-' + getDayFromString(matches[2]),
19        month: getMonthFromString(matches[3]),
20        year: parseInt(matches[4]),
21        numbers: [
22            parseInt(matches[5]),
23            parseInt(matches[6]),
24            parseInt(matches[7]),
25            parseInt(matches[8]),
26            parseInt(matches[9]),
27            parseInt(matches[10]),
28        ],
29        bonus: parseInt(matches[11]),
30        prize: parseInt(matches[12].replaceAll(',', ''))
31    };
32
33    results.push(data);
34});
35
36JSON.stringify(results);

We put everything in a file called historical_data.json in our assets folder. This looks like this:

 1{
 2  "data": [
 3    {
 4      "date":"2020-11-07",
 5      "month":11,
 6      "year":2020,
 7      "numbers":[
 8        3,
 9        33,
10        45,
11        50,
12        52,
13        56
14      ],
15      "bonus":44,
16      "prize":20000000
17    },
18    {
19      "date":"2020-11-04",
20      "month":11,
21      "year":2020,
22      "numbers":[
23        10,
24        20,
25        28,
26        32,
27        35,
28        50
29      ],
30      "bonus":24,
31      "prize":12980180
32    },
33....

Now we need our service to access that file instead of returning static data:

  1class LottoHistoryService {
  2  List<LottoResult> _cachedList;
  3
  4  Future<List<LottoResult>> getHistoricalResults() async {
  5    if (_cachedList == null) {
  6      List<dynamic> json = await _getJsonFromFile();
  7      _cachedList = _jsonToSearchTypes(json);
  8    }
  9
 10    return _cachedList;
 11  }
 12
 13  Future<List<dynamic>> _getJsonFromFile() async {
 14    String jsonString = await rootBundle.loadString('assets/historical_data.json');
 15
 16    return jsonDecode(jsonString)['data'];
 17  }
 18
 19  List<LottoResult> _jsonToSearchTypes(List<dynamic> json) {
 20    List<LottoResult> lottoResults = [];
 21
 22    for (var element in json) {
 23      lottoResults.add(
 24        LottoResult.fromJson(element)
 25      );
 26    }
 27
 28    return lottoResults;
 29  }
 30}
 31
 32class LottoResult {
 33  Prizes prizes;
 34  DateTime dateTime;
 35  CorrectNumbers correctNumbers;
 36
 37  LottoResult({
 38    @required this.prizes,
 39    @required this.dateTime,
 40    @required this.correctNumbers
 41  });
 42
 43  LottoResult.fromJson(Map<dynamic, dynamic> json) {
 44    this.dateTime = DateTime.parse(json['date']);
 45    this.prizes = Prizes(
 46      match6: Prize(amount: json['prize']),
 47      match5plusBonus: Prize(amount: 1000000),
 48      match5: Prize(amount: 1000000),
 49      match4: Prize(amount: 1750),
 50      match3: Prize(amount: 140),
 51      match2: Prize(amount: 0),
 52    );
 53
 54    List<int> numbers = [];
 55
 56    for (var number in json['numbers']) {
 57      numbers.add(number);
 58    }
 59    this.correctNumbers = CorrectNumbers(
 60      numbers: numbers
 61    );
 62  }
 63}
 64
 65class Prizes {
 66  Prize match6;
 67  Prize match5plusBonus;
 68  Prize match5;
 69  Prize match4;
 70  Prize match3;
 71  Prize match2;
 72
 73  Prizes({
 74    @required this.match6,
 75    @required this.match5plusBonus,
 76    @required this.match5,
 77    @required this.match4,
 78    @required this.match3,
 79    @required this.match2,
 80  });
 81}
 82
 83class Prize {
 84  int amount;
 85  int winners;
 86
 87  Prize({
 88    @required this.amount,
 89    this.winners = 0
 90  });
 91}
 92
 93class CorrectNumbers {
 94  @required List<int> numbers;
 95  @required int bonus;
 96
 97  CorrectNumbers({
 98    this.numbers,
 99    this.bonus
100  });
101}

It’s important that we gave our model a fromJson() method. This way, we can just read the JSON file from our assets folder and put it into the fromJson() method which returns the same class it returned before. Other than that, we don’t do much apart from caching the result.

Flutter lotto static GridView completed
The final result

Final words

We used the archive data of the UK lottery and an architecture with a bloc and a service to provide a lotto retrospective. Check out, how your favorite numbers would have performed in the UK lottery in 2020 :).

GET FULL CODE

Comment this 🤌

You are replying to 's commentRemove reference