Flutter Logo Quiz Game

Flutter Logo Quiz Game

I made a logo quiz game. I use an API for logos, brand names and descriptions. (https://brandfetch.com/). Each game level is PageView widget page. In the game level, there is an icon and buttons that consist of brand name. Each correct letter gives 10 points to user and wrong letters loses 1 point. If the user changes pages the level timer stops. At the end of the level timer stops and if the user couldn't find the brand name user gets 0 point. If user finds brand name game shows brand description.

Screenshot_1647897935.png

AndroidManifest.xml

Internet permission. Above the <application> tag.

<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />

pubspec.yaml

http: ^0.13.4
flutter_dotenv: ^5.0.2
flutter_svg: ^1.0.3
shared_preferences: ^2.0.13

Project Structure

  • main.dart
  • helpers.dart
  • constants.dart
  • models
    • logo_format.dart
    • logo.dart
    • brand.dart
    • game_brand.dart
  • services
    • logo_api.dart
    • shared_prefs.dart
  • pages
    • home
      • home.dart
    • game
      • game.dart
      • components
        • game_level.dart

main.dart

import 'package:flutter/material.dart';

import 'pages/home/home.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  await dotenv.load(fileName: "assets/.env");

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Logo Quiz',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(title: 'Flutter Logo Quiz'),
    );
  }
}

helpers.dart

String timeFormat(int second) {
  final dur = Duration(seconds: second);

  return dur.inMinutes.remainder(60).toString().padLeft(2, '0') +
      ':' +
      dur.inSeconds.remainder(60).toString().padLeft(2, '0');
}

constants.dart

const brandDomainList = [
  'netflix.com',
  'airbnb.com',
  'github.com',
  'reddit.com',
  'slack.com',
  'wordpress.com',
  'pepsi.com',
  'starbucks.com',
  'pinterest.com',
  'walmart.com',
  'toyota.com',
  'subway.com',
  'amazon.com',
  'dominos.com',
  'playstation.com',
  'spotify.com',
  'youtube.com',
  'linkedin.com',
  'paypal.com',
  'stackoverflow.com',
];

/models/logo_format.dart

class LogoFormat {
  final String src;
  final String format;

  const LogoFormat({required this.src, required this.format});

  factory LogoFormat.fromJson(Map<String, dynamic> json) {
    return LogoFormat(
        src: json['src'] as String, format: json['format'] as String);
  }
}

/models/logo.dart

import 'logo_format.dart';

class Logo {
  final String type;
  final List<LogoFormat> formats;

  const Logo({required this.type, required this.formats});

  factory Logo.fromJson(Map<String, dynamic> json) {
    return Logo(
        type: json['type'] as String,
        formats: json['formats']
            .map<LogoFormat>((f) => LogoFormat.fromJson(f))
            .toList());
  }
}

/models/brand.dart

import 'logo.dart';

class Brand {
  final String name;
  final String description;
  final List<Logo> logos;

  const Brand(
      {required this.name, required this.description, required this.logos});

  factory Brand.fromJson(Map<String, dynamic> json) {
    return Brand(
        name: json['name'] as String,
        description: json['description'] as String,
        logos: json['logos'].map<Logo>((l) => Logo.fromJson(l)).toList());
  }
}

/models/game_brand.dart

class GameBrand {
  final String name;
  final String description;
  final String icon;
  final String logo;

  const GameBrand(
      {required this.name,
      required this.description,
      required this.icon,
      required this.logo});
}

/services/logo_api.dart

import 'dart:convert';

import 'package:http/http.dart' as http;
import '../models/brand.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

import '../models/game_brand.dart';

class LogoApi {
  static Future<GameBrand> getBrand(String brandDomain) async {
    // Get api key
    final brandtoken = dotenv.env['brandtoken'];

    final response = await http.get(
        Uri.parse('https://api.brandfetch.io/v2/brands/$brandDomain'),
        headers: {'Authorization': 'Bearer $brandtoken'});

    if (response.statusCode == 200) {
      final brand = Brand.fromJson(jsonDecode(response.body));

      final gameBrand = GameBrand(
          name: brand.name,
          description: brand.description,
          icon: brand.logos.firstWhere((l) => l.type == 'icon').formats[0].src,
          logo: brand.logos
              .firstWhere((l) => l.type == 'logo')
              .formats
              .firstWhere((f) => f.format == 'svg')
              .src);

      return gameBrand;
    } else {
      throw Exception('Failed to fetch brand');
    }
  }
}

