├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── flutter-ci.yml ├── .gitignore ├── .idea ├── .gitignore └── runConfigurations │ ├── development.xml │ └── production.xml ├── .metadata ├── .vscode └── launch.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 ├── 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 ├── constants.dart ├── data │ ├── app_error.dart │ ├── local │ │ ├── app_shared_preferences.dart │ │ ├── theme_data_source.dart │ │ └── theme_data_source_impl.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 │ │ ├── app_shared_preferences_provider.dart │ │ ├── auth_data_source_provider.dart │ │ ├── auth_repository_provider.dart │ │ ├── dio_provider.dart │ │ ├── firebase_auth_provider.dart │ │ ├── news_data_source_provider.dart │ │ ├── news_repository_provider.dart │ │ ├── theme_data_source_provider.dart │ │ └── theme_repository_provider.dart │ ├── remote │ │ ├── app_dio.dart │ │ ├── auth_data_source.dart │ │ ├── auth_data_source_impl.dart │ │ ├── news_data_source.dart │ │ └── news_data_source_impl.dart │ └── repository │ │ ├── auth_repository.dart │ │ ├── auth_repository_impl.dart │ │ ├── news_repository.dart │ │ ├── news_repository_impl.dart │ │ ├── theme_repository.dart │ │ └── theme_repository_impl.dart ├── gen │ ├── assets.gen.dart │ └── fonts.gen.dart ├── l10n │ ├── intl_messages_en.arb │ └── intl_messages_ja.arb ├── main.dart ├── ui │ ├── app_theme.dart │ ├── component │ │ ├── article_item.dart │ │ ├── container_with_loading.dart │ │ ├── dialog.dart │ │ ├── image.dart │ │ ├── loading.dart │ │ └── network_image.dart │ ├── detail │ │ └── detail_page.dart │ ├── home │ │ ├── home_page.dart │ │ └── home_view_model.dart │ ├── loading_state_view_model.dart │ ├── signIn │ │ └── sign_in_page.dart │ └── user_view_model.dart └── util │ ├── error_snackbar.dart │ └── ext │ ├── async_snapshot.dart │ └── date_time.dart ├── package-lock.json ├── package.json ├── pubspec.lock ├── pubspec.yaml ├── scripts └── codecov.sh ├── test ├── data │ ├── app_error_test.dart │ ├── dummy │ │ ├── dummy_article.dart │ │ ├── dummy_news.dart │ │ └── dummy_response_news_api.dart │ ├── local │ │ └── fake_theme_data_source_impl.dart │ ├── remote │ │ ├── fake_app_dio.dart │ │ └── fake_news_data_source_impl.dart │ └── repository │ │ └── fake_news_repository_impl.dart ├── ui │ ├── app_theme_test.dart │ ├── view_model_test.dart │ └── widget_test.dart └── util │ └── ext │ └── date_time_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 | ### What is the value of this and can you measure success? 4 | 5 | ### Screenshots (Optional) 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "20:00" 14 | open-pull-requests-limit: 10 15 | -------------------------------------------------------------------------------- /.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: subosito/flutter-action@v1 15 | with: 16 | channel: 'beta' 17 | # TODO: Workaround https://github.com/flutter/flutter/issues/72737 18 | flutter-version: 1.24.0-10.2.pre 19 | 20 | - name: Cache Gradle modules 21 | uses: actions/cache@v2 22 | env: 23 | cache-number: ${{ secrets.CACHE_NUMBER }} 24 | with: 25 | path: | 26 | ~/android/.gradle 27 | ~/.gradle/cache 28 | # ~/.gradle/wrapper 29 | key: ${{ runner.os }}-gradle-${{ env.cache-number }}-${{ hashFiles('android/build.gradle') }}-${{ hashFiles('android/app/build.gradle') }} 30 | restore-keys: | 31 | ${{ runner.os }}-gradle-${{ env.cache-name }}-${{ hashFiles('android/build.gradle') }} 32 | ${{ runner.os }}-gradle-${{ env.cache-name }}- 33 | ${{ runner.os }}-gradle- 34 | ${{ runner.os }}- 35 | 36 | - name: Cache CocoaPods modules 37 | uses: actions/cache@v2 38 | env: 39 | cache-number: ${{ secrets.CACHE_NUMBER }} 40 | with: 41 | path: Pods 42 | key: ${{ runner.os }}-pods-${{ env.cache-number }}-${{ hashFiles('ios/Podfile.lock') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pods-${{ env.cache-name }}- 45 | ${{ runner.os }}-pods- 46 | ${{ runner.os }}- 47 | 48 | - name: Cache Flutter modules 49 | uses: actions/cache@v2 50 | env: 51 | cache-number: ${{ secrets.CACHE_NUMBER }} 52 | with: 53 | path: | 54 | /Users/runner/hostedtoolcache/flutter 55 | # ~/.pub-cache 56 | key: ${{ runner.os }}-pub-${{ env.cache-number }}-${{ env.flutter_version }}-${{ hashFiles('pubspec.lock') }} 57 | restore-keys: | 58 | ${{ runner.os }}-pub-${{ env.flutter_version }}- 59 | ${{ runner.os }}-pub- 60 | ${{ runner.os }}- 61 | 62 | - name: Get flutter dependencies. 63 | run: make dependencies 64 | 65 | - name: Check for any formatting and statically analyze the code. 66 | run: make format-analyze 67 | 68 | - name: Run widget tests for our flutter project. 69 | env: 70 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 71 | run: | 72 | make unit-test 73 | make codecov 74 | 75 | - name: Build ipa and apk 76 | run: | 77 | make build-android-prd 78 | make build-ios-prd 79 | 80 | - name: Slack Notification 81 | uses: homoluctus/slatify@master 82 | if: always() 83 | with: 84 | type: ${{ job.status }} 85 | job_name: '*Flutter Build*' 86 | mention: 'here' 87 | mention_if: 'failure' 88 | channel: '#develop' 89 | username: 'Github Actions' 90 | icon_emoji: ':octocat:' 91 | url: ${{ secrets.SLACK_WEBHOOK_URL }} 92 | commit: true 93 | token: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | -------------------------------------------------------------------------------- /.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 | # **/*.g.dart 109 | #lib/l10n/messages_* 110 | #l10n-arb/intl_messages.arb 111 | 112 | ## Test coverage 113 | coverage/ 114 | -------------------------------------------------------------------------------- /.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: e606910f28be51c8151f6169072afe3b3a8b3c5e 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 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 | flutter channel beta 4 | flutter upgrade 5 | flutter pub get 6 | npm install 7 | gem update cocoapods 8 | cd ios/ && pod install && cd .. 9 | 10 | .PHONY: dependencies 11 | dependencies: 12 | flutter pub get 13 | 14 | .PHONY: analyze 15 | analyze: 16 | flutter analyze 17 | 18 | .PHONY: format 19 | format: 20 | flutter format lib/ 21 | 22 | .PHONY: format-analyze 23 | format-analyze: 24 | flutter format --dry-run lib/ 25 | flutter analyze 26 | 27 | .PHONY: build-runner 28 | build-runner: 29 | flutter packages pub run build_runner build --delete-conflicting-outputs 30 | 31 | .PHONY: run-dev 32 | run-dev: 33 | flutter run --flavor development --dart-define=FLAVOR=development --target lib/main.dart 34 | 35 | .PHONY: run-prd 36 | run-prd: 37 | flutter run --release --flavor production --dart-define=FLAVOR=production --target lib/main.dart 38 | 39 | .PHONY: build-android-dev 40 | build-android-dev: 41 | flutter build apk --flavor development --dart-define=FLAVOR=development --target lib/main.dart 42 | 43 | .PHONY: build-android-prd 44 | build-android-prd: 45 | flutter build apk --release --flavor production --dart-define=FLAVOR=production --target lib/main.dart 46 | 47 | .PHONY: build-ios-dev 48 | build-ios-dev: 49 | cd ios/ && pod install && cd .. 50 | flutter build ios --no-codesign --flavor development --dart-define=FLAVOR=development --target lib/main.dart 51 | 52 | .PHONY: build-ios-prd 53 | build-ios-prd: 54 | cd ios/ && pod install && cd .. 55 | flutter build ios --release --no-codesign --flavor production --dart-define=FLAVOR=production --target lib/main.dart 56 | 57 | .PHONY: unit-test 58 | unit-test: 59 | flutter test --coverage --coverage-path=./coverage/lcov.info 60 | 61 | .PHONY: codecov 62 | codecov: 63 | ./scripts/codecov.sh ${CODECOV_TOKEN} 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Architecture Blueprints 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 | Flutter Architecture Blueprints is a project that introduces MVVM architecture and project structure approaches to developing Flutter apps. 28 | 29 | ## Documentation 30 | 31 | - [Install Flutter](https://flutter.dev/get-started/) 32 | - [Flutter documentation](https://flutter.dev/docs) 33 | - [Contributing to Flutter](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/CONTRIBUTING.md) 34 | 35 | ## Requirements 36 | 37 | - [Flutter 1.22.0+ (beta channel)](https://flutter.dev/docs/get-started/install) 38 | - [Dart 2.10.0+](https://github.com/dart-lang/sdk/wiki/Installing-beta-and-dev-releases-with-brew,-choco,-and-apt-get#installing) 39 | - [npm](https://treehouse.github.io/installation-guides/mac/node-mac.html) 40 | 41 | ## Environment 42 | 43 | 44 | 45 | **iOS** 46 | - iOS 13+ 47 | 48 | **Android** 49 | - Android 5.1+ 50 | - minSdkVersion 22 51 | - targetSdkVersion 30 52 | 53 | ## App architecture 54 | - Base on [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) + [Repository](https://docs.microsoft.com/ja-jp/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) 55 | 56 | ## Code Style 57 | - [Effective Dart](https://dart.dev/guides/language/effective-dart) 58 | 59 | ## Assets, Fonts 60 | 61 | **If added some assets or fonts** 62 | 63 | - Use [FlutterGen](https://github.com/FlutterGen/flutter_gen/) 64 | 65 | ## Models 66 | 67 | **If added some models for api results** 68 | 69 | - Use [Freezed](https://pub.dev/packages/freezed) 70 | 71 | ## Localizations 72 | 73 | **If added some localizations (i.g. edited [*.arb](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/lib/l10n))** 74 | 75 | - Use [Official Flutter localization package](https://docs.google.com/document/d/10e0saTfAv32OZLRmONy866vnaw0I2jwL8zukykpgWBc) 76 | 77 | ## Git Commit message style 78 | 79 | - [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) 80 | 81 | ## Code collections 82 | 83 | #### Project settings 84 | |Working status|Category|Description|Codes| 85 | |:---:|---|---|---| 86 | | ✅ | Dart | Dart version | [pubspec.yaml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/aed5d8fab3dee4fa8a967a8ecd7092fd2f727d5f/pubspec.yaml#L20-L22) | 87 | | ✅ | Dart | Switching between Development and Production environment | [constants.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/constants.dart), [runConfigurations](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/.idea/runConfigurations), [Makefile](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/Makefile#L25-L41) | 88 | | ✅ | Dart | Lint / Analyze | [analysis_options.yaml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/analysis_options.yaml) | 89 | | ✅ | Android | Kotlin version | [build.gradle](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/android/build.gradle#L2) | 90 | | ✅ | Android | Apk attributes | build.gradle ([compileSdkVersion](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/android/app/build.gradle#L30), [applicationId](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/android/app/build.gradle#L43), [minSdkVersion](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/android/app/build.gradle#L44), [targetSdkVersion](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/android/app/build.gradle#L45)) | 91 | | ✅ | Android | Switching between Development and Production environment | [build.gradle](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/android/app/build.gradle#L50-L75), [Flavor dirs](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/android/app/src), [signingConfigs](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/android/app/signingConfigs) | 92 | | ✅ | iOS | Xcode version | [compatibilityVersion](https://github.com/wasabeef/flutter-architecture-blueprints/blob/3ae7135cc040fecf5bbb2100a353f6594037752d/ios/Runner.xcodeproj/project.pbxproj#L182) | 93 | | ✅ | iOS | Podfile | [Podfile](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/ios/Podfile) | 94 | | ✅ | iOS | Switching between Development and Production environment | [xcconfig](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/ios/Config), [Podfile](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/ios/Podfile#L7-L12) | 95 | | ✅ | [Firebase](https://firebase.flutter.dev/docs/overview) | [Android] Switching between Development and Production google-service.json using flavors | [development and production](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/android/app/src) | 96 | | ✅ | [Firebase](https://firebase.flutter.dev/docs/overview) | [iOS] Switching between Development and Production GoogleService-Info.plist using run script| [copy_google_service.sh](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/ios/Scripts/copy_google_service.sh), [development and production](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/ios/Runner/Resources/Firebase) | 97 | | ✅ | [Firebase Auth](https://firebase.flutter.dev/docs/auth/overview) | SignIn, SignOut | [auth_data_source_impl.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/remote/auth_data_source_impl.dart) | 98 | | ✅ | [Firebase Crashlytics](https://firebase.flutter.dev/docs/crashlytics/overview) | Crash Reports | [main.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/main.dart) | 99 | | ✅ | [Firebase Performance](https://firebase.flutter.dev/docs/performance/overview) | Network monitoring with [dio_firebase_performance](https://pub.dev/packages/dio_firebase_performance) | [app_dio.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/eb749c742216088cbf2ff821f463e3de02d7d3b3/lib/data/remote/app_dio.dart#L27-L28) | 100 | 101 | #### Architecture 102 | 103 | |Working status|Category|Description|Codes| 104 | |:---:|---|---|---| 105 | | ✅ | Base | Using [Riverpod](https://pub.dev/packages/riverpod) + [Hooks](https://pub.dev/packages/flutter_hooks) + [ChangeNotifier](https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#changenotifier) + MVVM | [home_page.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/ui/home/home_page.dart#L41-L47), [home_view_model.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/ui/home/home_view_model.dart), [news_repository.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/repository/news_repository.dart), [news_data_source.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/remote/news_data_source.dart) | 106 | | ✅ | Networking | Using [dio](https://pub.dev/packages/dio) | [app_dio.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/remote/app_dio.dart), [news_data_source_impl.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/lib/data/remote/news_data_source_impl.dart#L16-L33) | 107 | | ✅ | Caching | Using [dio_http_cache](https://pub.dev/packages/dio_http_cache) | [app_dio.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/remote/app_dio.dart#L26-L30), [news_data_source_impl.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/remote/news_data_source_impl.dart#L35) | 108 | | ✅ | Data | Using [Freezed](https://pub.dev/packages/freezed) | [model classes](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/model) | 109 | | ✅ | Persist Data | Using [shared_preferences](https://pub.dev/packages/shared_preferences) | [theme_data_source_impl.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/local/theme_data_source_impl.dart) | 110 | | ✅ | Constants | Define constants and route names | [constants.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/constants.dart) | 111 | | ✅ | Localization | Switching between two languages with [Intl package](https://docs.google.com/document/d/10e0saTfAv32OZLRmONy866vnaw0I2jwL8zukykpgWBc/edit) | [*.arb](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/lib/l10n) | 112 | | ✅ | Error handling | Using Result pattern - A value that represents either a success or a failure, including an associated value in each case. | [result.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/model/result.dart), [news_repository_impl.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/data/repository/news_repository_impl.dart#L16), [home_page.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/e8f0ed78a62e5b27609e60206bd121295a13faac/lib/ui/home/home_page.dart#L51-L63) | 113 | 114 | #### UI 115 | |Working status|Category|Description|Codes| 116 | |:---:|---|---|---| 117 | | ✅ | Theme | Dynamically Switch between light and dark themes | [app_theme.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/app.dart#L22-L24) | 118 | | ✅ | Font | Using [Google font](https://pub.dev/packages/google_fonts) | [app_theme.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/lib/ui/app_theme.dart#L62) | 119 | | ✅ | Transition | Simple animation between screens using [Hero](https://flutter.dev/docs/development/ui/animations/hero-animations) | [article_item.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/lib/ui/component/article_item.dart#L28), [detail_page.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/be26dc3f7ff27ee2710326abe8ed09893a35386c/lib/ui/detail/datail_page.dart#L13-L24) | 120 | 121 | #### Testing 122 | |Working status|Category|Description|Codes| 123 | |:---:|---|---|---| 124 | | ✅ | API(Repositories) | Using [Mockito](https://pub.dev/packages/mockito) | [view_mode_test.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/test/ui/view_model_test.dart), [app_theme_test.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/test/ui/app_theme_test.dart) | 125 | | ✅️ | UI | Using [Mockito](https://pub.dev/packages/mockito) | [widget_test.dart](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/test/ui/widget_test.dart) | 126 | | ✅ | Coverage reports | Send the report to [Codecov](https://codecov.io/) on CI |[codecov.yml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/codecov.yml), [codecov.sh](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/scripts/codecov.sh), [flutte-ci.yml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/8e2a373af5e4603aaa75d3c9b9af8150400ab46e/.github/workflows/flutter-ci.yml#L66-L71) | 127 | 128 | #### CI 129 | |Working status|Category|Description|Codes| 130 | |:---:|---|---|---| 131 | | ✅ | Git | Git hooks for format and analyze | [package.json](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/package.json#L4-L11), [Makefile](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/Makefile#L9-L12)| 132 | | ✅ | Git | .gitignore settings | [.gitignore](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/.gitignore) | 133 | | ✅ | Build | Using [Codemagic](https://codemagic.io/) |[codemagic.yaml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/codemagic.yaml)| 134 | | ✅ | Build | Using [Bitrise](https://www.bitrise.io/) |[bitrise.yml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/bitrise.yml)| 135 | | ✅ | Build | Using [Github Actions](https://github.com/features/actions) | [.github/workflows/flutter-ci.yml](https://github.com/wasabeef/flutter-architecture-blueprints/blob/main/.github/workflows/flutter-ci.yml) | 136 | 137 | ## Getting Started 138 | 139 | 140 | 141 | ### Setup 142 | 143 | ```shell script 144 | $ make setup 145 | $ make build-runner 146 | ``` 147 | 148 | ### Make .apk and .ipa file 149 | 150 | Android 151 | ```shell script 152 | $ make build-android-dev 153 | $ make build-android-prd 154 | ``` 155 | 156 | iOS 157 | ```shell script 158 | $ make build-ios-dev 159 | $ make build-ios-prd 160 | ``` 161 | 162 | ### Run app 163 | ```shell script 164 | $ make run-dev 165 | $ make run-prd 166 | ``` 167 | 168 |
169 | 170 | ### How to add assets(images..) 171 | 1. Add assets 172 | 2. Run [FlutterGen](https://github.com/fluttergen) 173 | 174 | ### How to add localizations 175 | 1. Edit [*.arb](https://github.com/wasabeef/flutter-architecture-blueprints/tree/main/lib/l10n) files. 176 | 2. Run generate the `flutter pub get` 177 | 178 | ## Special Thanks. 179 | 180 | - [News API](https://newsapi.org/) 181 | 182 | **Contributors** 183 | - [lcdsmao](https://github.com/lcdsmao) 184 | 185 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:effective_dart/analysis_options.yaml 2 | analyzer: 3 | exclude: 4 | - "lib/generated_plugin_registrant.dart" 5 | - "lib/l10n/messages_*.dart" 6 | - "lib/data/model/*.g.dart" 7 | - "lib/data/model/*.freezed.dart" 8 | 9 | linter: 10 | rules: 11 | public_member_api_docs: false 12 | prefer_const_constructors: true -------------------------------------------------------------------------------- /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 | sourceSets { 34 | main.java.srcDirs += 'src/main/kotlin' 35 | } 36 | 37 | lintOptions { 38 | disable 'InvalidPackage' 39 | checkReleaseBuilds false // issue https://github.com/flutter/flutter/issues/22397 40 | } 41 | 42 | defaultConfig { 43 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 44 | applicationId "jp.wasabeef.app" 45 | minSdkVersion 22 46 | targetSdkVersion 30 47 | versionCode flutterVersionCode.toInteger() 48 | versionName flutterVersionName 49 | } 50 | 51 | // SigningConfigs 52 | apply from: 'signingConfigs/debug.gradle', to: android 53 | apply from: 'signingConfigs/release.gradle', to: android 54 | 55 | buildTypes { 56 | release { 57 | signingConfig signingConfigs.release 58 | } 59 | debug { 60 | debuggable true 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | 65 | flavorDimensions "environment" 66 | productFlavors { 67 | development { 68 | dimension "environment" 69 | applicationIdSuffix ".dev" 70 | versionNameSuffix "-Dev" 71 | } 72 | 73 | production { 74 | dimension "environment" 75 | } 76 | } 77 | } 78 | 79 | flutter { 80 | source '../..' 81 | } 82 | 83 | 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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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.4.20' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.4' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /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 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/fonts/Rotunda-Bold.otf -------------------------------------------------------------------------------- /assets/images/2.0x/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/2.0x/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/2.0x/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/2.0x/icon_placeholder.jpg -------------------------------------------------------------------------------- /assets/images/3.0x/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/3.0x/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/3.0x/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/3.0x/icon_placeholder.jpg -------------------------------------------------------------------------------- /assets/images/article_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/article_placeholder.webp -------------------------------------------------------------------------------- /assets/images/icon_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/assets/images/icon_placeholder.jpg -------------------------------------------------------------------------------- /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 | - script@1: 17 | title: Do anything with Script step 18 | - flutter-installer@0: 19 | inputs: 20 | - version: beta 21 | - cache-pull@2: {} 22 | - flutter-analyze@0.2: 23 | inputs: 24 | - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION" 25 | - flutter-test@0: 26 | inputs: 27 | - additional_params: "--coverage-path=./coverage/lcov.info" 28 | - generate_code_coverage_files: 'yes' 29 | - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION" 30 | - codecov@1: 31 | inputs: 32 | - CODECOV_TOKEN: "$CODECOV_TOKEN" 33 | - flutter-build@0: 34 | inputs: 35 | - ios_additional_params: "--release --no-codesign --flavor production --dart-define=FLAVOR=production 36 | --target lib/main.dart" 37 | - is_debug_mode: 'true' 38 | - ios_output_pattern: |- 39 | *build/ios/iphoneos/*.app 40 | *build/ios/iphoneos/*.ipa 41 | - android_additional_params: "--release --flavor production --dart-define=FLAVOR=production 42 | --target lib/main.dart" 43 | - deploy-to-bitrise-io@1: {} 44 | - cache-push@2: {} 45 | - slack@3: 46 | inputs: 47 | - webhook_url: "$SLACK_WEBHOOK_URL" 48 | pull-request-build: 49 | steps: 50 | - activate-ssh-key@4: 51 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 52 | - git-clone@4: {} 53 | - script@1: 54 | title: Do anything with Script step 55 | - flutter-installer@0: 56 | inputs: 57 | - version: beta 58 | - cache-pull@2: {} 59 | - flutter-analyze@0.2: 60 | inputs: 61 | - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION" 62 | - flutter-test@0: 63 | inputs: 64 | - additional_params: "--coverage-path=./coverage/lcov.info" 65 | - generate_code_coverage_files: 'yes' 66 | - project_location: "$BITRISE_FLUTTER_PROJECT_LOCATION" 67 | - codecov@1: 68 | inputs: 69 | - CODECOV_TOKEN: "$CODECOV_TOKEN" 70 | - flutter-build@0: 71 | inputs: 72 | - ios_additional_params: "--no-codesign --flavor development --dart-define=FLAVOR=development 73 | --target lib/main.dart" 74 | - is_debug_mode: 'true' 75 | - ios_output_pattern: |- 76 | *build/ios/iphoneos/*.app 77 | *build/ios/iphoneos/*.ipa 78 | - android_additional_params: "--flavor development --dart-define=FLAVOR=development 79 | --target lib/main.dart" 80 | - deploy-to-bitrise-io@1: {} 81 | - cache-push@2: {} 82 | - slack@3: 83 | inputs: 84 | - webhook_url: "$SLACK_WEBHOOK_URL" 85 | app: 86 | envs: 87 | - opts: 88 | is_expand: false 89 | BITRISE_FLUTTER_PROJECT_LOCATION: "." 90 | - opts: 91 | is_expand: false 92 | BITRISE_PROJECT_PATH: ios/Runner.xcworkspace 93 | - opts: 94 | is_expand: false 95 | BITRISE_SCHEME: Production 96 | - opts: 97 | is_expand: false 98 | BITRISE_EXPORT_METHOD: production 99 | - opts: 100 | is_expand: false 101 | ANDROID_SDK_VERSION: 29 102 | -------------------------------------------------------------------------------- /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: Flutter pub get 37 | script: make dependencies 38 | 39 | - name: Flutte Analysis 40 | script: make analyze 41 | 42 | - name: Flutter Unit test 43 | script: make unit-test 44 | 45 | - name: Codecov upload 46 | script: make codecov 47 | 48 | - name: Build 49 | script: | 50 | #!/bin/sh 51 | set -e # exit on first failed commandset 52 | set -x # print all executed commands to the log 53 | /usr/bin/plutil -replace CFBundleIdentifier -string jp.wasabeef.app ios/Runner/Info.plist 54 | make build-android-prd 55 | make build-ios-prd 56 | 57 | artifacts: 58 | - build/**/outputs/**/*.apk 59 | - build/**/outputs/**/*.aab 60 | - build/**/outputs/**/mapping.txt 61 | - build/ios/ipa/*.ipa 62 | - /tmp/xcodebuild_logs/*.log 63 | - flutter_drive.log 64 | 65 | publishing: 66 | slack: 67 | channel: '#develop' 68 | notify_on_build_start: false 69 | github_releases: 70 | prerelease: false 71 | artifact_patterns: 72 | - 'app-production-release.apk' 73 | - '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=pevelopment 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '9.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 (6.33.0): 8 | - Firebase/CoreOnly 9 | - FirebaseAuth (~> 6.9.2) 10 | - Firebase/CoreOnly (6.33.0): 11 | - FirebaseCore (= 6.10.3) 12 | - Firebase/Crashlytics (6.33.0): 13 | - Firebase/CoreOnly 14 | - FirebaseCrashlytics (~> 4.6.1) 15 | - Firebase/Performance (6.33.0): 16 | - Firebase/CoreOnly 17 | - FirebasePerformance (~> 3.3.0) 18 | - firebase_auth (0.18.4-1): 19 | - Firebase/Auth (~> 6.33.0) 20 | - Firebase/CoreOnly (~> 6.33.0) 21 | - firebase_core 22 | - Flutter 23 | - firebase_core (0.5.3): 24 | - Firebase/CoreOnly (~> 6.33.0) 25 | - Flutter 26 | - firebase_crashlytics (0.2.4): 27 | - Firebase/CoreOnly (~> 6.33.0) 28 | - Firebase/Crashlytics (~> 6.33.0) 29 | - firebase_core 30 | - firebase_performance (0.4.3): 31 | - Firebase/CoreOnly (~> 6.33.0) 32 | - Firebase/Performance (~> 6.33.0) 33 | - firebase_core 34 | - Flutter 35 | - FirebaseABTesting (4.2.0): 36 | - FirebaseCore (~> 6.10) 37 | - FirebaseAuth (6.9.2): 38 | - FirebaseCore (~> 6.10) 39 | - GoogleUtilities/AppDelegateSwizzler (~> 6.7) 40 | - GoogleUtilities/Environment (~> 6.7) 41 | - GTMSessionFetcher/Core (~> 1.1) 42 | - FirebaseCore (6.10.3): 43 | - FirebaseCoreDiagnostics (~> 1.6) 44 | - GoogleUtilities/Environment (~> 6.7) 45 | - GoogleUtilities/Logger (~> 6.7) 46 | - FirebaseCoreDiagnostics (1.7.0): 47 | - GoogleDataTransport (~> 7.4) 48 | - GoogleUtilities/Environment (~> 6.7) 49 | - GoogleUtilities/Logger (~> 6.7) 50 | - nanopb (~> 1.30906.0) 51 | - FirebaseCrashlytics (4.6.2): 52 | - FirebaseCore (~> 6.10) 53 | - FirebaseInstallations (~> 1.6) 54 | - GoogleDataTransport (~> 7.2) 55 | - nanopb (~> 1.30906.0) 56 | - PromisesObjC (~> 1.2) 57 | - FirebaseInstallations (1.7.0): 58 | - FirebaseCore (~> 6.10) 59 | - GoogleUtilities/Environment (~> 6.7) 60 | - GoogleUtilities/UserDefaults (~> 6.7) 61 | - PromisesObjC (~> 1.2) 62 | - FirebasePerformance (3.3.2): 63 | - FirebaseCore (~> 6.9) 64 | - FirebaseInstallations (~> 1.5) 65 | - FirebaseRemoteConfig (~> 4.7) 66 | - GoogleDataTransport (~> 7.0) 67 | - GoogleToolboxForMac/Logger (~> 2.1) 68 | - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" 69 | - GoogleUtilities/Environment (~> 6.2) 70 | - GoogleUtilities/ISASwizzler (~> 6.2) 71 | - GoogleUtilities/MethodSwizzler (~> 6.2) 72 | - GTMSessionFetcher/Core (~> 1.1) 73 | - Protobuf (~> 3.12) 74 | - FirebaseRemoteConfig (4.9.1): 75 | - FirebaseABTesting (~> 4.2) 76 | - FirebaseCore (~> 6.10) 77 | - FirebaseInstallations (~> 1.6) 78 | - GoogleUtilities/Environment (~> 6.7) 79 | - "GoogleUtilities/NSData+zlib (~> 6.7)" 80 | - Flutter (1.0.0) 81 | - FMDB (2.7.5): 82 | - FMDB/standard (= 2.7.5) 83 | - FMDB/standard (2.7.5) 84 | - google_sign_in (0.0.1): 85 | - Flutter 86 | - GoogleSignIn (~> 5.0) 87 | - GoogleDataTransport (7.5.1): 88 | - nanopb (~> 1.30906.0) 89 | - GoogleSignIn (5.0.2): 90 | - AppAuth (~> 1.2) 91 | - GTMAppAuth (~> 1.0) 92 | - GTMSessionFetcher/Core (~> 1.1) 93 | - GoogleToolboxForMac/Defines (2.3.0) 94 | - GoogleToolboxForMac/Logger (2.3.0): 95 | - GoogleToolboxForMac/Defines (= 2.3.0) 96 | - "GoogleToolboxForMac/NSData+zlib (2.3.0)": 97 | - GoogleToolboxForMac/Defines (= 2.3.0) 98 | - GoogleUtilities/AppDelegateSwizzler (6.7.2): 99 | - GoogleUtilities/Environment 100 | - GoogleUtilities/Logger 101 | - GoogleUtilities/Network 102 | - GoogleUtilities/Environment (6.7.2): 103 | - PromisesObjC (~> 1.2) 104 | - GoogleUtilities/ISASwizzler (6.7.2) 105 | - GoogleUtilities/Logger (6.7.2): 106 | - GoogleUtilities/Environment 107 | - GoogleUtilities/MethodSwizzler (6.7.2): 108 | - GoogleUtilities/Logger 109 | - GoogleUtilities/Network (6.7.2): 110 | - GoogleUtilities/Logger 111 | - "GoogleUtilities/NSData+zlib" 112 | - GoogleUtilities/Reachability 113 | - "GoogleUtilities/NSData+zlib (6.7.2)" 114 | - GoogleUtilities/Reachability (6.7.2): 115 | - GoogleUtilities/Logger 116 | - GoogleUtilities/UserDefaults (6.7.2): 117 | - GoogleUtilities/Logger 118 | - GTMAppAuth (1.1.0): 119 | - AppAuth/Core (~> 1.4) 120 | - GTMSessionFetcher (~> 1.4) 121 | - GTMSessionFetcher (1.5.0): 122 | - GTMSessionFetcher/Full (= 1.5.0) 123 | - GTMSessionFetcher/Core (1.5.0) 124 | - GTMSessionFetcher/Full (1.5.0): 125 | - GTMSessionFetcher/Core (= 1.5.0) 126 | - nanopb (1.30906.0): 127 | - nanopb/decode (= 1.30906.0) 128 | - nanopb/encode (= 1.30906.0) 129 | - nanopb/decode (1.30906.0) 130 | - nanopb/encode (1.30906.0) 131 | - path_provider (0.0.1): 132 | - Flutter 133 | - PromisesObjC (1.2.11) 134 | - Protobuf (3.13.0) 135 | - shared_preferences (0.0.1): 136 | - Flutter 137 | - sqflite (0.0.2): 138 | - Flutter 139 | - FMDB (>= 2.7.5) 140 | - ua_client_hints (1.0.3): 141 | - Flutter 142 | 143 | DEPENDENCIES: 144 | - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) 145 | - firebase_core (from `.symlinks/plugins/firebase_core/ios`) 146 | - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) 147 | - firebase_performance (from `.symlinks/plugins/firebase_performance/ios`) 148 | - Flutter (from `Flutter`) 149 | - google_sign_in (from `.symlinks/plugins/google_sign_in/ios`) 150 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 151 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 152 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 153 | - ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`) 154 | 155 | SPEC REPOS: 156 | trunk: 157 | - AppAuth 158 | - Firebase 159 | - FirebaseABTesting 160 | - FirebaseAuth 161 | - FirebaseCore 162 | - FirebaseCoreDiagnostics 163 | - FirebaseCrashlytics 164 | - FirebaseInstallations 165 | - FirebasePerformance 166 | - FirebaseRemoteConfig 167 | - FMDB 168 | - GoogleDataTransport 169 | - GoogleSignIn 170 | - GoogleToolboxForMac 171 | - GoogleUtilities 172 | - GTMAppAuth 173 | - GTMSessionFetcher 174 | - nanopb 175 | - PromisesObjC 176 | - Protobuf 177 | 178 | EXTERNAL SOURCES: 179 | firebase_auth: 180 | :path: ".symlinks/plugins/firebase_auth/ios" 181 | firebase_core: 182 | :path: ".symlinks/plugins/firebase_core/ios" 183 | firebase_crashlytics: 184 | :path: ".symlinks/plugins/firebase_crashlytics/ios" 185 | firebase_performance: 186 | :path: ".symlinks/plugins/firebase_performance/ios" 187 | Flutter: 188 | :path: Flutter 189 | google_sign_in: 190 | :path: ".symlinks/plugins/google_sign_in/ios" 191 | path_provider: 192 | :path: ".symlinks/plugins/path_provider/ios" 193 | shared_preferences: 194 | :path: ".symlinks/plugins/shared_preferences/ios" 195 | sqflite: 196 | :path: ".symlinks/plugins/sqflite/ios" 197 | ua_client_hints: 198 | :path: ".symlinks/plugins/ua_client_hints/ios" 199 | 200 | SPEC CHECKSUMS: 201 | AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 202 | Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 203 | firebase_auth: d5159db3873478d1ac839af7b10d2f831516136a 204 | firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659 205 | firebase_crashlytics: 7b37f8e79a82175606a3447852c9efa8e10034b3 206 | firebase_performance: 05238feeae91e4ce494ec8f772e7bf05b15ec178 207 | FirebaseABTesting: 8a9d8df3acc2b43f4a22014ddf9f601bca6af699 208 | FirebaseAuth: c92d49ada7948d1a23466e3db17bc4c2039dddc3 209 | FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd 210 | FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 211 | FirebaseCrashlytics: 1a747c9cc084a24dc6d9511c991db1cd078154eb 212 | FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2 213 | FirebasePerformance: 34de2b03ddfddbca26a716468a50877fd065fbe5 214 | FirebaseRemoteConfig: 35a729305f254fb15a2e541d4b36f3a379da7fdc 215 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 216 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 217 | google_sign_in: 6bd214b9c154f881422f5fe27b66aaa7bbd580cc 218 | GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 219 | GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 220 | GoogleToolboxForMac: 1350d40e86a76f7863928d63bcb0b89c84c521c5 221 | GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 222 | GTMAppAuth: 197a8dabfea5d665224aa00d17f164fc2248dab9 223 | GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52 224 | nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc 225 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 226 | PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f 227 | Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 228 | shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d 229 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 230 | ua_client_hints: 21afb74063bbc16dd66a09d7a21fba69075c250a 231 | 232 | PODFILE CHECKSUM: 118f96eff828f227e97a23ba2f91700fae9664fb 233 | 234 | COCOAPODS: 1.10.0 235 | -------------------------------------------------------------------------------- /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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import 'constants.dart'; 8 | import 'ui/app_theme.dart'; 9 | import 'ui/detail/detail_page.dart'; 10 | import 'ui/home/home_page.dart'; 11 | import 'ui/signIn/sign_in_page.dart'; 12 | 13 | class App extends HookWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | final appTheme = context.read(appThemeNotifierProvider); 17 | final setting = 18 | useProvider(appThemeNotifierProvider.select((value) => value.setting)); 19 | useFuture(useMemoized(appTheme.themeMode, [setting])); 20 | return GetMaterialApp( 21 | title: 'Flutter Architecture Blueprints', 22 | theme: lightTheme, 23 | darkTheme: darkTheme, 24 | themeMode: setting ?? ThemeMode.light, 25 | home: HomePage(), 26 | localizationsDelegates: L10n.localizationsDelegates, 27 | supportedLocales: L10n.supportedLocales, 28 | routes: { 29 | Constants.pageHome: (context) => HomePage(), 30 | Constants.pageSignIn: (context) => SignInPage(), 31 | Constants.pageDetail: (context) => DetailPage(), 32 | }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/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 | if (_instance != null) { 16 | return _instance; 17 | } 18 | 19 | final flavor = EnumToString.fromString( 20 | Flavor.values, 21 | const String.fromEnvironment('FLAVOR'), 22 | ); 23 | 24 | switch (flavor) { 25 | case Flavor.development: 26 | _instance = Constants._dev(); 27 | break; 28 | case Flavor.production: 29 | default: 30 | _instance = Constants._prd(); 31 | } 32 | return _instance; 33 | } 34 | 35 | factory Constants._dev() { 36 | return const Constants( 37 | endpoint: 'https://newsapi.org', 38 | apiKey: '98c8df982b8b4da8b86cd70e851fc521', 39 | ); 40 | } 41 | 42 | factory Constants._prd() { 43 | return const Constants( 44 | endpoint: 'https://newsapi.org', 45 | apiKey: '4bc454db94464956aea4cbb01f4bf9f4', 46 | ); 47 | } 48 | 49 | // Routing name 50 | static const String pageHome = '/home'; 51 | static const String pageSignIn = '/signIn'; 52 | static const String pageDetail = '/detail'; 53 | 54 | static Constants _instance; 55 | 56 | final String endpoint; 57 | final String apiKey; 58 | } 59 | -------------------------------------------------------------------------------- /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 | String message; 18 | 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.DEFAULT: 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.CONNECT_TIMEOUT: 36 | case DioErrorType.RECEIVE_TIMEOUT: 37 | type = AppErrorType.timeout; 38 | break; 39 | case DioErrorType.SEND_TIMEOUT: 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_shared_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class AppSharedPreferences { 4 | SharedPreferences _prefs; 5 | 6 | Future getInstance() async { 7 | _prefs ??= await SharedPreferences.getInstance(); 8 | return _prefs; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/data/local/theme_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class ThemeDataSource { 4 | Future loadThemeMode(); 5 | 6 | Future saveThemeMode(ThemeMode theme); 7 | } 8 | -------------------------------------------------------------------------------- /lib/data/local/theme_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:enum_to_string/enum_to_string.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'app_shared_preferences.dart'; 5 | import 'theme_data_source.dart'; 6 | 7 | class ThemeDataSourceImpl extends ThemeDataSource { 8 | ThemeDataSourceImpl(this._prefs); 9 | 10 | static const String keyThemeMode = 'theme_mode'; 11 | 12 | final AppSharedPreferences _prefs; 13 | 14 | @override 15 | Future loadThemeMode() async { 16 | final prefs = await _prefs.getInstance(); 17 | return EnumToString.fromString( 18 | ThemeMode.values, prefs.getString(keyThemeMode)); 19 | } 20 | 21 | @override 22 | Future saveThemeMode(ThemeMode theme) async { 23 | final prefs = await _prefs.getInstance(); 24 | return prefs.setString(keyThemeMode, EnumToString.convertToString(theme)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/data/model/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'source.dart'; 4 | 5 | part 'article.freezed.dart'; 6 | 7 | part 'article.g.dart'; 8 | 9 | @freezed 10 | abstract class Article with _$Article { 11 | factory Article({ 12 | Source source, 13 | String author, 14 | String title, 15 | String description, 16 | String url, 17 | String urlToImage, 18 | DateTime publishedAt, 19 | String content, 20 | }) = _Article; 21 | 22 | factory Article.fromJson(Map json) => 23 | _$ArticleFromJson(json); 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/model/article.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: 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 3 | 4 | part of 'article.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | Article _$ArticleFromJson(Map json) { 12 | return _Article.fromJson(json); 13 | } 14 | 15 | /// @nodoc 16 | class _$ArticleTearOff { 17 | const _$ArticleTearOff(); 18 | 19 | // ignore: unused_element 20 | _Article call( 21 | {Source source, 22 | String author, 23 | String title, 24 | String description, 25 | String url, 26 | String urlToImage, 27 | DateTime publishedAt, 28 | String content}) { 29 | return _Article( 30 | source: source, 31 | author: author, 32 | title: title, 33 | description: description, 34 | url: url, 35 | urlToImage: urlToImage, 36 | publishedAt: publishedAt, 37 | content: content, 38 | ); 39 | } 40 | 41 | // ignore: unused_element 42 | Article fromJson(Map json) { 43 | return Article.fromJson(json); 44 | } 45 | } 46 | 47 | /// @nodoc 48 | // ignore: unused_element 49 | const $Article = _$ArticleTearOff(); 50 | 51 | /// @nodoc 52 | mixin _$Article { 53 | Source get source; 54 | String get author; 55 | String get title; 56 | String get description; 57 | String get url; 58 | String get urlToImage; 59 | DateTime get publishedAt; 60 | String get content; 61 | 62 | Map toJson(); 63 | $ArticleCopyWith
get copyWith; 64 | } 65 | 66 | /// @nodoc 67 | abstract class $ArticleCopyWith<$Res> { 68 | factory $ArticleCopyWith(Article value, $Res Function(Article) then) = 69 | _$ArticleCopyWithImpl<$Res>; 70 | $Res call( 71 | {Source source, 72 | String author, 73 | String title, 74 | String description, 75 | String url, 76 | String urlToImage, 77 | DateTime publishedAt, 78 | String content}); 79 | 80 | $SourceCopyWith<$Res> get source; 81 | } 82 | 83 | /// @nodoc 84 | class _$ArticleCopyWithImpl<$Res> implements $ArticleCopyWith<$Res> { 85 | _$ArticleCopyWithImpl(this._value, this._then); 86 | 87 | final Article _value; 88 | // ignore: unused_field 89 | final $Res Function(Article) _then; 90 | 91 | @override 92 | $Res call({ 93 | Object source = freezed, 94 | Object author = freezed, 95 | Object title = freezed, 96 | Object description = freezed, 97 | Object url = freezed, 98 | Object urlToImage = freezed, 99 | Object publishedAt = freezed, 100 | Object content = freezed, 101 | }) { 102 | return _then(_value.copyWith( 103 | source: source == freezed ? _value.source : source as Source, 104 | author: author == freezed ? _value.author : author as String, 105 | title: title == freezed ? _value.title : title as String, 106 | description: 107 | description == freezed ? _value.description : description as String, 108 | url: url == freezed ? _value.url : url as String, 109 | urlToImage: 110 | urlToImage == freezed ? _value.urlToImage : urlToImage as String, 111 | publishedAt: 112 | publishedAt == freezed ? _value.publishedAt : publishedAt as DateTime, 113 | content: content == freezed ? _value.content : content as String, 114 | )); 115 | } 116 | 117 | @override 118 | $SourceCopyWith<$Res> get source { 119 | if (_value.source == null) { 120 | return null; 121 | } 122 | return $SourceCopyWith<$Res>(_value.source, (value) { 123 | return _then(_value.copyWith(source: value)); 124 | }); 125 | } 126 | } 127 | 128 | /// @nodoc 129 | abstract class _$ArticleCopyWith<$Res> implements $ArticleCopyWith<$Res> { 130 | factory _$ArticleCopyWith(_Article value, $Res Function(_Article) then) = 131 | __$ArticleCopyWithImpl<$Res>; 132 | @override 133 | $Res call( 134 | {Source source, 135 | String author, 136 | String title, 137 | String description, 138 | String url, 139 | String urlToImage, 140 | DateTime publishedAt, 141 | String content}); 142 | 143 | @override 144 | $SourceCopyWith<$Res> get source; 145 | } 146 | 147 | /// @nodoc 148 | class __$ArticleCopyWithImpl<$Res> extends _$ArticleCopyWithImpl<$Res> 149 | implements _$ArticleCopyWith<$Res> { 150 | __$ArticleCopyWithImpl(_Article _value, $Res Function(_Article) _then) 151 | : super(_value, (v) => _then(v as _Article)); 152 | 153 | @override 154 | _Article get _value => super._value as _Article; 155 | 156 | @override 157 | $Res call({ 158 | Object source = freezed, 159 | Object author = freezed, 160 | Object title = freezed, 161 | Object description = freezed, 162 | Object url = freezed, 163 | Object urlToImage = freezed, 164 | Object publishedAt = freezed, 165 | Object content = freezed, 166 | }) { 167 | return _then(_Article( 168 | source: source == freezed ? _value.source : source as Source, 169 | author: author == freezed ? _value.author : author as String, 170 | title: title == freezed ? _value.title : title as String, 171 | description: 172 | description == freezed ? _value.description : description as String, 173 | url: url == freezed ? _value.url : url as String, 174 | urlToImage: 175 | urlToImage == freezed ? _value.urlToImage : urlToImage as String, 176 | publishedAt: 177 | publishedAt == freezed ? _value.publishedAt : publishedAt as DateTime, 178 | content: content == freezed ? _value.content : content as String, 179 | )); 180 | } 181 | } 182 | 183 | @JsonSerializable() 184 | 185 | /// @nodoc 186 | class _$_Article implements _Article { 187 | _$_Article( 188 | {this.source, 189 | this.author, 190 | this.title, 191 | this.description, 192 | this.url, 193 | this.urlToImage, 194 | this.publishedAt, 195 | this.content}); 196 | 197 | factory _$_Article.fromJson(Map json) => 198 | _$_$_ArticleFromJson(json); 199 | 200 | @override 201 | final Source source; 202 | @override 203 | final String author; 204 | @override 205 | final String title; 206 | @override 207 | final String description; 208 | @override 209 | final String url; 210 | @override 211 | final String urlToImage; 212 | @override 213 | final DateTime publishedAt; 214 | @override 215 | final String content; 216 | 217 | @override 218 | String toString() { 219 | return 'Article(source: $source, author: $author, title: $title, description: $description, url: $url, urlToImage: $urlToImage, publishedAt: $publishedAt, content: $content)'; 220 | } 221 | 222 | @override 223 | bool operator ==(dynamic other) { 224 | return identical(this, other) || 225 | (other is _Article && 226 | (identical(other.source, source) || 227 | const DeepCollectionEquality().equals(other.source, source)) && 228 | (identical(other.author, author) || 229 | const DeepCollectionEquality().equals(other.author, author)) && 230 | (identical(other.title, title) || 231 | const DeepCollectionEquality().equals(other.title, title)) && 232 | (identical(other.description, description) || 233 | const DeepCollectionEquality() 234 | .equals(other.description, description)) && 235 | (identical(other.url, url) || 236 | const DeepCollectionEquality().equals(other.url, url)) && 237 | (identical(other.urlToImage, urlToImage) || 238 | const DeepCollectionEquality() 239 | .equals(other.urlToImage, urlToImage)) && 240 | (identical(other.publishedAt, publishedAt) || 241 | const DeepCollectionEquality() 242 | .equals(other.publishedAt, publishedAt)) && 243 | (identical(other.content, content) || 244 | const DeepCollectionEquality().equals(other.content, content))); 245 | } 246 | 247 | @override 248 | int get hashCode => 249 | runtimeType.hashCode ^ 250 | const DeepCollectionEquality().hash(source) ^ 251 | const DeepCollectionEquality().hash(author) ^ 252 | const DeepCollectionEquality().hash(title) ^ 253 | const DeepCollectionEquality().hash(description) ^ 254 | const DeepCollectionEquality().hash(url) ^ 255 | const DeepCollectionEquality().hash(urlToImage) ^ 256 | const DeepCollectionEquality().hash(publishedAt) ^ 257 | const DeepCollectionEquality().hash(content); 258 | 259 | @override 260 | _$ArticleCopyWith<_Article> get copyWith => 261 | __$ArticleCopyWithImpl<_Article>(this, _$identity); 262 | 263 | @override 264 | Map toJson() { 265 | return _$_$_ArticleToJson(this); 266 | } 267 | } 268 | 269 | abstract class _Article implements Article { 270 | factory _Article( 271 | {Source source, 272 | String author, 273 | String title, 274 | String description, 275 | String url, 276 | String urlToImage, 277 | DateTime publishedAt, 278 | String content}) = _$_Article; 279 | 280 | factory _Article.fromJson(Map json) = _$_Article.fromJson; 281 | 282 | @override 283 | Source get source; 284 | @override 285 | String get author; 286 | @override 287 | String get title; 288 | @override 289 | String get description; 290 | @override 291 | String get url; 292 | @override 293 | String get urlToImage; 294 | @override 295 | DateTime get publishedAt; 296 | @override 297 | String get content; 298 | @override 299 | _$ArticleCopyWith<_Article> get copyWith; 300 | } 301 | -------------------------------------------------------------------------------- /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) { 10 | return _$_Article( 11 | source: json['source'] == null 12 | ? null 13 | : Source.fromJson(json['source'] as Map), 14 | author: json['author'] as String, 15 | title: json['title'] as String, 16 | description: json['description'] as String, 17 | url: json['url'] as String, 18 | urlToImage: json['urlToImage'] as String, 19 | publishedAt: json['publishedAt'] == null 20 | ? null 21 | : DateTime.parse(json['publishedAt'] as String), 22 | content: json['content'] as String, 23 | ); 24 | } 25 | 26 | Map _$_$_ArticleToJson(_$_Article instance) => 27 | { 28 | 'source': instance.source, 29 | 'author': instance.author, 30 | 'title': instance.title, 31 | 'description': instance.description, 32 | 'url': instance.url, 33 | 'urlToImage': instance.urlToImage, 34 | 'publishedAt': instance.publishedAt?.toIso8601String(), 35 | 'content': instance.content, 36 | }; 37 | -------------------------------------------------------------------------------- /lib/data/model/news.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'article.dart'; 4 | 5 | part 'news.freezed.dart'; 6 | 7 | part 'news.g.dart'; 8 | 9 | @freezed 10 | abstract class News with _$News { 11 | factory News({ 12 | @required String status, 13 | @required int totalResults, 14 | List
articles, 15 | }) = _News; 16 | 17 | factory News.fromJson(Map json) => _$NewsFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/data/model/news.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: 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 3 | 4 | part of 'news.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | News _$NewsFromJson(Map json) { 12 | return _News.fromJson(json); 13 | } 14 | 15 | /// @nodoc 16 | class _$NewsTearOff { 17 | const _$NewsTearOff(); 18 | 19 | // ignore: unused_element 20 | _News call( 21 | {@required String status, 22 | @required int totalResults, 23 | List
articles}) { 24 | return _News( 25 | status: status, 26 | totalResults: totalResults, 27 | articles: articles, 28 | ); 29 | } 30 | 31 | // ignore: unused_element 32 | News fromJson(Map json) { 33 | return News.fromJson(json); 34 | } 35 | } 36 | 37 | /// @nodoc 38 | // ignore: unused_element 39 | const $News = _$NewsTearOff(); 40 | 41 | /// @nodoc 42 | mixin _$News { 43 | String get status; 44 | int get totalResults; 45 | List
get articles; 46 | 47 | Map toJson(); 48 | $NewsCopyWith get copyWith; 49 | } 50 | 51 | /// @nodoc 52 | abstract class $NewsCopyWith<$Res> { 53 | factory $NewsCopyWith(News value, $Res Function(News) then) = 54 | _$NewsCopyWithImpl<$Res>; 55 | $Res call({String status, int totalResults, List
articles}); 56 | } 57 | 58 | /// @nodoc 59 | class _$NewsCopyWithImpl<$Res> implements $NewsCopyWith<$Res> { 60 | _$NewsCopyWithImpl(this._value, this._then); 61 | 62 | final News _value; 63 | // ignore: unused_field 64 | final $Res Function(News) _then; 65 | 66 | @override 67 | $Res call({ 68 | Object status = freezed, 69 | Object totalResults = freezed, 70 | Object articles = freezed, 71 | }) { 72 | return _then(_value.copyWith( 73 | status: status == freezed ? _value.status : status as String, 74 | totalResults: 75 | totalResults == freezed ? _value.totalResults : totalResults as int, 76 | articles: 77 | articles == freezed ? _value.articles : articles as List
, 78 | )); 79 | } 80 | } 81 | 82 | /// @nodoc 83 | abstract class _$NewsCopyWith<$Res> implements $NewsCopyWith<$Res> { 84 | factory _$NewsCopyWith(_News value, $Res Function(_News) then) = 85 | __$NewsCopyWithImpl<$Res>; 86 | @override 87 | $Res call({String status, int totalResults, List
articles}); 88 | } 89 | 90 | /// @nodoc 91 | class __$NewsCopyWithImpl<$Res> extends _$NewsCopyWithImpl<$Res> 92 | implements _$NewsCopyWith<$Res> { 93 | __$NewsCopyWithImpl(_News _value, $Res Function(_News) _then) 94 | : super(_value, (v) => _then(v as _News)); 95 | 96 | @override 97 | _News get _value => super._value as _News; 98 | 99 | @override 100 | $Res call({ 101 | Object status = freezed, 102 | Object totalResults = freezed, 103 | Object articles = freezed, 104 | }) { 105 | return _then(_News( 106 | status: status == freezed ? _value.status : status as String, 107 | totalResults: 108 | totalResults == freezed ? _value.totalResults : totalResults as int, 109 | articles: 110 | articles == freezed ? _value.articles : articles as List
, 111 | )); 112 | } 113 | } 114 | 115 | @JsonSerializable() 116 | 117 | /// @nodoc 118 | class _$_News implements _News { 119 | _$_News({@required this.status, @required this.totalResults, this.articles}) 120 | : assert(status != null), 121 | assert(totalResults != null); 122 | 123 | factory _$_News.fromJson(Map json) => 124 | _$_$_NewsFromJson(json); 125 | 126 | @override 127 | final String status; 128 | @override 129 | final int totalResults; 130 | @override 131 | final List
articles; 132 | 133 | @override 134 | String toString() { 135 | return 'News(status: $status, totalResults: $totalResults, articles: $articles)'; 136 | } 137 | 138 | @override 139 | bool operator ==(dynamic other) { 140 | return identical(this, other) || 141 | (other is _News && 142 | (identical(other.status, status) || 143 | const DeepCollectionEquality().equals(other.status, status)) && 144 | (identical(other.totalResults, totalResults) || 145 | const DeepCollectionEquality() 146 | .equals(other.totalResults, totalResults)) && 147 | (identical(other.articles, articles) || 148 | const DeepCollectionEquality() 149 | .equals(other.articles, articles))); 150 | } 151 | 152 | @override 153 | int get hashCode => 154 | runtimeType.hashCode ^ 155 | const DeepCollectionEquality().hash(status) ^ 156 | const DeepCollectionEquality().hash(totalResults) ^ 157 | const DeepCollectionEquality().hash(articles); 158 | 159 | @override 160 | _$NewsCopyWith<_News> get copyWith => 161 | __$NewsCopyWithImpl<_News>(this, _$identity); 162 | 163 | @override 164 | Map toJson() { 165 | return _$_$_NewsToJson(this); 166 | } 167 | } 168 | 169 | abstract class _News implements News { 170 | factory _News( 171 | {@required String status, 172 | @required int totalResults, 173 | List
articles}) = _$_News; 174 | 175 | factory _News.fromJson(Map json) = _$_News.fromJson; 176 | 177 | @override 178 | String get status; 179 | @override 180 | int get totalResults; 181 | @override 182 | List
get articles; 183 | @override 184 | _$NewsCopyWith<_News> get copyWith; 185 | } 186 | -------------------------------------------------------------------------------- /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) { 10 | return _$_News( 11 | status: json['status'] as String, 12 | totalResults: json['totalResults'] as int, 13 | articles: (json['articles'] as List) 14 | ?.map((e) => 15 | e == null ? null : Article.fromJson(e as Map)) 16 | ?.toList(), 17 | ); 18 | } 19 | 20 | Map _$_$_NewsToJson(_$_News instance) => { 21 | 'status': instance.status, 22 | 'totalResults': instance.totalResults, 23 | 'articles': instance.articles, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/data/model/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | import '../app_error.dart'; 5 | 6 | part 'result.freezed.dart'; 7 | 8 | @freezed 9 | abstract class Result with _$Result { 10 | const Result._(); 11 | 12 | const factory Result.success({T data}) = Success; 13 | 14 | const factory Result.failure({@required AppError error}) = Failure; 15 | 16 | static Result guard(T Function() body) { 17 | try { 18 | return Result.success(data: body()); 19 | } on Exception catch (e) { 20 | return Result.failure(error: AppError(e)); 21 | } 22 | } 23 | 24 | static Future> guardFuture(Future Function() future) async { 25 | try { 26 | return Result.success(data: await future()); 27 | } on Exception catch (e) { 28 | return Result.failure(error: AppError(e)); 29 | } 30 | } 31 | 32 | bool get isSuccess => when(success: (data) => true, failure: (e) => false); 33 | 34 | bool get isFailure => !isSuccess; 35 | 36 | void ifSuccess(Function(T data) body) { 37 | maybeWhen( 38 | success: (data) => body(data), 39 | orElse: () { 40 | // no-op 41 | }, 42 | ); 43 | } 44 | 45 | void ifFailure(Function(AppError e) body) { 46 | maybeWhen( 47 | failure: (e) => body(e), 48 | orElse: () { 49 | // no-op 50 | }, 51 | ); 52 | } 53 | 54 | T get dataOrThrow { 55 | return when( 56 | success: (data) => data, 57 | failure: (e) => throw e, 58 | ); 59 | } 60 | } 61 | 62 | extension ResultObjectExt on T { 63 | Result get asSuccess => Result.success(data: this); 64 | 65 | Result asFailure(Exception e) => Result.failure(error: AppError(e)); 66 | } 67 | -------------------------------------------------------------------------------- /lib/data/model/result.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: 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 3 | 4 | part of 'result.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ResultTearOff { 14 | const _$ResultTearOff(); 15 | 16 | // ignore: unused_element 17 | Success success({T data}) { 18 | return Success( 19 | data: data, 20 | ); 21 | } 22 | 23 | // ignore: unused_element 24 | Failure failure({@required AppError error}) { 25 | return Failure( 26 | error: error, 27 | ); 28 | } 29 | } 30 | 31 | /// @nodoc 32 | // ignore: unused_element 33 | const $Result = _$ResultTearOff(); 34 | 35 | /// @nodoc 36 | mixin _$Result { 37 | @optionalTypeArgs 38 | Result when({ 39 | @required Result success(T data), 40 | @required Result failure(AppError error), 41 | }); 42 | @optionalTypeArgs 43 | Result maybeWhen({ 44 | Result success(T data), 45 | Result failure(AppError error), 46 | @required Result orElse(), 47 | }); 48 | @optionalTypeArgs 49 | Result map({ 50 | @required Result success(Success value), 51 | @required Result failure(Failure value), 52 | }); 53 | @optionalTypeArgs 54 | Result maybeMap({ 55 | Result success(Success value), 56 | Result failure(Failure value), 57 | @required Result orElse(), 58 | }); 59 | } 60 | 61 | /// @nodoc 62 | abstract class $ResultCopyWith { 63 | factory $ResultCopyWith(Result value, $Res Function(Result) then) = 64 | _$ResultCopyWithImpl; 65 | } 66 | 67 | /// @nodoc 68 | class _$ResultCopyWithImpl implements $ResultCopyWith { 69 | _$ResultCopyWithImpl(this._value, this._then); 70 | 71 | final Result _value; 72 | // ignore: unused_field 73 | final $Res Function(Result) _then; 74 | } 75 | 76 | /// @nodoc 77 | abstract class $SuccessCopyWith { 78 | factory $SuccessCopyWith(Success value, $Res Function(Success) then) = 79 | _$SuccessCopyWithImpl; 80 | $Res call({T data}); 81 | } 82 | 83 | /// @nodoc 84 | class _$SuccessCopyWithImpl extends _$ResultCopyWithImpl 85 | implements $SuccessCopyWith { 86 | _$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then) 87 | : super(_value, (v) => _then(v as Success)); 88 | 89 | @override 90 | Success get _value => super._value as Success; 91 | 92 | @override 93 | $Res call({ 94 | Object data = freezed, 95 | }) { 96 | return _then(Success( 97 | data: data == freezed ? _value.data : data as T, 98 | )); 99 | } 100 | } 101 | 102 | /// @nodoc 103 | class _$Success extends Success with DiagnosticableTreeMixin { 104 | const _$Success({this.data}) : super._(); 105 | 106 | @override 107 | final T data; 108 | 109 | @override 110 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 111 | return 'Result<$T>.success(data: $data)'; 112 | } 113 | 114 | @override 115 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 116 | super.debugFillProperties(properties); 117 | properties 118 | ..add(DiagnosticsProperty('type', 'Result<$T>.success')) 119 | ..add(DiagnosticsProperty('data', data)); 120 | } 121 | 122 | @override 123 | bool operator ==(dynamic other) { 124 | return identical(this, other) || 125 | (other is Success && 126 | (identical(other.data, data) || 127 | const DeepCollectionEquality().equals(other.data, data))); 128 | } 129 | 130 | @override 131 | int get hashCode => 132 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(data); 133 | 134 | @override 135 | $SuccessCopyWith> get copyWith => 136 | _$SuccessCopyWithImpl>(this, _$identity); 137 | 138 | @override 139 | @optionalTypeArgs 140 | Result when({ 141 | @required Result success(T data), 142 | @required Result failure(AppError error), 143 | }) { 144 | assert(success != null); 145 | assert(failure != null); 146 | return success(data); 147 | } 148 | 149 | @override 150 | @optionalTypeArgs 151 | Result maybeWhen({ 152 | Result success(T data), 153 | Result failure(AppError error), 154 | @required Result orElse(), 155 | }) { 156 | assert(orElse != null); 157 | if (success != null) { 158 | return success(data); 159 | } 160 | return orElse(); 161 | } 162 | 163 | @override 164 | @optionalTypeArgs 165 | Result map({ 166 | @required Result success(Success value), 167 | @required Result failure(Failure value), 168 | }) { 169 | assert(success != null); 170 | assert(failure != null); 171 | return success(this); 172 | } 173 | 174 | @override 175 | @optionalTypeArgs 176 | Result maybeMap({ 177 | Result success(Success value), 178 | Result failure(Failure value), 179 | @required Result orElse(), 180 | }) { 181 | assert(orElse != null); 182 | if (success != null) { 183 | return success(this); 184 | } 185 | return orElse(); 186 | } 187 | } 188 | 189 | abstract class Success extends Result { 190 | const Success._() : super._(); 191 | const factory Success({T data}) = _$Success; 192 | 193 | T get data; 194 | $SuccessCopyWith> get copyWith; 195 | } 196 | 197 | /// @nodoc 198 | abstract class $FailureCopyWith { 199 | factory $FailureCopyWith(Failure value, $Res Function(Failure) then) = 200 | _$FailureCopyWithImpl; 201 | $Res call({AppError error}); 202 | } 203 | 204 | /// @nodoc 205 | class _$FailureCopyWithImpl extends _$ResultCopyWithImpl 206 | implements $FailureCopyWith { 207 | _$FailureCopyWithImpl(Failure _value, $Res Function(Failure) _then) 208 | : super(_value, (v) => _then(v as Failure)); 209 | 210 | @override 211 | Failure get _value => super._value as Failure; 212 | 213 | @override 214 | $Res call({ 215 | Object error = freezed, 216 | }) { 217 | return _then(Failure( 218 | error: error == freezed ? _value.error : error as AppError, 219 | )); 220 | } 221 | } 222 | 223 | /// @nodoc 224 | class _$Failure extends Failure with DiagnosticableTreeMixin { 225 | const _$Failure({@required this.error}) 226 | : assert(error != null), 227 | super._(); 228 | 229 | @override 230 | final AppError error; 231 | 232 | @override 233 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 234 | return 'Result<$T>.failure(error: $error)'; 235 | } 236 | 237 | @override 238 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 239 | super.debugFillProperties(properties); 240 | properties 241 | ..add(DiagnosticsProperty('type', 'Result<$T>.failure')) 242 | ..add(DiagnosticsProperty('error', error)); 243 | } 244 | 245 | @override 246 | bool operator ==(dynamic other) { 247 | return identical(this, other) || 248 | (other is Failure && 249 | (identical(other.error, error) || 250 | const DeepCollectionEquality().equals(other.error, error))); 251 | } 252 | 253 | @override 254 | int get hashCode => 255 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); 256 | 257 | @override 258 | $FailureCopyWith> get copyWith => 259 | _$FailureCopyWithImpl>(this, _$identity); 260 | 261 | @override 262 | @optionalTypeArgs 263 | Result when({ 264 | @required Result success(T data), 265 | @required Result failure(AppError error), 266 | }) { 267 | assert(success != null); 268 | assert(failure != null); 269 | return failure(error); 270 | } 271 | 272 | @override 273 | @optionalTypeArgs 274 | Result maybeWhen({ 275 | Result success(T data), 276 | Result failure(AppError error), 277 | @required Result orElse(), 278 | }) { 279 | assert(orElse != null); 280 | if (failure != null) { 281 | return failure(error); 282 | } 283 | return orElse(); 284 | } 285 | 286 | @override 287 | @optionalTypeArgs 288 | Result map({ 289 | @required Result success(Success value), 290 | @required Result failure(Failure value), 291 | }) { 292 | assert(success != null); 293 | assert(failure != null); 294 | return failure(this); 295 | } 296 | 297 | @override 298 | @optionalTypeArgs 299 | Result maybeMap({ 300 | Result success(Success value), 301 | Result failure(Failure value), 302 | @required Result orElse(), 303 | }) { 304 | assert(orElse != null); 305 | if (failure != null) { 306 | return failure(this); 307 | } 308 | return orElse(); 309 | } 310 | } 311 | 312 | abstract class Failure extends Result { 313 | const Failure._() : super._(); 314 | const factory Failure({@required AppError error}) = _$Failure; 315 | 316 | AppError get error; 317 | $FailureCopyWith> get copyWith; 318 | } 319 | -------------------------------------------------------------------------------- /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 | 6 | part 'source.g.dart'; 7 | 8 | @freezed 9 | abstract class Source with _$Source { 10 | factory Source({ 11 | String id, 12 | String name, 13 | }) = _Source; 14 | 15 | factory Source.fromJson(Map json) => _$SourceFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/data/model/source.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: 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 3 | 4 | part of 'source.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | Source _$SourceFromJson(Map json) { 12 | return _Source.fromJson(json); 13 | } 14 | 15 | /// @nodoc 16 | class _$SourceTearOff { 17 | const _$SourceTearOff(); 18 | 19 | // ignore: unused_element 20 | _Source call({String id, String name}) { 21 | return _Source( 22 | id: id, 23 | name: name, 24 | ); 25 | } 26 | 27 | // ignore: unused_element 28 | Source fromJson(Map json) { 29 | return Source.fromJson(json); 30 | } 31 | } 32 | 33 | /// @nodoc 34 | // ignore: unused_element 35 | const $Source = _$SourceTearOff(); 36 | 37 | /// @nodoc 38 | mixin _$Source { 39 | String get id; 40 | String get name; 41 | 42 | Map toJson(); 43 | $SourceCopyWith get copyWith; 44 | } 45 | 46 | /// @nodoc 47 | abstract class $SourceCopyWith<$Res> { 48 | factory $SourceCopyWith(Source value, $Res Function(Source) then) = 49 | _$SourceCopyWithImpl<$Res>; 50 | $Res call({String id, String name}); 51 | } 52 | 53 | /// @nodoc 54 | class _$SourceCopyWithImpl<$Res> implements $SourceCopyWith<$Res> { 55 | _$SourceCopyWithImpl(this._value, this._then); 56 | 57 | final Source _value; 58 | // ignore: unused_field 59 | final $Res Function(Source) _then; 60 | 61 | @override 62 | $Res call({ 63 | Object id = freezed, 64 | Object name = freezed, 65 | }) { 66 | return _then(_value.copyWith( 67 | id: id == freezed ? _value.id : id as String, 68 | name: name == freezed ? _value.name : name as String, 69 | )); 70 | } 71 | } 72 | 73 | /// @nodoc 74 | abstract class _$SourceCopyWith<$Res> implements $SourceCopyWith<$Res> { 75 | factory _$SourceCopyWith(_Source value, $Res Function(_Source) then) = 76 | __$SourceCopyWithImpl<$Res>; 77 | @override 78 | $Res call({String id, String name}); 79 | } 80 | 81 | /// @nodoc 82 | class __$SourceCopyWithImpl<$Res> extends _$SourceCopyWithImpl<$Res> 83 | implements _$SourceCopyWith<$Res> { 84 | __$SourceCopyWithImpl(_Source _value, $Res Function(_Source) _then) 85 | : super(_value, (v) => _then(v as _Source)); 86 | 87 | @override 88 | _Source get _value => super._value as _Source; 89 | 90 | @override 91 | $Res call({ 92 | Object id = freezed, 93 | Object name = freezed, 94 | }) { 95 | return _then(_Source( 96 | id: id == freezed ? _value.id : id as String, 97 | name: name == freezed ? _value.name : name as String, 98 | )); 99 | } 100 | } 101 | 102 | @JsonSerializable() 103 | 104 | /// @nodoc 105 | class _$_Source with DiagnosticableTreeMixin implements _Source { 106 | _$_Source({this.id, this.name}); 107 | 108 | factory _$_Source.fromJson(Map json) => 109 | _$_$_SourceFromJson(json); 110 | 111 | @override 112 | final String id; 113 | @override 114 | final String name; 115 | 116 | @override 117 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 118 | return 'Source(id: $id, name: $name)'; 119 | } 120 | 121 | @override 122 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 123 | super.debugFillProperties(properties); 124 | properties 125 | ..add(DiagnosticsProperty('type', 'Source')) 126 | ..add(DiagnosticsProperty('id', id)) 127 | ..add(DiagnosticsProperty('name', name)); 128 | } 129 | 130 | @override 131 | bool operator ==(dynamic other) { 132 | return identical(this, other) || 133 | (other is _Source && 134 | (identical(other.id, id) || 135 | const DeepCollectionEquality().equals(other.id, id)) && 136 | (identical(other.name, name) || 137 | const DeepCollectionEquality().equals(other.name, name))); 138 | } 139 | 140 | @override 141 | int get hashCode => 142 | runtimeType.hashCode ^ 143 | const DeepCollectionEquality().hash(id) ^ 144 | const DeepCollectionEquality().hash(name); 145 | 146 | @override 147 | _$SourceCopyWith<_Source> get copyWith => 148 | __$SourceCopyWithImpl<_Source>(this, _$identity); 149 | 150 | @override 151 | Map toJson() { 152 | return _$_$_SourceToJson(this); 153 | } 154 | } 155 | 156 | abstract class _Source implements Source { 157 | factory _Source({String id, String name}) = _$_Source; 158 | 159 | factory _Source.fromJson(Map json) = _$_Source.fromJson; 160 | 161 | @override 162 | String get id; 163 | @override 164 | String get name; 165 | @override 166 | _$SourceCopyWith<_Source> get copyWith; 167 | } 168 | -------------------------------------------------------------------------------- /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) { 10 | return _$_Source( 11 | id: json['id'] as String, 12 | name: json['name'] as String, 13 | ); 14 | } 15 | 16 | Map _$_$_SourceToJson(_$_Source instance) => { 17 | 'id': instance.id, 18 | 'name': instance.name, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/data/provider/app_shared_preferences_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../local/app_shared_preferences.dart'; 4 | 5 | final prefsProvider = 6 | Provider((ref) => AppSharedPreferences()); 7 | -------------------------------------------------------------------------------- /lib/data/provider/auth_data_source_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../remote/auth_data_source.dart'; 4 | import '../remote/auth_data_source_impl.dart'; 5 | import 'firebase_auth_provider.dart'; 6 | 7 | final authDataSourceProvider = Provider( 8 | (ref) => AuthDataSourceImpl(ref.read(firebaseAuthProvider))); 9 | -------------------------------------------------------------------------------- /lib/data/provider/auth_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../repository/auth_repository.dart'; 4 | import '../repository/auth_repository_impl.dart'; 5 | import 'auth_data_source_provider.dart'; 6 | 7 | final authRepositoryProvider = Provider( 8 | (ref) => AuthRepositoryImpl(ref.read(authDataSourceProvider))); 9 | -------------------------------------------------------------------------------- /lib/data/provider/dio_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../remote/app_dio.dart'; 5 | 6 | final dioProvider = Provider((_) => AppDio.getInstance()); 7 | -------------------------------------------------------------------------------- /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 = 5 | Provider((_) => FirebaseAuth.instance); 6 | -------------------------------------------------------------------------------- /lib/data/provider/news_data_source_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../remote/news_data_source.dart'; 4 | import '../remote/news_data_source_impl.dart'; 5 | import 'dio_provider.dart'; 6 | 7 | final newsDataSourceProvider = Provider( 8 | (ref) => NewsDataSourceImpl(dio: ref.read(dioProvider))); 9 | -------------------------------------------------------------------------------- /lib/data/provider/news_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../repository/news_repository.dart'; 4 | import '../repository/news_repository_impl.dart'; 5 | import 'news_data_source_provider.dart'; 6 | 7 | final newsRepositoryProvider = Provider( 8 | (ref) => NewsRepositoryImpl(dataSource: ref.read(newsDataSourceProvider))); 9 | -------------------------------------------------------------------------------- /lib/data/provider/theme_data_source_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../local/theme_data_source_impl.dart'; 4 | import 'app_shared_preferences_provider.dart'; 5 | 6 | final themeDataSourceProvider = 7 | Provider((ref) => ThemeDataSourceImpl(ref.read(prefsProvider))); 8 | -------------------------------------------------------------------------------- /lib/data/provider/theme_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | import '../repository/theme_repository.dart'; 4 | import '../repository/theme_repository_impl.dart'; 5 | import 'theme_data_source_provider.dart'; 6 | 7 | final themeRepositoryProvider = Provider((ref) => 8 | ThemeRepositoryImpl(dataSource: ref.read(themeDataSourceProvider))); 9 | -------------------------------------------------------------------------------- /lib/data/remote/app_dio.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/adapter.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio_firebase_performance/dio_firebase_performance.dart'; 4 | import 'package:dio_http_cache/dio_http_cache.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:ua_client_hints/ua_client_hints.dart'; 7 | 8 | import '../../constants.dart'; 9 | 10 | // ignore: prefer_mixin 11 | class AppDio with DioMixin implements Dio { 12 | AppDio._([BaseOptions options]) { 13 | options = BaseOptions( 14 | baseUrl: Constants.of().endpoint, 15 | contentType: 'application/json', 16 | connectTimeout: 30000, 17 | sendTimeout: 30000, 18 | receiveTimeout: 30000, 19 | ); 20 | 21 | this.options = options; 22 | interceptors.add(InterceptorsWrapper(onRequest: (options) async { 23 | options.headers.addAll(await userAgentClientHintsHeader()); 24 | })); 25 | 26 | // API Cache 27 | interceptors.add(DioCacheManager( 28 | CacheConfig( 29 | baseUrl: Constants.of().endpoint, 30 | ), 31 | ).interceptor); 32 | 33 | // Firebase Performance 34 | interceptors.add(DioFirebasePerformanceInterceptor()); 35 | 36 | if (kDebugMode) { 37 | // Local Log 38 | interceptors.add(LogInterceptor(responseBody: true, requestBody: true)); 39 | } 40 | 41 | httpClientAdapter = DefaultHttpClientAdapter(); 42 | } 43 | 44 | static Dio getInstance() => AppDio._(); 45 | } 46 | -------------------------------------------------------------------------------- /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:firebase_auth/firebase_auth.dart' as firebase; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:google_sign_in/google_sign_in.dart'; 4 | 5 | import 'auth_data_source.dart'; 6 | 7 | class AuthDataSourceImpl implements AuthDataSource { 8 | AuthDataSourceImpl(this._firebaseAuth); 9 | 10 | final firebase.FirebaseAuth _firebaseAuth; 11 | 12 | @override 13 | Future signIn() async { 14 | final account = await GoogleSignIn().signIn(); 15 | if (account == null) { 16 | return throw StateError('Maybe user canceled.'); 17 | } 18 | final auth = await account.authentication; 19 | final firebase.AuthCredential authCredential = 20 | firebase.GoogleAuthProvider.credential( 21 | idToken: auth.idToken, 22 | accessToken: auth.accessToken, 23 | ); 24 | 25 | final credential = await _firebaseAuth.signInWithCredential(authCredential); 26 | final currentUser = await firebase.FirebaseAuth.instance.currentUser; 27 | assert(credential.user.uid == currentUser.uid); 28 | return credential.user; 29 | } 30 | 31 | @override 32 | Future signOut() { 33 | return GoogleSignIn() 34 | .signOut() 35 | .then((_) => _firebaseAuth.signOut()) 36 | .catchError((error) { 37 | debugPrint(error.toString()); 38 | throw error; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/data/remote/news_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../model/news.dart'; 2 | 3 | // ignore: one_member_abstracts 4 | abstract class NewsDataSource { 5 | Future getNews(); 6 | } 7 | -------------------------------------------------------------------------------- /lib/data/remote/news_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio_http_cache/dio_http_cache.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import '../../constants.dart'; 8 | import '../../util/ext/date_time.dart'; 9 | import '../model/news.dart'; 10 | import 'news_data_source.dart'; 11 | 12 | class NewsDataSourceImpl implements NewsDataSource { 13 | NewsDataSourceImpl({@required Dio dio}) : _dio = dio; 14 | 15 | final Dio _dio; 16 | 17 | @override 18 | Future getNews() { 19 | return _dio 20 | .get>( 21 | '/v2/everything', 22 | queryParameters: { 23 | 'q': ['anim', 'manga'][Random().nextInt(2)], // For checking reload. 24 | 'from': DateTime.now() 25 | .subtract( 26 | const Duration(days: 28), 27 | ) 28 | .toLocal() 29 | .formatYYYYMMdd(), 30 | 'sortBy': 'publishedAt', 31 | 'language': 'en', 32 | 'apiKey': Constants.of().apiKey, 33 | }, 34 | // In-memory cache time-to-live 35 | options: buildCacheOptions(const Duration(seconds: 5)), 36 | ) 37 | .then((response) => News.fromJson(response.data)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/data/repository/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart' as firebase; 2 | 3 | import '../model/result.dart'; 4 | 5 | abstract class AuthRepository { 6 | Future> signIn(); 7 | 8 | Future> signOut(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/data/repository/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart' as firebase; 2 | 3 | import '../model/result.dart'; 4 | import '../remote/auth_data_source.dart'; 5 | import 'auth_repository.dart'; 6 | 7 | class AuthRepositoryImpl implements AuthRepository { 8 | AuthRepositoryImpl(this._dataSource); 9 | 10 | final AuthDataSource _dataSource; 11 | 12 | @override 13 | Future> signIn() { 14 | return Result.guardFuture(_dataSource.signIn); 15 | } 16 | 17 | @override 18 | Future> signOut() { 19 | return Result.guardFuture(_dataSource.signOut); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/repository/news_repository.dart: -------------------------------------------------------------------------------- 1 | import '../model/news.dart'; 2 | import '../model/result.dart'; 3 | 4 | // ignore: one_member_abstracts 5 | abstract class NewsRepository { 6 | Future> getNews(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/data/repository/news_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../model/news.dart'; 4 | import '../model/result.dart'; 5 | import '../remote/news_data_source.dart'; 6 | import 'news_repository.dart'; 7 | 8 | class NewsRepositoryImpl implements NewsRepository { 9 | NewsRepositoryImpl({@required NewsDataSource dataSource}) 10 | : _dataSource = dataSource; 11 | 12 | final NewsDataSource _dataSource; 13 | 14 | @override 15 | Future> getNews() { 16 | return Result.guardFuture(_dataSource.getNews); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/data/repository/theme_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class ThemeRepository { 4 | Future loadThemeMode(); 5 | 6 | Future saveThemeMode(ThemeMode theme); 7 | } 8 | -------------------------------------------------------------------------------- /lib/data/repository/theme_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../local/theme_data_source.dart'; 4 | import 'theme_repository.dart'; 5 | 6 | class ThemeRepositoryImpl implements ThemeRepository { 7 | ThemeRepositoryImpl({@required ThemeDataSource dataSource}) 8 | : _dataSource = dataSource; 9 | 10 | final ThemeDataSource _dataSource; 11 | 12 | @override 13 | Future loadThemeMode() { 14 | return _dataSource.loadThemeMode(); 15 | } 16 | 17 | @override 18 | Future saveThemeMode(ThemeMode theme) { 19 | return _dataSource.saveThemeMode(theme); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/gen/assets.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:flutter_svg/flutter_svg.dart'; 8 | import 'package:flutter/services.dart'; 9 | 10 | class $AssetsImagesGen { 11 | const $AssetsImagesGen(); 12 | 13 | AssetGenImage get articlePlaceholder => 14 | const AssetGenImage('assets/images/article_placeholder.webp'); 15 | AssetGenImage get iconPlaceholder => 16 | const AssetGenImage('assets/images/icon_placeholder.jpg'); 17 | } 18 | 19 | class $AssetsSvgsGen { 20 | const $AssetsSvgsGen(); 21 | 22 | SvgGenImage get firebase => const SvgGenImage('assets/svgs/firebase.svg'); 23 | } 24 | 25 | class Assets { 26 | Assets._(); 27 | 28 | static const $AssetsImagesGen images = $AssetsImagesGen(); 29 | static const $AssetsSvgsGen svgs = $AssetsSvgsGen(); 30 | } 31 | 32 | class AssetGenImage extends AssetImage { 33 | const AssetGenImage(String assetName) 34 | : _assetName = assetName, 35 | super(assetName); 36 | final String _assetName; 37 | 38 | Image image({ 39 | Key key, 40 | ImageFrameBuilder frameBuilder, 41 | ImageLoadingBuilder loadingBuilder, 42 | ImageErrorWidgetBuilder errorBuilder, 43 | String semanticLabel, 44 | bool excludeFromSemantics = false, 45 | double width, 46 | double height, 47 | Color color, 48 | BlendMode colorBlendMode, 49 | BoxFit fit, 50 | AlignmentGeometry alignment = Alignment.center, 51 | ImageRepeat repeat = ImageRepeat.noRepeat, 52 | Rect centerSlice, 53 | bool matchTextDirection = false, 54 | bool gaplessPlayback = false, 55 | bool isAntiAlias = false, 56 | FilterQuality filterQuality = FilterQuality.low, 57 | }) { 58 | return Image( 59 | key: key, 60 | image: this, 61 | frameBuilder: frameBuilder, 62 | loadingBuilder: loadingBuilder, 63 | errorBuilder: errorBuilder, 64 | semanticLabel: semanticLabel, 65 | excludeFromSemantics: excludeFromSemantics, 66 | width: width, 67 | height: height, 68 | color: color, 69 | colorBlendMode: colorBlendMode, 70 | fit: fit, 71 | alignment: alignment, 72 | repeat: repeat, 73 | centerSlice: centerSlice, 74 | matchTextDirection: matchTextDirection, 75 | gaplessPlayback: gaplessPlayback, 76 | isAntiAlias: isAntiAlias, 77 | filterQuality: filterQuality, 78 | ); 79 | } 80 | 81 | String get path => _assetName; 82 | } 83 | 84 | class SvgGenImage { 85 | const SvgGenImage(this._assetName); 86 | 87 | final String _assetName; 88 | 89 | SvgPicture svg({ 90 | Key key, 91 | bool matchTextDirection = false, 92 | AssetBundle bundle, 93 | String package, 94 | double width, 95 | double height, 96 | BoxFit fit = BoxFit.contain, 97 | AlignmentGeometry alignment = Alignment.center, 98 | bool allowDrawingOutsideViewBox = false, 99 | WidgetBuilder placeholderBuilder, 100 | Color color, 101 | BlendMode colorBlendMode = BlendMode.srcIn, 102 | String semanticsLabel, 103 | bool excludeFromSemantics = false, 104 | Clip clipBehavior = Clip.hardEdge, 105 | }) { 106 | return SvgPicture.asset( 107 | _assetName, 108 | key: key, 109 | matchTextDirection: matchTextDirection, 110 | bundle: bundle, 111 | package: package, 112 | width: width, 113 | height: height, 114 | fit: fit, 115 | alignment: alignment, 116 | allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, 117 | placeholderBuilder: placeholderBuilder, 118 | color: color, 119 | colorBlendMode: colorBlendMode, 120 | semanticsLabel: semanticsLabel, 121 | excludeFromSemantics: excludeFromSemantics, 122 | clipBehavior: clipBehavior, 123 | ); 124 | } 125 | 126 | String get path => _assetName; 127 | } 128 | -------------------------------------------------------------------------------- /lib/gen/fonts.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | class FontFamily { 7 | FontFamily._(); 8 | 9 | static const String rotunda = 'Rotunda'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/l10n/intl_messages_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "ok": "OK", 4 | "@ok": {}, 5 | "cancel": "CANCEL", 6 | "@cancel": {}, 7 | "home": "Home", 8 | "@home": {}, 9 | "signIn": "SignIn", 10 | "@signIn": {}, 11 | "googleSignIn": "Google Sign-In", 12 | "@googleSignIn": {}, 13 | "googleSignOut": "Sign-Out", 14 | "@googleSignOut": {}, 15 | "detail": "Detail", 16 | "@detail": {}, 17 | "error": "Error", 18 | "@error": {}, 19 | "failedSwitchTheme": "Failed to switch the theme.", 20 | "@failedSwitchTheme": {}, 21 | "noResult": "For Empty screen", 22 | "@noResult": {}, 23 | "displayName": "Display Name", 24 | "@displayName": {}, 25 | "email": "Email", 26 | "@email": {}, 27 | "uid": "UserId", 28 | "@uid": {} 29 | } -------------------------------------------------------------------------------- /lib/l10n/intl_messages_ja.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "ja", 3 | "ok": "OK", 4 | "@ok": {}, 5 | "cancel": "CANCEL", 6 | "@cancel": {}, 7 | "home": "ホーム", 8 | "@home": {}, 9 | "signIn": "ログイン", 10 | "@signIn": {}, 11 | "googleSignIn": "Google サインイン", 12 | "@googleSignIn": {}, 13 | "googleSignOut": "サインアウト", 14 | "@googleSignOut": {}, 15 | "detail": "詳細", 16 | "@detail": {}, 17 | "error": "Error", 18 | "@error": {}, 19 | "failedSwitchTheme": "テーマの切り替えに失敗しました。", 20 | "@failedSwitchTheme": {}, 21 | "noResult": "データがありません!", 22 | "@noResult": {}, 23 | "displayName": "Display Name", 24 | "@displayName": {}, 25 | "email": "Email", 26 | "@email": {}, 27 | "uid": "UserId", 28 | "@uid": {} 29 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | 9 | import 'app.dart'; 10 | 11 | Future main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | 14 | // Firebase 15 | await Firebase.initializeApp(); 16 | // Crashlytics 17 | await FirebaseCrashlytics.instance 18 | .setCrashlyticsCollectionEnabled(kDebugMode); 19 | Function originalOnError = FlutterError.onError; 20 | FlutterError.onError = (errorDetails) async { 21 | await FirebaseCrashlytics.instance.recordFlutterError(errorDetails); 22 | originalOnError(errorDetails); 23 | }; 24 | 25 | if (kReleaseMode) { 26 | debugPrint = (message, {wrapWidth}) {}; 27 | } 28 | 29 | runZonedGuarded(() { 30 | runApp(ProviderScope(child: App())); 31 | }, (error, stackTrace) { 32 | FirebaseCrashlytics.instance.recordError(error, stackTrace); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../data/provider/theme_repository_provider.dart'; 6 | import '../data/repository/theme_repository.dart'; 7 | import '../gen/fonts.gen.dart'; 8 | 9 | // Color converter: https://www.w3schools.com/colors/colors_converter.asp 10 | // Transparency list 11 | // 100% FF 12 | // 95% F2 13 | // 90% E6 14 | // 87% DE 15 | // 85% D9 16 | // 80% CC 17 | // 75% BF 18 | // 70% B3 19 | // 65% A6 20 | // 60% 99 21 | // 55% 8C 22 | // 54% 8A 23 | // 50% 80 24 | // 45% 73 25 | // 40% 66 26 | // 35% 59 27 | // 32% 52 28 | // 30% 4D 29 | // 26% 42 30 | // 25% 40 31 | // 20% 33 32 | // 16% 29 33 | // 15% 26 34 | // 12% 1F 35 | // 10% 1A 36 | // 5% 0D 37 | // 0% 00 38 | 39 | final appThemeNotifierProvider = ChangeNotifierProvider( 40 | (ref) => AppTheme(ref.read(themeRepositoryProvider))); 41 | 42 | const headline1 = TextStyle( 43 | fontSize: 24, 44 | fontFamily: FontFamily.rotunda, 45 | fontWeight: FontWeight.bold, 46 | ); 47 | 48 | const buttonTextStyle = TextStyle( 49 | fontSize: 24, 50 | color: Colors.white, 51 | fontFamily: FontFamily.rotunda, 52 | fontWeight: FontWeight.bold, 53 | ); 54 | 55 | const accentColor = Color(0xff17c063); 56 | const errorColor = Color(0xffff5544); 57 | 58 | ThemeData get lightTheme { 59 | return ThemeData.light().copyWith( 60 | visualDensity: VisualDensity.adaptivePlatformDensity, 61 | textTheme: 62 | GoogleFonts.notoSansTextTheme(ThemeData.light().textTheme).copyWith( 63 | headline1: headline1, 64 | button: buttonTextStyle, 65 | ), 66 | accentColor: accentColor, 67 | errorColor: errorColor, 68 | ); 69 | } 70 | 71 | ThemeData get darkTheme { 72 | return ThemeData.dark().copyWith( 73 | visualDensity: VisualDensity.adaptivePlatformDensity, 74 | textTheme: 75 | GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme).copyWith( 76 | headline1: headline1, 77 | button: buttonTextStyle, 78 | ), 79 | accentColor: accentColor, 80 | errorColor: errorColor, 81 | ); 82 | } 83 | 84 | class AppTheme extends ChangeNotifier { 85 | AppTheme(this._repository); 86 | 87 | static const _defaultThemeMode = ThemeMode.light; 88 | 89 | final ThemeRepository _repository; 90 | 91 | ThemeMode _setting; 92 | 93 | ThemeMode get setting => _setting; 94 | 95 | Future themeMode() async { 96 | if (setting == null) { 97 | _setting = await _repository.loadThemeMode() ?? _defaultThemeMode; 98 | } 99 | return setting; 100 | } 101 | 102 | Future _loadLightMode() async { 103 | _setting = ThemeMode.light; 104 | await _repository.saveThemeMode(setting).whenComplete(notifyListeners); 105 | } 106 | 107 | Future _loadDarkMode() async { 108 | _setting = ThemeMode.dark; 109 | await _repository.saveThemeMode(setting).whenComplete(notifyListeners); 110 | } 111 | 112 | Future toggle() async { 113 | setting == ThemeMode.light ? await _loadDarkMode() : await _loadLightMode(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/ui/component/article_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import '../../constants.dart'; 6 | import '../../data/model/article.dart'; 7 | import 'network_image.dart'; 8 | 9 | class ArticleItem extends StatelessWidget { 10 | const ArticleItem(this._article); 11 | 12 | final Article _article; 13 | 14 | static BorderRadius borderRadiusAll = BorderRadius.circular(8); 15 | static BorderRadius borderRadiusTop = const BorderRadius.only( 16 | topRight: Radius.circular(8), 17 | topLeft: Radius.circular(8), 18 | bottomLeft: Radius.circular(0), 19 | bottomRight: Radius.circular(0), 20 | ); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Card( 25 | shape: RoundedRectangleBorder(borderRadius: borderRadiusAll), 26 | elevation: 4, 27 | child: GestureDetector( 28 | child: Column( 29 | children: [ 30 | Hero( 31 | tag: _article.url, 32 | child: SizedBox( 33 | width: double.infinity, 34 | height: 200, 35 | child: ClipRRect( 36 | borderRadius: borderRadiusTop, 37 | child: networkImage(_article.urlToImage, fit: BoxFit.cover), 38 | )), 39 | ), 40 | Padding( 41 | padding: const EdgeInsets.all(8), 42 | child: Text( 43 | _article.title ?? 'No Title', 44 | style: const TextStyle(fontSize: 12), 45 | ), 46 | ), 47 | ], 48 | ), 49 | onTap: () => Get.toNamed(Constants.pageDetail, arguments: _article), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/ui/component/container_with_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../loading_state_view_model.dart'; 6 | import 'loading.dart'; 7 | 8 | class ContainerWithLoading extends StatelessWidget { 9 | ContainerWithLoading({Widget child}) : _child = child; 10 | 11 | final Widget _child; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Stack(children: [ 16 | _child, 17 | HookBuilder(builder: (context) { 18 | final state = useProvider(loadingStateProvider); 19 | return state.isLoading ? const Loading() : const SizedBox(); 20 | }) 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/component/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | void showSimpleDialog(BuildContext context, 6 | {String title, String text, VoidCallback onPressed}) { 7 | showDialog( 8 | context: context, 9 | builder: (context) { 10 | return AlertDialog( 11 | backgroundColor: Colors.white, 12 | title: Text(title), 13 | content: Text(text), 14 | actions: [ 15 | FlatButton( 16 | child: Text( 17 | L10n.of(context).cancel, 18 | style: 19 | TextStyle(color: Theme.of(context).colorScheme.secondary), 20 | ), 21 | onPressed: Get.back, 22 | ), 23 | FlatButton( 24 | child: Text( 25 | L10n.of(context).ok, 26 | style: 27 | TextStyle(color: Theme.of(context).colorScheme.secondary), 28 | ), 29 | onPressed: onPressed, 30 | ), 31 | ], 32 | ); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/component/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import '../../gen/assets.gen.dart'; 5 | 6 | ImageProvider loadProfileImage(String imageUrl) { 7 | return imageUrl == null || imageUrl.isEmpty 8 | ? Assets.images.iconPlaceholder 9 | : CachedNetworkImageProvider(imageUrl); 10 | } 11 | -------------------------------------------------------------------------------- /lib/ui/component/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Loading extends StatelessWidget { 4 | const Loading(); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: CircularProgressIndicator( 10 | valueColor: 11 | AlwaysStoppedAnimation(Theme.of(context).accentColor))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/ui/component/network_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import '../../gen/assets.gen.dart'; 5 | 6 | Widget networkImage(String url, {BoxFit fit}) { 7 | final placeholder = Assets.images.articlePlaceholder.image(fit: fit); 8 | 9 | if (url == null) { 10 | return placeholder; 11 | } else { 12 | return CachedNetworkImage( 13 | imageUrl: url, 14 | fit: fit, 15 | errorWidget: (context, url, dynamic error) => placeholder, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/detail/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | import '../../data/model/article.dart'; 5 | import '../component/network_image.dart'; 6 | 7 | class DetailPage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | final Article article = Get.arguments; 11 | 12 | return Scaffold( 13 | body: GestureDetector( 14 | child: Center( 15 | child: Hero( 16 | tag: article.url, 17 | child: networkImage(article.urlToImage), 18 | ), 19 | ), 20 | onTap: Get.back, 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../constants.dart'; 8 | import '../../util/error_snackbar.dart'; 9 | import '../../util/ext/async_snapshot.dart'; 10 | import '../app_theme.dart'; 11 | import '../component/article_item.dart'; 12 | import '../component/container_with_loading.dart'; 13 | import '../component/image.dart'; 14 | import '../loading_state_view_model.dart'; 15 | import '../user_view_model.dart'; 16 | import 'home_view_model.dart'; 17 | 18 | class HomePage extends StatelessWidget { 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text(L10n.of(context).home, 24 | style: Theme.of(context).textTheme.headline1), 25 | actions: [ 26 | // action button 27 | IconButton( 28 | icon: const Icon(Icons.color_lens), 29 | onPressed: () async => context 30 | .read(appThemeNotifierProvider) 31 | .toggle() 32 | .catchError((error) { 33 | showErrorSnackbar( 34 | L10n.of(context).error, L10n.of(context).failedSwitchTheme); 35 | }), 36 | ), 37 | IconButton( 38 | icon: HookBuilder(builder: (context) { 39 | final user = useProvider( 40 | userViewModelProvider.select((value) => value.user)); 41 | return CircleAvatar( 42 | backgroundImage: loadProfileImage(user?.photoURL), 43 | backgroundColor: Colors.transparent, 44 | radius: 12, 45 | ); 46 | }), 47 | onPressed: () => Get.toNamed(Constants.pageSignIn)) 48 | ], 49 | ), 50 | body: ContainerWithLoading( 51 | child: HookBuilder( 52 | builder: (context) { 53 | final homeViewModel = context.read(homeViewModelProvider); 54 | final news = useProvider( 55 | homeViewModelProvider.select((value) => value.news)); 56 | final snapshot = useFuture(useMemoized(() { 57 | return context 58 | .read(loadingStateProvider) 59 | .whileLoading(homeViewModel.fetchNews); 60 | }, [news.toString()])); 61 | 62 | // Not yet load the contents. 63 | if (!snapshot.isDone) return Container(); 64 | 65 | return news.when(success: (data) { 66 | if (data.articles.isEmpty) { 67 | return const Text('Empty screen'); 68 | } 69 | return RefreshIndicator( 70 | onRefresh: () async => homeViewModel.fetchNews(), 71 | child: ListView.builder( 72 | itemCount: data.articles.length, 73 | itemBuilder: (_, index) { 74 | return ArticleItem(data.articles[index]); 75 | }, 76 | ), 77 | ); 78 | }, failure: (e) { 79 | return Text('Error Screen: $e'); 80 | }); 81 | }, 82 | ), 83 | )); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/ui/home/home_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../data/model/news.dart'; 5 | import '../../data/model/result.dart'; 6 | import '../../data/provider/news_repository_provider.dart'; 7 | import '../../data/repository/news_repository.dart'; 8 | 9 | final homeViewModelProvider = ChangeNotifierProvider( 10 | (ref) => HomeViewModel(ref.read(newsRepositoryProvider))); 11 | 12 | class HomeViewModel extends ChangeNotifier { 13 | HomeViewModel(this._repository); 14 | 15 | final NewsRepository _repository; 16 | 17 | // Result use case No.1 18 | Result _news; 19 | 20 | Result get news => _news; 21 | 22 | Future fetchNews() { 23 | return _repository 24 | .getNews() 25 | .then((value) => _news = value) 26 | .whenComplete(notifyListeners); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/loading_state_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:hooks_riverpod/all.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/signIn/sign_in_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:gap/gap.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../gen/assets.gen.dart'; 8 | import '../component/container_with_loading.dart'; 9 | import '../component/image.dart'; 10 | import '../loading_state_view_model.dart'; 11 | import '../user_view_model.dart'; 12 | 13 | class SignInPage extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text(L10n.of(context).signIn, 19 | style: Theme.of(context).textTheme.headline1), 20 | ), 21 | body: ContainerWithLoading( 22 | child: Column( 23 | children: [ 24 | const Gap(16), 25 | Container( 26 | margin: const EdgeInsets.symmetric(horizontal: 16), 27 | decoration: BoxDecoration( 28 | border: Border.all(color: Theme.of(context).dividerColor), 29 | borderRadius: BorderRadius.circular(8), 30 | ), 31 | child: Padding( 32 | padding: 33 | const EdgeInsets.symmetric(vertical: 12, horizontal: 8), 34 | child: HookBuilder(builder: (context) { 35 | final user = useProvider( 36 | userViewModelProvider.select((value) => value.user)); 37 | 38 | return Row( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Container( 42 | width: 60, 43 | height: 60, 44 | decoration: BoxDecoration( 45 | shape: BoxShape.circle, 46 | image: DecorationImage( 47 | fit: BoxFit.cover, 48 | image: loadProfileImage(user?.photoURL), 49 | ))), 50 | const Gap(12), 51 | Expanded( 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | Text( 56 | user?.displayName ?? L10n.of(context).displayName, 57 | style: const TextStyle( 58 | fontSize: 20, fontWeight: FontWeight.bold), 59 | ), 60 | const Gap(10), 61 | Text( 62 | user?.email ?? L10n.of(context).email, 63 | style: const TextStyle(fontSize: 14), 64 | ), 65 | const Gap(10), 66 | Text( 67 | user?.uid ?? L10n.of(context).uid, 68 | style: const TextStyle(fontSize: 12), 69 | ), 70 | ], 71 | ), 72 | ) 73 | ], 74 | ); 75 | }), 76 | ), 77 | ), 78 | const Gap(12), 79 | FlatButton( 80 | height: 64, 81 | color: const Color(0xff4285f4), 82 | onPressed: () { 83 | context.read(loadingStateProvider).whileLoading(() { 84 | return context.read(userViewModelProvider).signIn(); 85 | }); 86 | }, 87 | child: Padding( 88 | padding: const EdgeInsets.all(8), 89 | child: Row( 90 | mainAxisSize: MainAxisSize.min, 91 | children: [ 92 | Assets.svgs.firebase.svg(width: 48, height: 48), 93 | const Gap(8), 94 | Text(L10n.of(context).googleSignIn, 95 | style: Theme.of(context).textTheme.button) 96 | ], 97 | ), 98 | ), 99 | ), 100 | const Gap(8), 101 | FlatButton( 102 | height: 64, 103 | color: const Color(0xffc53829), 104 | onPressed: () => context.read(userViewModelProvider).signOut(), 105 | child: Padding( 106 | padding: const EdgeInsets.all(8), 107 | child: Row( 108 | mainAxisSize: MainAxisSize.min, 109 | children: [ 110 | const Gap(8), 111 | Text(L10n.of(context).googleSignOut, 112 | style: Theme.of(context).textTheme.button) 113 | ], 114 | ), 115 | ), 116 | ) 117 | ], 118 | ), 119 | ), 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/ui/user_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart' as firebase; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../data/provider/auth_repository_provider.dart'; 6 | import '../data/repository/auth_repository.dart'; 7 | 8 | final userViewModelProvider = ChangeNotifierProvider( 9 | (ref) => UserViewModel(ref.read(authRepositoryProvider))); 10 | 11 | class UserViewModel extends ChangeNotifier { 12 | UserViewModel(this._repository); 13 | 14 | final AuthRepository _repository; 15 | 16 | firebase.User _user; 17 | 18 | firebase.User get user => _user; 19 | 20 | bool get isAuthenticated => _user != null; 21 | 22 | Future signIn() { 23 | return _repository.signIn().then((result) { 24 | // Result use case No.2 25 | result.ifSuccess((data) { 26 | _user = data; 27 | notifyListeners(); 28 | }); 29 | }); 30 | } 31 | 32 | Future signOut() { 33 | return _repository.signOut().then((result) { 34 | return result.when( 35 | success: (_) { 36 | _user = null; 37 | notifyListeners(); 38 | }, 39 | failure: (_) => result); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/util/error_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | Function get err => () => Get.snackbar('Error Title', 'Failed: Change Theme'); 6 | 7 | void showErrorSnackbar(String title, String message) { 8 | Get.snackbar( 9 | title, 10 | message, 11 | backgroundColor: Theme.of(Get.context).errorColor, 12 | snackPosition: SnackPosition.BOTTOM, 13 | margin: const EdgeInsets.only(bottom: 8, right: 8, left: 8), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/util/ext/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/util/ext/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-architecture-blueprints", 3 | "author": "wasabeef", 4 | "husky": { 5 | "hooks": { 6 | "pre-push": "make format-analyze" 7 | } 8 | }, 9 | "devDependencies": { 10 | "husky": "^4.3.7" 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' # Remove this line if you wish to publish to pub.dev 6 | 7 | environment: 8 | sdk: '>=2.10.0 <3.0.0' 9 | flutter: '>=1.21.0' 10 | 11 | dependency_overrides: 12 | # For DateTime and l10n 13 | # TODO: Workaround https://github.com/FirebaseExtended/flutterfire/issues/4342 14 | intl: ^0.17.0-nullsafety.2 15 | 16 | dependencies: 17 | flutter: 18 | sdk: flutter 19 | flutter_localizations: 20 | sdk: flutter 21 | 22 | # For Google and Firebase 23 | firebase_core: ^0.5.3 24 | firebase_auth: ^0.18.4+1 25 | google_sign_in: ^4.5.9 26 | firebase_crashlytics: ^0.2.3 27 | firebase_performance: ^0.4.2 28 | 29 | # For Architecture 30 | flutter_hooks: ^0.15.0 31 | hooks_riverpod: ^0.12.1 32 | 33 | # For Networking 34 | dio: ^3.0.10 35 | dio_http_cache: ^0.2.11 36 | dio_firebase_performance: ^0.2.0 37 | # For User-Agent Client Hints 38 | ua_client_hints: ^1.0.3 39 | 40 | # For Model 41 | json_serializable: ^3.5.0 42 | freezed_annotation: ^0.12.0 43 | 44 | # For Key-Value local storage 45 | shared_preferences: ^0.5.12+4 46 | 47 | # Convert between Enum and String 48 | enum_to_string: ^1.0.14 49 | 50 | # For UIs 51 | gap: ^1.2.0 52 | cupertino_icons: ^1.0.0 53 | google_fonts: ^1.1.1 54 | cached_network_image: ^2.5.0 55 | flutter_svg: ^0.19.2+1 56 | 57 | # For Utilities 58 | get: ^3.24.0 59 | 60 | dev_dependencies: 61 | flutter_test: 62 | sdk: flutter 63 | 64 | build_runner: ^1.10.12 65 | 66 | # For Model 67 | freezed: ^0.12.6 68 | 69 | # For Analyzer 70 | effective_dart: ^1.3.0 71 | 72 | # For Testing 73 | mockito: 74 | image_test_utils: 75 | 76 | flutter_gen: 77 | integrations: 78 | flutter_svg: true 79 | 80 | flutter: 81 | uses-material-design: true 82 | generate: true 83 | 84 | assets: 85 | - assets/images/ 86 | - assets/svgs/ 87 | 88 | fonts: 89 | - family: Rotunda 90 | fonts: 91 | - asset: assets/fonts/Rotunda-Bold.otf 92 | weight: 700 93 | -------------------------------------------------------------------------------- /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(type: DioErrorType.CONNECT_TIMEOUT), 12 | ).type, 13 | equals(AppErrorType.timeout)); 14 | 15 | expect( 16 | AppError( 17 | DioError(type: DioErrorType.RECEIVE_TIMEOUT), 18 | ).type, 19 | equals(AppErrorType.timeout)); 20 | 21 | expect( 22 | AppError( 23 | DioError(type: DioErrorType.SEND_TIMEOUT), 24 | ).type, 25 | equals(AppErrorType.network)); 26 | 27 | expect( 28 | AppError( 29 | DioError( 30 | type: DioErrorType.RESPONSE, response: Response(statusCode: 400)), 31 | ).type, 32 | equals(AppErrorType.badRequest)); 33 | 34 | expect( 35 | AppError( 36 | DioError( 37 | type: DioErrorType.RESPONSE, response: Response(statusCode: 401)), 38 | ).type, 39 | equals(AppErrorType.unauthorized)); 40 | 41 | expect( 42 | AppError( 43 | DioError( 44 | type: DioErrorType.RESPONSE, response: Response(statusCode: 500)), 45 | ).type, 46 | equals(AppErrorType.server)); 47 | 48 | expect( 49 | AppError( 50 | DioError(type: DioErrorType.CANCEL), 51 | ).type, 52 | equals(AppErrorType.cancel)); 53 | 54 | expect( 55 | AppError( 56 | DioError( 57 | error: const SocketException('Failed host lookup: wasabeef.jp'), 58 | type: DioErrorType.DEFAULT), 59 | ).type, 60 | equals(AppErrorType.network)); 61 | 62 | expect( 63 | AppError( 64 | DioError(type: DioErrorType.DEFAULT), 65 | ).type, 66 | equals(AppErrorType.unknown)); 67 | 68 | expect(AppError(const FileSystemException()).type, 69 | equals(AppErrorType.unknown)); 70 | 71 | expect(AppError(null).type, equals(AppErrorType.unknown)); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /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/local/fake_theme_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/data/local/theme_data_source.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class FakeThemeDataSourceImpl extends ThemeDataSource { 5 | @override 6 | Future loadThemeMode() async { 7 | return ThemeMode.dark; 8 | } 9 | 10 | @override 11 | Future saveThemeMode(ThemeMode theme) async { 12 | // no-op 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | FakeAppDio(); 10 | 11 | @override 12 | Future> get( 13 | String path, { 14 | Map queryParameters, 15 | Options options, 16 | CancelToken cancelToken, 17 | ProgressCallback onReceiveProgress, 18 | }) async { 19 | if (path.contains('/v2/everything')) { 20 | return FakeResponse( 21 | json.decode(dummyResponseNewsApi) as Map) 22 | as Response; 23 | } else { 24 | throw UnimplementedError(); 25 | } 26 | } 27 | 28 | @override 29 | void noSuchMethod(Invocation invocation) { 30 | throw UnimplementedError(); 31 | } 32 | } 33 | 34 | class FakeResponse implements Response> { 35 | FakeResponse(this.data); 36 | 37 | @override 38 | final Map data; 39 | 40 | @override 41 | void noSuchMethod(Invocation invocation) { 42 | throw UnimplementedError(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 extends NewsDataSource { 7 | @override 8 | Future getNews() async { 9 | return dummyNews; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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.dart'; 4 | 5 | import '../dummy/dummy_news.dart'; 6 | 7 | class FakeNewsRepositoryImpl extends NewsRepository { 8 | @override 9 | Future> getNews() async { 10 | return Result.success(data: dummyNews); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/ui/app_theme_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/ui/app_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | class MockAppTheme extends Mock implements AppTheme {} 9 | 10 | void main() { 11 | setUp(() async { 12 | SharedPreferences.setMockInitialValues( 13 | {'theme_mode': 'dark'}); 14 | }); 15 | 16 | test('AppTheme + ThemeRepository Test', () async { 17 | final container = ProviderContainer( 18 | overrides: [ 19 | // or use SharedPreferences.setMockInitialValues 20 | // themeDataSourceProvider 21 | // .overrideWithValue(AsyncValue.data(FakeThemeDataSourceImpl())), 22 | ], 23 | ); 24 | final appTheme = container.read(appThemeNotifierProvider); 25 | await expectLater(appTheme.themeMode(), completion(ThemeMode.dark)); 26 | expect(appTheme.setting, ThemeMode.dark); 27 | 28 | await appTheme.toggle(); 29 | 30 | await expectLater(appTheme.themeMode(), completion(ThemeMode.light)); 31 | expect(appTheme.setting, ThemeMode.light); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/ui/view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/constants.dart'; 2 | import 'package:app/data/model/result.dart'; 3 | import 'package:app/data/provider/dio_provider.dart'; 4 | import 'package:app/data/provider/news_data_source_provider.dart'; 5 | import 'package:app/data/provider/news_repository_provider.dart'; 6 | import 'package:app/data/remote/app_dio.dart'; 7 | import 'package:app/ui/home/home_view_model.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.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 | void main() { 17 | test('HomeViewModel Test', () async { 18 | final container = ProviderContainer( 19 | overrides: [ 20 | newsRepositoryProvider.overrideWithValue(FakeNewsRepositoryImpl()) 21 | ], 22 | ); 23 | final viewModel = container.read(homeViewModelProvider); 24 | await expectLater( 25 | viewModel.fetchNews(), completion(Result.success(data: dummyNews))); 26 | }); 27 | 28 | test('NewsRepository Test', () async { 29 | final container = ProviderContainer( 30 | overrides: [ 31 | newsDataSourceProvider.overrideWithValue(FakeNewsDataSourceImpl()) 32 | ], 33 | ); 34 | final newsRepository = container.read(newsRepositoryProvider); 35 | await expectLater( 36 | newsRepository.getNews(), completion(Result.success(data: dummyNews))); 37 | }); 38 | 39 | test('NewsDataSource Test', () async { 40 | final container = ProviderContainer( 41 | overrides: [dioProvider.overrideWithValue(FakeAppDio())], 42 | ); 43 | final dataSource = container.read(newsDataSourceProvider); 44 | await expectLater(dataSource.getNews(), completion(isNotNull)); 45 | }); 46 | 47 | test('AppDio options Test', () async { 48 | final realDio = AppDio.getInstance(); 49 | expect(realDio.options.baseUrl, Constants.of().endpoint); 50 | expect(realDio.options.contentType, 'application/json'); 51 | expect(realDio.options.connectTimeout, 30000); 52 | expect(realDio.options.sendTimeout, 30000); 53 | expect(realDio.options.receiveTimeout, 30000); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/ui/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/app.dart'; 2 | import 'package:app/constants.dart'; 3 | import 'package:app/data/model/result.dart'; 4 | import 'package:app/ui/app_theme.dart'; 5 | import 'package:app/ui/component/article_item.dart'; 6 | import 'package:app/ui/component/loading.dart'; 7 | import 'package:app/ui/detail/detail_page.dart'; 8 | import 'package:app/ui/home/home_page.dart'; 9 | import 'package:app/ui/home/home_view_model.dart'; 10 | import 'package:app/ui/user_view_model.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_gen/gen_l10n/l10n.dart'; 13 | import 'package:flutter_test/flutter_test.dart'; 14 | import 'package:get/get.dart'; 15 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 16 | import 'package:image_test_utils/image_test_utils.dart'; 17 | import 'package:mockito/mockito.dart'; 18 | 19 | import '../data/dummy/dummy_article.dart'; 20 | import '../data/dummy/dummy_news.dart'; 21 | 22 | class MockAppTheme extends Mock implements AppTheme {} 23 | 24 | class MockHomeViewModel extends Mock implements HomeViewModel {} 25 | 26 | class MockUserViewModel extends Mock implements UserViewModel {} 27 | 28 | class MockNavigatorObserver extends Mock implements NavigatorObserver {} 29 | 30 | void main() { 31 | Get.testMode = true; 32 | 33 | final mockAppTheme = MockAppTheme(); 34 | when(mockAppTheme.setting).thenReturn(ThemeMode.light); 35 | when(mockAppTheme.themeMode()) 36 | .thenAnswer((_) => Future.value(ThemeMode.light)); 37 | 38 | final mockHomeViewModel = MockHomeViewModel(); 39 | when(mockHomeViewModel.fetchNews()).thenAnswer((_) => Future.value()); 40 | when(mockHomeViewModel.news).thenReturn(Result.success(data: dummyNews)); 41 | 42 | final mockUserViewModel = MockUserViewModel(); 43 | when(mockUserViewModel.signIn()).thenAnswer((_) => Future.value()); 44 | when(mockUserViewModel.signOut()).thenAnswer((_) => Future.value()); 45 | 46 | final mockNavigatorObserver = MockNavigatorObserver(); 47 | 48 | testWidgets('App widget test', (tester) async { 49 | await tester.pumpWidget( 50 | ProviderScope( 51 | overrides: [ 52 | appThemeNotifierProvider.overrideWithValue(mockAppTheme), 53 | homeViewModelProvider.overrideWithValue(mockHomeViewModel), 54 | userViewModelProvider.overrideWithValue(mockUserViewModel), 55 | ], 56 | child: App(), 57 | ), 58 | ); 59 | }); 60 | 61 | testWidgets('HomePage widget test', (tester) async { 62 | await provideMockedNetworkImages(() async { 63 | final page = HomePage(); 64 | await tester.pumpWidget( 65 | ProviderScope( 66 | overrides: [ 67 | appThemeNotifierProvider.overrideWithValue(mockAppTheme), 68 | homeViewModelProvider.overrideWithValue(mockHomeViewModel), 69 | userViewModelProvider.overrideWithValue(mockUserViewModel), 70 | ], 71 | child: GetMaterialApp( 72 | home: page, 73 | localizationsDelegates: L10n.localizationsDelegates, 74 | supportedLocales: L10n.supportedLocales, 75 | ), 76 | ), 77 | ); 78 | await tester.pumpAndSettle(); 79 | expect(find.byWidget(page), findsOneWidget); 80 | }); 81 | }); 82 | 83 | testWidgets('Article widget test', (tester) async { 84 | final article = ArticleItem(dummyArticle); 85 | await provideMockedNetworkImages(() async { 86 | await tester.pumpWidget(GetMaterialApp( 87 | home: article, 88 | routes: { 89 | Constants.pageDetail: (context) => DetailPage(), 90 | }, 91 | navigatorObservers: [mockNavigatorObserver], 92 | )); 93 | 94 | expect(find.byWidget(article), findsOneWidget); 95 | await tester.tap(find.byType(Hero)); 96 | await tester.pumpAndSettle(); 97 | verify(mockNavigatorObserver.didPush(any, any)); 98 | expect(find.byType(DetailPage), findsOneWidget); 99 | }); 100 | }); 101 | 102 | testWidgets('Loading widget test', (tester) async { 103 | const loading = Loading(); 104 | await tester.pumpWidget(loading); 105 | expect(find.byWidget(loading), findsOneWidget); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /test/util/ext/date_time_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/util/ext/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 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huskyjp/flutter-mvvp-riverpod-architecture/1c264e2ae5614805416b5da8736d97a6f8a96733/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 | --------------------------------------------------------------------------------