Flutter 로고 퀴즈 게임

89637 단어
로고 퀴즈 게임을 만들었습니다. 로고, 브랜드 이름 및 설명에 API를 사용합니다. ( brandfetch.com ). 각 게임 레벨은 PageView 위젯 페이지입니다. 게임 레벨에는 브랜드 이름으로 구성된 아이콘과 버튼이 있습니다. 각 올바른 문자는 사용자에게 10점을 주고 잘못된 문자는 1점을 잃습니다. 사용자가 페이지를 변경하면 레벨 타이머가 중지됩니다. 레벨이 끝나면 타이머가 멈추고 사용자가 브랜드 이름을 찾을 수 없으면 0점을 얻습니다. 사용자가 브랜드 이름을 찾으면 게임에 브랜드 설명이 표시됩니다.



AndroidManifest.xml
인터넷 허가. 태그 위.

<!-- 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


프로젝트 구조


  • 메인.다트
  • helpers.dart
  • 상수.다트
  • 모델
  • logo_format.dart
  • 로고.다트
  • 브랜드.다트
  • game_brand.dart

  • 서비스
  • logo_api.dart
  • shared_prefs.dart

  • 페이지

  • 홈.다트

  • 게임
  • game.dart
  • 구성 요소
  • game_level.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');
    }
    


    상수.다트

    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});
    }
    


    /서비스/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');
        }
      }
    }
    


    /서비스/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);
      }
    }
    


    /페이지/홈/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);
                        }));
                      }
                    }
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    


    /페이지/게임/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;
        });
      }
    }
    

    좋은 웹페이지 즐겨찾기