/services/shared_prefs.dart

import 'package:shared_preferences/shared_preferences.dart';

class SharedPrefs {
  static Future<SharedPreferences> _prefs() async {
    return await SharedPreferences.getInstance();
  }

  static Future<int> getTotalScore() async {
    final sharedPreferences = await _prefs();
    return sharedPreferences.getInt('totalscore') ?? 0;
  }

  static Future<void> setTotalScore(int tscore) async {
    final sharedPreferences = await _prefs();
    await sharedPreferences.setInt('totalscore', tscore);
  }

  static Future<void> clearTotalScore() async {
    final sharedPreferences = await _prefs();
    await sharedPreferences.setInt('totalscore', 0);
  }
}

/pages/home/home.dart

import 'package:flutter/material.dart';
import 'package:logo_quiz/constants.dart';
import 'package:logo_quiz/pages/game/game.dart';
import 'package:logo_quiz/services/logo_api.dart';
import '../../models/game_brand.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<GameBrand> _brands = <GameBrand>[];

  // for show loading indicator
  bool _fetchingBrands = false;
  // for show error
  bool _fetchingBrandsError = false;

  /*
  ** Get brands from api and turn them GameBrand
  ** object and store them _brands list
  */
  Future<void> _initBrands() async {
    setState(() {
      _fetchingBrands = true;
    });

    for (var b in brandDomainList) {
      try {
        final brand = await LogoApi.getBrand(b);
        _brands.add(brand);
      } catch (ex) {
        setState(() {
          _fetchingBrandsError = true;
          _fetchingBrands = false;
        });

        return;
      }
    }

    setState(() {
      _fetchingBrandsError = false;
      _fetchingBrands = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Flutter Logo Quiz',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 36,
              ),
            ),
            const SizedBox(height: 25),
            ElevatedButton(
              child: _fetchingBrands
                  ? const Text('Loading...')
                  : _fetchingBrandsError
                      ? const Text('Error')
                      : const Text('Start'),
              onPressed: () async {
                // Send brands list to game page
                if (!_fetchingBrands) {
                  await _initBrands();

                  if (!_fetchingBrandsError) {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) {
                      return Game(brands: _brands);
                    }));
                  }
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

/pages/game/game.dart

import 'package:flutter/material.dart';

import '../../models/game_brand.dart';
import '../../services/shared_prefs.dart';
import 'components/game_level.dart';

class Game extends StatefulWidget {
  final List<GameBrand> brands;

  const Game({Key? key, required this.brands}) : super(key: key);

  @override
  State<Game> createState() => _GameState();
}

class _GameState extends State<Game> {
  int _currentLevel = 0;

  @override
  void initState() {
    super.initState();

    SharedPrefs.clearTotalScore();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _buildLevels(),
    );
  }

  /*
  ** Builds game pages and send them
  ** brand, level number and current level number
  ** For stop timer in level page game controls
  ** the page number equal to current page number
  */
  Widget _buildLevels() {
    return SafeArea(
      child: PageView.builder(
        onPageChanged: (pageIndex) {
          setState(() {
            _currentLevel = pageIndex;
          });
        },
        itemCount: widget.brands.length,
        itemBuilder: (BuildContext context, int index) {
          return GameLevel(
              gameBrand: widget.brands[index],
              levelNumber: index,
              currentLevel: _currentLevel);
        },
      ),
    );
  }
}

/game/components/game_level.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:logo_quiz/helpers.dart';
import 'package:logo_quiz/models/game_brand.dart';
import 'dart:math' as math;
import 'package:flutter_svg/flutter_svg.dart';
import 'package:logo_quiz/services/shared_prefs.dart';

class GameLevel extends StatefulWidget {
  final GameBrand gameBrand;
  final int levelNumber;
  final int currentLevel;

  const GameLevel(
      {Key? key,
      required this.gameBrand,
      required this.levelNumber,
      required this.currentLevel})
      : super(key: key);

  @override
  State<GameLevel> createState() => _GameLevelState();
}

