├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── flutter-ci.yml ├── .gitignore ├── .idea ├── .gitignore └── runConfigurations │ ├── development.xml │ └── production.xml ├── .metadata ├── .tool-versions ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ ├── signingConfigs │ │ ├── debug.gradle │ │ ├── debug.keystore │ │ ├── release.gradle │ │ └── release.keystore │ └── src │ │ ├── development │ │ ├── AndroidManifest.xml │ │ ├── google-services.json │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── jp │ │ │ │ └── wasabeef │ │ │ │ └── app │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── production │ │ ├── AndroidManifest.xml │ │ ├── google-services.json │ │ └── res │ │ └── values │ │ └── strings.xml ├── app_android.iml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── app.iml ├── assets ├── fonts │ └── Rotunda-Bold.otf ├── images │ ├── 2.0x │ │ ├── article_placeholder.webp │ │ └── icon_placeholder.jpg │ ├── 3.0x │ │ ├── article_placeholder.webp │ │ └── icon_placeholder.jpg │ ├── article_placeholder.webp │ └── icon_placeholder.jpg ├── svgs │ ├── firebase.svg │ ├── news.svg │ └── video.svg └── videos │ └── bigbuckbunny.mp4 ├── bitrise.yml ├── codecov.yml ├── codemagic.yaml ├── ios ├── Config │ ├── Development.xcconfig │ └── Production.xcconfig ├── Flutter │ └── AppFrameworkInfo.plist ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ ├── Development.xcscheme │ │ ├── Production.xcscheme │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Resources │ │ └── Firebase │ │ │ ├── Development │ │ │ └── GoogleService-Info.plist │ │ │ └── Production │ │ │ └── GoogleService-Info.plist │ ├── Runner-Bridging-Header.h │ └── ja.lproj │ │ ├── LaunchScreen.strings │ │ └── Main.strings └── Scripts │ └── copy_google_service.sh ├── l10n.yaml ├── lib ├── app.dart ├── data │ ├── app_error.dart │ ├── local │ │ ├── app_user.dart │ │ └── app_user.freezed.dart │ ├── model │ │ ├── article.dart │ │ ├── article.freezed.dart │ │ ├── article.g.dart │ │ ├── news.dart │ │ ├── news.freezed.dart │ │ ├── news.g.dart │ │ ├── result.dart │ │ ├── result.freezed.dart │ │ ├── source.dart │ │ ├── source.freezed.dart │ │ └── source.g.dart │ ├── provider │ │ └── firebase_auth_provider.dart │ ├── remote │ │ ├── app_dio.dart │ │ ├── auth_data_source.dart │ │ ├── auth_data_source_impl.dart │ │ ├── news_data_source.dart │ │ └── news_data_source.g.dart │ └── repository │ │ ├── auth_repository.dart │ │ ├── auth_repository_impl.dart │ │ ├── news_repository.dart │ │ └── news_repository_impl.dart ├── foundation │ ├── constants.dart │ └── extension │ │ ├── async_snapshot.dart │ │ ├── date_time.dart │ │ └── object.dart ├── gen │ ├── assets.gen.dart │ └── fonts.gen.dart ├── l10n │ ├── intl_messages_en.arb │ └── intl_messages_ja.arb ├── main.dart └── ui │ ├── component │ ├── image │ │ └── image.dart │ ├── loading │ │ ├── container_with_loading.dart │ │ └── loading.dart │ └── snack_bar │ │ └── error_snackbar.dart │ ├── detail │ └── detail_page.dart │ ├── home │ └── home_page.dart │ ├── hook │ ├── use_asset_vide_player_controller.dart │ ├── use_l10n.dart │ └── use_router.dart │ ├── loading_state_view_model.dart │ ├── news │ ├── article_item.dart │ ├── connected_new_page.dart │ ├── news_page.dart │ └── news_view_model.dart │ ├── route │ ├── app_route.dart │ └── app_route.gr.dart │ ├── signIn │ └── sign_in_page.dart │ ├── theme │ ├── app_colors.dart │ ├── app_text_theme.dart │ ├── app_theme.dart │ └── font_size.dart │ ├── user_view_model.dart │ └── video │ └── video_page.dart ├── package-lock.json ├── package.json ├── pubspec.lock ├── pubspec.yaml ├── renovate.json ├── scripts └── codecov.sh ├── test ├── data │ ├── app_error_test.dart │ ├── dummy │ │ ├── dummy_article.dart │ │ ├── dummy_news.dart │ │ └── dummy_response_news_api.dart │ ├── remote │ │ ├── fake_app_dio.dart │ │ └── fake_news_data_source_impl.dart │ └── repository │ │ └── fake_news_repository_impl.dart ├── foundation │ └── extension │ │ └── date_time_test.dart └── ui │ ├── view_model_test.dart │ └── widget_test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png └── Icon-512.png ├── index.html └── manifest.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: wasabeef # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this change? 2 | 3 | ### Screenshots (Optional) 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/flutter-ci.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-java@v2 15 | with: 16 | distribution: 'zulu' 17 | java-version: '11' 18 | - uses: subosito/flutter-action@v1 19 | with: 20 | flutter-version: '2.5.3' 21 | - uses: dart-lang/setup-dart@v1 22 | with: 23 | sdk: '2.14.4' 24 | 25 | - name: Cache Gradle modules 26 | uses: actions/cache@v2.1.6 27 | env: 28 | cache-number: ${{ secrets.CACHE_NUMBER }} 29 | with: 30 | path: | 31 | ~/android/.gradle 32 | ~/.gradle/cache 33 | # ~/.gradle/wrapper 34 | key: ${{ runner.os }}-gradle-${{ env.cache-number }}-${{ hashFiles('android/build.gradle') }}-${{ hashFiles('android/app/build.gradle') }} 35 | restore-keys: | 36 | ${{ runner.os }}-gradle-${{ env.cache-name }}-${{ hashFiles('android/build.gradle') }} 37 | ${{ runner.os }}-gradle-${{ env.cache-name }}- 38 | ${{ runner.os }}-gradle- 39 | ${{ runner.os }}- 40 | 41 | - name: Cache CocoaPods modules 42 | uses: actions/cache@v2.1.6 43 | env: 44 | cache-number: ${{ secrets.CACHE_NUMBER }} 45 | with: 46 | path: Pods 47 | key: ${{ runner.os }}-pods-${{ env.cache-number }}-${{ hashFiles('ios/Podfile.lock') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pods-${{ env.cache-name }}- 50 | ${{ runner.os }}-pods- 51 | ${{ runner.os }}- 52 | 53 | - name: Cache Flutter modules 54 | uses: actions/cache@v2.1.6 55 | env: 56 | cache-number: ${{ secrets.CACHE_NUMBER }} 57 | with: 58 | path: | 59 | ~/.pub-cache/bin 60 | key: ${{ runner.os }}-pub-${{ env.cache-number }}-${{ env.flutter_version }}-${{ hashFiles('pubspec.lock') }} 61 | restore-keys: | 62 | ${{ runner.os }}-pub-${{ env.flutter_version }}- 63 | ${{ runner.os }}-pub- 64 | ${{ runner.os }}- 65 | 66 | - name: Get flutter dependencies. 67 | run: | 68 | make setup 69 | export PATH="$PATH":"$HOME/.pub-cache/bin" 70 | 71 | make dependencies 72 | 73 | - name: Check for any formatting and statically analyze the code. 74 | run: make format-analyze 75 | 76 | - name: Run widget tests for our flutter project. 77 | env: 78 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 79 | run: | 80 | make unit-test 81 | make codecov 82 | 83 | - name: Build ipa and apk 84 | run: | 85 | make build-android-prd 86 | make build-ios-prd 87 | 88 | - name: Slack Notification 89 | uses: homoluctus/slatify@master 90 | if: always() 91 | with: 92 | type: ${{ job.status }} 93 | job_name: '*Flutter Build*' 94 | mention: 'here' 95 | mention_if: 'failure' 96 | channel: '#develop' 97 | username: 'Github Actions' 98 | icon_emoji: ':octocat:' 99 | url: ${{ secrets.SLACK_WEBHOOK_URL }} 100 | commit: true 101 | token: ${{ secrets.GITHUB_TOKEN }} 102 | 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | .idea/.gitignore 20 | .idea/dictionaries/ 21 | .idea/libraries/ 22 | .idea/misc.xml 23 | .idea/modules.xml 24 | .idea/vcs.xml 25 | .idea/workspace.xml 26 | .idea/saveactions_settings.xml 27 | android/.idea/ 28 | ios/.idea/ 29 | 30 | # The .vscode folder contains launch configuration and tasks you configure in 31 | # VS Code which you may wish to be included in version control, so this line 32 | # is commented out by default. 33 | #.vscode/ 34 | 35 | # Flutter/Dart/Pub related 36 | **/doc/api/ 37 | .dart_tool/ 38 | .flutter-plugins 39 | .flutter-plugins-dependencies 40 | .packages 41 | .pub-cache/ 42 | .pub/ 43 | /build/ 44 | generated_plugin_registrant.dart 45 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 46 | 47 | # Fastlane related 48 | android/fastlane/report.xml 49 | ios/fastlane/report.xml 50 | 51 | # Android related 52 | **/android/**/gradle-wrapper.jar 53 | **/android/.gradle 54 | **/android/captures/ 55 | **/android/gradlew 56 | **/android/gradlew.bat 57 | **/android/local.properties 58 | **/android/settings_aar.gradle 59 | **/android/**/GeneratedPluginRegistrant.java 60 | **/android/**/gen 61 | 62 | # iOS/XCode related 63 | **/ios/**/*.mode1v3 64 | **/ios/**/*.mode2v3 65 | **/ios/**/*.moved-aside 66 | **/ios/**/*.pbxuser 67 | **/ios/**/*.perspectivev3 68 | **/ios/**/*sync/ 69 | **/ios/**/.sconsign.dblite 70 | **/ios/**/.tags* 71 | **/ios/**/.vagrant/ 72 | **/ios/**/DerivedData/ 73 | **/ios/**/Icon? 74 | **/ios/**/Pods/ 75 | **/ios/**/.symlinks/ 76 | **/ios/**/profile 77 | **/ios/**/xcuserdata 78 | **/ios/.generated/ 79 | **/ios/build/ 80 | **/ios/Flutter/.last_build_id 81 | **/ios/Flutter/App.framework 82 | **/ios/Flutter/Flutter.framework 83 | **/ios/Flutter/Generated.xcconfig 84 | **/ios/Flutter/app.flx 85 | **/ios/Flutter/app.zip 86 | **/ios/Flutter/flutter_assets/ 87 | **/ios/Flutter/flutter_export_environment.sh 88 | **/ios/Flutter/Flutter.podspec 89 | **/ios/ServiceDefinitions.json 90 | **/ios/Runner/GeneratedPluginRegistrant.* 91 | **/ios/Runner/GoogleService-Info.plist 92 | !**/ios/**/default.mode1v3 93 | !**/ios/**/default.mode2v3 94 | !**/ios/**/default.pbxuser 95 | !**/ios/**/default.perspectivev3 96 | 97 | # Web related 98 | *.dart.js 99 | *.info.json # Produced by the --dump-info flag. 100 | *.js # When generated by dart2js. Don't specify *.js if your 101 | # project includes source files written in JavaScript. 102 | *.js_ 103 | *.js.deps 104 | *.js.map 105 | node_modules/ 106 | 107 | ## Generated code 108 | #*.gen.dart 109 | #*.g.dart 110 | #*.freezed.dart 111 | #*.gr.dart 112 | #l10n*.dart 113 | 114 | ## Test coverage 115 | coverage/ 116 | 117 | # fvm 118 | .fvm/flutter_sdk -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/development.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/production.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | dart 2.14.4 2 | flutter 2.5.3-stable 3 | nodejs 17.0.1 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Flutter development", 6 | "program": "lib/main.dart", 7 | "request": "launch", 8 | "type": "dart", 9 | "args": [ 10 | "--dart-define=FLAVOR=development", 11 | "--flavor", 12 | "development" 13 | ] 14 | }, 15 | { 16 | "name": "Flutter production", 17 | "program": "lib/main.dart", 18 | "request": "launch", 19 | "type": "dart", 20 | "args": [ 21 | "--dart-define=FLAVOR=production", 22 | "--flavor", 23 | "production" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": "~/.asdf/installs/flutter/2.5.3-stable", 3 | "dart.sdkPath": "~/.asdf/installs/dart/2.14.4/dart-sdk" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Daichi Furiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup 2 | setup: 3 | npm install 4 | 5 | .PHONY: dependencies 6 | dependencies: 7 | flutter pub get 8 | 9 | .PHONY: analyze 10 | analyze: 11 | flutter analyze 12 | 13 | .PHONY: format 14 | format: 15 | flutter format lib/ 16 | 17 | .PHONY: format-analyze 18 | format-analyze: 19 | flutter format --dry-run lib/ 20 | flutter analyze 21 | 22 | .PHONY: build-runner 23 | build-runner: 24 | flutter packages pub run build_runner build --delete-conflicting-outputs 25 | 26 | .PHONY: run-dev 27 | run-dev: 28 | flutter run --flavor development --dart-define=FLAVOR=development --target lib/main.dart 29 | 30 | .PHONY: run-prd 31 | run-prd: 32 | flutter run --release --flavor production --dart-define=FLAVOR=production --target lib/main.dart 33 | 34 | .PHONY: build-android-dev 35 | build-android-dev: 36 | flutter build apk --flavor development --dart-define=FLAVOR=development --target lib/main.dart 37 | 38 | .PHONY: build-android-prd 39 | build-android-prd: 40 | flutter build apk --release --flavor production --dart-define=FLAVOR=production --target lib/main.dart 41 | 42 | .PHONY: build-ios-dev 43 | build-ios-dev: 44 | flutter build ios --no-codesign --flavor development --dart-define=FLAVOR=development --target lib/main.dart 45 | 46 | .PHONY: build-ios-prd 47 | build-ios-prd: 48 | flutter build ios --release --no-codesign --flavor production --dart-define=FLAVOR=production --target lib/main.dart 49 | 50 | .PHONY: unit-test 51 | unit-test: 52 | flutter test --coverage --coverage-path=./coverage/lcov.info 53 | 54 | .PHONY: codecov 55 | codecov: 56 | ./scripts/codecov.sh ${CODECOV_TOKEN} 57 | 58 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | analyzer: 3 | exclude: 4 | - "**/generated_plugin_registrant.dart" 5 | - "**/build/**" 6 | - "**/generated_*.dart" 7 | - "**/*.g.dart" 8 | - "**/*.freezed.dart" 9 | - "**/*.gr.dart" 10 | - "**/l10n*.dart" 11 | - "**/*.gen.dart" -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | apply plugin: 'com.google.firebase.crashlytics' 28 | 29 | android { 30 | // Workaround for support to multiple CIs (Bitrise and Codemagic). 31 | compileSdkVersion System.getenv('ANDROID_SDK_VERSION') ? System.getenv('ANDROID_SDK_VERSION').toInteger() : 30 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | lintOptions { 47 | disable 'InvalidPackage' 48 | checkReleaseBuilds false // issue https://github.com/flutter/flutter/issues/22397 49 | } 50 | 51 | defaultConfig { 52 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 53 | applicationId "jp.wasabeef.app" 54 | minSdkVersion 22 55 | targetSdkVersion 30 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | } 59 | 60 | // SigningConfigs 61 | apply from: 'signingConfigs/debug.gradle', to: android 62 | apply from: 'signingConfigs/release.gradle', to: android 63 | 64 | buildTypes { 65 | release { 66 | signingConfig signingConfigs.release 67 | } 68 | debug { 69 | debuggable true 70 | signingConfig signingConfigs.debug 71 | } 72 | } 73 | 74 | flavorDimensions "environment" 75 | productFlavors { 76 | development { 77 | dimension "environment" 78 | applicationIdSuffix ".dev" 79 | versionNameSuffix "-Dev" 80 | } 81 | 82 | production { 83 | dimension "environment" 84 | } 85 | } 86 | } 87 | 88 | flutter { 89 | source '../..' 90 | } 91 | 92 | dependencies { 93 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 94 | } 95 | 96 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /android/app/signingConfigs/debug.gradle: -------------------------------------------------------------------------------- 1 | signingConfigs { 2 | debug { 3 | storeFile file("debug.keystore") 4 | storePassword "android" 5 | keyAlias "androiddebugkey" 6 | keyPassword "android" 7 | } 8 | } 9 | 10 | // $ keytool -v -list -keystore 11 | // Certificate fingerprints: 12 | // MD5: 28:22:7C:A4:B9:2F:6E:C7:D5:58:62:48:EB:7E:82:C3 13 | // SHA1: 94:25:A9:50:9C:0E:AE:AA:00:37:5E:D6:71:E3:BC:ED:17:E5:0C:A3 14 | // SHA256: 04:92:39:09:3D:1C:B6:16:BE:55:58:A3:5F:3B:BB:CB:0B:E7:F1:DA:AA:26:C5:2D:BD:2F:44:CF:AE:47:CF:87 -------------------------------------------------------------------------------- /android/app/signingConfigs/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/signingConfigs/debug.keystore -------------------------------------------------------------------------------- /android/app/signingConfigs/release.gradle: -------------------------------------------------------------------------------- 1 | // TODO Must be re-created. 2 | signingConfigs { 3 | release { 4 | storeFile file("release.keystore") 5 | storePassword "aabbccdd" 6 | keyAlias "fab" 7 | keyPassword "aabbccdd" 8 | } 9 | } 10 | 11 | // $ keytool -v -list -keystore 12 | // Certificate fingerprints: 13 | // MD5: 9A:69:D4:B6:BF:BE:C1:A5:C5:BB:05:7B:36:84:D7:5C 14 | // SHA1: AA:88:1A:A8:47:7A:90:73:16:D6:F1:B7:D5:25:7B:92:BC:C9:4B:FD 15 | // SHA256: 22:39:5E:46:8C:55:00:6C:3A:4B:68:7D:66:20:0A:42:07:75:90:71:29:31:DA:1C:6E:48:FC:60:58:6E:0C:2D -------------------------------------------------------------------------------- /android/app/signingConfigs/release.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/signingConfigs/release.keystore -------------------------------------------------------------------------------- /android/app/src/development/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/development/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "339920746703", 4 | "firebase_url": "https://flutter-arch-blueprints-dev.firebaseio.com", 5 | "project_id": "flutter-arch-blueprints-dev", 6 | "storage_bucket": "flutter-arch-blueprints-dev.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:339920746703:android:5a97ba145b698f05ee9fba", 12 | "android_client_info": { 13 | "package_name": "jp.wasabeef.app.dev" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "339920746703-28cfben1heai5p0aqk8p3nl73toi4n03.apps.googleusercontent.com", 19 | "client_type": 1, 20 | "android_info": { 21 | "package_name": "jp.wasabeef.app.dev", 22 | "certificate_hash": "aa881aa8477a907316d6f1b7d5257b92bcc94bfd" 23 | } 24 | }, 25 | { 26 | "client_id": "339920746703-itjdedk624qhvhjjfipgdp62ororfho1.apps.googleusercontent.com", 27 | "client_type": 1, 28 | "android_info": { 29 | "package_name": "jp.wasabeef.app.dev", 30 | "certificate_hash": "9425a9509c0eaeaa00375ed671e3bced17e50ca3" 31 | } 32 | }, 33 | { 34 | "client_id": "339920746703-g2k3mt9j2bva5v555d9ps2j2ord7ecba.apps.googleusercontent.com", 35 | "client_type": 3 36 | } 37 | ], 38 | "api_key": [ 39 | { 40 | "current_key": "AIzaSyDmkQUMEuXgB4FM50dcizxumL12DUQG8ng" 41 | } 42 | ], 43 | "services": { 44 | "appinvite_service": { 45 | "other_platform_oauth_client": [ 46 | { 47 | "client_id": "339920746703-g2k3mt9j2bva5v555d9ps2j2ord7ecba.apps.googleusercontent.com", 48 | "client_type": 3 49 | }, 50 | { 51 | "client_id": "339920746703-140i3bhk3lqtpn2p2e7035btcp61p32i.apps.googleusercontent.com", 52 | "client_type": 2, 53 | "ios_info": { 54 | "bundle_id": "jp.wasabeef.app.dev" 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | } 61 | ], 62 | "configuration_version": "1" 63 | } -------------------------------------------------------------------------------- /android/app/src/development/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | FAB Dev 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 15 | 22 | 26 | 30 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/jp/wasabeef/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package jp.wasabeef.app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/production/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/production/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "208498828836", 4 | "firebase_url": "https://flutter-arc-blueprints.firebaseio.com", 5 | "project_id": "flutter-arc-blueprints", 6 | "storage_bucket": "flutter-arc-blueprints.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:208498828836:android:58145629940ce4a96b5ab1", 12 | "android_client_info": { 13 | "package_name": "jp.wasabeef.app" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "208498828836-3323hr2v1b84i3s8sv4921462l1vtc1i.apps.googleusercontent.com", 19 | "client_type": 1, 20 | "android_info": { 21 | "package_name": "jp.wasabeef.app", 22 | "certificate_hash": "9425a9509c0eaeaa00375ed671e3bced17e50ca3" 23 | } 24 | }, 25 | { 26 | "client_id": "208498828836-it7b64n6biakq3u964s0sudjbnklmkl2.apps.googleusercontent.com", 27 | "client_type": 1, 28 | "android_info": { 29 | "package_name": "jp.wasabeef.app", 30 | "certificate_hash": "aa881aa8477a907316d6f1b7d5257b92bcc94bfd" 31 | } 32 | }, 33 | { 34 | "client_id": "208498828836-rbgoklet6680lrcccnov4a25vs9k5du8.apps.googleusercontent.com", 35 | "client_type": 3 36 | } 37 | ], 38 | "api_key": [ 39 | { 40 | "current_key": "AIzaSyAzj4n6ihX9kVt_KI24xEgqFtfO7GOdZEE" 41 | } 42 | ], 43 | "services": { 44 | "appinvite_service": { 45 | "other_platform_oauth_client": [ 46 | { 47 | "client_id": "208498828836-rbgoklet6680lrcccnov4a25vs9k5du8.apps.googleusercontent.com", 48 | "client_type": 3 49 | }, 50 | { 51 | "client_id": "208498828836-139a9k718tbf1ijjmplm9sef21crjqv9.apps.googleusercontent.com", 52 | "client_type": 2, 53 | "ios_info": { 54 | "bundle_id": "jp.wasabeef.app" 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | } 61 | ], 62 | "configuration_version": "1" 63 | } -------------------------------------------------------------------------------- /android/app/src/production/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | FAB 4 | 5 | -------------------------------------------------------------------------------- /android/app_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.5.31' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.0.3' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | kotlin.stdlib.default.dependency=false -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/fonts/Rotunda-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/fonts/Rotunda-Bold.otf -------------------------------------------------------------------------------- /assets/images/2.0x/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/2.0x/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/2.0x/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/2.0x/icon_placeholder.jpg -------------------------------------------------------------------------------- /assets/images/3.0x/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/3.0x/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/3.0x/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/3.0x/icon_placeholder.jpg -------------------------------------------------------------------------------- /assets/images/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/images/icon_placeholder.jpg -------------------------------------------------------------------------------- /assets/svgs/news.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /assets/videos/bigbuckbunny.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/assets/videos/bigbuckbunny.mp4 -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '8' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: flutter 5 | trigger_map: 6 | - push_branch: main 7 | workflow: main-build 8 | - pull_request_source_branch: "*" 9 | workflow: pull-request-build 10 | workflows: 11 | main-build: 12 | steps: 13 | - activate-ssh-key@4: 14 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 15 | - git-clone@4: {} 16 | - cache-pull@2: {} 17 | - script@1: 18 | inputs: 19 | - content: |- 20 | #!/usr/bin/env bash 21 | make setup 22 | export PATH=$PATH:$HOME/.pub-cache/bin 23 | envman add --key PATH --value $PATH 24 | 25 | make dependencies 26 | title: Get flutter dependencies 27 | - script@1: 28 | inputs: 29 | - content: |- 30 | #!/usr/bin/env bash 31 | make format-analyze 32 | title: Check for any formatting and statically analyze the code 33 | - script@1: 34 | inputs: 35 | - content: |- 36 | #!/usr/bin/env bash 37 | make unit-test 38 | title: Run widget tests for our flutter project 39 | - codecov@2.0: 40 | inputs: 41 | - CODECOV_TOKEN: "$CODECOV_TOKEN" 42 | - script@1: 43 | inputs: 44 | - content: |- 45 | #!/usr/bin/env bash 46 | make build-android-prd 47 | make build-ios-prd 48 | title: Build ipa and apk 49 | - deploy-to-bitrise-io@1: {} 50 | - cache-push@2: {} 51 | - slack@3: 52 | inputs: 53 | - webhook_url: "$SLACK_WEBHOOK_URL" 54 | pull-request-build: 55 | steps: 56 | - activate-ssh-key@4: 57 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 58 | - git-clone@4: {} 59 | - cache-pull@2: { } 60 | - script@1: 61 | inputs: 62 | - content: |- 63 | #!/usr/bin/env bash 64 | make setup 65 | export PATH=$PATH:$HOME/.pub-cache/bin 66 | envman add --key PATH --value $PATH 67 | 68 | make dependencies 69 | title: Get flutter dependencies 70 | - script@1: 71 | inputs: 72 | - content: |- 73 | #!/usr/bin/env bash 74 | make format-analyze 75 | title: Check for any formatting and statically analyze the code 76 | - script@1: 77 | inputs: 78 | - content: |- 79 | #!/usr/bin/env bash 80 | make unit-test 81 | title: Run widget tests for our flutter project 82 | - codecov@2.0: 83 | inputs: 84 | - CODECOV_TOKEN: "$CODECOV_TOKEN" 85 | - script@1: 86 | inputs: 87 | - content: |- 88 | #!/usr/bin/env bash 89 | make build-android-prd 90 | make build-ios-prd 91 | title: Build ipa and apk 92 | - deploy-to-bitrise-io@1: {} 93 | - cache-push@2: {} 94 | - slack@3: 95 | inputs: 96 | - webhook_url: "$SLACK_WEBHOOK_URL" 97 | app: 98 | envs: 99 | - opts: 100 | is_expand: false 101 | BITRISE_FLUTTER_PROJECT_LOCATION: "." 102 | - opts: 103 | is_expand: false 104 | BITRISE_PROJECT_PATH: ios/Runner.xcworkspace 105 | - opts: 106 | is_expand: false 107 | BITRISE_SCHEME: Production 108 | - opts: 109 | is_expand: false 110 | BITRISE_EXPORT_METHOD: production 111 | - opts: 112 | is_expand: false 113 | ANDROID_SDK_VERSION: 29 114 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.io/docs/commit-status 2 | codecov: 3 | notify: 4 | require_ci_to_pass: yes 5 | 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | # basic 11 | target: 0% 12 | threshold: 0% 13 | base: 0% 14 | # advanced 15 | branches: null 16 | if_no_uploads: error 17 | if_not_found: success 18 | if_ci_failed: error 19 | only_pulls: false 20 | flags: null 21 | paths: null 22 | 23 | ignore: 24 | - "**/*.g.dart" 25 | - "**/*.freezed.dart" -------------------------------------------------------------------------------- /codemagic.yaml: -------------------------------------------------------------------------------- 1 | # Automatically generated on 2020-08-07 UTC from https://codemagic.io/app/5f2c1b6a8ff73d000f7bc1e1/settings 2 | # Note that this configuration is not an exact match to UI settings. Review and adjust as necessary. 3 | 4 | workflows: 5 | main-merge: 6 | name: Main Workflow 7 | max_build_duration: 60 8 | environment: 9 | vars: 10 | FCI_FLUTTER_SCHEME: Production 11 | ANDROID_SDK_VERSION: 30 12 | CODECOV_TOKEN: Encrypted(Z0FBQUFBQmZMOXV6aEw2a0VyRWE0VTljem1XeG95MVFQRm5YNUhUaU94SE1FcU5pdDRya1ZnM29DR2FQOGpJS1dydm14UkU4eFE4TzdReDZZWlIxU1lEcU13dm8zRHdQd211R3pTNGFIeHJwUXE3N3VxTk1iMndYLWFnSE1JMHA2TndHVkw3MWRmbU0=) 13 | flutter: beta 14 | xcode: latest 15 | cocoapods: default 16 | cache: 17 | cache_paths: 18 | - $FLUTTER_ROOT/.pub-cache 19 | - $FCI_BUILD_DIR/ios/Pods 20 | - $FCI_BUILD_DIR/android/.gradle 21 | triggering: 22 | events: 23 | - push 24 | - pull_request 25 | - tag 26 | branch_patterns: 27 | - pattern: main 28 | include: true 29 | source: true 30 | scripts: 31 | - name: Setup local properties 32 | script: | 33 | # set up local properties 34 | echo "flutter.sdk=$HOME/programs/flutter" > "$FCI_BUILD_DIR/android/local.properties" 35 | 36 | - name: Get flutter dependencies 37 | script: | 38 | make setup 39 | export PATH="$PATH":"$HOME/.pub-cache/bin" 40 | 41 | make dependencies 42 | 43 | - name: Check for any formatting and statically analyze the code 44 | script: make format-analyze 45 | 46 | - name: Flutter Unit test 47 | script: make unit-test 48 | 49 | - name: Codecov upload 50 | script: make codecov 51 | 52 | - name: Build 53 | script: | 54 | #!/bin/sh 55 | set -e # exit on first failed commandset 56 | set -x # print all executed commands to the log 57 | /usr/bin/plutil -replace CFBundleIdentifier -string jp.wasabeef.app ios/Runner/Info.plist 58 | make build-android-prd 59 | make build-ios-prd 60 | 61 | artifacts: 62 | - build/**/outputs/**/*.apk 63 | - build/**/outputs/**/*.aab 64 | - build/**/outputs/**/mapping.txt 65 | - build/ios/ipa/*.ipa 66 | - /tmp/xcodebuild_logs/*.log 67 | - flutter_drive.log 68 | 69 | publishing: 70 | slack: 71 | channel: '#develop' 72 | notify_on_build_start: false 73 | github_releases: 74 | prerelease: false 75 | artifact_patterns: 76 | - 'app-production-release.apk' 77 | - 'Runner.ipa' -------------------------------------------------------------------------------- /ios/Config/Development.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Flutter/Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release-development.xcconfig" 3 | TRACK_WIDGET_CREATION= 4 | 5 | FLUTTER_FLAVOR=development 6 | FLUTTER_TARGET=$PROJECT_DIR/../lib/main.dart 7 | PRODUCT_BUNDLE_IDENTIFIER=jp.wasabeef.app.dev 8 | DISPLAY_NAME=FAB-dev 9 | 10 | FIREBASE_DIR=$PROJECT_DIR/$PROJECT_NAME/Resources/Firebase/Development 11 | 12 | // GOOGLE prefix key configs were copied from GoogleService-Info.plist 13 | GOOGLE_REVERSED_CLIENT_ID = com.googleusercontent.apps.339920746703-140i3bhk3lqtpn2p2e7035btcp61p32i 14 | -------------------------------------------------------------------------------- /ios/Config/Production.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Flutter/Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release-production.xcconfig" 3 | TRACK_WIDGET_CREATION= 4 | 5 | FLUTTER_FLAVOR=production 6 | FLUTTER_TARGET=$PROJECT_DIR/../lib/main.dart 7 | PRODUCT_BUNDLE_IDENTIFIER=jp.wasabeef.app 8 | DISPLAY_NAME=FAB 9 | 10 | FIREBASE_DIR=$PROJECT_DIR/$PROJECT_NAME/Resources/Firebase/Production 11 | 12 | // GOOGLE prefix key configs were copied from GoogleService-Info.plist 13 | GOOGLE_REVERSED_CLIENT_ID = com.googleusercontent.apps.208498828836-139a9k718tbf1ijjmplm9sef21crjqv9 14 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug-Development' => :debug, 9 | 'Debug-Production' => :debug, 10 | 'Release-Development' => :release, 11 | 'Release-Production' => :release, 12 | } 13 | 14 | def flutter_root 15 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 16 | unless File.exist?(generated_xcode_build_settings_path) 17 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 18 | end 19 | 20 | File.foreach(generated_xcode_build_settings_path) do |line| 21 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 22 | return matches[1].strip if matches 23 | end 24 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 25 | end 26 | 27 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 28 | 29 | flutter_ios_podfile_setup 30 | 31 | target 'Runner' do 32 | use_frameworks! 33 | use_modular_headers! 34 | 35 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_ios_build_settings(target) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AppAuth (1.4.0): 3 | - AppAuth/Core (= 1.4.0) 4 | - AppAuth/ExternalUserAgent (= 1.4.0) 5 | - AppAuth/Core (1.4.0) 6 | - AppAuth/ExternalUserAgent (1.4.0) 7 | - Firebase/Auth (8.8.0): 8 | - Firebase/CoreOnly 9 | - FirebaseAuth (~> 8.8.0) 10 | - Firebase/CoreOnly (8.8.0): 11 | - FirebaseCore (= 8.8.0) 12 | - Firebase/Crashlytics (8.8.0): 13 | - Firebase/CoreOnly 14 | - FirebaseCrashlytics (~> 8.8.0) 15 | - Firebase/Performance (8.8.0): 16 | - Firebase/CoreOnly 17 | - FirebasePerformance (~> 8.8.0) 18 | - firebase_auth (3.1.4): 19 | - Firebase/Auth (= 8.8.0) 20 | - firebase_core 21 | - Flutter 22 | - firebase_core (1.8.0): 23 | - Firebase/CoreOnly (= 8.8.0) 24 | - Flutter 25 | - firebase_crashlytics (2.2.4): 26 | - Firebase/Crashlytics (= 8.8.0) 27 | - firebase_core 28 | - Flutter 29 | - firebase_performance (0.7.1-2): 30 | - Firebase/Performance (= 8.8.0) 31 | - firebase_core 32 | - Flutter 33 | - FirebaseABTesting (8.8.0): 34 | - FirebaseCore (~> 8.0) 35 | - FirebaseAuth (8.8.0): 36 | - FirebaseCore (~> 8.0) 37 | - GoogleUtilities/AppDelegateSwizzler (~> 7.4) 38 | - GoogleUtilities/Environment (~> 7.4) 39 | - GTMSessionFetcher/Core (~> 1.5) 40 | - FirebaseCore (8.8.0): 41 | - FirebaseCoreDiagnostics (~> 8.0) 42 | - GoogleUtilities/Environment (~> 7.4) 43 | - GoogleUtilities/Logger (~> 7.4) 44 | - FirebaseCoreDiagnostics (8.8.0): 45 | - GoogleDataTransport (~> 9.0) 46 | - GoogleUtilities/Environment (~> 7.4) 47 | - GoogleUtilities/Logger (~> 7.4) 48 | - nanopb (~> 2.30908.0) 49 | - FirebaseCrashlytics (8.8.0): 50 | - FirebaseCore (~> 8.0) 51 | - FirebaseInstallations (~> 8.0) 52 | - GoogleDataTransport (~> 9.0) 53 | - GoogleUtilities/Environment (~> 7.4) 54 | - nanopb (~> 2.30908.0) 55 | - PromisesObjC (< 3.0, >= 1.2) 56 | - FirebaseInstallations (8.8.0): 57 | - FirebaseCore (~> 8.0) 58 | - GoogleUtilities/Environment (~> 7.4) 59 | - GoogleUtilities/UserDefaults (~> 7.4) 60 | - PromisesObjC (< 3.0, >= 1.2) 61 | - FirebasePerformance (8.8.0): 62 | - FirebaseCore (~> 8.0) 63 | - FirebaseInstallations (~> 8.0) 64 | - FirebaseRemoteConfig (~> 8.0) 65 | - GoogleDataTransport (~> 9.0) 66 | - GoogleUtilities/Environment (~> 7.4) 67 | - GoogleUtilities/ISASwizzler (~> 7.4) 68 | - GoogleUtilities/MethodSwizzler (~> 7.4) 69 | - nanopb (~> 2.30908.0) 70 | - FirebaseRemoteConfig (8.8.0): 71 | - FirebaseABTesting (~> 8.0) 72 | - FirebaseCore (~> 8.0) 73 | - FirebaseInstallations (~> 8.0) 74 | - GoogleUtilities/Environment (~> 7.4) 75 | - "GoogleUtilities/NSData+zlib (~> 7.4)" 76 | - Flutter (1.0.0) 77 | - google_sign_in (0.0.1): 78 | - Flutter 79 | - GoogleSignIn (~> 5.0) 80 | - GoogleDataTransport (9.1.2): 81 | - GoogleUtilities/Environment (~> 7.2) 82 | - nanopb (~> 2.30908.0) 83 | - PromisesObjC (< 3.0, >= 1.2) 84 | - GoogleSignIn (5.0.2): 85 | - AppAuth (~> 1.2) 86 | - GTMAppAuth (~> 1.0) 87 | - GTMSessionFetcher/Core (~> 1.1) 88 | - GoogleUtilities/AppDelegateSwizzler (7.6.0): 89 | - GoogleUtilities/Environment 90 | - GoogleUtilities/Logger 91 | - GoogleUtilities/Network 92 | - GoogleUtilities/Environment (7.6.0): 93 | - PromisesObjC (< 3.0, >= 1.2) 94 | - GoogleUtilities/ISASwizzler (7.6.0) 95 | - GoogleUtilities/Logger (7.6.0): 96 | - GoogleUtilities/Environment 97 | - GoogleUtilities/MethodSwizzler (7.6.0): 98 | - GoogleUtilities/Logger 99 | - GoogleUtilities/Network (7.6.0): 100 | - GoogleUtilities/Logger 101 | - "GoogleUtilities/NSData+zlib" 102 | - GoogleUtilities/Reachability 103 | - "GoogleUtilities/NSData+zlib (7.6.0)" 104 | - GoogleUtilities/Reachability (7.6.0): 105 | - GoogleUtilities/Logger 106 | - GoogleUtilities/UserDefaults (7.6.0): 107 | - GoogleUtilities/Logger 108 | - GTMAppAuth (1.2.2): 109 | - AppAuth/Core (~> 1.4) 110 | - GTMSessionFetcher/Core (~> 1.5) 111 | - GTMSessionFetcher/Core (1.7.0) 112 | - nanopb (2.30908.0): 113 | - nanopb/decode (= 2.30908.0) 114 | - nanopb/encode (= 2.30908.0) 115 | - nanopb/decode (2.30908.0) 116 | - nanopb/encode (2.30908.0) 117 | - path_provider (0.0.1): 118 | - Flutter 119 | - PromisesObjC (2.0.0) 120 | - ua_client_hints (1.1.0): 121 | - Flutter 122 | - video_player (0.0.1): 123 | - Flutter 124 | 125 | DEPENDENCIES: 126 | - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) 127 | - firebase_core (from `.symlinks/plugins/firebase_core/ios`) 128 | - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) 129 | - firebase_performance (from `.symlinks/plugins/firebase_performance/ios`) 130 | - Flutter (from `Flutter`) 131 | - google_sign_in (from `.symlinks/plugins/google_sign_in/ios`) 132 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 133 | - ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`) 134 | - video_player (from `.symlinks/plugins/video_player/ios`) 135 | 136 | SPEC REPOS: 137 | trunk: 138 | - AppAuth 139 | - Firebase 140 | - FirebaseABTesting 141 | - FirebaseAuth 142 | - FirebaseCore 143 | - FirebaseCoreDiagnostics 144 | - FirebaseCrashlytics 145 | - FirebaseInstallations 146 | - FirebasePerformance 147 | - FirebaseRemoteConfig 148 | - GoogleDataTransport 149 | - GoogleSignIn 150 | - GoogleUtilities 151 | - GTMAppAuth 152 | - GTMSessionFetcher 153 | - nanopb 154 | - PromisesObjC 155 | 156 | EXTERNAL SOURCES: 157 | firebase_auth: 158 | :path: ".symlinks/plugins/firebase_auth/ios" 159 | firebase_core: 160 | :path: ".symlinks/plugins/firebase_core/ios" 161 | firebase_crashlytics: 162 | :path: ".symlinks/plugins/firebase_crashlytics/ios" 163 | firebase_performance: 164 | :path: ".symlinks/plugins/firebase_performance/ios" 165 | Flutter: 166 | :path: Flutter 167 | google_sign_in: 168 | :path: ".symlinks/plugins/google_sign_in/ios" 169 | path_provider: 170 | :path: ".symlinks/plugins/path_provider/ios" 171 | ua_client_hints: 172 | :path: ".symlinks/plugins/ua_client_hints/ios" 173 | video_player: 174 | :path: ".symlinks/plugins/video_player/ios" 175 | 176 | SPEC CHECKSUMS: 177 | AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 178 | Firebase: 629510f1a9ddb235f3a7c5c8ceb23ba887f0f814 179 | firebase_auth: 0b7168e2db1bff6e5b40a7dc268447b8dedb59c1 180 | firebase_core: 3b4c707f5a8eff38f52fd5580895bcd89357bf42 181 | firebase_crashlytics: 5e4c7b5695a7ffe144a55dacfddebbf8eb36028a 182 | firebase_performance: 01839bfbbc5c98b00b5a9172328f5bc21059e5ef 183 | FirebaseABTesting: 981336dd14d84787e33466e4247f77ec2343f8d9 184 | FirebaseAuth: bcf0adeff88bda5dcb3beeabe5760f1226ab7b2f 185 | FirebaseCore: 98b29e3828f0a53651c363937a7f7d92a19f1ba2 186 | FirebaseCoreDiagnostics: fe77f42da6329d6d83d21fd9d621a6b704413bfc 187 | FirebaseCrashlytics: 3660c045c8e45cc4276110562a0ef44cf43c8157 188 | FirebaseInstallations: 2563cb18a723ef9c6ef18318a49519b75dce613c 189 | FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c 190 | FirebaseRemoteConfig: f6365883d7950d784ee97bcdbbf1e442d4fa6119 191 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 192 | google_sign_in: c5cecea71f3be43282263550556e311c4cc03582 193 | GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 194 | GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 195 | GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 196 | GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89 197 | GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 198 | nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 199 | path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de 200 | PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 201 | ua_client_hints: 314de6b4221bf713b1a6a5fb8bc68917ad3eaaec 202 | video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb 203 | 204 | PODFILE CHECKSUM: af976f751d801ced1cf35e237c7db47ea4b2ec3e 205 | 206 | COCOAPODS: 1.11.2 207 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Development.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Production.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | PreviewsEnabled 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | $(DISPLAY_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | app 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleIdentifier 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLName 31 | SNS Sign In 32 | CFBundleURLSchemes 33 | 34 | $(GOOGLE_REVERSED_CLIENT_ID) 35 | 36 | 37 | 38 | CFBundleVersion 39 | $(FLUTTER_BUILD_NUMBER) 40 | LSRequiresIPhoneOS 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | UIViewControllerBasedStatusBarAppearance 60 | 61 | io.flutter.embedded_views_preview 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ios/Runner/Resources/Firebase/Development/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 339920746703-140i3bhk3lqtpn2p2e7035btcp61p32i.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.339920746703-140i3bhk3lqtpn2p2e7035btcp61p32i 9 | ANDROID_CLIENT_ID 10 | 339920746703-1omo33mfc8f7cfjm8pa8qutqpu2q3h7c.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyCxPP2BfUCFmWkYSNswVtT3PQrVXnAQJkg 13 | GCM_SENDER_ID 14 | 339920746703 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | jp.wasabeef.app.dev 19 | PROJECT_ID 20 | flutter-arch-blueprints-dev 21 | STORAGE_BUCKET 22 | flutter-arch-blueprints-dev.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:339920746703:ios:96090bb3427da435ee9fba 35 | DATABASE_URL 36 | https://flutter-arch-blueprints-dev.firebaseio.com 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Resources/Firebase/Production/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 208498828836-139a9k718tbf1ijjmplm9sef21crjqv9.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.208498828836-139a9k718tbf1ijjmplm9sef21crjqv9 9 | ANDROID_CLIENT_ID 10 | 208498828836-3323hr2v1b84i3s8sv4921462l1vtc1i.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyAjP8sr4gOdweOwfbrXKY0ya_hqP7GS2nY 13 | GCM_SENDER_ID 14 | 208498828836 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | jp.wasabeef.app 19 | PROJECT_ID 20 | flutter-arc-blueprints 21 | STORAGE_BUCKET 22 | flutter-arc-blueprints.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:208498828836:ios:f3fcdda35b7c7aa16b5ab1 35 | DATABASE_URL 36 | https://flutter-arc-blueprints.firebaseio.com 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/ja.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Runner/ja.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Scripts/copy_google_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BASE_PATH=$PROJECT_DIR/$PROJECT_NAME 4 | 5 | echo execute copy GoogleService-Info.plist 6 | FIREBASE_INFO_PLIST="GoogleService-Info.plist" 7 | FIREBASE_INFO_PLIST_DST=$BASE_PATH/$FIREBASE_INFO_PLIST 8 | FIREBASE_INFO_PLIST_SRC=$FIREBASE_DIR/$FIREBASE_INFO_PLIST 9 | 10 | echo $FIREBASE_INFO_PLIST_SRC 11 | cp -Rf $FIREBASE_INFO_PLIST_SRC $FIREBASE_INFO_PLIST_DST 12 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: intl_messages_en.arb 3 | output-localization-file: l10n.dart 4 | output-class: L10n 5 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/route/app_route.dart'; 2 | import 'package:app/ui/theme/app_theme.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | class App extends HookConsumerWidget { 9 | const App({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final theme = ref.watch(appThemeProvider); 14 | final themeMode = ref.watch(appThemeModeProvider); 15 | final appRouter = useMemoized(() => AppRouter()); 16 | return MaterialApp.router( 17 | theme: theme.data, 18 | darkTheme: AppTheme.dark().data, 19 | themeMode: themeMode, 20 | localizationsDelegates: L10n.localizationsDelegates, 21 | supportedLocales: L10n.supportedLocales, 22 | routeInformationParser: appRouter.defaultRouteParser(), 23 | routerDelegate: appRouter.delegate(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/data/app_error.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | enum AppErrorType { 7 | network, 8 | badRequest, 9 | unauthorized, 10 | cancel, 11 | timeout, 12 | server, 13 | unknown, 14 | } 15 | 16 | class AppError { 17 | late String message; 18 | late AppErrorType type; 19 | 20 | AppError(Exception? error) { 21 | if (error is DioError) { 22 | debugPrint('AppError(DioError): ' 23 | 'type is ${error.type}, message is ${error.message}'); 24 | message = error.message; 25 | switch (error.type) { 26 | case DioErrorType.other: 27 | if (error.error is SocketException) { 28 | // SocketException: Failed host lookup: '***' 29 | // (OS Error: No address associated with hostname, errno = 7) 30 | type = AppErrorType.network; 31 | } else { 32 | type = AppErrorType.unknown; 33 | } 34 | break; 35 | case DioErrorType.connectTimeout: 36 | case DioErrorType.receiveTimeout: 37 | type = AppErrorType.timeout; 38 | break; 39 | case DioErrorType.sendTimeout: 40 | type = AppErrorType.network; 41 | break; 42 | case DioErrorType.response: 43 | // TODO(api): need define more http status; 44 | switch (error.response?.statusCode) { 45 | case HttpStatus.badRequest: // 400 46 | type = AppErrorType.badRequest; 47 | break; 48 | case HttpStatus.unauthorized: // 401 49 | type = AppErrorType.unauthorized; 50 | break; 51 | case HttpStatus.internalServerError: // 500 52 | case HttpStatus.badGateway: // 502 53 | case HttpStatus.serviceUnavailable: // 503 54 | case HttpStatus.gatewayTimeout: // 504 55 | type = AppErrorType.server; 56 | break; 57 | default: 58 | type = AppErrorType.unknown; 59 | break; 60 | } 61 | break; 62 | case DioErrorType.cancel: 63 | type = AppErrorType.cancel; 64 | break; 65 | default: 66 | type = AppErrorType.unknown; 67 | } 68 | } else { 69 | debugPrint('AppError(UnKnown): $error'); 70 | type = AppErrorType.unknown; 71 | message = 'AppError: $error'; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/data/local/app_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'app_user.freezed.dart'; 5 | 6 | @freezed 7 | abstract class AppUser with _$AppUser { 8 | factory AppUser({ 9 | String? userId, 10 | String? imageUrl, 11 | String? name, 12 | String? email, 13 | }) = _AppUser; 14 | 15 | factory AppUser.from(User? firebaseUser) { 16 | return AppUser( 17 | userId: firebaseUser?.uid, 18 | imageUrl: firebaseUser?.photoURL, 19 | name: firebaseUser?.displayName, 20 | email: firebaseUser?.email, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/data/local/app_user.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 4 | 5 | part of 'app_user.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | /// @nodoc 17 | class _$AppUserTearOff { 18 | const _$AppUserTearOff(); 19 | 20 | _AppUser call( 21 | {String? userId, String? imageUrl, String? name, String? email}) { 22 | return _AppUser( 23 | userId: userId, 24 | imageUrl: imageUrl, 25 | name: name, 26 | email: email, 27 | ); 28 | } 29 | } 30 | 31 | /// @nodoc 32 | const $AppUser = _$AppUserTearOff(); 33 | 34 | /// @nodoc 35 | mixin _$AppUser { 36 | String? get userId => throw _privateConstructorUsedError; 37 | String? get imageUrl => throw _privateConstructorUsedError; 38 | String? get name => throw _privateConstructorUsedError; 39 | String? get email => throw _privateConstructorUsedError; 40 | 41 | @JsonKey(ignore: true) 42 | $AppUserCopyWith get copyWith => throw _privateConstructorUsedError; 43 | } 44 | 45 | /// @nodoc 46 | abstract class $AppUserCopyWith<$Res> { 47 | factory $AppUserCopyWith(AppUser value, $Res Function(AppUser) then) = 48 | _$AppUserCopyWithImpl<$Res>; 49 | $Res call({String? userId, String? imageUrl, String? name, String? email}); 50 | } 51 | 52 | /// @nodoc 53 | class _$AppUserCopyWithImpl<$Res> implements $AppUserCopyWith<$Res> { 54 | _$AppUserCopyWithImpl(this._value, this._then); 55 | 56 | final AppUser _value; 57 | // ignore: unused_field 58 | final $Res Function(AppUser) _then; 59 | 60 | @override 61 | $Res call({ 62 | Object? userId = freezed, 63 | Object? imageUrl = freezed, 64 | Object? name = freezed, 65 | Object? email = freezed, 66 | }) { 67 | return _then(_value.copyWith( 68 | userId: userId == freezed 69 | ? _value.userId 70 | : userId // ignore: cast_nullable_to_non_nullable 71 | as String?, 72 | imageUrl: imageUrl == freezed 73 | ? _value.imageUrl 74 | : imageUrl // ignore: cast_nullable_to_non_nullable 75 | as String?, 76 | name: name == freezed 77 | ? _value.name 78 | : name // ignore: cast_nullable_to_non_nullable 79 | as String?, 80 | email: email == freezed 81 | ? _value.email 82 | : email // ignore: cast_nullable_to_non_nullable 83 | as String?, 84 | )); 85 | } 86 | } 87 | 88 | /// @nodoc 89 | abstract class _$AppUserCopyWith<$Res> implements $AppUserCopyWith<$Res> { 90 | factory _$AppUserCopyWith(_AppUser value, $Res Function(_AppUser) then) = 91 | __$AppUserCopyWithImpl<$Res>; 92 | @override 93 | $Res call({String? userId, String? imageUrl, String? name, String? email}); 94 | } 95 | 96 | /// @nodoc 97 | class __$AppUserCopyWithImpl<$Res> extends _$AppUserCopyWithImpl<$Res> 98 | implements _$AppUserCopyWith<$Res> { 99 | __$AppUserCopyWithImpl(_AppUser _value, $Res Function(_AppUser) _then) 100 | : super(_value, (v) => _then(v as _AppUser)); 101 | 102 | @override 103 | _AppUser get _value => super._value as _AppUser; 104 | 105 | @override 106 | $Res call({ 107 | Object? userId = freezed, 108 | Object? imageUrl = freezed, 109 | Object? name = freezed, 110 | Object? email = freezed, 111 | }) { 112 | return _then(_AppUser( 113 | userId: userId == freezed 114 | ? _value.userId 115 | : userId // ignore: cast_nullable_to_non_nullable 116 | as String?, 117 | imageUrl: imageUrl == freezed 118 | ? _value.imageUrl 119 | : imageUrl // ignore: cast_nullable_to_non_nullable 120 | as String?, 121 | name: name == freezed 122 | ? _value.name 123 | : name // ignore: cast_nullable_to_non_nullable 124 | as String?, 125 | email: email == freezed 126 | ? _value.email 127 | : email // ignore: cast_nullable_to_non_nullable 128 | as String?, 129 | )); 130 | } 131 | } 132 | 133 | /// @nodoc 134 | 135 | class _$_AppUser implements _AppUser { 136 | _$_AppUser({this.userId, this.imageUrl, this.name, this.email}); 137 | 138 | @override 139 | final String? userId; 140 | @override 141 | final String? imageUrl; 142 | @override 143 | final String? name; 144 | @override 145 | final String? email; 146 | 147 | @override 148 | String toString() { 149 | return 'AppUser(userId: $userId, imageUrl: $imageUrl, name: $name, email: $email)'; 150 | } 151 | 152 | @override 153 | bool operator ==(dynamic other) { 154 | return identical(this, other) || 155 | (other is _AppUser && 156 | (identical(other.userId, userId) || 157 | const DeepCollectionEquality().equals(other.userId, userId)) && 158 | (identical(other.imageUrl, imageUrl) || 159 | const DeepCollectionEquality() 160 | .equals(other.imageUrl, imageUrl)) && 161 | (identical(other.name, name) || 162 | const DeepCollectionEquality().equals(other.name, name)) && 163 | (identical(other.email, email) || 164 | const DeepCollectionEquality().equals(other.email, email))); 165 | } 166 | 167 | @override 168 | int get hashCode => 169 | runtimeType.hashCode ^ 170 | const DeepCollectionEquality().hash(userId) ^ 171 | const DeepCollectionEquality().hash(imageUrl) ^ 172 | const DeepCollectionEquality().hash(name) ^ 173 | const DeepCollectionEquality().hash(email); 174 | 175 | @JsonKey(ignore: true) 176 | @override 177 | _$AppUserCopyWith<_AppUser> get copyWith => 178 | __$AppUserCopyWithImpl<_AppUser>(this, _$identity); 179 | } 180 | 181 | abstract class _AppUser implements AppUser { 182 | factory _AppUser( 183 | {String? userId, 184 | String? imageUrl, 185 | String? name, 186 | String? email}) = _$_AppUser; 187 | 188 | @override 189 | String? get userId => throw _privateConstructorUsedError; 190 | @override 191 | String? get imageUrl => throw _privateConstructorUsedError; 192 | @override 193 | String? get name => throw _privateConstructorUsedError; 194 | @override 195 | String? get email => throw _privateConstructorUsedError; 196 | @override 197 | @JsonKey(ignore: true) 198 | _$AppUserCopyWith<_AppUser> get copyWith => 199 | throw _privateConstructorUsedError; 200 | } 201 | -------------------------------------------------------------------------------- /lib/data/model/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/source.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'article.freezed.dart'; 5 | part 'article.g.dart'; 6 | 7 | @freezed 8 | abstract class Article with _$Article { 9 | factory Article({ 10 | Source? source, 11 | String? author, 12 | String? title, 13 | String? description, 14 | String? url, 15 | String? urlToImage, 16 | DateTime? publishedAt, 17 | String? content, 18 | }) = _Article; 19 | 20 | factory Article.fromJson(Map json) => 21 | _$ArticleFromJson(json); 22 | } 23 | -------------------------------------------------------------------------------- /lib/data/model/article.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Article _$$_ArticleFromJson(Map json) => _$_Article( 10 | source: json['source'] == null 11 | ? null 12 | : Source.fromJson(json['source'] as Map), 13 | author: json['author'] as String?, 14 | title: json['title'] as String?, 15 | description: json['description'] as String?, 16 | url: json['url'] as String?, 17 | urlToImage: json['urlToImage'] as String?, 18 | publishedAt: json['publishedAt'] == null 19 | ? null 20 | : DateTime.parse(json['publishedAt'] as String), 21 | content: json['content'] as String?, 22 | ); 23 | 24 | Map _$$_ArticleToJson(_$_Article instance) => 25 | { 26 | 'source': instance.source, 27 | 'author': instance.author, 28 | 'title': instance.title, 29 | 'description': instance.description, 30 | 'url': instance.url, 31 | 'urlToImage': instance.urlToImage, 32 | 'publishedAt': instance.publishedAt?.toIso8601String(), 33 | 'content': instance.content, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/data/model/news.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/article.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'news.freezed.dart'; 5 | part 'news.g.dart'; 6 | 7 | @freezed 8 | abstract class News with _$News { 9 | factory News({ 10 | required String status, 11 | required int totalResults, 12 | required List
articles, 13 | }) = _News; 14 | 15 | factory News.fromJson(Map json) => _$NewsFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/model/news.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 4 | 5 | part of 'news.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | News _$NewsFromJson(Map json) { 17 | return _News.fromJson(json); 18 | } 19 | 20 | /// @nodoc 21 | class _$NewsTearOff { 22 | const _$NewsTearOff(); 23 | 24 | _News call( 25 | {required String status, 26 | required int totalResults, 27 | required List
articles}) { 28 | return _News( 29 | status: status, 30 | totalResults: totalResults, 31 | articles: articles, 32 | ); 33 | } 34 | 35 | News fromJson(Map json) { 36 | return News.fromJson(json); 37 | } 38 | } 39 | 40 | /// @nodoc 41 | const $News = _$NewsTearOff(); 42 | 43 | /// @nodoc 44 | mixin _$News { 45 | String get status => throw _privateConstructorUsedError; 46 | int get totalResults => throw _privateConstructorUsedError; 47 | List
get articles => throw _privateConstructorUsedError; 48 | 49 | Map toJson() => throw _privateConstructorUsedError; 50 | @JsonKey(ignore: true) 51 | $NewsCopyWith get copyWith => throw _privateConstructorUsedError; 52 | } 53 | 54 | /// @nodoc 55 | abstract class $NewsCopyWith<$Res> { 56 | factory $NewsCopyWith(News value, $Res Function(News) then) = 57 | _$NewsCopyWithImpl<$Res>; 58 | $Res call({String status, int totalResults, List
articles}); 59 | } 60 | 61 | /// @nodoc 62 | class _$NewsCopyWithImpl<$Res> implements $NewsCopyWith<$Res> { 63 | _$NewsCopyWithImpl(this._value, this._then); 64 | 65 | final News _value; 66 | // ignore: unused_field 67 | final $Res Function(News) _then; 68 | 69 | @override 70 | $Res call({ 71 | Object? status = freezed, 72 | Object? totalResults = freezed, 73 | Object? articles = freezed, 74 | }) { 75 | return _then(_value.copyWith( 76 | status: status == freezed 77 | ? _value.status 78 | : status // ignore: cast_nullable_to_non_nullable 79 | as String, 80 | totalResults: totalResults == freezed 81 | ? _value.totalResults 82 | : totalResults // ignore: cast_nullable_to_non_nullable 83 | as int, 84 | articles: articles == freezed 85 | ? _value.articles 86 | : articles // ignore: cast_nullable_to_non_nullable 87 | as List
, 88 | )); 89 | } 90 | } 91 | 92 | /// @nodoc 93 | abstract class _$NewsCopyWith<$Res> implements $NewsCopyWith<$Res> { 94 | factory _$NewsCopyWith(_News value, $Res Function(_News) then) = 95 | __$NewsCopyWithImpl<$Res>; 96 | @override 97 | $Res call({String status, int totalResults, List
articles}); 98 | } 99 | 100 | /// @nodoc 101 | class __$NewsCopyWithImpl<$Res> extends _$NewsCopyWithImpl<$Res> 102 | implements _$NewsCopyWith<$Res> { 103 | __$NewsCopyWithImpl(_News _value, $Res Function(_News) _then) 104 | : super(_value, (v) => _then(v as _News)); 105 | 106 | @override 107 | _News get _value => super._value as _News; 108 | 109 | @override 110 | $Res call({ 111 | Object? status = freezed, 112 | Object? totalResults = freezed, 113 | Object? articles = freezed, 114 | }) { 115 | return _then(_News( 116 | status: status == freezed 117 | ? _value.status 118 | : status // ignore: cast_nullable_to_non_nullable 119 | as String, 120 | totalResults: totalResults == freezed 121 | ? _value.totalResults 122 | : totalResults // ignore: cast_nullable_to_non_nullable 123 | as int, 124 | articles: articles == freezed 125 | ? _value.articles 126 | : articles // ignore: cast_nullable_to_non_nullable 127 | as List
, 128 | )); 129 | } 130 | } 131 | 132 | /// @nodoc 133 | @JsonSerializable() 134 | class _$_News implements _News { 135 | _$_News( 136 | {required this.status, 137 | required this.totalResults, 138 | required this.articles}); 139 | 140 | factory _$_News.fromJson(Map json) => _$$_NewsFromJson(json); 141 | 142 | @override 143 | final String status; 144 | @override 145 | final int totalResults; 146 | @override 147 | final List
articles; 148 | 149 | @override 150 | String toString() { 151 | return 'News(status: $status, totalResults: $totalResults, articles: $articles)'; 152 | } 153 | 154 | @override 155 | bool operator ==(dynamic other) { 156 | return identical(this, other) || 157 | (other is _News && 158 | (identical(other.status, status) || 159 | const DeepCollectionEquality().equals(other.status, status)) && 160 | (identical(other.totalResults, totalResults) || 161 | const DeepCollectionEquality() 162 | .equals(other.totalResults, totalResults)) && 163 | (identical(other.articles, articles) || 164 | const DeepCollectionEquality() 165 | .equals(other.articles, articles))); 166 | } 167 | 168 | @override 169 | int get hashCode => 170 | runtimeType.hashCode ^ 171 | const DeepCollectionEquality().hash(status) ^ 172 | const DeepCollectionEquality().hash(totalResults) ^ 173 | const DeepCollectionEquality().hash(articles); 174 | 175 | @JsonKey(ignore: true) 176 | @override 177 | _$NewsCopyWith<_News> get copyWith => 178 | __$NewsCopyWithImpl<_News>(this, _$identity); 179 | 180 | @override 181 | Map toJson() { 182 | return _$$_NewsToJson(this); 183 | } 184 | } 185 | 186 | abstract class _News implements News { 187 | factory _News( 188 | {required String status, 189 | required int totalResults, 190 | required List
articles}) = _$_News; 191 | 192 | factory _News.fromJson(Map json) = _$_News.fromJson; 193 | 194 | @override 195 | String get status => throw _privateConstructorUsedError; 196 | @override 197 | int get totalResults => throw _privateConstructorUsedError; 198 | @override 199 | List
get articles => throw _privateConstructorUsedError; 200 | @override 201 | @JsonKey(ignore: true) 202 | _$NewsCopyWith<_News> get copyWith => throw _privateConstructorUsedError; 203 | } 204 | -------------------------------------------------------------------------------- /lib/data/model/news.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'news.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_News _$$_NewsFromJson(Map json) => _$_News( 10 | status: json['status'] as String, 11 | totalResults: json['totalResults'] as int, 12 | articles: (json['articles'] as List) 13 | .map((e) => Article.fromJson(e as Map)) 14 | .toList(), 15 | ); 16 | 17 | Map _$$_NewsToJson(_$_News instance) => { 18 | 'status': instance.status, 19 | 'totalResults': instance.totalResults, 20 | 'articles': instance.articles, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/data/model/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/app_error.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | part 'result.freezed.dart'; 6 | 7 | @freezed 8 | abstract class Result with _$Result { 9 | const Result._(); 10 | 11 | const factory Result.success({required T data}) = Success; 12 | 13 | const factory Result.failure({required AppError error}) = Failure; 14 | 15 | static Result guard(T Function() body) { 16 | try { 17 | return Result.success(data: body()); 18 | } on Exception catch (e) { 19 | return Result.failure(error: AppError(e)); 20 | } 21 | } 22 | 23 | static Future> guardFuture(Future Function() future) async { 24 | try { 25 | return Result.success(data: await future()); 26 | } on Exception catch (e) { 27 | return Result.failure(error: AppError(e)); 28 | } 29 | } 30 | 31 | bool get isSuccess => when(success: (data) => true, failure: (e) => false); 32 | 33 | bool get isFailure => !isSuccess; 34 | 35 | void ifSuccess(Function(T data) body) { 36 | maybeWhen( 37 | success: (data) => body(data), 38 | orElse: () { 39 | // no-op 40 | }, 41 | ); 42 | } 43 | 44 | void ifFailure(Function(AppError e) body) { 45 | maybeWhen( 46 | failure: (e) => body(e), 47 | orElse: () { 48 | // no-op 49 | }, 50 | ); 51 | } 52 | 53 | T get dataOrThrow { 54 | return when( 55 | success: (data) => data, 56 | failure: (e) => throw e, 57 | ); 58 | } 59 | } 60 | 61 | extension ResultObjectExt on T { 62 | Result get asSuccess => Result.success(data: this); 63 | 64 | Result asFailure(Exception e) => Result.failure(error: AppError(e)); 65 | } 66 | -------------------------------------------------------------------------------- /lib/data/model/result.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 4 | 5 | part of 'result.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | /// @nodoc 17 | class _$ResultTearOff { 18 | const _$ResultTearOff(); 19 | 20 | Success success({required T data}) { 21 | return Success( 22 | data: data, 23 | ); 24 | } 25 | 26 | Failure failure({required AppError error}) { 27 | return Failure( 28 | error: error, 29 | ); 30 | } 31 | } 32 | 33 | /// @nodoc 34 | const $Result = _$ResultTearOff(); 35 | 36 | /// @nodoc 37 | mixin _$Result { 38 | @optionalTypeArgs 39 | TResult when({ 40 | required TResult Function(T data) success, 41 | required TResult Function(AppError error) failure, 42 | }) => 43 | throw _privateConstructorUsedError; 44 | @optionalTypeArgs 45 | TResult? whenOrNull({ 46 | TResult Function(T data)? success, 47 | TResult Function(AppError error)? failure, 48 | }) => 49 | throw _privateConstructorUsedError; 50 | @optionalTypeArgs 51 | TResult maybeWhen({ 52 | TResult Function(T data)? success, 53 | TResult Function(AppError error)? failure, 54 | required TResult orElse(), 55 | }) => 56 | throw _privateConstructorUsedError; 57 | @optionalTypeArgs 58 | TResult map({ 59 | required TResult Function(Success value) success, 60 | required TResult Function(Failure value) failure, 61 | }) => 62 | throw _privateConstructorUsedError; 63 | @optionalTypeArgs 64 | TResult? mapOrNull({ 65 | TResult Function(Success value)? success, 66 | TResult Function(Failure value)? failure, 67 | }) => 68 | throw _privateConstructorUsedError; 69 | @optionalTypeArgs 70 | TResult maybeMap({ 71 | TResult Function(Success value)? success, 72 | TResult Function(Failure value)? failure, 73 | required TResult orElse(), 74 | }) => 75 | throw _privateConstructorUsedError; 76 | } 77 | 78 | /// @nodoc 79 | abstract class $ResultCopyWith { 80 | factory $ResultCopyWith(Result value, $Res Function(Result) then) = 81 | _$ResultCopyWithImpl; 82 | } 83 | 84 | /// @nodoc 85 | class _$ResultCopyWithImpl implements $ResultCopyWith { 86 | _$ResultCopyWithImpl(this._value, this._then); 87 | 88 | final Result _value; 89 | // ignore: unused_field 90 | final $Res Function(Result) _then; 91 | } 92 | 93 | /// @nodoc 94 | abstract class $SuccessCopyWith { 95 | factory $SuccessCopyWith(Success value, $Res Function(Success) then) = 96 | _$SuccessCopyWithImpl; 97 | $Res call({T data}); 98 | } 99 | 100 | /// @nodoc 101 | class _$SuccessCopyWithImpl extends _$ResultCopyWithImpl 102 | implements $SuccessCopyWith { 103 | _$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then) 104 | : super(_value, (v) => _then(v as Success)); 105 | 106 | @override 107 | Success get _value => super._value as Success; 108 | 109 | @override 110 | $Res call({ 111 | Object? data = freezed, 112 | }) { 113 | return _then(Success( 114 | data: data == freezed 115 | ? _value.data 116 | : data // ignore: cast_nullable_to_non_nullable 117 | as T, 118 | )); 119 | } 120 | } 121 | 122 | /// @nodoc 123 | 124 | class _$Success extends Success with DiagnosticableTreeMixin { 125 | const _$Success({required this.data}) : super._(); 126 | 127 | @override 128 | final T data; 129 | 130 | @override 131 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 132 | return 'Result<$T>.success(data: $data)'; 133 | } 134 | 135 | @override 136 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 137 | super.debugFillProperties(properties); 138 | properties 139 | ..add(DiagnosticsProperty('type', 'Result<$T>.success')) 140 | ..add(DiagnosticsProperty('data', data)); 141 | } 142 | 143 | @override 144 | bool operator ==(dynamic other) { 145 | return identical(this, other) || 146 | (other is Success && 147 | (identical(other.data, data) || 148 | const DeepCollectionEquality().equals(other.data, data))); 149 | } 150 | 151 | @override 152 | int get hashCode => 153 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(data); 154 | 155 | @JsonKey(ignore: true) 156 | @override 157 | $SuccessCopyWith> get copyWith => 158 | _$SuccessCopyWithImpl>(this, _$identity); 159 | 160 | @override 161 | @optionalTypeArgs 162 | TResult when({ 163 | required TResult Function(T data) success, 164 | required TResult Function(AppError error) failure, 165 | }) { 166 | return success(data); 167 | } 168 | 169 | @override 170 | @optionalTypeArgs 171 | TResult? whenOrNull({ 172 | TResult Function(T data)? success, 173 | TResult Function(AppError error)? failure, 174 | }) { 175 | return success?.call(data); 176 | } 177 | 178 | @override 179 | @optionalTypeArgs 180 | TResult maybeWhen({ 181 | TResult Function(T data)? success, 182 | TResult Function(AppError error)? failure, 183 | required TResult orElse(), 184 | }) { 185 | if (success != null) { 186 | return success(data); 187 | } 188 | return orElse(); 189 | } 190 | 191 | @override 192 | @optionalTypeArgs 193 | TResult map({ 194 | required TResult Function(Success value) success, 195 | required TResult Function(Failure value) failure, 196 | }) { 197 | return success(this); 198 | } 199 | 200 | @override 201 | @optionalTypeArgs 202 | TResult? mapOrNull({ 203 | TResult Function(Success value)? success, 204 | TResult Function(Failure value)? failure, 205 | }) { 206 | return success?.call(this); 207 | } 208 | 209 | @override 210 | @optionalTypeArgs 211 | TResult maybeMap({ 212 | TResult Function(Success value)? success, 213 | TResult Function(Failure value)? failure, 214 | required TResult orElse(), 215 | }) { 216 | if (success != null) { 217 | return success(this); 218 | } 219 | return orElse(); 220 | } 221 | } 222 | 223 | abstract class Success extends Result { 224 | const factory Success({required T data}) = _$Success; 225 | const Success._() : super._(); 226 | 227 | T get data => throw _privateConstructorUsedError; 228 | @JsonKey(ignore: true) 229 | $SuccessCopyWith> get copyWith => 230 | throw _privateConstructorUsedError; 231 | } 232 | 233 | /// @nodoc 234 | abstract class $FailureCopyWith { 235 | factory $FailureCopyWith(Failure value, $Res Function(Failure) then) = 236 | _$FailureCopyWithImpl; 237 | $Res call({AppError error}); 238 | } 239 | 240 | /// @nodoc 241 | class _$FailureCopyWithImpl extends _$ResultCopyWithImpl 242 | implements $FailureCopyWith { 243 | _$FailureCopyWithImpl(Failure _value, $Res Function(Failure) _then) 244 | : super(_value, (v) => _then(v as Failure)); 245 | 246 | @override 247 | Failure get _value => super._value as Failure; 248 | 249 | @override 250 | $Res call({ 251 | Object? error = freezed, 252 | }) { 253 | return _then(Failure( 254 | error: error == freezed 255 | ? _value.error 256 | : error // ignore: cast_nullable_to_non_nullable 257 | as AppError, 258 | )); 259 | } 260 | } 261 | 262 | /// @nodoc 263 | 264 | class _$Failure extends Failure with DiagnosticableTreeMixin { 265 | const _$Failure({required this.error}) : super._(); 266 | 267 | @override 268 | final AppError error; 269 | 270 | @override 271 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 272 | return 'Result<$T>.failure(error: $error)'; 273 | } 274 | 275 | @override 276 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 277 | super.debugFillProperties(properties); 278 | properties 279 | ..add(DiagnosticsProperty('type', 'Result<$T>.failure')) 280 | ..add(DiagnosticsProperty('error', error)); 281 | } 282 | 283 | @override 284 | bool operator ==(dynamic other) { 285 | return identical(this, other) || 286 | (other is Failure && 287 | (identical(other.error, error) || 288 | const DeepCollectionEquality().equals(other.error, error))); 289 | } 290 | 291 | @override 292 | int get hashCode => 293 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); 294 | 295 | @JsonKey(ignore: true) 296 | @override 297 | $FailureCopyWith> get copyWith => 298 | _$FailureCopyWithImpl>(this, _$identity); 299 | 300 | @override 301 | @optionalTypeArgs 302 | TResult when({ 303 | required TResult Function(T data) success, 304 | required TResult Function(AppError error) failure, 305 | }) { 306 | return failure(error); 307 | } 308 | 309 | @override 310 | @optionalTypeArgs 311 | TResult? whenOrNull({ 312 | TResult Function(T data)? success, 313 | TResult Function(AppError error)? failure, 314 | }) { 315 | return failure?.call(error); 316 | } 317 | 318 | @override 319 | @optionalTypeArgs 320 | TResult maybeWhen({ 321 | TResult Function(T data)? success, 322 | TResult Function(AppError error)? failure, 323 | required TResult orElse(), 324 | }) { 325 | if (failure != null) { 326 | return failure(error); 327 | } 328 | return orElse(); 329 | } 330 | 331 | @override 332 | @optionalTypeArgs 333 | TResult map({ 334 | required TResult Function(Success value) success, 335 | required TResult Function(Failure value) failure, 336 | }) { 337 | return failure(this); 338 | } 339 | 340 | @override 341 | @optionalTypeArgs 342 | TResult? mapOrNull({ 343 | TResult Function(Success value)? success, 344 | TResult Function(Failure value)? failure, 345 | }) { 346 | return failure?.call(this); 347 | } 348 | 349 | @override 350 | @optionalTypeArgs 351 | TResult maybeMap({ 352 | TResult Function(Success value)? success, 353 | TResult Function(Failure value)? failure, 354 | required TResult orElse(), 355 | }) { 356 | if (failure != null) { 357 | return failure(this); 358 | } 359 | return orElse(); 360 | } 361 | } 362 | 363 | abstract class Failure extends Result { 364 | const factory Failure({required AppError error}) = _$Failure; 365 | const Failure._() : super._(); 366 | 367 | AppError get error => throw _privateConstructorUsedError; 368 | @JsonKey(ignore: true) 369 | $FailureCopyWith> get copyWith => 370 | throw _privateConstructorUsedError; 371 | } 372 | -------------------------------------------------------------------------------- /lib/data/model/source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'source.freezed.dart'; 5 | part 'source.g.dart'; 6 | 7 | @freezed 8 | abstract class Source with _$Source { 9 | factory Source({ 10 | String? id, 11 | String? name, 12 | }) = _Source; 13 | 14 | factory Source.fromJson(Map json) => _$SourceFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /lib/data/model/source.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 4 | 5 | part of 'source.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | Source _$SourceFromJson(Map json) { 17 | return _Source.fromJson(json); 18 | } 19 | 20 | /// @nodoc 21 | class _$SourceTearOff { 22 | const _$SourceTearOff(); 23 | 24 | _Source call({String? id, String? name}) { 25 | return _Source( 26 | id: id, 27 | name: name, 28 | ); 29 | } 30 | 31 | Source fromJson(Map json) { 32 | return Source.fromJson(json); 33 | } 34 | } 35 | 36 | /// @nodoc 37 | const $Source = _$SourceTearOff(); 38 | 39 | /// @nodoc 40 | mixin _$Source { 41 | String? get id => throw _privateConstructorUsedError; 42 | String? get name => throw _privateConstructorUsedError; 43 | 44 | Map toJson() => throw _privateConstructorUsedError; 45 | @JsonKey(ignore: true) 46 | $SourceCopyWith get copyWith => throw _privateConstructorUsedError; 47 | } 48 | 49 | /// @nodoc 50 | abstract class $SourceCopyWith<$Res> { 51 | factory $SourceCopyWith(Source value, $Res Function(Source) then) = 52 | _$SourceCopyWithImpl<$Res>; 53 | $Res call({String? id, String? name}); 54 | } 55 | 56 | /// @nodoc 57 | class _$SourceCopyWithImpl<$Res> implements $SourceCopyWith<$Res> { 58 | _$SourceCopyWithImpl(this._value, this._then); 59 | 60 | final Source _value; 61 | // ignore: unused_field 62 | final $Res Function(Source) _then; 63 | 64 | @override 65 | $Res call({ 66 | Object? id = freezed, 67 | Object? name = freezed, 68 | }) { 69 | return _then(_value.copyWith( 70 | id: id == freezed 71 | ? _value.id 72 | : id // ignore: cast_nullable_to_non_nullable 73 | as String?, 74 | name: name == freezed 75 | ? _value.name 76 | : name // ignore: cast_nullable_to_non_nullable 77 | as String?, 78 | )); 79 | } 80 | } 81 | 82 | /// @nodoc 83 | abstract class _$SourceCopyWith<$Res> implements $SourceCopyWith<$Res> { 84 | factory _$SourceCopyWith(_Source value, $Res Function(_Source) then) = 85 | __$SourceCopyWithImpl<$Res>; 86 | @override 87 | $Res call({String? id, String? name}); 88 | } 89 | 90 | /// @nodoc 91 | class __$SourceCopyWithImpl<$Res> extends _$SourceCopyWithImpl<$Res> 92 | implements _$SourceCopyWith<$Res> { 93 | __$SourceCopyWithImpl(_Source _value, $Res Function(_Source) _then) 94 | : super(_value, (v) => _then(v as _Source)); 95 | 96 | @override 97 | _Source get _value => super._value as _Source; 98 | 99 | @override 100 | $Res call({ 101 | Object? id = freezed, 102 | Object? name = freezed, 103 | }) { 104 | return _then(_Source( 105 | id: id == freezed 106 | ? _value.id 107 | : id // ignore: cast_nullable_to_non_nullable 108 | as String?, 109 | name: name == freezed 110 | ? _value.name 111 | : name // ignore: cast_nullable_to_non_nullable 112 | as String?, 113 | )); 114 | } 115 | } 116 | 117 | /// @nodoc 118 | @JsonSerializable() 119 | class _$_Source with DiagnosticableTreeMixin implements _Source { 120 | _$_Source({this.id, this.name}); 121 | 122 | factory _$_Source.fromJson(Map json) => 123 | _$$_SourceFromJson(json); 124 | 125 | @override 126 | final String? id; 127 | @override 128 | final String? name; 129 | 130 | @override 131 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 132 | return 'Source(id: $id, name: $name)'; 133 | } 134 | 135 | @override 136 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 137 | super.debugFillProperties(properties); 138 | properties 139 | ..add(DiagnosticsProperty('type', 'Source')) 140 | ..add(DiagnosticsProperty('id', id)) 141 | ..add(DiagnosticsProperty('name', name)); 142 | } 143 | 144 | @override 145 | bool operator ==(dynamic other) { 146 | return identical(this, other) || 147 | (other is _Source && 148 | (identical(other.id, id) || 149 | const DeepCollectionEquality().equals(other.id, id)) && 150 | (identical(other.name, name) || 151 | const DeepCollectionEquality().equals(other.name, name))); 152 | } 153 | 154 | @override 155 | int get hashCode => 156 | runtimeType.hashCode ^ 157 | const DeepCollectionEquality().hash(id) ^ 158 | const DeepCollectionEquality().hash(name); 159 | 160 | @JsonKey(ignore: true) 161 | @override 162 | _$SourceCopyWith<_Source> get copyWith => 163 | __$SourceCopyWithImpl<_Source>(this, _$identity); 164 | 165 | @override 166 | Map toJson() { 167 | return _$$_SourceToJson(this); 168 | } 169 | } 170 | 171 | abstract class _Source implements Source { 172 | factory _Source({String? id, String? name}) = _$_Source; 173 | 174 | factory _Source.fromJson(Map json) = _$_Source.fromJson; 175 | 176 | @override 177 | String? get id => throw _privateConstructorUsedError; 178 | @override 179 | String? get name => throw _privateConstructorUsedError; 180 | @override 181 | @JsonKey(ignore: true) 182 | _$SourceCopyWith<_Source> get copyWith => throw _privateConstructorUsedError; 183 | } 184 | -------------------------------------------------------------------------------- /lib/data/model/source.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'source.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Source _$$_SourceFromJson(Map json) => _$_Source( 10 | id: json['id'] as String?, 11 | name: json['name'] as String?, 12 | ); 13 | 14 | Map _$$_SourceToJson(_$_Source instance) => { 15 | 'id': instance.id, 16 | 'name': instance.name, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/data/provider/firebase_auth_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | final firebaseAuthProvider = Provider((_) => FirebaseAuth.instance); 5 | -------------------------------------------------------------------------------- /lib/data/remote/app_dio.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/foundation/constants.dart'; 2 | import 'package:dio/adapter.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio_firebase_performance/dio_firebase_performance.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:ua_client_hints/ua_client_hints.dart'; 8 | 9 | final dioProvider = Provider((_) => AppDio.getInstance()); 10 | 11 | // ignore: prefer_mixin 12 | class AppDio with DioMixin implements Dio { 13 | AppDio._([BaseOptions? options]) { 14 | options = BaseOptions( 15 | baseUrl: Constants.of().endpoint, 16 | contentType: 'application/json', 17 | connectTimeout: 30000, 18 | sendTimeout: 30000, 19 | receiveTimeout: 30000, 20 | ); 21 | 22 | this.options = options; 23 | interceptors.add(InterceptorsWrapper(onRequest: (options, handler) async { 24 | options.headers.addAll(await userAgentClientHintsHeader()); 25 | handler.next(options); 26 | })); 27 | 28 | // Firebase Performance 29 | interceptors.add(DioFirebasePerformanceInterceptor()); 30 | 31 | if (kDebugMode) { 32 | // Local Log 33 | interceptors.add(LogInterceptor(responseBody: true, requestBody: true)); 34 | } 35 | 36 | httpClientAdapter = DefaultHttpClientAdapter(); 37 | } 38 | 39 | static Dio getInstance() => AppDio._(); 40 | } 41 | -------------------------------------------------------------------------------- /lib/data/remote/auth_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | 3 | abstract class AuthDataSource { 4 | Future signIn(); 5 | 6 | Future signOut(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/data/remote/auth_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/provider/firebase_auth_provider.dart'; 2 | import 'package:app/data/remote/auth_data_source.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart' as firebase; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | final authDataSourceProvider = Provider((ref) => AuthDataSourceImpl(ref.read)); 9 | 10 | class AuthDataSourceImpl implements AuthDataSource { 11 | AuthDataSourceImpl(this._reader); 12 | 13 | final Reader _reader; 14 | 15 | late final firebase.FirebaseAuth _firebaseAuth = 16 | _reader(firebaseAuthProvider); 17 | 18 | @override 19 | Future signIn() async { 20 | final account = await GoogleSignIn().signIn(); 21 | if (account == null) { 22 | return throw StateError('Maybe user canceled.'); 23 | } 24 | final auth = await account.authentication; 25 | final firebase.AuthCredential authCredential = 26 | firebase.GoogleAuthProvider.credential( 27 | idToken: auth.idToken, 28 | accessToken: auth.accessToken, 29 | ); 30 | 31 | final credential = await _firebaseAuth.signInWithCredential(authCredential); 32 | final currentUser = firebase.FirebaseAuth.instance.currentUser; 33 | assert(credential.user?.uid == currentUser?.uid); 34 | return credential.user; 35 | } 36 | 37 | @override 38 | Future signOut() { 39 | return GoogleSignIn() 40 | .signOut() 41 | .then((_) => _firebaseAuth.signOut()) 42 | .catchError((error) { 43 | debugPrint(error.toString()); 44 | throw error; 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/data/remote/news_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/news.dart'; 2 | import 'package:app/data/remote/app_dio.dart'; 3 | import 'package:dio/dio.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:retrofit/retrofit.dart'; 6 | 7 | part 'news_data_source.g.dart'; 8 | 9 | final newsDataSourceProvider = Provider((ref) => NewsDataSource(ref.read)); 10 | 11 | @RestApi() 12 | abstract class NewsDataSource { 13 | factory NewsDataSource(Reader reader) => _NewsDataSource(reader(dioProvider)); 14 | 15 | @GET('/v2/everything') 16 | Future getNews({ 17 | @Query("q") required String query, 18 | @Query("from") required String from, 19 | @Query("sortBy") String? sortBy = 'publishedAt', 20 | @Query("language") String? language = 'en', 21 | @Query("apiKey") required String apiKey, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /lib/data/remote/news_data_source.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'news_data_source.dart'; 4 | 5 | // ************************************************************************** 6 | // RetrofitGenerator 7 | // ************************************************************************** 8 | 9 | class _NewsDataSource implements NewsDataSource { 10 | _NewsDataSource(this._dio, {this.baseUrl}); 11 | 12 | final Dio _dio; 13 | 14 | String? baseUrl; 15 | 16 | @override 17 | Future getNews( 18 | {required query, 19 | required from, 20 | sortBy = 'publishedAt', 21 | language = 'en', 22 | required apiKey}) async { 23 | const _extra = {}; 24 | final queryParameters = { 25 | r'q': query, 26 | r'from': from, 27 | r'sortBy': sortBy, 28 | r'language': language, 29 | r'apiKey': apiKey 30 | }; 31 | queryParameters.removeWhere((k, v) => v == null); 32 | final _headers = {}; 33 | final _data = {}; 34 | final _result = await _dio.fetch>(_setStreamType( 35 | Options(method: 'GET', headers: _headers, extra: _extra) 36 | .compose(_dio.options, '/v2/everything', 37 | queryParameters: queryParameters, data: _data) 38 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 39 | final value = News.fromJson(_result.data!); 40 | return value; 41 | } 42 | 43 | RequestOptions _setStreamType(RequestOptions requestOptions) { 44 | if (T != dynamic && 45 | !(requestOptions.responseType == ResponseType.bytes || 46 | requestOptions.responseType == ResponseType.stream)) { 47 | if (T == String) { 48 | requestOptions.responseType = ResponseType.plain; 49 | } else { 50 | requestOptions.responseType = ResponseType.json; 51 | } 52 | } 53 | return requestOptions; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/data/repository/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/local/app_user.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | 4 | abstract class AuthRepository { 5 | Future> signIn(); 6 | 7 | Future> signOut(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/data/repository/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/local/app_user.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | import 'package:app/data/remote/auth_data_source.dart'; 4 | import 'package:app/data/remote/auth_data_source_impl.dart'; 5 | import 'package:app/data/repository/auth_repository.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | final authRepositoryProvider = Provider((ref) => AuthRepositoryImpl(ref.read)); 9 | 10 | class AuthRepositoryImpl implements AuthRepository { 11 | AuthRepositoryImpl(this._reader); 12 | 13 | final Reader _reader; 14 | 15 | late final AuthDataSource _dataSource = _reader(authDataSourceProvider); 16 | 17 | @override 18 | Future> signIn() { 19 | return Result.guardFuture( 20 | () async => AppUser.from(await _dataSource.signIn())); 21 | } 22 | 23 | @override 24 | Future> signOut() { 25 | return Result.guardFuture(_dataSource.signOut); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/data/repository/news_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/news.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | 4 | abstract class NewsRepository { 5 | Future> getNews(); 6 | } 7 | -------------------------------------------------------------------------------- /lib/data/repository/news_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:app/data/model/news.dart'; 4 | import 'package:app/data/model/result.dart'; 5 | import 'package:app/data/remote/news_data_source.dart'; 6 | import 'package:app/data/repository/news_repository.dart'; 7 | import 'package:app/foundation/constants.dart'; 8 | import 'package:app/foundation/extension/date_time.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | final newsRepositoryProvider = Provider((ref) => NewsRepositoryImpl(ref.read)); 12 | 13 | class NewsRepositoryImpl implements NewsRepository { 14 | NewsRepositoryImpl(this._reader); 15 | 16 | final Reader _reader; 17 | 18 | late final NewsDataSource _dataSource = _reader(newsDataSourceProvider); 19 | 20 | @override 21 | Future> getNews() { 22 | return Result.guardFuture( 23 | () async => await _dataSource.getNews( 24 | query: ['anim', 'manga'][Random().nextInt(2)], 25 | // For checking reload. 26 | from: DateTime.now() 27 | .subtract(const Duration(days: 28)) 28 | .toLocal() 29 | .formatYYYYMMdd(), 30 | apiKey: Constants.instance.apiKey, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/foundation/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:enum_to_string/enum_to_string.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | enum Flavor { development, production } 6 | 7 | @immutable 8 | class Constants { 9 | const Constants._({ 10 | required this.endpoint, 11 | required this.apiKey, 12 | }); 13 | 14 | factory Constants.of() { 15 | final flavor = EnumToString.fromString( 16 | Flavor.values, 17 | const String.fromEnvironment('FLAVOR'), 18 | ); 19 | 20 | switch (flavor) { 21 | case Flavor.development: 22 | return Constants._dev(); 23 | case Flavor.production: 24 | default: 25 | return Constants._prd(); 26 | } 27 | } 28 | 29 | factory Constants._dev() { 30 | return const Constants._( 31 | endpoint: 'https://newsapi.org', 32 | apiKey: '98c8df982b8b4da8b86cd70e851fc521', 33 | ); 34 | } 35 | 36 | factory Constants._prd() { 37 | return const Constants._( 38 | endpoint: 'https://newsapi.org', 39 | apiKey: '4bc454db94464956aea4cbb01f4bf9f4', 40 | ); 41 | } 42 | 43 | static late final Constants instance = Constants.of(); 44 | 45 | final String endpoint; 46 | final String apiKey; 47 | } 48 | -------------------------------------------------------------------------------- /lib/foundation/extension/async_snapshot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | extension AsyncSnapshotExt on AsyncSnapshot { 4 | bool get isNothing => connectionState == ConnectionState.none; 5 | 6 | bool get isActive => connectionState == ConnectionState.active; 7 | 8 | bool get isDone => connectionState == ConnectionState.done; 9 | 10 | bool get isWaiting => connectionState == ConnectionState.waiting; 11 | } 12 | -------------------------------------------------------------------------------- /lib/foundation/extension/date_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateTimeExt on DateTime { 4 | String format(DateFormat format) { 5 | return format.format(this); 6 | } 7 | 8 | String formatYYYYMMdd() { 9 | return DateFormat('yyyy-MM-dd').format(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/foundation/extension/object.dart: -------------------------------------------------------------------------------- 1 | extension GenericExt on T { 2 | R let(R Function(T t) transform) => transform(this); 3 | 4 | R? safeCast() => this is R ? (this as R) : null; 5 | } 6 | -------------------------------------------------------------------------------- /lib/gen/assets.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | // ignore_for_file: directives_ordering 7 | 8 | import 'package:flutter/widgets.dart'; 9 | import 'package:flutter_svg/flutter_svg.dart'; 10 | import 'package:flutter/services.dart'; 11 | 12 | class $AssetsImagesGen { 13 | const $AssetsImagesGen(); 14 | 15 | AssetGenImage get articlePlaceholder => 16 | const AssetGenImage('assets/images/article_placeholder.webp'); 17 | AssetGenImage get iconPlaceholder => 18 | const AssetGenImage('assets/images/icon_placeholder.jpg'); 19 | } 20 | 21 | class $AssetsSvgsGen { 22 | const $AssetsSvgsGen(); 23 | 24 | SvgGenImage get firebase => const SvgGenImage('assets/svgs/firebase.svg'); 25 | SvgGenImage get news => const SvgGenImage('assets/svgs/news.svg'); 26 | SvgGenImage get video => const SvgGenImage('assets/svgs/video.svg'); 27 | } 28 | 29 | class $AssetsVideosGen { 30 | const $AssetsVideosGen(); 31 | 32 | String get bigbuckbunny => 'assets/videos/bigbuckbunny.mp4'; 33 | } 34 | 35 | class Assets { 36 | Assets._(); 37 | 38 | static const $AssetsImagesGen images = $AssetsImagesGen(); 39 | static const $AssetsSvgsGen svgs = $AssetsSvgsGen(); 40 | static const $AssetsVideosGen videos = $AssetsVideosGen(); 41 | } 42 | 43 | class AssetGenImage extends AssetImage { 44 | const AssetGenImage(String assetName) : super(assetName); 45 | 46 | Image image({ 47 | Key? key, 48 | ImageFrameBuilder? frameBuilder, 49 | ImageLoadingBuilder? loadingBuilder, 50 | ImageErrorWidgetBuilder? errorBuilder, 51 | String? semanticLabel, 52 | bool excludeFromSemantics = false, 53 | double? width, 54 | double? height, 55 | Color? color, 56 | BlendMode? colorBlendMode, 57 | BoxFit? fit, 58 | AlignmentGeometry alignment = Alignment.center, 59 | ImageRepeat repeat = ImageRepeat.noRepeat, 60 | Rect? centerSlice, 61 | bool matchTextDirection = false, 62 | bool gaplessPlayback = false, 63 | bool isAntiAlias = false, 64 | FilterQuality filterQuality = FilterQuality.low, 65 | }) { 66 | return Image( 67 | key: key, 68 | image: this, 69 | frameBuilder: frameBuilder, 70 | loadingBuilder: loadingBuilder, 71 | errorBuilder: errorBuilder, 72 | semanticLabel: semanticLabel, 73 | excludeFromSemantics: excludeFromSemantics, 74 | width: width, 75 | height: height, 76 | color: color, 77 | colorBlendMode: colorBlendMode, 78 | fit: fit, 79 | alignment: alignment, 80 | repeat: repeat, 81 | centerSlice: centerSlice, 82 | matchTextDirection: matchTextDirection, 83 | gaplessPlayback: gaplessPlayback, 84 | isAntiAlias: isAntiAlias, 85 | filterQuality: filterQuality, 86 | ); 87 | } 88 | 89 | String get path => assetName; 90 | } 91 | 92 | class SvgGenImage { 93 | const SvgGenImage(this._assetName); 94 | 95 | final String _assetName; 96 | 97 | SvgPicture svg({ 98 | Key? key, 99 | bool matchTextDirection = false, 100 | AssetBundle? bundle, 101 | String? package, 102 | double? width, 103 | double? height, 104 | BoxFit fit = BoxFit.contain, 105 | AlignmentGeometry alignment = Alignment.center, 106 | bool allowDrawingOutsideViewBox = false, 107 | WidgetBuilder? placeholderBuilder, 108 | Color? color, 109 | BlendMode colorBlendMode = BlendMode.srcIn, 110 | String? semanticsLabel, 111 | bool excludeFromSemantics = false, 112 | Clip clipBehavior = Clip.hardEdge, 113 | }) { 114 | return SvgPicture.asset( 115 | _assetName, 116 | key: key, 117 | matchTextDirection: matchTextDirection, 118 | bundle: bundle, 119 | package: package, 120 | width: width, 121 | height: height, 122 | fit: fit, 123 | alignment: alignment, 124 | allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, 125 | placeholderBuilder: placeholderBuilder, 126 | color: color, 127 | colorBlendMode: colorBlendMode, 128 | semanticsLabel: semanticsLabel, 129 | excludeFromSemantics: excludeFromSemantics, 130 | clipBehavior: clipBehavior, 131 | ); 132 | } 133 | 134 | String get path => _assetName; 135 | } 136 | -------------------------------------------------------------------------------- /lib/gen/fonts.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | // ignore_for_file: directives_ordering 7 | 8 | class FontFamily { 9 | FontFamily._(); 10 | 11 | static const String rotunda = 'Rotunda'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/l10n/intl_messages_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "ok": "OK", 4 | "cancel": "CANCEL", 5 | "news": "News", 6 | "signIn": "SignIn", 7 | "video": "Video", 8 | "googleSignIn": "Google Sign-In", 9 | "googleSignOut": "Sign-Out", 10 | "detail": "Detail", 11 | "error": "An error occurred", 12 | "reload": "Reload", 13 | "fetchFailed": "Could not get the data", 14 | "pleaseRetry": "Please try again", 15 | "noResult": "For Empty screen", 16 | "noTitle": "No Title", 17 | "displayName": "Display Name", 18 | "email": "Email", 19 | "uid": "UserId" 20 | } -------------------------------------------------------------------------------- /lib/l10n/intl_messages_ja.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "ja", 3 | "ok": "OK", 4 | "cancel": "CANCEL", 5 | "news": "ニュース", 6 | "signIn": "ログイン", 7 | "video": "ビデオ", 8 | "googleSignIn": "Google サインイン", 9 | "googleSignOut": "サインアウト", 10 | "detail": "詳細", 11 | "error": "エラーが発生しました", 12 | "reload": "更新する", 13 | "fetchFailed": "データの取得に失敗しました", 14 | "pleaseRetry": "もう一度お試しください", 15 | "noResult": "データがありません!", 16 | "noTitle": "No Title", 17 | "displayName": "Display Name", 18 | "email": "メール", 19 | "uid": "ユーザID" 20 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:app/app.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 9 | 10 | Future main() async { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | 13 | // debugPaintBaselinesEnabled = true; 14 | // debugPaintSizeEnabled = true; 15 | // debugPaintLayerBordersEnabled = true; 16 | 17 | // Firebase 18 | await Firebase.initializeApp(); 19 | // Crashlytics 20 | await FirebaseCrashlytics.instance 21 | .setCrashlyticsCollectionEnabled(kDebugMode); 22 | Function originalOnError = FlutterError.onError!; 23 | FlutterError.onError = (errorDetails) async { 24 | await FirebaseCrashlytics.instance.recordFlutterError(errorDetails); 25 | originalOnError(errorDetails); 26 | }; 27 | 28 | if (kReleaseMode) { 29 | debugPrint = (message, {wrapWidth}) {}; 30 | } 31 | 32 | runZonedGuarded(() { 33 | runApp(const ProviderScope(child: App())); 34 | }, (error, stackTrace) { 35 | FirebaseCrashlytics.instance.recordError(error, stackTrace); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /lib/ui/component/image/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/gen/assets.gen.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | Image profileImage(String? imageUrl) { 5 | if (imageUrl == null || imageUrl.isEmpty) { 6 | return Assets.images.iconPlaceholder.image(); 7 | } 8 | return Image.network(imageUrl); 9 | } 10 | 11 | ImageProvider profileImageProvider(String? imageUrl) { 12 | if (imageUrl == null || imageUrl.isEmpty) { 13 | return Assets.images.iconPlaceholder; 14 | } 15 | return NetworkImage(imageUrl); 16 | } 17 | 18 | Image networkImage(String? url, {BoxFit? fit}) { 19 | final placeholder = Assets.images.articlePlaceholder.image(fit: fit); 20 | if (url == null || url.isEmpty) { 21 | return placeholder; 22 | } 23 | return Image.network( 24 | url, 25 | fit: fit, 26 | errorBuilder: (context, url, dynamic error) => placeholder, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/component/loading/container_with_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/component/loading/loading.dart'; 2 | import 'package:app/ui/loading_state_view_model.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | class ContainerWithLoading extends ConsumerWidget { 7 | const ContainerWithLoading({ 8 | Key? key, 9 | required this.child, 10 | }) : super(key: key); 11 | 12 | final Widget child; 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final state = ref.watch(loadingStateProvider); 17 | return Stack(children: [ 18 | child, 19 | state.isLoading ? const Loading() : const SizedBox(), 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/component/loading/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/theme/app_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | class Loading extends ConsumerWidget { 6 | const Loading({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context, WidgetRef ref) { 10 | final theme = ref.watch(appThemeProvider); 11 | return Center( 12 | child: CircularProgressIndicator( 13 | valueColor: AlwaysStoppedAnimation(theme.appColors.accent), 14 | ), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/component/snack_bar/error_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void showErrorSnackbar({ 5 | required BuildContext context, 6 | required String message, 7 | }) { 8 | final snackBar = SnackBar( 9 | content: Text(message), 10 | margin: const EdgeInsets.only(bottom: 16, right: 8, left: 8), 11 | padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 16), 12 | shape: const RoundedRectangleBorder( 13 | borderRadius: BorderRadius.all(Radius.circular(12)), 14 | ), 15 | behavior: SnackBarBehavior.floating, 16 | ); 17 | ScaffoldMessenger.of(context).showSnackBar(snackBar); 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/detail/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/article.dart'; 2 | import 'package:app/ui/component/image/image.dart'; 3 | import 'package:app/ui/hook/use_router.dart'; 4 | import 'package:auto_route/auto_route.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_hooks/flutter_hooks.dart'; 7 | 8 | class DetailPage extends HookWidget { 9 | const DetailPage({ 10 | Key? key, 11 | @QueryParam('article') this.article, 12 | }) : super(key: key); 13 | 14 | final Article? article; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | assert(article != null, "Article is required."); 19 | final router = useRouter(); 20 | return Scaffold( 21 | body: GestureDetector( 22 | child: Center( 23 | child: Hero( 24 | tag: article!, 25 | child: networkImage(article?.urlToImage), 26 | ), 27 | ), 28 | onTap: router.pop, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/gen/assets.gen.dart'; 2 | import 'package:app/ui/hook/use_l10n.dart'; 3 | import 'package:app/ui/route/app_route.gr.dart'; 4 | import 'package:app/ui/theme/app_theme.dart'; 5 | import 'package:auto_route/auto_route.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | 9 | class HomePage extends HookConsumerWidget { 10 | const HomePage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final theme = ref.watch(appThemeProvider); 15 | final l10n = useL10n(); 16 | return AutoTabsScaffold( 17 | routes: const [ 18 | NewsRoute(), 19 | VideoRoute(), 20 | ], 21 | bottomNavigationBuilder: (context, tabsRouter) { 22 | return BottomNavigationBar( 23 | currentIndex: tabsRouter.activeIndex, 24 | showSelectedLabels: false, 25 | showUnselectedLabels: false, 26 | onTap: tabsRouter.setActiveIndex, 27 | iconSize: 20, 28 | items: [ 29 | BottomNavigationBarItem( 30 | icon: Assets.svgs.news.svg( 31 | width: 20, 32 | color: tabsRouter.current.name == NewsRoute.name 33 | ? theme.appColors.accent 34 | : theme.appColors.disabled, 35 | ), 36 | label: l10n.news, 37 | ), 38 | BottomNavigationBarItem( 39 | icon: Assets.svgs.video.svg( 40 | width: 20, 41 | color: tabsRouter.current.name == VideoRoute.name 42 | ? theme.appColors.accent 43 | : theme.appColors.disabled, 44 | ), 45 | label: l10n.news, 46 | ), 47 | ], 48 | ); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/hook/use_asset_vide_player_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:video_player/video_player.dart'; 4 | 5 | VideoPlayerController useAssetVideoController({ 6 | required String asset, 7 | String? package, 8 | bool autoPlay = false, 9 | bool looping = false, 10 | }) { 11 | return use( 12 | _AssetVideoPlayerControllerHook( 13 | asset: asset, 14 | package: package, 15 | autoPlay: autoPlay, 16 | looping: looping, 17 | keys: [asset, package, autoPlay, looping], 18 | ), 19 | ); 20 | } 21 | 22 | class _AssetVideoPlayerControllerHook extends Hook { 23 | const _AssetVideoPlayerControllerHook({ 24 | required this.asset, 25 | this.package, 26 | required this.autoPlay, 27 | required this.looping, 28 | List? keys, 29 | }) : super(keys: keys); 30 | 31 | final String asset; 32 | final String? package; 33 | final bool autoPlay; 34 | final bool looping; 35 | 36 | @override 37 | _AssetVideoPlayerControllerHookState createState() => 38 | _AssetVideoPlayerControllerHookState(); 39 | } 40 | 41 | class _AssetVideoPlayerControllerHookState 42 | extends HookState { 43 | late final VideoPlayerController _controller = 44 | VideoPlayerController.asset(hook.asset, package: hook.package); 45 | 46 | @override 47 | void initHook() { 48 | _controller 49 | ..initialize() 50 | ..setLooping(hook.looping); 51 | 52 | if (hook.autoPlay) { 53 | _controller.play(); 54 | } 55 | } 56 | 57 | @override 58 | VideoPlayerController build(BuildContext context) => _controller; 59 | 60 | @override 61 | void dispose() => _controller.dispose(); 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/hook/use_l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | L10n useL10n() { 5 | final context = useContext(); 6 | return L10n.of(context)!; 7 | } 8 | -------------------------------------------------------------------------------- /lib/ui/hook/use_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | StackRouter useRouter() { 5 | final context = useContext(); 6 | return AutoRouter.of(context); 7 | } 8 | -------------------------------------------------------------------------------- /lib/ui/loading_state_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | final loadingStateProvider = 5 | ChangeNotifierProvider((ref) => LoadingStateViewModel()); 6 | 7 | class LoadingStateViewModel extends ChangeNotifier { 8 | bool isLoading = false; 9 | 10 | Future whileLoading(Future Function() future) { 11 | return Future.microtask(toLoading) 12 | .then((_) => future()) 13 | .whenComplete(toIdle); 14 | } 15 | 16 | void toLoading() { 17 | if (isLoading) return; 18 | isLoading = true; 19 | notifyListeners(); 20 | } 21 | 22 | void toIdle() { 23 | if (!isLoading) return; 24 | isLoading = false; 25 | notifyListeners(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/news/article_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/article.dart'; 2 | import 'package:app/ui/component/image/image.dart'; 3 | import 'package:app/ui/hook/use_l10n.dart'; 4 | import 'package:app/ui/hook/use_router.dart'; 5 | import 'package:app/ui/route/app_route.dart'; 6 | import 'package:app/ui/theme/app_theme.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | class ArticleItem extends HookConsumerWidget { 12 | const ArticleItem({ 13 | Key? key, 14 | required this.article, 15 | }) : super(key: key); 16 | 17 | final Article article; 18 | 19 | static BorderRadius borderRadiusAll = BorderRadius.circular(8); 20 | static BorderRadius borderRadiusTop = const BorderRadius.only( 21 | topRight: Radius.circular(8), 22 | topLeft: Radius.circular(8), 23 | bottomLeft: Radius.circular(0), 24 | bottomRight: Radius.circular(0), 25 | ); 26 | 27 | @override 28 | Widget build(BuildContext context, WidgetRef ref) { 29 | final theme = ref.watch(appThemeProvider); 30 | final router = useRouter(); 31 | final l10n = useL10n(); 32 | return Card( 33 | shape: RoundedRectangleBorder(borderRadius: borderRadiusAll), 34 | elevation: 4, 35 | child: InkWell( 36 | child: Column( 37 | children: [ 38 | Hero( 39 | tag: article, 40 | child: SizedBox( 41 | width: double.infinity, 42 | height: 200, 43 | child: ClipRRect( 44 | borderRadius: borderRadiusTop, 45 | child: networkImage(article.urlToImage, fit: BoxFit.cover), 46 | ), 47 | ), 48 | ), 49 | Padding( 50 | padding: const EdgeInsets.all(8), 51 | child: Text( 52 | article.title ?? l10n.noTitle, 53 | style: theme.textTheme.h20.dense(), 54 | ), 55 | ), 56 | ], 57 | ), 58 | onTap: () => router.push(DetailRoute(article: article)), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/news/connected_new_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/component/image/image.dart'; 2 | import 'package:app/ui/component/snack_bar/error_snackbar.dart'; 3 | import 'package:app/ui/hook/use_l10n.dart'; 4 | import 'package:app/ui/hook/use_router.dart'; 5 | import 'package:app/ui/news/news_page.dart'; 6 | import 'package:app/ui/route/app_route.dart'; 7 | import 'package:app/ui/theme/app_theme.dart'; 8 | import 'package:app/ui/user_view_model.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 11 | 12 | class ConnectedNewsPage extends HookConsumerWidget { 13 | const ConnectedNewsPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final theme = ref.watch(appThemeProvider); 18 | final l10n = useL10n(); 19 | final router = useRouter(); 20 | 21 | final user = ref.watch(userViewModelProvider.select((value) => value.user)); 22 | 23 | return Scaffold( 24 | appBar: AppBar( 25 | title: Text( 26 | l10n.news, 27 | style: theme.textTheme.h60.bold().rotunda(), 28 | ), 29 | actions: [ 30 | IconButton( 31 | icon: const Icon(Icons.error), 32 | onPressed: () { 33 | return showErrorSnackbar(context: context, message: l10n.error); 34 | }, 35 | ), 36 | IconButton( 37 | icon: CircleAvatar( 38 | backgroundImage: profileImageProvider(user?.imageUrl), 39 | backgroundColor: Colors.transparent, 40 | radius: 12, 41 | ), 42 | onPressed: () => router.push(const SignInRoute()), 43 | ) 44 | ], 45 | ), 46 | body: const NewsPage(), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/news/news_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/foundation/extension/async_snapshot.dart'; 2 | import 'package:app/ui/component/loading/container_with_loading.dart'; 3 | import 'package:app/ui/hook/use_l10n.dart'; 4 | import 'package:app/ui/loading_state_view_model.dart'; 5 | import 'package:app/ui/news/article_item.dart'; 6 | import 'package:app/ui/news/news_view_model.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_hooks/flutter_hooks.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | class NewsPage extends HookConsumerWidget { 12 | const NewsPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final l10n = useL10n(); 17 | final homeViewModel = ref.read(newsViewModelProvider); 18 | final news = ref.watch(newsViewModelProvider.select((value) => value.news)); 19 | 20 | final snapshot = useFuture( 21 | useMemoized(() { 22 | return ref 23 | .read(loadingStateProvider) 24 | .whileLoading(homeViewModel.fetchNews); 25 | }, [news?.toString()]), 26 | ); 27 | 28 | return ContainerWithLoading( 29 | child: snapshot.isWaiting || news == null 30 | ? const SizedBox() 31 | : news.when(success: (data) { 32 | if (data.articles.isEmpty) { 33 | return Center(child: Text(l10n.noResult)); 34 | } 35 | return RefreshIndicator( 36 | onRefresh: () async => homeViewModel.fetchNews(), 37 | child: ListView.builder( 38 | itemCount: data.articles.length, 39 | itemBuilder: (_, index) { 40 | return ArticleItem(article: data.articles[index]); 41 | }, 42 | ), 43 | ); 44 | }, failure: (e) { 45 | return Center(child: Text(l10n.fetchFailed)); 46 | }), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/news/news_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/news.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | import 'package:app/data/repository/news_repository.dart'; 4 | import 'package:app/data/repository/news_repository_impl.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | final newsViewModelProvider = 9 | ChangeNotifierProvider((ref) => NewsViewModel(ref.read)); 10 | 11 | class NewsViewModel extends ChangeNotifier { 12 | NewsViewModel(this._reader); 13 | 14 | final Reader _reader; 15 | 16 | late final NewsRepository _repository = _reader(newsRepositoryProvider); 17 | 18 | // Result use case No.1 19 | Result? _news; 20 | 21 | Result? get news => _news; 22 | 23 | Future fetchNews() { 24 | return _repository 25 | .getNews() 26 | .then((value) => _news = value) 27 | .whenComplete(notifyListeners); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/route/app_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/detail/detail_page.dart'; 2 | import 'package:app/ui/home/home_page.dart'; 3 | import 'package:app/ui/news/news_page.dart'; 4 | import 'package:app/ui/signIn/sign_in_page.dart'; 5 | import 'package:app/ui/video/video_page.dart'; 6 | import 'package:auto_route/auto_route.dart'; 7 | 8 | export 'app_route.gr.dart'; 9 | 10 | @AdaptiveAutoRouter( 11 | replaceInRouteName: 'Page,Route', 12 | routes: [ 13 | AutoRoute( 14 | path: '/', 15 | page: HomePage, 16 | initial: true, 17 | children: [ 18 | AutoRoute( 19 | path: 'news', 20 | page: NewsPage, 21 | ), 22 | AutoRoute( 23 | path: 'video', 24 | page: VideoPage, 25 | ), 26 | ], 27 | ), 28 | AutoRoute( 29 | path: '/signIn', 30 | page: SignInPage, 31 | fullscreenDialog: true, 32 | ), 33 | AutoRoute( 34 | path: '/detail', 35 | page: DetailPage, 36 | ), 37 | ], 38 | ) 39 | class $AppRouter {} 40 | -------------------------------------------------------------------------------- /lib/ui/route/app_route.gr.dart: -------------------------------------------------------------------------------- 1 | // ************************************************************************** 2 | // AutoRouteGenerator 3 | // ************************************************************************** 4 | 5 | // GENERATED CODE - DO NOT MODIFY BY HAND 6 | 7 | // ************************************************************************** 8 | // AutoRouteGenerator 9 | // ************************************************************************** 10 | 11 | import 'package:app/data/model/article.dart' as _i8; 12 | import 'package:app/ui/detail/detail_page.dart' as _i3; 13 | import 'package:app/ui/home/home_page.dart' as _i1; 14 | import 'package:app/ui/news/news_page.dart' as _i4; 15 | import 'package:app/ui/signIn/sign_in_page.dart' as _i2; 16 | import 'package:app/ui/video/video_page.dart' as _i5; 17 | import 'package:auto_route/auto_route.dart' as _i6; 18 | import 'package:flutter/material.dart' as _i7; 19 | 20 | class AppRouter extends _i6.RootStackRouter { 21 | AppRouter([_i7.GlobalKey<_i7.NavigatorState>? navigatorKey]) 22 | : super(navigatorKey); 23 | 24 | @override 25 | final Map pagesMap = { 26 | HomeRoute.name: (routeData) { 27 | return _i6.AdaptivePage( 28 | routeData: routeData, child: const _i1.HomePage()); 29 | }, 30 | SignInRoute.name: (routeData) { 31 | return _i6.AdaptivePage( 32 | routeData: routeData, 33 | child: _i2.SignInPage(), 34 | fullscreenDialog: true); 35 | }, 36 | DetailRoute.name: (routeData) { 37 | final queryParams = routeData.queryParams; 38 | final args = routeData.argsAs( 39 | orElse: () => DetailRouteArgs(article: queryParams.get('article'))); 40 | return _i6.AdaptivePage( 41 | routeData: routeData, child: _i3.DetailPage(article: args.article)); 42 | }, 43 | NewsRoute.name: (routeData) { 44 | return _i6.AdaptivePage( 45 | routeData: routeData, child: _i4.NewsPage()); 46 | }, 47 | VideoRoute.name: (routeData) { 48 | return _i6.AdaptivePage( 49 | routeData: routeData, child: _i5.VideoPage()); 50 | } 51 | }; 52 | 53 | @override 54 | List<_i6.RouteConfig> get routes => [ 55 | _i6.RouteConfig(HomeRoute.name, path: '/', children: [ 56 | _i6.RouteConfig(NewsRoute.name, path: 'news', parent: HomeRoute.name), 57 | _i6.RouteConfig(VideoRoute.name, 58 | path: 'video', parent: HomeRoute.name) 59 | ]), 60 | _i6.RouteConfig(SignInRoute.name, path: '/signIn'), 61 | _i6.RouteConfig(DetailRoute.name, path: '/detail') 62 | ]; 63 | } 64 | 65 | /// generated route for [_i1.HomePage] 66 | class HomeRoute extends _i6.PageRouteInfo { 67 | const HomeRoute({List<_i6.PageRouteInfo>? children}) 68 | : super(name, path: '/', initialChildren: children); 69 | 70 | static const String name = 'HomeRoute'; 71 | } 72 | 73 | /// generated route for [_i2.SignInPage] 74 | class SignInRoute extends _i6.PageRouteInfo { 75 | const SignInRoute() : super(name, path: '/signIn'); 76 | 77 | static const String name = 'SignInRoute'; 78 | } 79 | 80 | /// generated route for [_i3.DetailPage] 81 | class DetailRoute extends _i6.PageRouteInfo { 82 | DetailRoute({_i8.Article? article}) 83 | : super(name, 84 | path: '/detail', 85 | args: DetailRouteArgs(article: article), 86 | rawQueryParams: {'article': article}); 87 | 88 | static const String name = 'DetailRoute'; 89 | } 90 | 91 | class DetailRouteArgs { 92 | const DetailRouteArgs({this.article}); 93 | 94 | final _i8.Article? article; 95 | } 96 | 97 | /// generated route for [_i4.NewsPage] 98 | class NewsRoute extends _i6.PageRouteInfo { 99 | const NewsRoute() : super(name, path: 'news'); 100 | 101 | static const String name = 'NewsRoute'; 102 | } 103 | 104 | /// generated route for [_i5.VideoPage] 105 | class VideoRoute extends _i6.PageRouteInfo { 106 | const VideoRoute() : super(name, path: 'video'); 107 | 108 | static const String name = 'VideoRoute'; 109 | } 110 | -------------------------------------------------------------------------------- /lib/ui/signIn/sign_in_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/gen/assets.gen.dart'; 2 | import 'package:app/ui/component/image/image.dart'; 3 | import 'package:app/ui/component/loading/container_with_loading.dart'; 4 | import 'package:app/ui/hook/use_l10n.dart'; 5 | import 'package:app/ui/loading_state_view_model.dart'; 6 | import 'package:app/ui/theme/app_theme.dart'; 7 | import 'package:app/ui/user_view_model.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:gap/gap.dart'; 10 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 11 | 12 | class SignInPage extends HookConsumerWidget { 13 | const SignInPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final theme = ref.watch(appThemeProvider); 18 | final l10n = useL10n(); 19 | final user = ref.watch(userViewModelProvider.select((value) => value.user)); 20 | final userViewModel = ref.read(userViewModelProvider); 21 | final loading = ref.read(loadingStateProvider); 22 | 23 | return Scaffold( 24 | appBar: AppBar( 25 | title: Text( 26 | l10n.signIn, 27 | style: theme.textTheme.h60.bold().rotunda(), 28 | ), 29 | ), 30 | body: ContainerWithLoading( 31 | child: Column( 32 | children: [ 33 | const Gap(16), 34 | Container( 35 | margin: const EdgeInsets.symmetric(horizontal: 16), 36 | decoration: BoxDecoration( 37 | border: Border.all(color: theme.appColors.divider), 38 | borderRadius: BorderRadius.circular(8), 39 | ), 40 | child: Padding( 41 | padding: const EdgeInsets.symmetric( 42 | vertical: 12, 43 | horizontal: 8, 44 | ), 45 | child: Row( 46 | crossAxisAlignment: CrossAxisAlignment.start, 47 | children: [ 48 | Container( 49 | width: 60, 50 | height: 60, 51 | decoration: BoxDecoration( 52 | shape: BoxShape.circle, 53 | image: DecorationImage( 54 | fit: BoxFit.cover, 55 | image: profileImageProvider(user?.imageUrl), 56 | ), 57 | ), 58 | ), 59 | const Gap(12), 60 | Expanded( 61 | child: Column( 62 | crossAxisAlignment: CrossAxisAlignment.start, 63 | children: [ 64 | Text( 65 | user?.name ?? l10n.displayName, 66 | style: theme.textTheme.h50, 67 | ), 68 | const Gap(10), 69 | Text( 70 | user?.email ?? l10n.email, 71 | style: theme.textTheme.h30, 72 | ), 73 | const Gap(10), 74 | Text( 75 | user?.userId ?? l10n.uid, 76 | style: theme.textTheme.h30, 77 | ), 78 | ], 79 | ), 80 | ) 81 | ], 82 | ), 83 | ), 84 | ), 85 | const Gap(12), 86 | TextButton.icon( 87 | style: TextButton.styleFrom( 88 | backgroundColor: theme.appColors.signIn, 89 | ), 90 | onPressed: () { 91 | loading.whileLoading(() async { 92 | return ref.read(userViewModelProvider).signIn(); 93 | }); 94 | }, 95 | icon: Assets.svgs.firebase.svg(width: 24), 96 | label: Text( 97 | l10n.googleSignIn, 98 | style: theme.textTheme.h50.copyWith( 99 | color: Colors.white, 100 | ), 101 | ), 102 | ), 103 | const Gap(8), 104 | TextButton( 105 | style: TextButton.styleFrom( 106 | backgroundColor: theme.appColors.signOut, 107 | ), 108 | onPressed: userViewModel.signOut, 109 | child: Text( 110 | l10n.googleSignOut, 111 | style: theme.textTheme.h50.copyWith( 112 | color: Colors.white, 113 | ), 114 | ), 115 | ) 116 | ], 117 | ), 118 | ), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/ui/theme/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Color converter: https://www.w3schools.com/colors/colors_converter.asp 4 | // Transparency list 5 | // 100% FF 6 | // 95% F2 7 | // 90% E6 8 | // 87% DE 9 | // 85% D9 10 | // 80% CC 11 | // 75% BF 12 | // 70% B3 13 | // 65% A6 14 | // 60% 99 15 | // 55% 8C 16 | // 54% 8A 17 | // 50% 80 18 | // 45% 73 19 | // 40% 66 20 | // 35% 59 21 | // 32% 52 22 | // 30% 4D 23 | // 26% 42 24 | // 25% 40 25 | // 20% 33 26 | // 16% 29 27 | // 15% 26 28 | // 12% 1F 29 | // 10% 1A 30 | // 5% 0D 31 | 32 | class AppColors { 33 | const AppColors({ 34 | required this.background, 35 | required this.accent, 36 | required this.disabled, 37 | required this.error, 38 | required this.divider, 39 | required this.signIn, 40 | required this.signOut, 41 | }); 42 | 43 | factory AppColors.light() { 44 | return const AppColors( 45 | background: Colors.white, 46 | accent: Color(0xff17c063), 47 | disabled: Colors.black12, 48 | error: Color(0xffff7466), 49 | divider: Colors.black54, 50 | signIn: Color(0xff4285f4), 51 | signOut: Color(0xffc53829), 52 | ); 53 | } 54 | 55 | factory AppColors.dark() { 56 | return const AppColors( 57 | background: Color(0xff121212), 58 | accent: Color(0xff17c063), 59 | disabled: Colors.white12, 60 | error: Color(0xffff5544), 61 | divider: Colors.white54, 62 | signIn: Color(0xff4285f4), 63 | signOut: Color(0xffc53829), 64 | ); 65 | } 66 | 67 | final Color background; 68 | final Color accent; 69 | final Color disabled; 70 | final Color error; 71 | final Color divider; 72 | 73 | final Color signIn; 74 | final Color signOut; 75 | } 76 | -------------------------------------------------------------------------------- /lib/ui/theme/app_text_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/gen/fonts.gen.dart'; 2 | import 'package:app/ui/theme/font_size.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class AppTextTheme { 6 | const AppTextTheme._({ 7 | required this.h10, 8 | required this.h20, 9 | required this.h30, 10 | required this.h40, 11 | required this.h50, 12 | required this.h60, 13 | required this.h70, 14 | required this.h80, 15 | }); 16 | 17 | factory AppTextTheme() { 18 | const _normalRegular = TextStyle( 19 | fontWeight: FontWeight.w400, 20 | height: 1.5, 21 | leadingDistribution: TextLeadingDistribution.even, 22 | ); 23 | return AppTextTheme._( 24 | h10: const TextStyle(fontSize: FontSize.pt10).merge(_normalRegular), 25 | h20: const TextStyle(fontSize: FontSize.pt12).merge(_normalRegular), 26 | h30: const TextStyle(fontSize: FontSize.pt14).merge(_normalRegular), 27 | h40: const TextStyle(fontSize: FontSize.pt16).merge(_normalRegular), 28 | h50: const TextStyle(fontSize: FontSize.pt20).merge(_normalRegular), 29 | h60: const TextStyle(fontSize: FontSize.pt24).merge(_normalRegular), 30 | h70: const TextStyle(fontSize: FontSize.pt32).merge(_normalRegular), 31 | h80: const TextStyle(fontSize: FontSize.pt40).merge(_normalRegular), 32 | ); 33 | } 34 | 35 | /// pt10 36 | final TextStyle h10; 37 | 38 | /// pt12 39 | final TextStyle h20; 40 | 41 | /// pt14 42 | final TextStyle h30; 43 | 44 | /// pt16 45 | final TextStyle h40; 46 | 47 | /// pt20 48 | final TextStyle h50; 49 | 50 | /// pt24 51 | final TextStyle h60; 52 | 53 | /// pt32 54 | final TextStyle h70; 55 | 56 | /// pt40 57 | final TextStyle h80; 58 | } 59 | 60 | extension TextStyleExt on TextStyle { 61 | TextStyle bold() => copyWith(fontWeight: FontWeight.w700); 62 | 63 | TextStyle comfort() => copyWith(height: 1.8); 64 | 65 | TextStyle dense() => copyWith(height: 1.2); 66 | 67 | TextStyle rotunda() => copyWith(fontFamily: FontFamily.rotunda); 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/theme/app_colors.dart'; 2 | import 'package:app/ui/theme/app_text_theme.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:google_fonts/google_fonts.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | export 'package:app/ui/theme/app_text_theme.dart' show TextStyleExt; 8 | 9 | final appThemeModeProvider = 10 | StateNotifierProvider, ThemeMode>( 11 | (ref) => StateController(ThemeMode.dark), 12 | ); 13 | 14 | final appThemeProvider = Provider( 15 | (ref) { 16 | final mode = ref.watch(appThemeModeProvider); 17 | switch (mode) { 18 | case ThemeMode.dark: 19 | return AppTheme.dark(); 20 | case ThemeMode.light: 21 | default: 22 | return AppTheme.light(); 23 | } 24 | }, 25 | ); 26 | 27 | class AppTheme { 28 | AppTheme({ 29 | required this.mode, 30 | required this.data, 31 | required this.textTheme, 32 | required this.appColors, 33 | }); 34 | 35 | factory AppTheme.light() { 36 | const mode = ThemeMode.light; 37 | final appColors = AppColors.light(); 38 | final themeData = ThemeData.light().copyWith( 39 | scaffoldBackgroundColor: appColors.background, 40 | textTheme: GoogleFonts.notoSansTextTheme(ThemeData.light().textTheme), 41 | snackBarTheme: SnackBarThemeData( 42 | backgroundColor: appColors.error, 43 | behavior: SnackBarBehavior.floating, 44 | ), 45 | ); 46 | return AppTheme( 47 | mode: mode, 48 | data: themeData, 49 | textTheme: AppTextTheme(), 50 | appColors: appColors, 51 | ); 52 | } 53 | 54 | factory AppTheme.dark() { 55 | const mode = ThemeMode.dark; 56 | final appColors = AppColors.dark(); 57 | final themeData = ThemeData.dark().copyWith( 58 | scaffoldBackgroundColor: appColors.background, 59 | textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme), 60 | snackBarTheme: SnackBarThemeData( 61 | backgroundColor: appColors.error, 62 | behavior: SnackBarBehavior.floating, 63 | ), 64 | ); 65 | return AppTheme( 66 | mode: mode, 67 | data: themeData, 68 | textTheme: AppTextTheme(), 69 | appColors: appColors, 70 | ); 71 | } 72 | 73 | final ThemeMode mode; 74 | final ThemeData data; 75 | final AppTextTheme textTheme; 76 | final AppColors appColors; 77 | } 78 | -------------------------------------------------------------------------------- /lib/ui/theme/font_size.dart: -------------------------------------------------------------------------------- 1 | class FontSize { 2 | /// 40pt 3 | static const double pt40 = 40; 4 | 5 | /// 32pt 6 | static const double pt32 = 32; 7 | 8 | /// 28pt 9 | static const double pt28 = 28; 10 | 11 | /// 24pt 12 | static const double pt24 = 24; 13 | 14 | /// 20pt 15 | static const double pt20 = 20; 16 | 17 | /// 18pt 18 | static const double pt18 = 18; 19 | 20 | /// 16pt 21 | static const double pt16 = 16; 22 | 23 | /// 14pt 24 | static const double pt14 = 14; 25 | 26 | /// 12pt 27 | static const double pt12 = 12; 28 | 29 | /// 10pt 30 | static const double pt10 = 10; 31 | 32 | /// 8pt 33 | static const double pt8 = 8; 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/user_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/local/app_user.dart'; 2 | import 'package:app/data/repository/auth_repository.dart'; 3 | import 'package:app/data/repository/auth_repository_impl.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | final userViewModelProvider = 8 | ChangeNotifierProvider((ref) => UserViewModel(ref.read)); 9 | 10 | class UserViewModel extends ChangeNotifier { 11 | UserViewModel(this._reader); 12 | 13 | final Reader _reader; 14 | 15 | late final AuthRepository _repository = _reader(authRepositoryProvider); 16 | 17 | AppUser? _user; 18 | 19 | AppUser? get user => _user; 20 | 21 | bool get isAuthenticated => _user != null; 22 | 23 | Future signIn() { 24 | return _repository.signIn().then((result) { 25 | // Result use case No.2 26 | result.ifSuccess((data) { 27 | _user = data; 28 | notifyListeners(); 29 | }); 30 | }); 31 | } 32 | 33 | Future signOut() { 34 | return _repository.signOut().then((result) { 35 | return result.when( 36 | success: (_) { 37 | _user = null; 38 | notifyListeners(); 39 | }, 40 | failure: (_) => result, 41 | ); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/video/video_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/gen/assets.gen.dart'; 2 | import 'package:app/ui/hook/use_asset_vide_player_controller.dart'; 3 | import 'package:app/ui/hook/use_l10n.dart'; 4 | import 'package:app/ui/theme/app_theme.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:video_player/video_player.dart'; 8 | 9 | class VideoPage extends HookConsumerWidget { 10 | const VideoPage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final theme = ref.watch(appThemeProvider); 15 | final l10n = useL10n(); 16 | final videoController = useAssetVideoController( 17 | asset: Assets.videos.bigbuckbunny, 18 | autoPlay: true, 19 | looping: true, 20 | ); 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text( 24 | l10n.video, 25 | style: theme.textTheme.h60.bold().rotunda(), 26 | ), 27 | ), 28 | body: Center( 29 | child: AspectRatio( 30 | aspectRatio: 16 / 9, 31 | child: VideoPlayer(videoController), 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-architecture-blueprints", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "husky": "^6.0.0" 9 | } 10 | }, 11 | "node_modules/husky": { 12 | "version": "6.0.0", 13 | "resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz", 14 | "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", 15 | "dev": true, 16 | "bin": { 17 | "husky": "lib/bin.js" 18 | }, 19 | "funding": { 20 | "url": "https://github.com/sponsors/typicode" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "husky": { 26 | "version": "6.0.0", 27 | "resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz", 28 | "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", 29 | "dev": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-architecture-blueprints", 3 | "private": true, 4 | "husky": { 5 | "hooks": { 6 | "pre-push": "make format-analyze" 7 | } 8 | }, 9 | "devDependencies": { 10 | "husky": "^6.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: Flutter Architecture Blueprints project. 3 | version: 1.0.0+1 4 | 5 | publish_to: 'none' 6 | 7 | environment: 8 | sdk: '>=2.13.0 <3.0.0' 9 | flutter: '>=2.0.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_localizations: 15 | sdk: flutter 16 | 17 | # For Google and Firebase 18 | firebase_core: ^1.8.0 19 | firebase_auth: ^3.1.4 20 | google_sign_in: ^5.2.0 21 | firebase_crashlytics: ^2.2.4 22 | firebase_performance: ^0.7.1+2 23 | 24 | # For Architecture 25 | flutter_hooks: ^0.18.0 26 | hooks_riverpod: ^1.0.0-dev.10 27 | 28 | # For Networking 29 | dio: ^4.0.1 30 | dio_firebase_performance: ^0.3.1-dev.3 31 | retrofit: ^2.0.1 32 | # For User-Agent Client Hints 33 | ua_client_hints: ^1.1.0 34 | 35 | # For Model 36 | json_serializable: ^5.0.2 37 | freezed_annotation: ^0.15.0 38 | 39 | # For Navigation 40 | auto_route: ^3.0.4 41 | 42 | # For DateTime 43 | intl: ^0.17.0 44 | 45 | # Convert between Enum and String 46 | enum_to_string: ^2.0.1 47 | 48 | # For UIs 49 | gap: ^2.0.0 50 | cupertino_icons: ^1.0.3 51 | google_fonts: ^2.1.0 52 | flutter_svg: ^0.23.0+1 53 | video_player: ^2.2.5 54 | 55 | dev_dependencies: 56 | flutter_test: 57 | sdk: flutter 58 | 59 | build_runner: ^2.0.4 60 | 61 | # For Networking 62 | retrofit_generator: ^2.0.0+1 63 | 64 | # For Assets 65 | flutter_gen_runner: ^4.0.1 66 | 67 | # For Navigation 68 | auto_route_generator: ^3.0.1 69 | 70 | # For Model 71 | freezed: ^0.15.0 72 | 73 | # For Analyzer 74 | flutter_lints: ^1.0.4 75 | 76 | # For Testing 77 | mocktail: ^0.2.0 78 | mocktail_image_network: ^0.2.0 79 | 80 | flutter_gen: 81 | integrations: 82 | flutter_svg: true 83 | 84 | flutter: 85 | uses-material-design: true 86 | generate: true 87 | 88 | assets: 89 | - assets/images/ 90 | - assets/svgs/ 91 | - assets/videos/ 92 | 93 | fonts: 94 | - family: Rotunda 95 | fonts: 96 | - asset: assets/fonts/Rotunda-Bold.otf 97 | weight: 700 98 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "group:monorepos" 5 | ], 6 | "timezone": "Asia/Tokyo", 7 | "schedule": ["before 10am on monday"], 8 | "labels": ["dependencies"], 9 | "branchPrefix": "chore-renovate-", 10 | "semanticCommits": "auto", 11 | "semanticCommitType": "build", 12 | "semanticCommitScope": null, 13 | "rangeStrategy": "replace", 14 | "rebaseWhen": "conflicted", 15 | "lockFileMaintenance": { 16 | "enabled": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o pipefail 4 | 5 | TOKEN="${1}" 6 | export CODECOV_TOKEN=${TOKEN} 7 | bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /test/data/app_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:app/data/app_error.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | test('AppError Test', () async { 9 | expect( 10 | AppError( 11 | DioError( 12 | type: DioErrorType.connectTimeout, 13 | requestOptions: RequestOptions(path: '')), 14 | ).type, 15 | equals(AppErrorType.timeout)); 16 | 17 | expect( 18 | AppError( 19 | DioError( 20 | type: DioErrorType.receiveTimeout, 21 | requestOptions: RequestOptions(path: '')), 22 | ).type, 23 | equals(AppErrorType.timeout)); 24 | 25 | expect( 26 | AppError( 27 | DioError( 28 | type: DioErrorType.sendTimeout, 29 | requestOptions: RequestOptions(path: '')), 30 | ).type, 31 | equals(AppErrorType.network)); 32 | 33 | expect( 34 | AppError( 35 | DioError( 36 | type: DioErrorType.response, 37 | requestOptions: RequestOptions(path: ''), 38 | response: Response( 39 | requestOptions: RequestOptions(path: ''), statusCode: 400)), 40 | ).type, 41 | equals(AppErrorType.badRequest)); 42 | 43 | expect( 44 | AppError( 45 | DioError( 46 | type: DioErrorType.response, 47 | requestOptions: RequestOptions(path: ''), 48 | response: Response( 49 | requestOptions: RequestOptions(path: ''), statusCode: 401)), 50 | ).type, 51 | equals(AppErrorType.unauthorized)); 52 | 53 | expect( 54 | AppError( 55 | DioError( 56 | type: DioErrorType.response, 57 | requestOptions: RequestOptions(path: ''), 58 | response: Response( 59 | requestOptions: RequestOptions(path: ''), statusCode: 500)), 60 | ).type, 61 | equals(AppErrorType.server)); 62 | 63 | expect( 64 | AppError( 65 | DioError( 66 | type: DioErrorType.cancel, 67 | requestOptions: RequestOptions(path: '')), 68 | ).type, 69 | equals(AppErrorType.cancel)); 70 | 71 | expect( 72 | AppError( 73 | DioError( 74 | error: const SocketException('Failed host lookup: wasabeef.jp'), 75 | type: DioErrorType.other, 76 | requestOptions: RequestOptions(path: '')), 77 | ).type, 78 | equals(AppErrorType.network)); 79 | 80 | expect( 81 | AppError( 82 | DioError( 83 | type: DioErrorType.other, 84 | requestOptions: RequestOptions(path: '')), 85 | ).type, 86 | equals(AppErrorType.unknown)); 87 | 88 | expect(AppError(const FileSystemException()).type, 89 | equals(AppErrorType.unknown)); 90 | 91 | expect(AppError(null).type, equals(AppErrorType.unknown)); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /test/data/dummy/dummy_article.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/article.dart'; 2 | import 'package:app/data/model/source.dart'; 3 | 4 | final dummyArticle = Article( 5 | source: Source(id: 'wasabeef', name: 'Daichi Furiya'), 6 | author: 'wasabeef', 7 | title: 'flutter architecture blueprints', 8 | description: 9 | 'Flutter Architecture Blueprint is a project that introduces MVVM' 10 | ' architecture and project structure approaches to' 11 | ' developing Flutter apps.', 12 | url: 'https://github.com/wasabeef/flutter-architecture-blueprints', 13 | publishedAt: DateTime.now(), 14 | content: 'content', 15 | ); 16 | -------------------------------------------------------------------------------- /test/data/dummy/dummy_news.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/article.dart'; 2 | import 'package:app/data/model/news.dart'; 3 | 4 | import 'dummy_article.dart'; 5 | 6 | final dummyNews = News(status: 'success', totalResults: 5, articles:
[ 7 | dummyArticle, 8 | dummyArticle, 9 | dummyArticle, 10 | dummyArticle, 11 | dummyArticle 12 | ]); 13 | -------------------------------------------------------------------------------- /test/data/remote/fake_app_dio.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:app/data/remote/app_dio.dart'; 4 | import 'package:dio/dio.dart'; 5 | 6 | import '../dummy/dummy_response_news_api.dart'; 7 | 8 | class FakeAppDio implements AppDio { 9 | @override 10 | BaseOptions get options => BaseOptions(); 11 | 12 | @override 13 | Future> fetch(RequestOptions requestOptions) async { 14 | if (requestOptions.path.contains('/v2/everything')) { 15 | return FakeResponse( 16 | json.decode(dummyResponseNewsApi) as Map?) 17 | as Response; 18 | } else { 19 | throw UnimplementedError(); 20 | } 21 | } 22 | 23 | @override 24 | void noSuchMethod(Invocation invocation) { 25 | throw UnimplementedError(); 26 | } 27 | } 28 | 29 | class FakeResponse implements Response> { 30 | FakeResponse(this.data); 31 | 32 | @override 33 | final Map? data; 34 | 35 | @override 36 | void noSuchMethod(Invocation invocation) { 37 | throw UnimplementedError(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/data/remote/fake_news_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/news.dart'; 2 | import 'package:app/data/remote/news_data_source.dart'; 3 | 4 | import '../dummy/dummy_news.dart'; 5 | 6 | class FakeNewsDataSourceImpl implements NewsDataSource { 7 | @override 8 | Future getNews({ 9 | required String query, 10 | required String from, 11 | String? sortBy = 'publishedAt', 12 | String? language = 'en', 13 | required String apiKey, 14 | }) async { 15 | // TODO: implement getNews 16 | return dummyNews; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/data/repository/fake_news_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/news.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | import 'package:app/data/repository/news_repository_impl.dart'; 4 | 5 | import '../dummy/dummy_news.dart'; 6 | 7 | class FakeNewsRepositoryImpl implements NewsRepositoryImpl { 8 | @override 9 | Future> getNews() async { 10 | return Result.success(data: dummyNews); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/foundation/extension/date_time_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/foundation/extension/date_time.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | void main() { 6 | test('DateTime Test', () async { 7 | final date = DateTime(2020, 8, 10, 5, 55, 6); 8 | 9 | expect(date.format(DateFormat('yyyyMMddHHmmss')), equals('20200810055506')); 10 | expect(date.formatYYYYMMdd(), equals('2020-08-10')); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/ui/view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/model/result.dart'; 2 | import 'package:app/data/remote/app_dio.dart'; 3 | import 'package:app/data/remote/news_data_source.dart'; 4 | import 'package:app/data/repository/news_repository_impl.dart'; 5 | import 'package:app/foundation/constants.dart'; 6 | import 'package:app/ui/news/news_view_model.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 9 | import 'package:mocktail/mocktail.dart'; 10 | 11 | import '../data/dummy/dummy_news.dart'; 12 | import '../data/remote/fake_app_dio.dart'; 13 | import '../data/remote/fake_news_data_source_impl.dart'; 14 | import '../data/repository/fake_news_repository_impl.dart'; 15 | 16 | class MockNewsRepositoryImpl extends Mock implements NewsRepositoryImpl {} 17 | 18 | void main() { 19 | test('HomeViewModel Test', () async { 20 | final container = ProviderContainer( 21 | overrides: [ 22 | newsRepositoryProvider.overrideWithValue(FakeNewsRepositoryImpl()) 23 | ], 24 | ); 25 | final viewModel = container.read(newsViewModelProvider); 26 | await expectLater( 27 | viewModel.fetchNews(), completion(Result.success(data: dummyNews))); 28 | }); 29 | 30 | test('NewsRepository Test', () async { 31 | final container = ProviderContainer( 32 | overrides: [ 33 | newsDataSourceProvider.overrideWithValue(FakeNewsDataSourceImpl()) 34 | ], 35 | ); 36 | final newsRepository = container.read(newsRepositoryProvider); 37 | await expectLater( 38 | newsRepository.getNews(), completion(Result.success(data: dummyNews))); 39 | }); 40 | 41 | test('NewsDataSource Test', () async { 42 | final container = ProviderContainer( 43 | overrides: [dioProvider.overrideWithValue(FakeAppDio())], 44 | ); 45 | final dataSource = container.read(newsDataSourceProvider); 46 | await expectLater( 47 | dataSource.getNews( 48 | apiKey: 'apikey', 49 | from: 'from', 50 | query: 'query', 51 | ), 52 | completion(isNotNull)); 53 | }); 54 | 55 | test('AppDio options Test', () async { 56 | final realDio = AppDio.getInstance(); 57 | expect(realDio.options.baseUrl, Constants.of().endpoint); 58 | expect(realDio.options.contentType, 'application/json'); 59 | expect(realDio.options.connectTimeout, 30000); 60 | expect(realDio.options.sendTimeout, 30000); 61 | expect(realDio.options.receiveTimeout, 30000); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/ui/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/app.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | import 'package:app/ui/component/loading/loading.dart'; 4 | import 'package:app/ui/news/news_page.dart'; 5 | import 'package:app/ui/news/news_view_model.dart'; 6 | import 'package:app/ui/route/app_route.gr.dart'; 7 | import 'package:app/ui/user_view_model.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 12 | import 'package:mocktail/mocktail.dart'; 13 | import 'package:mocktail_image_network/mocktail_image_network.dart'; 14 | 15 | import '../data/dummy/dummy_news.dart'; 16 | 17 | class MockNewsViewModel extends Mock implements NewsViewModel {} 18 | 19 | class MockUserViewModel extends Mock implements UserViewModel {} 20 | 21 | void main() { 22 | final mockNewsViewModel = MockNewsViewModel(); 23 | when(mockNewsViewModel.fetchNews).thenAnswer((_) => Future.value()); 24 | when(() => mockNewsViewModel.news) 25 | .thenReturn(Result.success(data: dummyNews)); 26 | 27 | final mockUserViewModel = MockUserViewModel(); 28 | when(mockUserViewModel.signIn).thenAnswer((_) => Future.value()); 29 | when(mockUserViewModel.signOut).thenAnswer((_) => Future.value()); 30 | 31 | testWidgets('App widget test', (tester) async { 32 | await tester.pumpWidget( 33 | ProviderScope( 34 | overrides: [ 35 | newsViewModelProvider.overrideWithValue(mockNewsViewModel), 36 | userViewModelProvider.overrideWithValue(mockUserViewModel), 37 | ], 38 | child: const App(), 39 | ), 40 | ); 41 | }); 42 | 43 | testWidgets('HomePage widget test', (tester) async { 44 | await mockNetworkImages(() async { 45 | final appRouter = AppRouter(); 46 | await tester.pumpWidget( 47 | ProviderScope( 48 | overrides: [ 49 | newsViewModelProvider.overrideWithValue(mockNewsViewModel), 50 | userViewModelProvider.overrideWithValue(mockUserViewModel), 51 | ], 52 | child: MaterialApp.router( 53 | localizationsDelegates: L10n.localizationsDelegates, 54 | supportedLocales: L10n.supportedLocales, 55 | routeInformationParser: appRouter.defaultRouteParser(), 56 | routerDelegate: appRouter.delegate(), 57 | ), 58 | ), 59 | ); 60 | await tester.pumpAndSettle(); 61 | 62 | expect(appRouter.current.name == HomeRoute.name, true); 63 | expect(find.byType(NewsPage), findsOneWidget); 64 | }); 65 | }); 66 | 67 | testWidgets('Loading widget test', (tester) async { 68 | const loading = Loading(); 69 | await tester.pumpWidget(const ProviderScope(child: loading)); 70 | expect(find.byWidget(loading), findsOneWidget); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabeef/flutter-architecture-blueprints/a4d87a6d0a7f1d249b89795b567fd6325ac34d51/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "short_name": "app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | --------------------------------------------------------------------------------