Flutter — simple GridView with flashcard game

Here we are building a simple GridView and make it like a flashcard game. Functions I will talk thourgh includes:

  • Loading a local json and parse it into data model
  • Map the data model into grid-view with click events / animations
  • Audio function to play short clip
  • Share function to upload and share String
  • Some of ideas on structuring the Flutter project, where each class won’t end of being 1000 lines long

I’d assume you have Flutter environment set up, and not totally new to the platform, if not please refer to my other post to begin with.

Step 1: Loading a local json

Dependency to add in pubspec.yaml

dependencies:
flutter:
sdk:
flutter

json_serializable: ^2.0.1
void main() => runApp(MyApp());

Create a entry point of the app, which includes another StatefulWidget class called QuestionView. This class is similar to the AppDelegate in swift or MainActivity in Android, where you want to keep logic to minimum with just the initial settings.

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flashcard',
home: new QuestionView()
);
}
}

I have three classes to make this flashcard gridview: Question the data model class, QuestionView the main view class to set up the grid view and initial settings, and the QuestionExtension which handles logic. The json file is stored in project/data/question.json, which should be also included inpubspec.yaml file.

flutter:
uses-material-design:
true
assets:
- data/
- data/image/
- assets/

The json structure looks like below, it has a list within a list, therefore you will need to add your own deserializer to parse the list of data.

{
"index": 0,
"data": [
{
"question": "老虎 lǎo hǔ",
"answer": 2,
"audio": "animala.mp3",
"picture": [
"animali.jpg",
"animalc.jpg",
"animala.jpg",
"animalg.jpg"
]
}
]
}

The Question.dart model class which also parses the json data

class Question {
  int index;
  List<Data> data;

  Question({this.index, this.data});

  factory Question.fromJson(Map<String, dynamic> parsedJson) {
    var list = parsedJson['data'] as List;
    List<Data> data = list.map((i) => Data.fromJson(i)).toList();
    return Question(index: parsedJson['index'], data: data);
  }
}

class Data {
  String question;
  int answer;
  String audio;
  List<String> imagesList;

  Data({this.question, this.answer, this.audio, this.imagesList});

  factory Data.fromJson(Map<String, dynamic> parsedJson) {
    var picFromJson = parsedJson['picture'];
    List<String> imgList = new List<String>.from(picFromJson);
    return Data(
        question: parsedJson['question'],
        answer: parsedJson['answer'],
        audio: parsedJson['audio'],
        imagesList: imgList);
  }
}

Create a QuestionExtension.dart class to handle all functions or Widget extension that you don’t want to include in the view class. The Future function creates tasks in an asynchronous way, and then( ) function throws a completed Future function as callback.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:visualmandarin/model/Question.dart';

Future<String> loadAsset(String path) async {
  return await rootBundle.loadString(path);
}

Future<Question> loadQuestion(String path) async {
  String jsonPage = await loadAsset(path);
  final jsonResponse = json.decode(jsonPage);
  return await Question.fromJson(jsonResponse);
}

void refreshPage() {
   loadQuestion(Keys.PATH).then((val) => setState(() {
          Question question = val;
          Data data = (question.data..shuffle()).first;
        }));
  }
}

Step 2: GridView with click events

import 'package:flutter/material.dart';
import 'package:visualmandarin/model/Question.dart';
import 'package:visualmandarin/question/QuestionExtension.dart';

class QuestionView extends StatefulWidget {
  QuestionView() : super();

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

class QuestionViewState extends State<QuestionView> {
  @override
  void initState() {
    loadQuestion('data/question.json');
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        child: Scaffold(
            appBar: AppBar(title: Text(Keys.data.question)),
            body: Container(
              child: GridView.count(
                  crossAxisCount: 2, children: loadQuestions(Keys.data)),
            )));
  }

  List<Widget> loadQuestions(Data data) {
    List<Widget> questionCell = [];
    for (int i = 0; i < data.imagesList.length; i++) {
      questionCell.add(Card(
        color: Colors.white,
        child: InkWell(
          child: Image(
              image: AssetImage('data/image/' + data.imagesList[i].toString())),
          onTap: () {
            if (data.answer == i) {
              setState(() {
                Keys.data = (Keys.question.data..shuffle()).first;
              });
            }
          },
        ),
      ));
    }
    return questionCell;
  }
}

class Keys {
  static Question question = Question();
  static Data data = Data();
}

Step 3: Animation Floating button with Audio play and Share

This function is inspired by the SO post here. The animation code is as follows with background color and icons. To create animations in Flutter is quite easy, a lot of functions are built in and works on both platform.

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:share/share.dart';

Widget getFloatingButton(BuildContext context, AnimationController controller,
    List<IconData> icons, Function func1) {
  return Column(
    mainAxisSize: MainAxisSize.min,
    children: List.generate(icons.length, (int index) {
      Widget child = Container(
        height: 70.0,
        width: 56.0,
        alignment: FractionalOffset.topCenter,
        child: ScaleTransition(
          scale: CurvedAnimation(
            parent: controller,
            curve: Interval(0.0, 1.0 - index / icons.length / 2.0,
                curve: Curves.easeOut),
          ),
          child: FloatingActionButton(
            backgroundColor: Colors.orange,
            child: Icon(icons[index], color: Colors.white),
            onPressed: () {
              index == 0 ? func1() : Share.share("share something");
            },
          ),
        ),
      );
      return child;
    }).toList()
      ..add(
        FloatingActionButton(
          backgroundColor: Colors.white,
          child: AnimatedBuilder(
            animation: controller,
            builder: (BuildContext context, Widget child) {
              return Transform(
                  transform:
                      Matrix4.rotationZ(controller.value * 0.5 * math.pi),
                  alignment: FractionalOffset.center,
                  child: Icon(
                      controller.isDismissed ? Icons.settings : Icons.close,
                      color: Colors.orange));
            },
          ),
          onPressed: () {
            if (controller.isDismissed)
              controller.forward();
            else
              controller.reverse();
          },
        ),
      ),
  );
}

To play a short local audio clip is just two lines of code:

AudioCache audioCache = new AudioCache();
audioCache.play(AUDIO_PATH);

The completed code is on github. My next step is to try and add firebase with local cache. My feeling with Flutter so far is that it does reduce a lot of boilerplate code, compares to native programming, because we can add logic to views while laying them out. But nevertheless it adds more complicity on structuring the project, to make it readable and extendable. I am looking forward for a more formatted standard.