class _GameLevelState extends State<GameLevel>
    with AutomaticKeepAliveClientMixin {
  final List<String> _selectedLetters = [];
  final ValueNotifier<int> _seconds = ValueNotifier<int>(300);
  final ValueNotifier<int> _score = ValueNotifier<int>(0);

  late final Timer _timer;

  bool _levelComplete = false;
  bool? _success;
  bool _isLevelStarted = false;
  int _letterOrder = 0;
  int _totalScore = 0;
  double _iconFlipValue = 0;
  double _descriptionOpacity = 1.0;
  String _iconOrLogo = 'icon';
  List<String> _shuffledBrandNameCharList = [];

  @override
  bool get wantKeepAlive => true;

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  void initState() {
    // Shuffle brand name characters for game buttons
    _shuffledBrandNameCharList = widget.gameBrand.name.characters.toList();
    _shuffledBrandNameCharList.shuffle();

    for (int i = 0; i < _shuffledBrandNameCharList.length; i++) {
      _selectedLetters.add('_');
    }

    _timer = Timer.periodic(
      const Duration(milliseconds: 1000),
      (timer) {
        if (_isLevelStarted &&
            widget.currentLevel == widget.levelNumber &&
            !_levelComplete) {
          _seconds.value--;

          if (_seconds.value <= 0) {
            setState(() {
              _score.value = 0;
              _levelComplete = true;
              _success = false;
              _iconFlipValue = 1;
              _descriptionOpacity = 0.0;
              _printTotalScore();
            });
          }
        }
      },
    );

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return Column(
      children: [
        const Spacer(),
        // Level number, complete indicator, time
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _levelNumber(),
            _levelCompleteIndicator(),
            _levelScore(),
            if (_levelComplete) _levelTotalScore(),
            _levelTime(),
          ],
        ),
        const Spacer(),
        // Icon
        _levelIcon(),
        const Spacer(),
        // Brand letters or description
        AnimatedOpacity(
            onEnd: () {
              setState(() {
                _descriptionOpacity = 1.0;
              });
            },
            duration: const Duration(milliseconds: 500),
            opacity: _descriptionOpacity,
            child: _brandNameOrDescription()),
        const Spacer(),
        // Brand name buttons
        if (!_levelComplete) _brandNameButtons(),
        const Spacer(),
      ],
    );
  }

  Widget _levelCompleteIndicator() {
    return Icon(Icons.verified,
        color: _levelComplete && _success == false
            ? Colors.red
            : _levelComplete && _success == true
                ? Colors.green
                : Colors.grey,
        size: 60);
  }

  Widget _levelNumber() {
    return Container(
        padding: const EdgeInsets.all(16.0),
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          border: Border.all(color: Colors.black12, width: 3),
          boxShadow: [
            BoxShadow(
                color: Colors.black.withOpacity(.3),
                blurRadius: 10,
                offset: const Offset(0, 0))
          ],
          gradient: const LinearGradient(
              colors: [Colors.blue, Color.fromARGB(255, 8, 114, 201)],
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter),
        ),
        child: Text('${widget.levelNumber + 1}',
            style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Colors.white)));
  }

  Widget _levelTime() {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.black12, width: 3),
        boxShadow: [
          BoxShadow(
              color: Colors.black.withOpacity(.3),
              blurRadius: 10,
              offset: const Offset(0, 0))
        ],
        gradient: const LinearGradient(
            colors: [Colors.blue, Color.fromARGB(255, 8, 114, 201)],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ValueListenableBuilder(
            valueListenable: _seconds,
            builder: (context, value, child) {
              return Text(timeFormat(_seconds.value),
                  style: const TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.white));
            }),
      ),
    );
  }

  Widget _levelScore() {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.black12, width: 3),
        boxShadow: [
          BoxShadow(
              color: Colors.black.withOpacity(.3),
              blurRadius: 10,
              offset: const Offset(0, 0))
        ],
        gradient: const LinearGradient(
            colors: [Colors.orange, Colors.deepOrange],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ValueListenableBuilder(
            valueListenable: _score,
            builder: (context, value, child) {
              return Text(_score.value.toString(),
                  style: const TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.white));
            }),
      ),
    );
  }

  Widget _levelTotalScore() {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.black12, width: 3),
        boxShadow: [
          BoxShadow(
              color: Colors.black.withOpacity(.3),
              blurRadius: 10,
              offset: const Offset(0, 0))
        ],
        gradient: const LinearGradient(
            colors: [Colors.red, Colors.brown],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ValueListenableBuilder(
            valueListenable: _score,
            builder: (context, value, child) {
              return Text(_totalScore.toString(),
                  style: const TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.white));
            }),
      ),
    );
  }

  Widget _levelIcon() {
    return AnimatedContainer(
      onEnd: () {
        setState(() {
          _iconOrLogo = 'logo';
          _iconFlipValue = 2;
        });
      },
      duration: const Duration(milliseconds: 1000),
      transform: Matrix4.rotationY(_iconFlipValue * math.pi),
      transformAlignment: Alignment.center,
      child: Stack(
        alignment: Alignment.center,
        children: [
          Container(color: Colors.black12, width: 175, height: 175),
          Container(color: Colors.white, width: 150, height: 150),
          SizedBox(
            width: 125,
            height: 125,
            child: _iconOrLogo == 'icon'
                ? Image.network(
                    widget.gameBrand.icon,
                    fit: BoxFit.contain,
                    cacheWidth: 100,
                    cacheHeight: 100,
                    loadingBuilder: (BuildContext context, Widget child,
                        ImageChunkEvent? loadingProgress) {
                      if (loadingProgress == null) {
                        return child;
                      }
                      return Center(
                        child: CircularProgressIndicator(
                          value: loadingProgress.expectedTotalBytes != null
                              ? loadingProgress.cumulativeBytesLoaded /
                                  loadingProgress.expectedTotalBytes!
                              : null,
                        ),
                      );
                    },
                    errorBuilder: (context, error, stackTrace) =>
                        const Icon(Icons.error),
                  )
                : SvgPicture.network(
                    widget.gameBrand.logo,
                    fit: BoxFit.contain,
                    placeholderBuilder: (context) {
                      return const CircularProgressIndicator();
                    },
                  ),
          ),
        ],
      ),
    );
  }

  Widget _brandNameOrDescription() {
    return _levelComplete
        ? Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(children: [
              Text(
                widget.gameBrand.name,
                textAlign: TextAlign.center,
                style:
                    const TextStyle(fontWeight: FontWeight.bold, fontSize: 28),
              ),
              const SizedBox(
                height: 5,
              ),
              Text(
                widget.gameBrand.description,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 18),
              ),
            ]),
          )
        : Padding(
            padding: const EdgeInsets.all(8),
            child: Wrap(
              alignment: WrapAlignment.center,
              spacing: 5,
              runSpacing: 5,
              children: _selectedLetters
                  .asMap()
                  .entries
                  .map<Widget>((e) => ElevatedButton(
                      onPressed: () {},
                      child: Text(e.value),
                      style: ElevatedButton.styleFrom(
                          primary: _levelComplete
                              ? Colors.green
                              : _letterOrder == e.key
                                  ? Colors.red
                                  : Colors.blue)))
                  .toList(),
            ),
          );
  }

  Widget _brandNameButtons() {
    List<Widget> wrapChildren = _shuffledBrandNameCharList
        .map<Widget>((c) => ElevatedButton(
            style: ElevatedButton.styleFrom(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(30),
                ),
                primary: Colors.blueGrey,
                textStyle: const TextStyle(fontSize: 18)),
            onPressed: () {
              setState(() {
                if (!_isLevelStarted) {
                  _isLevelStarted = true;
                }

                if (widget.gameBrand.name[_letterOrder] == c) {
                  _selectedLetters[_letterOrder] = c;
                  _letterOrder++;
                  _score.value += 10;

                  if (_letterOrder == widget.gameBrand.name.length) {
                    _levelComplete = true;
                    _success = true;
                    _iconFlipValue = 1;
                    _descriptionOpacity = 0.0;
                    _printTotalScore();
                  }
                } else {
                  _score.value -= 1;
                }
              });
            },
            child: Text(c)))
        .toList();

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Wrap(
          alignment: WrapAlignment.center,
          spacing: 5,
          runSpacing: 5,
          children: wrapChildren),
    );
  }

  Future<void> _printTotalScore() async {
    int tscore = await SharedPrefs.getTotalScore();
    tscore += _score.value;
    await SharedPrefs.setTotalScore(tscore);

    setState(() {
      _totalScore = tscore;
    });
  }
}