I made a logo quiz game. I use an API for logos, brand names and descriptions. (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.
AndroidManifest.xml
Internet permission. Above the 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
- home
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;
});
}
}
Top comments (0)