├── .env.example ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── README.md ├── _external └── wsserver │ ├── .gitignore │ ├── index.html │ ├── index.js │ ├── package.json │ └── yarn.lock ├── _files ├── logo.png ├── logo.psd ├── screenshot-1.jpg ├── screenshot-1.psd ├── screenshot-2.jpg ├── screenshot-3.jpg ├── screenshot-3.psd ├── screenshot.jpg └── splash.psd ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── learning_flutter_riverpod │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── 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-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── images │ ├── logo.png │ └── splash.png └── lottie │ └── .gitignore ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── 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-50x50@1x.png │ │ ├── Icon-App-50x50@2x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ ├── LaunchBackground.imageset │ │ ├── Contents.json │ │ └── background.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart └── src │ ├── app_routes.dart │ ├── common_widgets │ ├── connectivity_warning.dart │ └── error_screen.dart │ ├── features │ ├── auth │ │ ├── data │ │ │ ├── enums │ │ │ │ └── auth_status.dart │ │ │ ├── exceptions │ │ │ │ └── login_exception.dart │ │ │ ├── models │ │ │ │ ├── auth_state.dart │ │ │ │ ├── login.dart │ │ │ │ └── user.dart │ │ │ ├── providers │ │ │ │ └── auth_provider.dart │ │ │ └── repositories │ │ │ │ └── auth_repository.dart │ │ └── ui │ │ │ ├── auth_screen.dart │ │ │ ├── dashboard_screen.dart │ │ │ └── login_form.dart │ ├── calculator │ │ ├── data │ │ │ ├── models │ │ │ │ └── calculator.dart │ │ │ └── providers │ │ │ │ └── calculator_provider.dart │ │ └── ui │ │ │ └── calculator_screen.dart │ ├── counter │ │ ├── providers │ │ │ ├── counter_provider.dart │ │ │ └── timer_provider.dart │ │ └── ui │ │ │ ├── counter_screen.dart │ │ │ └── timer_screen.dart │ ├── home │ │ ├── data │ │ │ ├── models │ │ │ │ └── entry.dart │ │ │ └── providers │ │ │ │ └── home_provider.dart │ │ └── ui │ │ │ └── home_screen.dart │ ├── movies │ │ ├── data │ │ │ ├── environment_config.dart │ │ │ ├── exceptions │ │ │ │ └── movies_exception.dart │ │ │ ├── models │ │ │ │ ├── movie.dart │ │ │ │ └── movie_pagination.dart │ │ │ ├── providers │ │ │ │ └── movies_provider.dart │ │ │ └── repositories │ │ │ │ └── movie_repository.dart │ │ └── ui │ │ │ ├── error_body.dart │ │ │ ├── movie_box.dart │ │ │ ├── movies_paginated_screen.dart │ │ │ └── movies_screen.dart │ ├── network_status │ │ ├── providers │ │ │ └── network_status_provider.dart │ │ └── ui │ │ │ └── network_status_screen.dart │ ├── products │ │ ├── data │ │ │ ├── exceptions │ │ │ │ └── products_exception.dart │ │ │ ├── models │ │ │ │ ├── product.dart │ │ │ │ └── product_pagination.dart │ │ │ ├── providers │ │ │ │ ├── cart_provider.dart │ │ │ │ └── product_provider.dart │ │ │ ├── repositories │ │ │ │ └── product_repository.dart │ │ │ └── utils │ │ │ │ └── api_client.dart │ │ └── ui │ │ │ ├── error_body.dart │ │ │ ├── loading.dart │ │ │ ├── product_item.dart │ │ │ ├── products_screen.dart │ │ │ └── view_product_screen.dart │ ├── providers │ │ ├── providers │ │ │ └── providers_provider.dart │ │ └── ui │ │ │ └── providers_screen.dart │ ├── settings │ │ ├── data │ │ │ ├── models │ │ │ │ ├── theme_scheme.dart │ │ │ │ └── theme_state.dart │ │ │ └── providers │ │ │ │ └── theme_provider.dart │ │ └── ui │ │ │ └── settings_screen.dart │ ├── stopwatch │ │ ├── data │ │ │ ├── models │ │ │ │ └── timer.dart │ │ │ └── providers │ │ │ │ └── stopwatch_provider.dart │ │ └── ui │ │ │ └── stopwatch_screen.dart │ ├── trivia │ │ ├── data │ │ │ ├── enums │ │ │ │ ├── difficulty.dart │ │ │ │ └── quiz_status.dart │ │ │ ├── models │ │ │ │ ├── failure.dart │ │ │ │ ├── question.dart │ │ │ │ └── quiz_state.dart │ │ │ ├── providers │ │ │ │ └── trivia_provider.dart │ │ │ └── repositories │ │ │ │ ├── base_repository.dart │ │ │ │ └── quiz_repository.dart │ │ └── ui │ │ │ ├── answer_card.dart │ │ │ ├── box_shadow.dart │ │ │ ├── confetti_card.dart │ │ │ ├── custom_button.dart │ │ │ ├── quiz_error.dart │ │ │ ├── quiz_questions.dart │ │ │ ├── quiz_results.dart │ │ │ └── trivia_screen.dart │ └── websockets │ │ ├── providers │ │ └── websockets_provider.dart │ │ └── ui │ │ └── websockets_screen.dart │ └── utils │ ├── async_snackbar.dart │ ├── contants.dart │ ├── context_snackbar.dart │ └── list_scroll_mixin.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html ├── manifest.json └── splash │ ├── img │ └── light-background.png │ ├── splash.js │ └── style.css └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.env.example: -------------------------------------------------------------------------------- 1 | MOVIEDB_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | # environment files 47 | .env 48 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 17 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 18 | - platform: android 19 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 20 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 21 | - platform: ios 22 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 23 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 24 | - platform: linux 25 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 26 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 27 | - platform: macos 28 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 29 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 30 | - platform: web 31 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 32 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 33 | - platform: windows 34 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 35 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "riverpod_counter", 9 | "request": "launch", 10 | "type": "dart", 11 | "deviceId": "chrome" 12 | }, 13 | { 14 | "name": "riverpod_counter (device)", 15 | "request": "launch", 16 | "type": "dart", 17 | }, 18 | { 19 | "name": "riverpod_counter (profile mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "profile" 23 | }, 24 | { 25 | "name": "riverpod_counter (release mode)", 26 | "request": "launch", 27 | "type": "dart", 28 | "flutterMode": "release" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /_external/wsserver/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /_external/wsserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO chat 5 | 57 | 58 | 59 | 60 |
61 | 62 |
63 | 64 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /_external/wsserver/index.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("ws"); 2 | 3 | // start the server and specify the port number 4 | const port = 8080; 5 | const wss = new WebSocket.Server({ port: port }); 6 | console.log(`[WebSocket] Starting WebSocket server on localhost:${port}`); 7 | 8 | wss.on("connection", (ws, request) => { 9 | const clientIp = ws._socket.remoteAddress; 10 | 11 | console.log(`[WebSocket] Client with IP ${clientIp} has connected`); 12 | ws.send("Thanks for connecting to this nodejs websocket server"); 13 | 14 | ws.on("message", (message) => { 15 | if (message.toString() != "send_something") return; 16 | 17 | wss.clients.forEach((client) => { 18 | if (client.readyState === WebSocket.OPEN) { 19 | setInterval(() => { 20 | const strangeMessage = Date.now().toString(); 21 | 22 | client.send(strangeMessage); 23 | }, 4000); 24 | } 25 | }); 26 | 27 | console.log(`[WebSocket] Message ${message} was received`); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /_external/wsserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "nodemon" 8 | }, 9 | "dependencies": { 10 | "ws": "^8.9.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /_external/wsserver/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ws@^8.9.0: 6 | version "8.9.0" 7 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" 8 | integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== 9 | -------------------------------------------------------------------------------- /_files/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/logo.png -------------------------------------------------------------------------------- /_files/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/logo.psd -------------------------------------------------------------------------------- /_files/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot-1.jpg -------------------------------------------------------------------------------- /_files/screenshot-1.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot-1.psd -------------------------------------------------------------------------------- /_files/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot-2.jpg -------------------------------------------------------------------------------- /_files/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot-3.jpg -------------------------------------------------------------------------------- /_files/screenshot-3.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot-3.psd -------------------------------------------------------------------------------- /_files/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/screenshot.jpg -------------------------------------------------------------------------------- /_files/splash.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/_files/splash.psd -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | # implicit-dynamic: false 7 | 8 | exclude: 9 | - "**/*.g.dart" 10 | # - "**/*.freezed.dart" 11 | errors: 12 | invalid_annotation_target: ignore 13 | 14 | linter: 15 | rules: 16 | avoid_types_on_closure_parameters: true 17 | avoid_void_async: true 18 | cancel_subscriptions: true 19 | close_sinks: true 20 | directives_ordering: true 21 | package_api_docs: true 22 | package_prefixed_library_names: true 23 | prefer_relative_imports: true 24 | prefer_single_quotes: true 25 | test_types_in_equals: true 26 | throw_in_finally: true 27 | unawaited_futures: true 28 | unnecessary_statements: true 29 | use_super_parameters: false 30 | prefer_const_literals_to_create_immutables: true 31 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /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 | 25 | def flutterMinSdkVersion = localProperties.getProperty('flutter.minSdkVersion') 26 | if (flutterMinSdkVersion == null) { 27 | flutterMinSdkVersion = 29 28 | } 29 | 30 | def flutterTargetSdkVersion = localProperties.getProperty('flutter.targetSdkVersion') 31 | if (flutterTargetSdkVersion == null) { 32 | flutterTargetSdkVersion = 31 33 | } 34 | 35 | def flutterCompileSdkVersion = localProperties.getProperty('flutter.compileSdkVersion') 36 | if (flutterCompileSdkVersion == null) { 37 | flutterCompileSdkVersion = flutter.compileSdkVersion 38 | } 39 | 40 | apply plugin: 'com.android.application' 41 | apply plugin: 'kotlin-android' 42 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 43 | 44 | android { 45 | compileSdkVersion flutterCompileSdkVersion.toInteger() 46 | ndkVersion flutter.ndkVersion 47 | 48 | compileOptions { 49 | sourceCompatibility JavaVersion.VERSION_1_8 50 | targetCompatibility JavaVersion.VERSION_1_8 51 | } 52 | 53 | kotlinOptions { 54 | jvmTarget = '1.8' 55 | } 56 | 57 | sourceSets { 58 | main.java.srcDirs += 'src/main/kotlin' 59 | } 60 | 61 | defaultConfig { 62 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 63 | applicationId "com.example.learning_flutter_riverpod" 64 | // You can update the following values to match your application needs. 65 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 66 | minSdkVersion flutterMinSdkVersion.toInteger() 67 | targetSdkVersion flutterTargetSdkVersion.toInteger() 68 | versionCode flutterVersionCode.toInteger() 69 | versionName flutterVersionName 70 | } 71 | 72 | buildTypes { 73 | release { 74 | // TODO: Add your own signing config for the release build. 75 | // Signing with the debug keys for now, so `flutter run --release` works. 76 | signingConfig signingConfigs.debug 77 | } 78 | } 79 | } 80 | 81 | flutter { 82 | source '../..' 83 | } 84 | 85 | dependencies { 86 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 87 | } 88 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 10 | 11 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 37 | 38 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/learning_flutter_riverpod/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.learning_flutter_riverpod 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 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 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/assets/images/splash.png -------------------------------------------------------------------------------- /assets/lottie/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/assets/lottie/.gitignore -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /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/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | Learning Flutter Riverpod 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | learning_flutter_riverpod 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | UIStatusBarHidden 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 4 | import 'package:flutter_native_splash/flutter_native_splash.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:hive_flutter/hive_flutter.dart'; 7 | 8 | import 'src/app_routes.dart'; 9 | import 'src/common_widgets/error_screen.dart'; 10 | import 'src/features/settings/data/providers/theme_provider.dart'; 11 | import 'src/utils/contants.dart'; 12 | 13 | void main() async { 14 | WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); 15 | FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); 16 | await dotenv.load(fileName: '.env'); 17 | await Hive.initFlutter(); 18 | await Hive.openBox(Constants.settingsStorageKey); // settings 19 | await Hive.openBox(Constants.authStorageKey); // auth 20 | 21 | // api getters 22 | // now hide splash screen 23 | FlutterNativeSplash.remove(); 24 | 25 | runApp(const ProviderScope(child: MyApp())); 26 | } 27 | 28 | class MyApp extends ConsumerWidget { 29 | const MyApp({super.key}); 30 | 31 | @override 32 | Widget build(BuildContext context, WidgetRef ref) { 33 | final theme = ref.watch(themeProvider); 34 | 35 | return MaterialApp( 36 | debugShowCheckedModeBanner: false, 37 | initialRoute: '/', 38 | routes: appRoutes, 39 | onGenerateRoute: (settings) => appGeneratedRoutes(settings), 40 | onUnknownRoute: (settings) => MaterialPageRoute( 41 | builder: (context) => ErrorScreen( 42 | name: settings.name, 43 | ), 44 | ), 45 | theme: FlexThemeData.light(scheme: theme.scheme).copyWith( 46 | bottomSheetTheme: const BottomSheetThemeData( 47 | backgroundColor: Colors.transparent, 48 | ), 49 | ), 50 | darkTheme: FlexThemeData.dark(scheme: theme.scheme).copyWith( 51 | bottomSheetTheme: const BottomSheetThemeData( 52 | backgroundColor: Colors.transparent, 53 | ), 54 | ), 55 | themeMode: theme.mode, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/app_routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:page_transition/page_transition.dart'; 3 | 4 | import 'common_widgets/error_screen.dart'; 5 | import 'features/auth/ui/auth_screen.dart'; 6 | import 'features/auth/ui/dashboard_screen.dart'; 7 | import 'features/calculator/ui/calculator_screen.dart'; 8 | import 'features/counter/ui/counter_screen.dart'; 9 | import 'features/counter/ui/timer_screen.dart'; 10 | import 'features/home/ui/home_screen.dart'; 11 | import 'features/movies/ui/movies_paginated_screen.dart'; 12 | import 'features/movies/ui/movies_screen.dart'; 13 | import 'features/network_status/ui/network_status_screen.dart'; 14 | import 'features/products/ui/products_screen.dart'; 15 | import 'features/providers/ui/providers_screen.dart'; 16 | import 'features/settings/ui/settings_screen.dart'; 17 | import 'features/stopwatch/ui/stopwatch_screen.dart'; 18 | import 'features/trivia/ui/trivia_screen.dart'; 19 | import 'features/websockets/ui/websockets_screen.dart'; 20 | 21 | final Map appRoutes = { 22 | HomeScreen.route: (context) => const HomeScreen(), 23 | ErrorScreen.route: (context) => ErrorScreen(), 24 | ProvidersScreen.route: (context) => const ProvidersScreen(), 25 | CalculatorScreen.route: (context) => const CalculatorScreen(), 26 | NetworkStatusScreen.route: (context) => const NetworkStatusScreen(), 27 | StopwatchScreen.route: (context) => const StopwatchScreen(), 28 | TriviaScreen.route: (context) => const TriviaScreen(), 29 | WebsocketsScreen.route: (context) => const WebsocketsScreen(), 30 | MoviesScreen.route: (context) => const MoviesScreen(), 31 | MoviesPaginatedScreen.route: (context) => const MoviesPaginatedScreen(), 32 | ProductsScreen.route: (context) => ProductsScreen(), 33 | SettingsScreen.route: (context) => const SettingsScreen(), 34 | AuthScreen.route: (context) => const AuthScreen(), 35 | DashboardScreen.route: (context) => const DashboardScreen(), 36 | }; 37 | 38 | Route? appGeneratedRoutes(RouteSettings settings) { 39 | switch (settings.name) { 40 | case CounterScreen.route: 41 | return PageTransition( 42 | child: const CounterScreen(), type: PageTransitionType.bottomToTop); 43 | case TimerScreen.route: 44 | return PageTransition( 45 | child: const TimerScreen(), type: PageTransitionType.bottomToTop); 46 | default: 47 | break; 48 | } 49 | 50 | return null; 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/common_widgets/connectivity_warning.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import '../features/network_status/providers/network_status_provider.dart'; 6 | 7 | class ConnectivityWarning extends ConsumerWidget { 8 | const ConnectivityWarning({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | if (kDebugMode) { 13 | print('ConnectivityWarning.build'); 14 | } 15 | 16 | var network = ref.watch(networkAwareProvider); 17 | 18 | return network == NetworkStatus.off 19 | ? Container( 20 | padding: const EdgeInsets.all(16), 21 | height: 60, 22 | color: Colors.red.shade500, 23 | child: Row( 24 | children: [ 25 | Icon( 26 | Icons.wifi_off, 27 | color: Colors.grey.shade100, 28 | ), 29 | const SizedBox( 30 | width: 8, 31 | ), 32 | Text( 33 | 'No internet connection', 34 | style: Theme.of(context) 35 | .textTheme 36 | .bodyMedium! 37 | .copyWith(color: Colors.grey.shade100), 38 | ), 39 | ], 40 | ), 41 | ) 42 | : Container(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/common_widgets/error_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | class ErrorScreen extends ConsumerWidget { 5 | String? name; 6 | ErrorScreen({super.key, this.name}); 7 | static const route = '/error'; 8 | 9 | @override 10 | Widget build(BuildContext context, WidgetRef ref) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text('Error'), 14 | ), 15 | body: Center( 16 | child: Column( 17 | children: [ 18 | const Text('An error occurred'), 19 | name != null ? Text('$name') : Container(), 20 | ], 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/enums/auth_status.dart: -------------------------------------------------------------------------------- 1 | enum AuthStatus { 2 | unauthenticated, 3 | authenticated, 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/exceptions/login_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class LoginException implements Exception { 4 | String? message; 5 | 6 | LoginException({this.message}); 7 | 8 | LoginException.fromDioError(DioError dioError) { 9 | switch (dioError.type) { 10 | case DioErrorType.cancel: 11 | message = 'Request to API server was cancelled'; 12 | break; 13 | case DioErrorType.connectTimeout: 14 | message = 'Connection timeout with API server'; 15 | break; 16 | case DioErrorType.other: 17 | message = 'Connection to API server fiailed due to internet connection'; 18 | break; 19 | case DioErrorType.receiveTimeout: 20 | message = 'Received timeout in connection with API server'; 21 | break; 22 | case DioErrorType.response: 23 | message = _handeError(dioError.response!.statusCode); 24 | break; 25 | case DioErrorType.sendTimeout: 26 | message = 'Send timeout in connection with API server'; 27 | break; 28 | default: 29 | message = 'Something went wrong'; 30 | break; 31 | } 32 | } 33 | 34 | String _handeError(statusCode) { 35 | switch (statusCode) { 36 | case 400: 37 | return 'Bad Request'; 38 | case 404: 39 | return 'The requested resources was not found'; 40 | case 500: 41 | return 'Internal Server Error'; 42 | default: 43 | return 'Internal Server Error'; 44 | } 45 | } 46 | 47 | @override 48 | String toString() => message.toString(); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/models/auth_state.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import '../enums/auth_status.dart'; 3 | import 'user.dart'; 4 | 5 | class AuthState { 6 | final String? accessToken; 7 | final User? user; 8 | final AuthStatus status; 9 | final String? errorMessage; 10 | final bool loading; 11 | 12 | bool get hasError => errorMessage != null && errorMessage!.isNotEmpty; 13 | 14 | AuthState({ 15 | this.accessToken, 16 | this.user, 17 | this.status = AuthStatus.unauthenticated, 18 | this.errorMessage, 19 | this.loading = false, 20 | }); 21 | 22 | AuthState copyWith({ 23 | String? accessToken, 24 | User? user, 25 | AuthStatus? status, 26 | String? errorMessage, 27 | bool? loading, 28 | }) { 29 | return AuthState( 30 | accessToken: accessToken ?? this.accessToken, 31 | user: user ?? this.user, 32 | status: status ?? this.status, 33 | loading: loading ?? this.loading, 34 | errorMessage: errorMessage ?? this.errorMessage, 35 | ); 36 | } 37 | 38 | factory AuthState.initial() { 39 | return AuthState( 40 | status: AuthStatus.unauthenticated, 41 | user: null, 42 | accessToken: null, 43 | errorMessage: null, 44 | loading: false, 45 | ); 46 | } 47 | 48 | @override 49 | String toString() => 50 | 'status $status accessToken $accessToken user ${user.toString()} '; 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/models/login.dart: -------------------------------------------------------------------------------- 1 | class Login { 2 | final String? email; 3 | final String? password; 4 | 5 | const Login({ 6 | this.email, 7 | this.password, 8 | }); 9 | 10 | Login copyWith({ 11 | String? email, 12 | String? password, 13 | }) { 14 | return Login( 15 | email: email ?? this.email, 16 | password: password ?? this.password, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/models/user.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:convert'; 3 | 4 | class User { 5 | final String name; 6 | final String email; 7 | final String? avatar; 8 | 9 | User({required this.name, required this.email, this.avatar}); 10 | 11 | @override 12 | String toString() => 'name $name email $email avatar $avatar'; 13 | 14 | Map toMap() { 15 | return { 16 | 'name': name, 17 | 'email': email, 18 | 'avatar': avatar, 19 | }; 20 | } 21 | 22 | factory User.fromMap(Map map) { 23 | return User( 24 | name: map['name'] as String, 25 | email: map['email'] as String, 26 | avatar: map['avatar'] != null ? map['avatar'] as String : null, 27 | ); 28 | } 29 | 30 | String toJson() => json.encode(toMap()); 31 | 32 | factory User.fromJson(String source) => 33 | User.fromMap(json.decode(source) as Map); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/providers/auth_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:hive_flutter/hive_flutter.dart'; 4 | 5 | import '../../../../utils/contants.dart'; 6 | import '../enums/auth_status.dart'; 7 | import '../exceptions/login_exception.dart'; 8 | import '../models/auth_state.dart'; 9 | import '../models/user.dart'; 10 | import '../repositories/auth_repository.dart'; 11 | 12 | final authNotifierProvider = 13 | StateNotifierProvider((ref) { 14 | final repo = ref.watch(authRepositoryProvider); 15 | return AuthStateNotifier(repo); 16 | }); 17 | 18 | class AuthStateNotifier extends StateNotifier { 19 | final AuthRepository _repo; 20 | 21 | AuthStateNotifier(this._repo, [AuthState? state]) 22 | : super(state ?? AuthState.initial()) { 23 | checkAuthStatus(); 24 | } 25 | 26 | Future checkAuthStatus() async { 27 | // check storage for existing token/user 28 | final box = Hive.box(Constants.authStorageKey); 29 | final token = box.get('token'); 30 | final user = box.get('user'); 31 | 32 | // if authenticated, update state accordingly 33 | if ((token != null && token.toString().isNotEmpty) && 34 | (user != null && user.toString().isNotEmpty)) { 35 | state = state.copyWith( 36 | status: AuthStatus.authenticated, 37 | user: User.fromJson(user.toString()), 38 | accessToken: token.toString(), 39 | ); 40 | 41 | return; 42 | } 43 | state = state.copyWith( 44 | status: AuthStatus.unauthenticated, 45 | ); 46 | } 47 | 48 | Future login({required String email, required String password}) async { 49 | try { 50 | state = state.copyWith( 51 | loading: true, 52 | errorMessage: '', 53 | ); 54 | 55 | final token = await _repo.login(email: email, password: password); 56 | 57 | final user = User(email: email, name: email.split('@').first); 58 | 59 | final box = Hive.box(Constants.authStorageKey); 60 | await box.put('token', token); 61 | await box.put('user', user.toJson()); 62 | 63 | state = state.copyWith( 64 | accessToken: token, 65 | user: user, 66 | status: AuthStatus.authenticated, 67 | errorMessage: '', 68 | ); 69 | } on DioError catch (e) { 70 | final exc = LoginException.fromDioError(e); 71 | state = state.copyWith( 72 | errorMessage: exc.message, 73 | ); 74 | } catch (e) { 75 | state = state.copyWith( 76 | errorMessage: e.toString(), 77 | ); 78 | } finally { 79 | state = state.copyWith( 80 | loading: false, 81 | ); 82 | } 83 | } 84 | 85 | Future logout() async { 86 | state = state.copyWith( 87 | loading: true, 88 | errorMessage: '', 89 | ); 90 | 91 | // do some API stuff 92 | await Future.delayed(const Duration(milliseconds: 300)); 93 | 94 | final box = Hive.box(Constants.authStorageKey); 95 | await box.delete('token'); 96 | await box.delete('user'); 97 | 98 | state = state.copyWith( 99 | user: null, 100 | accessToken: null, 101 | status: AuthStatus.unauthenticated, 102 | loading: false, 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../exceptions/login_exception.dart'; 5 | 6 | abstract class AuthRepository { 7 | Future login({ 8 | required String email, 9 | required String password, 10 | }); 11 | } 12 | 13 | class ApiAuthRepository implements AuthRepository { 14 | final Dio _dio; 15 | 16 | ApiAuthRepository(this._dio); 17 | 18 | @override 19 | Future login( 20 | {required String email, required String password}) async { 21 | const url = 'https://reqres.in/api/login'; 22 | 23 | try { 24 | final data = { 25 | 'email': email, 26 | 'password': password, 27 | }; 28 | 29 | final response = await _dio.post(url, data: data); 30 | 31 | final token = response.data['token'] as String; 32 | return token; 33 | } on DioError catch (e) { 34 | throw LoginException(message: 'Unable to login'); 35 | } catch (e) { 36 | throw Exception('Unable to login'); 37 | } 38 | } 39 | } 40 | 41 | final authRepositoryProvider = Provider((ref) { 42 | return ApiAuthRepository(Dio()); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/src/features/auth/ui/auth_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../data/enums/auth_status.dart'; 5 | import '../data/providers/auth_provider.dart'; 6 | import 'dashboard_screen.dart'; 7 | import 'login_form.dart'; 8 | 9 | class AuthScreen extends StatelessWidget { 10 | const AuthScreen({Key? key}) : super(key: key); 11 | static const route = '/auth'; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Consumer( 16 | builder: (context, ref, child) { 17 | final authStatus = 18 | ref.watch(authNotifierProvider.select((value) => value.status)); 19 | 20 | return Container( 21 | child: authStatus == AuthStatus.unauthenticated 22 | ? const LoginForm() 23 | : authStatus == AuthStatus.authenticated 24 | ? const DashboardScreen() 25 | : const Center( 26 | child: CircularProgressIndicator(), 27 | ), 28 | ); 29 | }, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/features/auth/ui/dashboard_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../data/providers/auth_provider.dart'; 5 | 6 | class DashboardScreen extends ConsumerWidget { 7 | const DashboardScreen({Key? key}) : super(key: key); 8 | static const route = '/dashboard'; 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final auth = ref.watch(authNotifierProvider); 13 | 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: const Text('Dashboard'), 17 | ), 18 | body: Center( 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | crossAxisAlignment: CrossAxisAlignment.center, 22 | children: [ 23 | Text('Welcome back, ${auth.user?.name}'), 24 | Text('${auth.user?.email}'), 25 | const SizedBox( 26 | height: 20, 27 | ), 28 | ElevatedButton( 29 | onPressed: () { 30 | ref.read(authNotifierProvider.notifier).logout(); 31 | }, 32 | child: Text(auth.loading ? 'Logging Out' : 'Logout'), 33 | ), 34 | ], 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/features/calculator/data/models/calculator.dart: -------------------------------------------------------------------------------- 1 | class Calculator { 2 | final bool shouldAppend; 3 | final String equation; 4 | final String result; 5 | 6 | const Calculator({ 7 | this.shouldAppend = true, 8 | this.equation = '0', 9 | this.result = '0', 10 | }); 11 | 12 | Calculator copy({ 13 | bool? shouldAppend, 14 | String? equation, 15 | String? result, 16 | }) => 17 | Calculator( 18 | shouldAppend: shouldAppend ?? this.shouldAppend, 19 | equation: equation ?? this.equation, 20 | result: result ?? this.result, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/features/calculator/data/providers/calculator_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:math_expressions/math_expressions.dart'; 4 | 5 | import '../models/calculator.dart'; 6 | 7 | class CalcColors { 8 | static const Color background1 = Color(0Xff22252d); 9 | static const Color background2 = Color(0xff292d36); 10 | static const Color background3 = Color(0xff272b33); 11 | static const Color operators = Color(0xfff57b7b); 12 | static const Color delete = Color(0xff26f4ce); 13 | static const Color numbers = Colors.white; 14 | } 15 | 16 | class Utils { 17 | static bool isOperator(String buttonText, {bool hasEquals = false}) { 18 | final operators = [ 19 | '+', 20 | '-', 21 | '/', 22 | '÷', 23 | 'x', 24 | '.', 25 | if (hasEquals) ...['='] 26 | ]; 27 | 28 | return operators.contains(buttonText); 29 | } 30 | 31 | static bool isOperatorAtEnd(String equation) { 32 | if (equation.isNotEmpty) { 33 | return Utils.isOperator(equation.substring(equation.length - 1)); 34 | } 35 | return false; 36 | } 37 | } 38 | 39 | final calculatorProvider = 40 | StateNotifierProvider( 41 | (ref) => CalculatorNotifier()); 42 | 43 | class CalculatorNotifier extends StateNotifier { 44 | CalculatorNotifier() : super(const Calculator()); 45 | 46 | void append(String buttonText) { 47 | final equation = () { 48 | if (Utils.isOperator(buttonText) && 49 | Utils.isOperatorAtEnd(state.equation)) { 50 | final newEquation = 51 | state.equation.substring(0, state.equation.length - 1); 52 | 53 | return newEquation + buttonText; 54 | } else if (state.shouldAppend) { 55 | return state.equation == '0' ? buttonText : state.equation + buttonText; 56 | } 57 | 58 | return Utils.isOperator(buttonText) 59 | ? state.equation + buttonText 60 | : buttonText; 61 | }(); 62 | 63 | state = state.copy(equation: equation, shouldAppend: true); 64 | calculate(); 65 | } 66 | 67 | void delete() { 68 | final equation = state.equation; 69 | 70 | if (equation.isNotEmpty) { 71 | final newEquation = equation.substring(0, equation.length - 1); 72 | 73 | if (newEquation.isEmpty) { 74 | reset(); 75 | } else { 76 | state = state.copy(equation: newEquation); 77 | calculate(); 78 | } 79 | } 80 | } 81 | 82 | void equals() { 83 | calculate(); 84 | resetResult(); 85 | } 86 | 87 | void calculate() { 88 | // replace illegal expressions 89 | final expression = state.equation.replaceAll('x', '*').replaceAll('÷', '/'); 90 | 91 | try { 92 | final exp = Parser().parse(expression); 93 | final model = ContextModel(); 94 | 95 | final result = '${exp.evaluate(EvaluationType.REAL, model)}'; 96 | // copy current object with everything it had before, and set only results field 97 | state = state.copy(result: result); 98 | // ignore: empty_catches 99 | } catch (e) {} 100 | } 101 | 102 | void reset() { 103 | const equation = '0'; 104 | const result = '0'; 105 | 106 | state = state.copy(equation: equation, result: result); 107 | } 108 | 109 | void resetResult() { 110 | final equation = state.result; 111 | 112 | state = state.copy(equation: equation, shouldAppend: false); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/features/counter/providers/counter_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | 3 | final counterProvider = StateNotifierProvider.autoDispose((ref) => Counter()); 4 | 5 | class Counter extends StateNotifier { 6 | Counter() : super(0); 7 | 8 | void increment() => state++; 9 | } 10 | 11 | Stream numbers() => 12 | Stream.periodic(const Duration(seconds: 1), (e) => e + 1).take(10); 13 | -------------------------------------------------------------------------------- /lib/src/features/counter/providers/timer_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | 3 | final counterProvider = StateNotifierProvider.autoDispose((ref) => Counter()); 4 | 5 | class Counter extends StateNotifier { 6 | Counter() : super(0); 7 | 8 | void increment() => state++; 9 | } 10 | 11 | final limitCountProvider = StateProvider((ref) => 5); 12 | 13 | final streamProvider = StreamProvider((ref) { 14 | final int limit = ref.watch(limitCountProvider); 15 | 16 | return Stream.periodic(const Duration(seconds: 1), (number) => number + 1) 17 | .take(limit); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/src/features/counter/ui/counter_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import '../providers/counter_provider.dart'; 6 | 7 | class CounterScreen extends StatelessWidget { 8 | const CounterScreen({super.key}); 9 | 10 | static const route = '/counter'; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | if (kDebugMode) { 15 | print('CounterScreen.build'); 16 | } 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: const Text('Auto-Disposed Counter App'), 20 | ), 21 | body: Center( 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | crossAxisAlignment: CrossAxisAlignment.center, 25 | children: [ 26 | const CounterLabel(), 27 | const CounterDisplay(), 28 | const SizedBox( 29 | height: 20, 30 | ), 31 | TimerCountdown(), 32 | ], 33 | ), 34 | ), 35 | floatingActionButton: const CounterIncrement(), 36 | ); 37 | } 38 | } 39 | 40 | class CounterLabel extends ConsumerWidget { 41 | const CounterLabel({ 42 | super.key, 43 | }); 44 | 45 | @override 46 | Widget build(BuildContext context, WidgetRef ref) { 47 | if (kDebugMode) { 48 | print('CounterLabel.build'); 49 | } 50 | return const Text('The count is: '); 51 | } 52 | } 53 | 54 | class CounterDisplay extends ConsumerWidget { 55 | const CounterDisplay({super.key}); 56 | 57 | @override 58 | Widget build(BuildContext context, WidgetRef ref) { 59 | if (kDebugMode) { 60 | print('CounterDisplay.build'); 61 | } 62 | 63 | final counter = ref.watch(counterProvider); 64 | 65 | return Text('$counter'); 66 | } 67 | } 68 | 69 | class CounterIncrement extends ConsumerWidget { 70 | const CounterIncrement({super.key}); 71 | 72 | @override 73 | Widget build(BuildContext context, WidgetRef ref) { 74 | if (kDebugMode) { 75 | print('CounterIncrement.build'); 76 | } 77 | 78 | return FloatingActionButton( 79 | onPressed: () { 80 | ref.read(counterProvider.notifier).increment(); 81 | }, 82 | child: const Icon(Icons.add), 83 | ); 84 | } 85 | } 86 | 87 | class TimerCountdown extends ConsumerWidget { 88 | TimerCountdown({super.key}); 89 | 90 | final _numbers = numbers(); 91 | 92 | @override 93 | Widget build(BuildContext context, WidgetRef ref) { 94 | if (kDebugMode) { 95 | print('TimerCountdown.build'); 96 | } 97 | 98 | return StreamBuilder( 99 | stream: _numbers, 100 | builder: (context, snapshot) { 101 | switch (snapshot.connectionState) { 102 | case ConnectionState.none: 103 | case ConnectionState.waiting: 104 | return const Center( 105 | child: CircularProgressIndicator(), 106 | ); 107 | case ConnectionState.active: 108 | return Text('Countdown: ${snapshot.data}'); 109 | case ConnectionState.done: 110 | return SelectableText('Done at: ${snapshot.data}'); 111 | default: 112 | return Container(); 113 | } 114 | }, 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/features/counter/ui/timer_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import '../../../utils/context_snackbar.dart'; 6 | import '../providers/timer_provider.dart'; 7 | 8 | class TimerScreen extends ConsumerWidget { 9 | const TimerScreen({Key? key}) : super(key: key); 10 | 11 | static const route = '/timer'; 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | if (kDebugMode) { 16 | print('TimerScreen.build'); 17 | } 18 | 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: const Text('Auto-Disposed Timer App w/ Limit'), 22 | ), 23 | body: Center( 24 | child: Column( 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | children: const [ 28 | Text('The count is: '), 29 | SizedBox( 30 | height: 20, 31 | ), 32 | TimerDisplay(), 33 | SizedBox( 34 | height: 20, 35 | ), 36 | TimerCountdown(), 37 | SizedBox( 38 | height: 20, 39 | ), 40 | ], 41 | ), 42 | ), 43 | floatingActionButton: const TimerIncrement(), 44 | ); 45 | } 46 | } 47 | 48 | class TimerDisplay extends ConsumerWidget { 49 | const TimerDisplay({super.key}); 50 | 51 | @override 52 | Widget build(BuildContext context, WidgetRef ref) { 53 | if (kDebugMode) { 54 | print('TimerDisplay.build'); 55 | } 56 | 57 | final counter = ref.watch(counterProvider); 58 | final int limit = ref.watch(limitCountProvider); 59 | 60 | // ref.listen>( 61 | // counterProvider, 62 | // (_, state) => state.showSnackBarOnError(context), 63 | // ); 64 | 65 | ref.listen(counterProvider, (prev, next) { 66 | int count = int.parse(next.toString()); 67 | 68 | if (count >= limit) { 69 | ScaffoldMessenger.of(context).showSnackBar( 70 | SnackBar(content: Text('Exceeds limit of $limit')), 71 | ); 72 | } 73 | }); 74 | 75 | return Text('$counter'); 76 | } 77 | } 78 | 79 | class TimerIncrement extends ConsumerWidget { 80 | const TimerIncrement({super.key}); 81 | 82 | @override 83 | Widget build(BuildContext context, WidgetRef ref) { 84 | if (kDebugMode) { 85 | print('TimerIncrement.build'); 86 | } 87 | 88 | return FloatingActionButton( 89 | onPressed: () { 90 | ref.read(counterProvider.notifier).increment(); 91 | }, 92 | child: const Icon(Icons.add), 93 | ); 94 | } 95 | } 96 | 97 | class TimerCountdown extends ConsumerWidget { 98 | const TimerCountdown({super.key}); 99 | 100 | @override 101 | Widget build(BuildContext context, WidgetRef ref) { 102 | if (kDebugMode) { 103 | print('TimerCountdown.build'); 104 | } 105 | 106 | final stream = ref.watch(streamProvider); 107 | final int limit = ref.watch(limitCountProvider); 108 | 109 | ref.listen(streamProvider, (prev, next) { 110 | if (next.value! >= limit) { 111 | context.showSnackBar(context, message: 'Timer done at $limit'); 112 | } 113 | }); 114 | 115 | return stream.when( 116 | data: (data) => Text('Stream Provider: $data'), 117 | error: (error, stack) => Text('Error: $error'), 118 | loading: () => const CircularProgressIndicator(), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/features/home/data/models/entry.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Entry { 4 | String title; 5 | Icon icon; 6 | String route; 7 | 8 | Entry({required this.title, required this.icon, required this.route}); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/features/home/data/providers/home_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import '../../../auth/ui/auth_screen.dart'; 4 | import '../../../calculator/ui/calculator_screen.dart'; 5 | import '../../../counter/ui/counter_screen.dart'; 6 | 7 | import '../../../counter/ui/timer_screen.dart'; 8 | import '../../../movies/ui/movies_paginated_screen.dart'; 9 | import '../../../movies/ui/movies_screen.dart'; 10 | import '../../../network_status/ui/network_status_screen.dart'; 11 | import '../../../products/ui/products_screen.dart'; 12 | import '../../../providers/ui/providers_screen.dart'; 13 | import '../../../settings/ui/settings_screen.dart'; 14 | import '../../../stopwatch/ui/stopwatch_screen.dart'; 15 | import '../../../trivia/ui/trivia_screen.dart'; 16 | import '../../../websockets/ui/websockets_screen.dart'; 17 | import '../models/entry.dart'; 18 | 19 | final homeMenuEntriesProvider = StateProvider>((ref) { 20 | return [ 21 | Entry( 22 | title: 'Providers', 23 | icon: const Icon(Icons.shopping_cart), 24 | route: ProvidersScreen.route, 25 | ), 26 | Entry( 27 | title: 'Counter App', 28 | icon: const Icon(Icons.timer), 29 | route: CounterScreen.route, 30 | ), 31 | Entry( 32 | title: 'Timer with Alert', 33 | icon: const Icon(Icons.timer_10_select), 34 | route: TimerScreen.route, 35 | ), 36 | Entry( 37 | title: 'Calculator', 38 | icon: const Icon(Icons.calculate), 39 | route: CalculatorScreen.route, 40 | ), 41 | Entry( 42 | title: 'Network Status', 43 | icon: const Icon(Icons.network_check), 44 | route: NetworkStatusScreen.route, 45 | ), 46 | Entry( 47 | title: 'Stopwatch', 48 | icon: const Icon(Icons.timelapse_rounded), 49 | route: StopwatchScreen.route, 50 | ), 51 | Entry( 52 | title: 'Trivia', 53 | icon: const Icon(Icons.question_answer), 54 | route: TriviaScreen.route, 55 | ), 56 | Entry( 57 | title: 'Websockets', 58 | icon: const Icon(Icons.wifi_tethering), 59 | route: WebsocketsScreen.route, 60 | ), 61 | Entry( 62 | title: 'Movies', 63 | icon: const Icon(Icons.movie), 64 | route: MoviesScreen.route, 65 | ), 66 | Entry( 67 | title: 'Movies (Paginated)', 68 | icon: const Icon(Icons.list), 69 | route: MoviesPaginatedScreen.route, 70 | ), 71 | Entry( 72 | title: 'Products (Pagination via StateNotifier)', 73 | icon: const Icon(Icons.shopping_cart), 74 | route: ProductsScreen.route, 75 | ), 76 | Entry( 77 | title: 'Settings', 78 | icon: const Icon(Icons.settings), 79 | route: SettingsScreen.route, 80 | ), 81 | Entry( 82 | title: 'Auth', 83 | icon: const Icon(Icons.home), 84 | route: AuthScreen.route, 85 | ), 86 | ]; 87 | }); 88 | -------------------------------------------------------------------------------- /lib/src/features/home/ui/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import '../../../common_widgets/connectivity_warning.dart'; 4 | import '../../../utils/contants.dart'; 5 | import '../data/providers/home_provider.dart'; 6 | 7 | class HomeScreen extends ConsumerWidget { 8 | const HomeScreen({super.key}); 9 | static const route = '/'; 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final homeMenuEntries = ref.watch(homeMenuEntriesProvider); 14 | 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text(Constants.appName), 18 | ), 19 | body: ListView( 20 | children: [ 21 | const ConnectivityWarning(), 22 | Column( 23 | children: homeMenuEntries 24 | .map( 25 | (item) => ListTile( 26 | title: Text(item.title), 27 | leading: item.icon, 28 | trailing: const Icon(Icons.chevron_right), 29 | onTap: () => Navigator.pushNamed(context, item.route), 30 | ), 31 | ) 32 | .toList(), 33 | ), 34 | ], 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/environment_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | class EnvironmentConfig { 5 | // we add the api key by running 'flutter run --dart-define=movieApiKey=MYKEY 6 | // final movieApiKey = const String.fromEnvironment('movieKey'); 7 | 8 | // or get it from the .env file 9 | final movieApiKey = dotenv.env['MOVIEDB_KEY']; 10 | } 11 | 12 | final environmentConfigProvider = 13 | Provider((ref) => EnvironmentConfig()); 14 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/exceptions/movies_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class MoviesException implements Exception { 4 | String? message; 5 | 6 | MoviesException.fromDioError(DioError dioError) { 7 | switch (dioError.type) { 8 | case DioErrorType.cancel: 9 | message = 'Request to API server was cancelled'; 10 | break; 11 | case DioErrorType.connectTimeout: 12 | message = 'Connection timeout with API server'; 13 | break; 14 | case DioErrorType.other: 15 | message = 'Connection to API server fiailed due to internet connection'; 16 | break; 17 | case DioErrorType.receiveTimeout: 18 | message = 'Received timeout in connection with API server'; 19 | break; 20 | case DioErrorType.response: 21 | message = _handeError(dioError.response!.statusCode); 22 | break; 23 | case DioErrorType.sendTimeout: 24 | message = 'Send timeout in connection with API server'; 25 | break; 26 | default: 27 | message = 'Something went wrong'; 28 | break; 29 | } 30 | } 31 | 32 | String _handeError(statusCode) { 33 | switch (statusCode) { 34 | case 400: 35 | return 'Bad Request'; 36 | case 404: 37 | return 'The requested resources was not found'; 38 | case 500: 39 | return 'Internal Server Error'; 40 | default: 41 | return 'Internal Server Error'; 42 | } 43 | } 44 | 45 | @override 46 | String toString() => message.toString(); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/models/movie.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:convert'; 3 | 4 | class Movie { 5 | final String title; 6 | final String image; 7 | 8 | Movie({ 9 | required this.title, 10 | required this.image, 11 | }); 12 | 13 | String get fullImageUrl => 'https://image.tmdb.org/t/p/w500$image'; 14 | 15 | Movie copyWith({ 16 | String? title, 17 | String? image, 18 | }) { 19 | return Movie( 20 | title: title ?? this.title, 21 | image: image ?? this.image, 22 | ); 23 | } 24 | 25 | Map toMap() { 26 | return { 27 | 'title': title, 28 | 'image': image, 29 | }; 30 | } 31 | 32 | factory Movie.fromMap(Map map) { 33 | return Movie( 34 | title: map['title'] as String, 35 | image: map['poster_path'] as String, 36 | ); 37 | } 38 | 39 | String toJson() => json.encode(toMap()); 40 | 41 | factory Movie.fromJson(String source) => 42 | Movie.fromMap(json.decode(source) as Map); 43 | 44 | @override 45 | String toString() => 'Movie(title: $title, image: $image)'; 46 | 47 | @override 48 | bool operator ==(covariant Movie other) { 49 | if (identical(this, other)) return true; 50 | 51 | return other.title == title && other.image == image; 52 | } 53 | 54 | @override 55 | int get hashCode => title.hashCode ^ image.hashCode; 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/models/movie_pagination.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:collection/collection.dart'; 4 | 5 | import 'movie.dart'; 6 | 7 | class MoviePagination { 8 | final List movies; 9 | final int page; 10 | final String errorMessage; 11 | 12 | MoviePagination.initial() 13 | : movies = [], 14 | page = 1, 15 | errorMessage = ''; 16 | 17 | bool get refreshError => errorMessage != '' && movies.length <= 20; 18 | 19 | MoviePagination({ 20 | required this.movies, 21 | required this.page, 22 | required this.errorMessage, 23 | }); 24 | 25 | MoviePagination copyWith({ 26 | List? movies, 27 | int? page, 28 | String? errorMessage, 29 | }) { 30 | return MoviePagination( 31 | movies: movies ?? this.movies, 32 | page: page ?? this.page, 33 | errorMessage: errorMessage ?? this.errorMessage, 34 | ); 35 | } 36 | 37 | Map toMap() { 38 | return { 39 | 'movies': movies.map((x) => x.toMap()).toList(), 40 | 'page': page, 41 | 'errorMessage': errorMessage, 42 | }; 43 | } 44 | 45 | factory MoviePagination.fromMap(Map map) { 46 | return MoviePagination( 47 | movies: List.from( 48 | (map['movies'] as List).map( 49 | (x) => Movie.fromMap(x as Map), 50 | ), 51 | ), 52 | page: map['page'] as int, 53 | errorMessage: map['errorMessage'] as String, 54 | ); 55 | } 56 | 57 | String toJson() => json.encode(toMap()); 58 | 59 | factory MoviePagination.fromJson(String source) => 60 | MoviePagination.fromMap(json.decode(source) as Map); 61 | 62 | @override 63 | String toString() => 64 | 'MoviePagination(movies: $movies, page: $page, errorMessage: $errorMessage)'; 65 | 66 | @override 67 | bool operator ==(covariant MoviePagination other) { 68 | if (identical(this, other)) return true; 69 | final listEquals = const DeepCollectionEquality().equals; 70 | 71 | return listEquals(other.movies, movies) && 72 | other.page == page && 73 | other.errorMessage == errorMessage; 74 | } 75 | 76 | @override 77 | int get hashCode => movies.hashCode ^ page.hashCode ^ errorMessage.hashCode; 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/providers/movies_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | 3 | import '../exceptions/movies_exception.dart'; 4 | import '../models/movie.dart'; 5 | import '../models/movie_pagination.dart'; 6 | import '../repositories/movie_repository.dart'; 7 | 8 | final moviesFutureProvider = 9 | FutureProvider.autoDispose>((ref) async { 10 | ref.maintainState = true; 11 | 12 | final movieService = ref.watch(movieServiceProvider); 13 | final movies = await movieService.getMovies(); 14 | return movies; 15 | }); 16 | 17 | // add pagination 18 | final moviesPaginationNotifierProvider = StateNotifierProvider((ref) { 19 | final movieService = ref.read(movieServiceProvider); 20 | 21 | return MoviePaginationNotifier(movieService); 22 | }); 23 | 24 | class MoviePaginationNotifier extends StateNotifier { 25 | final MovieRepository _service; 26 | 27 | MoviePaginationNotifier( 28 | this._service, [ 29 | MoviePagination? state, 30 | ]) : super(state ?? MoviePagination.initial()) { 31 | getMovies(); 32 | } 33 | 34 | Future getMovies() async { 35 | try { 36 | final movies = await _service.getMovies(state.page); 37 | 38 | state = state 39 | .copyWith(movies: [...state.movies, ...movies], page: state.page + 1); 40 | } on MoviesException catch (e) { 41 | state = state.copyWith(errorMessage: e.message); 42 | } 43 | } 44 | 45 | void handleScrollWithIndex(int index) { 46 | final itemPosition = index + 1; 47 | final requestMoreData = itemPosition % 20 == 0 && itemPosition != 0; 48 | 49 | final pageToRequest = itemPosition ~/ 20; 50 | 51 | if (requestMoreData && pageToRequest + 1 >= state.page) { 52 | getMovies(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/features/movies/data/repositories/movie_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../environment_config.dart'; 5 | import '../exceptions/movies_exception.dart'; 6 | import '../models/movie.dart'; 7 | 8 | const String moviesUrl = 'https://api.themoviedb.org/3/movie'; 9 | 10 | final movieServiceProvider = Provider((ref) { 11 | final config = ref.watch(environmentConfigProvider); 12 | 13 | return MovieRepository(config, Dio()); 14 | }); 15 | 16 | class MovieRepository { 17 | final Dio _dio; 18 | final EnvironmentConfig _environmentConfig; 19 | 20 | MovieRepository(this._environmentConfig, this._dio); 21 | 22 | Future> getMovies([int page = 1]) async { 23 | try { 24 | final url = 25 | '$moviesUrl/popular?api_key=${_environmentConfig.movieApiKey}&language=en-US&page=$page'; 26 | final response = await _dio.get(url); 27 | 28 | final results = 29 | List>.from(response.data['results'] as Iterable); 30 | 31 | List movies = 32 | results.map((e) => Movie.fromMap(e)).toList(growable: false); 33 | 34 | return movies; 35 | } on DioError catch (e) { 36 | throw MoviesException.fromDioError(e); 37 | } catch (e) { 38 | throw Exception(e.toString()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/features/movies/ui/error_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../data/providers/movies_provider.dart'; 5 | 6 | class ErrorBody extends ConsumerWidget { 7 | final String message; 8 | 9 | const ErrorBody({required this.message, super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | return Center( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Text(message), 18 | const SizedBox( 19 | height: 20, 20 | ), 21 | ElevatedButton( 22 | onPressed: () => ref 23 | .refresh(moviesPaginationNotifierProvider.notifier) 24 | .getMovies(), 25 | child: const Text('Try again'), 26 | ) 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/features/movies/ui/movie_box.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | import '../data/models/movie.dart'; 7 | 8 | class MovieBox extends StatelessWidget { 9 | final Movie movie; 10 | 11 | const MovieBox({super.key, required this.movie}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Stack( 16 | children: [ 17 | ClipRRect( 18 | borderRadius: BorderRadius.circular(5.0), 19 | child: Image.network( 20 | movie.fullImageUrl, 21 | fit: BoxFit.cover, 22 | width: double.infinity, 23 | ), 24 | ), 25 | Positioned( 26 | bottom: 0, 27 | left: 0, 28 | right: 0, 29 | child: FrontBanner(text: movie.title), 30 | ) 31 | ], 32 | ); 33 | } 34 | } 35 | 36 | class FrontBanner extends StatelessWidget { 37 | final String text; 38 | 39 | const FrontBanner({ 40 | super.key, 41 | required this.text, 42 | }); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return ClipRRect( 47 | borderRadius: const BorderRadius.only( 48 | bottomLeft: Radius.circular(5), 49 | bottomRight: Radius.circular(5), 50 | ), 51 | child: BackdropFilter( 52 | filter: ImageFilter.blur(sigmaX: 1, sigmaY: 1), 53 | child: Container( 54 | color: Colors.grey.shade900.withOpacity(0.7), 55 | padding: const EdgeInsets.all(7), 56 | child: Center( 57 | child: Text( 58 | text, 59 | style: Theme.of(context).textTheme.bodyText2!.copyWith( 60 | fontWeight: FontWeight.bold, 61 | color: Colors.grey.shade200, 62 | ), 63 | maxLines: 2, 64 | textAlign: TextAlign.center, 65 | overflow: TextOverflow.ellipsis, 66 | ), 67 | ), 68 | ), 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/features/movies/ui/movies_paginated_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../data/providers/movies_provider.dart'; 5 | import 'error_body.dart'; 6 | import 'movie_box.dart'; 7 | 8 | class MoviesPaginatedScreen extends ConsumerWidget { 9 | const MoviesPaginatedScreen({super.key}); 10 | static const route = '/movies-paginated'; 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final paginationState = 15 | ref.watch(moviesPaginationNotifierProvider.notifier); 16 | 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: const Text('Movies - Paginated'), 20 | elevation: 0, 21 | centerTitle: true, 22 | ), 23 | body: Builder( 24 | builder: (context) { 25 | if (paginationState.state.refreshError) { 26 | return ErrorBody(message: '${paginationState.state.refreshError}'); 27 | } else if (paginationState.state.movies.isEmpty) { 28 | return const Center( 29 | child: CircularProgressIndicator(), 30 | ); 31 | } 32 | 33 | return RefreshIndicator( 34 | onRefresh: () async { 35 | await ref 36 | .refresh(moviesPaginationNotifierProvider.notifier) 37 | .getMovies(); 38 | }, 39 | child: GridView.builder( 40 | gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( 41 | maxCrossAxisExtent: 200, 42 | crossAxisSpacing: 12, 43 | mainAxisSpacing: 12, 44 | childAspectRatio: 0.7, 45 | ), 46 | itemCount: paginationState.state.movies.length, 47 | itemBuilder: (context, index) { 48 | // use the index for pagination 49 | paginationState.handleScrollWithIndex(index); 50 | 51 | return MovieBox(movie: paginationState.state.movies[index]); 52 | }, 53 | ), 54 | ); 55 | }, 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/features/movies/ui/movies_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../data/exceptions/movies_exception.dart'; 5 | import '../data/providers/movies_provider.dart'; 6 | import 'error_body.dart'; 7 | import 'movie_box.dart'; 8 | 9 | class MoviesScreen extends ConsumerWidget { 10 | const MoviesScreen({super.key}); 11 | static const route = '/movies'; 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final movies = ref.watch(moviesFutureProvider); 16 | 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: const Text('Movies'), 20 | elevation: 0, 21 | centerTitle: true, 22 | ), 23 | body: Container( 24 | padding: const EdgeInsets.only(top: 12), 25 | child: movies.when( 26 | data: (data) { 27 | return RefreshIndicator( 28 | onRefresh: () async { 29 | ref.refresh(moviesFutureProvider); 30 | }, 31 | child: GridView.extent( 32 | maxCrossAxisExtent: 200, 33 | crossAxisSpacing: 12, 34 | mainAxisSpacing: 12, 35 | childAspectRatio: 0.7, 36 | children: data.map((movie) => MovieBox(movie: movie)).toList(), 37 | ), 38 | ); 39 | }, 40 | error: (error, _) { 41 | if (error is MoviesException) { 42 | return ErrorBody(message: '${error.message}'); 43 | } 44 | 45 | return const ErrorBody( 46 | message: 'Oops, something unexpected happened'); 47 | }, 48 | loading: () => const Center( 49 | child: CircularProgressIndicator(), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/features/network_status/providers/network_status_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | enum NetworkStatus { notDetermined, on, off } 7 | 8 | class NetworkDetectorNotifier extends StateNotifier { 9 | StreamController controller = 10 | StreamController(); 11 | 12 | late NetworkStatus lastResult; 13 | 14 | NetworkDetectorNotifier() : super(NetworkStatus.notDetermined) { 15 | lastResult = NetworkStatus.notDetermined; 16 | 17 | Connectivity().onConnectivityChanged.listen((result) { 18 | // Use Connectivity() here to gather more info if you need to 19 | NetworkStatus? newState; 20 | switch (result) { 21 | case ConnectivityResult.mobile: 22 | case ConnectivityResult.wifi: 23 | newState = NetworkStatus.on; 24 | break; 25 | case ConnectivityResult.none: 26 | newState = NetworkStatus.off; 27 | // TODO: Handle this case. 28 | break; 29 | case ConnectivityResult.bluetooth: 30 | // TODO: Handle this case. 31 | break; 32 | case ConnectivityResult.ethernet: 33 | // TODO: Handle this case. 34 | break; 35 | } 36 | 37 | if (newState != state) { 38 | state = newState as NetworkStatus; 39 | } 40 | }); 41 | } 42 | } 43 | 44 | final networkAwareProvider = 45 | StateNotifierProvider.autoDispose((ref) => NetworkDetectorNotifier()); 46 | -------------------------------------------------------------------------------- /lib/src/features/network_status/ui/network_status_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import '../providers/network_status_provider.dart'; 6 | 7 | class NetworkStatusScreen extends ConsumerWidget { 8 | const NetworkStatusScreen({super.key}); 9 | 10 | static const route = '/network-status'; 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | if (kDebugMode) { 15 | print('NetworkStatusScreen.build'); 16 | } 17 | 18 | var network = ref.watch(networkAwareProvider); 19 | 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text('Network Status'), 23 | ), 24 | body: network == NetworkStatus.off 25 | ? const Center( 26 | child: Text('No Internet'), 27 | ) 28 | : const Center( 29 | child: Text('Internet is on'), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/features/products/data/exceptions/products_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class ProductsException implements Exception { 4 | String? message; 5 | 6 | ProductsException.fromDioError(DioError dioError) { 7 | switch (dioError.type) { 8 | case DioErrorType.cancel: 9 | message = 'Request to API server was cancelled'; 10 | break; 11 | case DioErrorType.connectTimeout: 12 | message = 'Connection timeout with API server'; 13 | break; 14 | case DioErrorType.other: 15 | message = 'Connection to API server fiailed due to internet connection'; 16 | break; 17 | case DioErrorType.receiveTimeout: 18 | message = 'Received timeout in connection with API server'; 19 | break; 20 | case DioErrorType.response: 21 | message = _handeError(dioError.response!.statusCode); 22 | break; 23 | case DioErrorType.sendTimeout: 24 | message = 'Send timeout in connection with API server'; 25 | break; 26 | default: 27 | message = 'Something went wrong'; 28 | break; 29 | } 30 | } 31 | 32 | String _handeError(statusCode) { 33 | switch (statusCode) { 34 | case 400: 35 | return 'Bad Request'; 36 | case 404: 37 | return 'The requested resources was not found'; 38 | case 500: 39 | return 'Internal Server Error'; 40 | default: 41 | return 'Internal Server Error'; 42 | } 43 | } 44 | 45 | @override 46 | String toString() => message.toString(); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/features/products/data/models/product.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | class Product extends Equatable { 6 | final int id; 7 | final String title; 8 | final String? description; 9 | final int price; 10 | final double? discountPercentage; 11 | final double? rating; 12 | final int? stock; 13 | final String? brand; 14 | final String? category; 15 | final String thumbnail; 16 | final List images; 17 | 18 | const Product({ 19 | required this.id, 20 | required this.title, 21 | this.description, 22 | required this.price, 23 | this.discountPercentage, 24 | this.rating, 25 | this.stock, 26 | this.brand, 27 | this.category, 28 | required this.thumbnail, 29 | required this.images, 30 | }); 31 | 32 | Product copyWith({ 33 | int? id, 34 | String? title, 35 | String? description, 36 | int? price, 37 | double? discountPercentage, 38 | double? rating, 39 | int? stock, 40 | String? brand, 41 | String? category, 42 | String? thumbnail, 43 | List? images, 44 | }) { 45 | return Product( 46 | id: id ?? this.id, 47 | title: title ?? this.title, 48 | description: description ?? this.description, 49 | price: price ?? this.price, 50 | discountPercentage: discountPercentage ?? this.discountPercentage, 51 | rating: rating ?? this.rating, 52 | stock: stock ?? this.stock, 53 | brand: brand ?? this.brand, 54 | category: category ?? this.category, 55 | thumbnail: thumbnail ?? this.thumbnail, 56 | images: images ?? this.images, 57 | ); 58 | } 59 | 60 | Map toMap() { 61 | return { 62 | 'id': id, 63 | 'title': title, 64 | 'description': description, 65 | 'price': price, 66 | 'discountPercentage': discountPercentage, 67 | 'rating': rating, 68 | 'stock': stock, 69 | 'brand': brand, 70 | 'category': category, 71 | 'thumbnail': thumbnail, 72 | 'images': images, 73 | }; 74 | } 75 | 76 | factory Product.fromMap(Map map) { 77 | return Product( 78 | id: int.parse(map['id'].toString()), 79 | title: map['title'] as String, 80 | description: 81 | map['description'] != null ? map['description'] as String : null, 82 | price: int.parse(map['price'].toString()), 83 | discountPercentage: map['discountPercentage'] != null 84 | ? map['discountPercentage'] as double 85 | : null, 86 | rating: 87 | map['rating'] != null ? double.parse(map['rating'].toString()) : null, 88 | stock: map['stock'] != null ? int.parse(map['stock'].toString()) : null, 89 | brand: map['brand'] != null ? map['brand'] as String : null, 90 | category: map['category'] != null ? map['category'] as String : null, 91 | thumbnail: map['thumbnail'] as String, 92 | images: (map['images'] as List).map((e) => e as String).toList(), 93 | ); 94 | } 95 | 96 | String toJson() => json.encode(toMap()); 97 | 98 | factory Product.fromJson(String source) => 99 | Product.fromMap(json.decode(source) as Map); 100 | 101 | @override 102 | bool get stringify => true; 103 | 104 | @override 105 | List get props { 106 | return [ 107 | id, 108 | title, 109 | description, 110 | price, 111 | discountPercentage, 112 | rating, 113 | stock, 114 | brand, 115 | category, 116 | thumbnail, 117 | images, 118 | ]; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/features/products/data/models/product_pagination.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | import 'product.dart'; 6 | 7 | class ProductPagination extends Equatable { 8 | List products; 9 | int page; 10 | bool loading; 11 | String? errorMessage; 12 | String? statusMessage; // used to display message 13 | 14 | ProductPagination.initial() 15 | : products = [], 16 | page = 1, 17 | loading = false, 18 | errorMessage = '', 19 | statusMessage = ''; 20 | 21 | bool get hasError => errorMessage != null && errorMessage!.isNotEmpty; 22 | 23 | ProductPagination({ 24 | required this.products, 25 | required this.page, 26 | required this.loading, 27 | this.errorMessage, 28 | this.statusMessage, 29 | }); 30 | 31 | ProductPagination copyWith({ 32 | List? products, 33 | int? page, 34 | bool? loading, 35 | String? errorMessage, 36 | String? statusMessage, 37 | }) { 38 | return ProductPagination( 39 | products: products ?? this.products, 40 | page: page ?? this.page, 41 | loading: loading ?? this.loading, 42 | errorMessage: errorMessage ?? this.errorMessage, 43 | statusMessage: statusMessage ?? this.statusMessage, 44 | ); 45 | } 46 | 47 | Map toMap() { 48 | return { 49 | 'products': products.map((x) => x.toMap()).toList(), 50 | 'page': page, 51 | 'loading': loading, 52 | 'errorMessage': errorMessage, 53 | 'statusMessage': statusMessage, 54 | }; 55 | } 56 | 57 | factory ProductPagination.fromMap(Map map) { 58 | return ProductPagination( 59 | products: List.from( 60 | (map['movies'] as List).map( 61 | (x) => Product.fromMap(x as Map), 62 | ), 63 | ), 64 | page: map['page'] as int, 65 | errorMessage: map['errorMessage'] as String, 66 | loading: map['loading'] as bool, 67 | statusMessage: map['statusMessage'] as String, 68 | ); 69 | } 70 | 71 | String toJson() => json.encode(toMap()); 72 | 73 | factory ProductPagination.fromJson(String source) => 74 | ProductPagination.fromMap(json.decode(source) as Map); 75 | 76 | @override 77 | String toString() => 78 | 'ProductPagination(products: $products, page: $page, errorMessage: $errorMessage, statusMessage: $statusMessage)'; 79 | 80 | @override 81 | List get props { 82 | return [ 83 | products, 84 | page, 85 | loading, 86 | errorMessage, 87 | statusMessage, 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/features/products/data/providers/cart_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 2 | 3 | /// Cart provider should handle everything pertaining to cart 4 | /// but for simplicity most functionality shall not be provided 5 | 6 | final cartProvider = StateProvider((ref) => 0); 7 | -------------------------------------------------------------------------------- /lib/src/features/products/data/providers/product_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../exceptions/products_exception.dart'; 6 | import '../models/product_pagination.dart'; 7 | import '../repositories/product_repository.dart'; 8 | import '../utils/api_client.dart'; 9 | 10 | final productsRepositoryProvider = Provider( 11 | (ref) => ApiProductRepository(ApiClient().instance), 12 | ); 13 | 14 | // add pagination state 15 | final productsProvider = 16 | StateNotifierProvider((ref) { 17 | final repo = ref.watch(productsRepositoryProvider); 18 | 19 | return ProductPaginationNotifier(repo); 20 | }); 21 | 22 | class ProductPaginationNotifier extends StateNotifier { 23 | final ProductRepository _repo; 24 | Timer _timer = Timer(const Duration(milliseconds: 0), () {}); 25 | bool noMoreItems = false; 26 | int itemsPerBatch = 20; 27 | 28 | ProductPaginationNotifier( 29 | this._repo, [ 30 | ProductPagination? state, 31 | ]) : super(state ?? ProductPagination.initial()) { 32 | getProducts(isInitialLoad: true); 33 | } 34 | 35 | Future getProducts({bool isInitialLoad = false}) async { 36 | try { 37 | if (!isInitialLoad) { 38 | if (_timer.isActive && state.products.isNotEmpty) { 39 | state = state.copyWith( 40 | statusMessage: 'Debounced', 41 | ); 42 | logger.i('Debounced'); 43 | return; 44 | } 45 | 46 | _timer = Timer(const Duration(seconds: 1), () {}); 47 | 48 | if (noMoreItems) { 49 | state = state.copyWith( 50 | statusMessage: 'No more Items', 51 | ); 52 | logger.i('No more Items'); 53 | return; 54 | } 55 | 56 | if (state.loading) { 57 | state = state.copyWith( 58 | statusMessage: 'Rejected. Already Loading', 59 | ); 60 | logger.i('Rejected. Already Loading'); 61 | return; 62 | } 63 | state = state.copyWith( 64 | statusMessage: 'Fetching Now', 65 | ); 66 | logger.i('Fetching NOW'); 67 | } 68 | 69 | state = state.copyWith( 70 | loading: true, 71 | errorMessage: '', 72 | ); 73 | 74 | final products = await _repo.getProducts(state.page); 75 | noMoreItems = products.length < itemsPerBatch; 76 | 77 | state = state.copyWith( 78 | products: [...state.products, ...products], 79 | page: state.page + 1, 80 | ); 81 | } on ProductsException catch (e) { 82 | state = state.copyWith(errorMessage: e.message); 83 | } catch (e) { 84 | state = state.copyWith(errorMessage: e.toString()); 85 | } finally { 86 | state = state.copyWith( 87 | loading: false, 88 | statusMessage: 'Completed', 89 | ); 90 | } 91 | } 92 | 93 | void reset() { 94 | state = ProductPagination.initial(); 95 | getProducts(isInitialLoad: true); 96 | } 97 | 98 | Future recordView(int id) async { 99 | try { 100 | await _repo.recordView(id); 101 | 102 | // TODO: update state here 103 | } on ProductsException catch (e) { 104 | // fail silently 105 | } catch (e) { 106 | // fail silently 107 | } 108 | } 109 | 110 | Future recordLike(int id) async { 111 | try { 112 | await _repo.likeProduct(id); 113 | 114 | // TODO: update state here 115 | } on ProductsException catch (e) { 116 | // fail silently 117 | } catch (e) { 118 | // fail silently 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/features/products/data/repositories/product_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:logger/logger.dart'; 3 | 4 | import '../exceptions/products_exception.dart'; 5 | import '../models/product.dart'; 6 | 7 | var logger = Logger(); 8 | 9 | abstract class ProductRepository { 10 | Future> getProducts([int page = 1]); 11 | 12 | Future getProduct(int id); 13 | 14 | Future likeProduct(int id); 15 | 16 | Future recordView(int id); 17 | } 18 | 19 | class ApiProductRepository implements ProductRepository { 20 | final Dio _dio; 21 | final limit = 20; 22 | final baseUrl = 'https://dummyjson.com'; 23 | 24 | ApiProductRepository(this._dio); 25 | 26 | @override 27 | Future> getProducts([int page = 1]) async { 28 | try { 29 | final url = '$baseUrl/products?limit=${limit}&skip=${page * limit}'; 30 | final response = await _dio.get(url); 31 | 32 | final results = List>.from( 33 | response.data['products'] as Iterable); 34 | 35 | List list = 36 | results.map((e) => Product.fromMap(e)).toList(growable: false); 37 | 38 | return list; 39 | } on DioError catch (e) { 40 | throw ProductsException.fromDioError(e); 41 | } catch (e) { 42 | logger.d(e); 43 | throw Exception(e.toString()); 44 | } 45 | } 46 | 47 | @override 48 | Future getProduct(int id) { 49 | // TODO: implement getProduct 50 | throw UnimplementedError(); 51 | } 52 | 53 | @override 54 | Future likeProduct(int id) { 55 | // TODO: implement likeProduct 56 | throw UnimplementedError(); 57 | } 58 | 59 | @override 60 | Future recordView(int id) { 61 | // TODO: implement recordView 62 | throw UnimplementedError(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/features/products/data/utils/api_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class ApiClient { 4 | late Dio _dio; 5 | 6 | ApiClient() { 7 | var options = BaseOptions( 8 | baseUrl: 'https://dummyjson.com', 9 | connectTimeout: 10000, 10 | receiveTimeout: 3000, 11 | ); 12 | 13 | _dio = Dio(options); 14 | } 15 | 16 | Dio get instance => _dio; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/features/products/ui/error_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../data/providers/product_provider.dart'; 5 | 6 | class ErrorBody extends ConsumerWidget { 7 | final String message; 8 | 9 | const ErrorBody({required this.message, super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | return Center( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Text(message), 18 | const SizedBox( 19 | height: 20, 20 | ), 21 | ElevatedButton( 22 | onPressed: () => 23 | ref.refresh(productsProvider.notifier).getProducts(), 24 | child: const Text('Try again'), 25 | ) 26 | ], 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/features/products/ui/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:fade_shimmer/fade_shimmer.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class Loading extends StatelessWidget { 5 | final int rows; 6 | const Loading({Key? key, this.rows = 1}) : super(key: key); 7 | final bool isDarkMode = false; 8 | final double height = 60 + 16 + 20; // 60=round, 16=separator, 20=margin-b0ttom 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final size = MediaQuery.of(context).size; 13 | 14 | return SizedBox( 15 | height: height * rows, 16 | width: double.infinity, 17 | child: ListView.separated( 18 | itemBuilder: (_, i) { 19 | final delay = (i * 300); 20 | return Row( 21 | children: [ 22 | FadeShimmer.round( 23 | size: 60, 24 | fadeTheme: isDarkMode ? FadeTheme.dark : FadeTheme.light, 25 | millisecondsDelay: delay, 26 | ), 27 | const SizedBox( 28 | width: 8, 29 | ), 30 | Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | FadeShimmer( 34 | height: 8, 35 | width: size.width * 0.7 - 60 - 20, 36 | radius: 4, 37 | millisecondsDelay: delay, 38 | fadeTheme: isDarkMode ? FadeTheme.dark : FadeTheme.light, 39 | ), 40 | const SizedBox( 41 | height: 6, 42 | ), 43 | FadeShimmer( 44 | height: 8, 45 | millisecondsDelay: delay, 46 | width: 170, 47 | radius: 4, 48 | fadeTheme: isDarkMode ? FadeTheme.dark : FadeTheme.light, 49 | ), 50 | ], 51 | ) 52 | ], 53 | ); 54 | }, 55 | itemCount: rows, 56 | separatorBuilder: (_, __) => const SizedBox( 57 | height: 16, 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/features/providers/providers/providers_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | final textProvider = Provider((ref) => 'Hello'); 5 | 6 | final futureProvider = FutureProvider((ref) async { 7 | await Future.delayed(const Duration(seconds: 2)); 8 | return 3; 9 | }); 10 | 11 | final streamProvider = StreamProvider((ref) { 12 | return Stream.periodic(const Duration(seconds: 1), (number) { 13 | if (number < 5) return number + 1; 14 | return 5; 15 | }); 16 | }); 17 | 18 | final stateProvider = StateProvider((ref) => 0); 19 | 20 | final stateNotifierProvider = 21 | StateNotifierProvider((ref) => CountNotifier(6)); 22 | 23 | class CountNotifier extends StateNotifier { 24 | CountNotifier(super.state); 25 | 26 | void add() { 27 | state += 1; 28 | } 29 | 30 | void subtract() { 31 | state -= 1; 32 | } 33 | } 34 | 35 | final changeNotifierProvider = 36 | ChangeNotifierProvider((ref) => ChangeCount()); 37 | 38 | class ChangeCount extends ChangeNotifier { 39 | int number = 6; 40 | 41 | void add() { 42 | number += 1; 43 | notifyListeners(); 44 | } 45 | 46 | void subtract() { 47 | number -= 1; 48 | notifyListeners(); 49 | } 50 | } 51 | 52 | final counterAutodisposedProvider = 53 | StateNotifierProvider.autoDispose((ref) => Counter()); 54 | 55 | class Counter extends StateNotifier { 56 | Counter() : super(0); 57 | 58 | void add() => state++; 59 | void subtract() => state--; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/features/settings/data/models/theme_scheme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ThemeScheme { 5 | final String name; 6 | final FlexScheme scheme; 7 | final Color color; 8 | 9 | const ThemeScheme({ 10 | required this.name, 11 | required this.scheme, 12 | required this.color, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/features/settings/data/models/theme_state.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ThemeState { 6 | final ThemeMode mode; 7 | final FlexScheme scheme; 8 | final bool isDarkMode; 9 | 10 | ThemeState({ 11 | required this.mode, 12 | required this.scheme, 13 | required this.isDarkMode, 14 | }); 15 | 16 | factory ThemeState.initial() { 17 | return ThemeState( 18 | mode: ThemeMode.light, 19 | scheme: FlexScheme.dellGenoa, 20 | isDarkMode: false, 21 | ); 22 | } 23 | 24 | ThemeState copyWith({ 25 | ThemeMode? mode, 26 | FlexScheme? scheme, 27 | bool? isDarkMode, 28 | }) { 29 | return ThemeState( 30 | mode: mode ?? this.mode, 31 | scheme: scheme ?? this.scheme, 32 | isDarkMode: isDarkMode ?? this.isDarkMode, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/features/settings/data/providers/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:enum_to_string/enum_to_string.dart'; 2 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:hive/hive.dart'; 6 | 7 | import '../../../../utils/contants.dart'; 8 | import '../models/theme_scheme.dart'; 9 | import '../models/theme_state.dart'; 10 | 11 | final themeProvider = 12 | StateNotifierProvider((ref) { 13 | final box = Hive.box(Constants.settingsStorageKey); 14 | var themeState = ThemeState.initial(); 15 | 16 | // scheme mode 17 | final scheme = box.get('scheme'); 18 | if (scheme != null && scheme != 'default') { 19 | final decodedScheme = 20 | EnumToString.fromString(FlexScheme.values, scheme.toString()); 21 | 22 | if (decodedScheme != null) { 23 | themeState = themeState.copyWith( 24 | scheme: decodedScheme, 25 | ); 26 | } 27 | } 28 | 29 | // dark theme mode 30 | final isDarkModeOn = box.get('darkMode'); 31 | 32 | if (isDarkModeOn != null && isDarkModeOn.toString() == 'yes') { 33 | return ThemeStateNotifier( 34 | themeState.copyWith(isDarkMode: true, mode: ThemeMode.dark), 35 | ); 36 | } 37 | 38 | return ThemeStateNotifier( 39 | themeState.copyWith(isDarkMode: false, mode: ThemeMode.system), 40 | ); 41 | }); 42 | 43 | class ThemeStateNotifier extends StateNotifier { 44 | ThemeStateNotifier(super.state); 45 | 46 | void toggle() { 47 | bool isDarkMode = state.isDarkMode; 48 | 49 | state = state.copyWith( 50 | mode: isDarkMode ? ThemeMode.light : ThemeMode.dark, 51 | isDarkMode: !isDarkMode, 52 | ); 53 | 54 | updateStorage(!isDarkMode); 55 | } 56 | 57 | void setMode(bool isDarkMode) { 58 | state = state.copyWith( 59 | mode: isDarkMode ? ThemeMode.dark : ThemeMode.light, 60 | isDarkMode: isDarkMode, 61 | ); 62 | 63 | updateStorage(isDarkMode); 64 | } 65 | 66 | void setScheme(FlexScheme scheme) { 67 | state = state.copyWith( 68 | scheme: scheme, 69 | ); 70 | 71 | final box = Hive.box(Constants.settingsStorageKey); 72 | box.put('scheme', EnumToString.convertToString(scheme)); 73 | } 74 | 75 | void updateStorage([bool isDarkModeOn = false]) { 76 | final box = Hive.box(Constants.settingsStorageKey); 77 | isDarkModeOn ? box.put('darkMode', 'yes') : box.delete('darkMode'); 78 | } 79 | } 80 | 81 | final schemesProvider = StateProvider>((ref) => [ 82 | const ThemeScheme( 83 | name: 'Deep Purple', 84 | scheme: FlexScheme.deepPurple, 85 | color: Color(0xff4A2EA3), 86 | ), 87 | const ThemeScheme( 88 | name: 'Aqua Blue', 89 | scheme: FlexScheme.aquaBlue, 90 | color: Color(0xff3BA3CD), 91 | ), 92 | const ThemeScheme( 93 | name: 'Amber', 94 | scheme: FlexScheme.amber, 95 | color: Color(0xffE65706), 96 | ), 97 | const ThemeScheme( 98 | name: 'Big Stone', 99 | scheme: FlexScheme.bigStone, 100 | color: Color(0xff223348), 101 | ), 102 | const ThemeScheme( 103 | name: 'Mallard', 104 | scheme: FlexScheme.mallardGreen, 105 | color: Color(0xff344A27), 106 | ), 107 | const ThemeScheme( 108 | name: 'Wasabi', 109 | scheme: FlexScheme.wasabi, 110 | color: Color(0xff77892B), 111 | ), 112 | const ThemeScheme( 113 | name: 'Red Wine', 114 | scheme: FlexScheme.redWine, 115 | color: Color(0xff9E2237), 116 | ), 117 | const ThemeScheme( 118 | name: 'Material', 119 | scheme: FlexScheme.material, 120 | color: Color(0xff6709EF), 121 | ), 122 | ]); 123 | -------------------------------------------------------------------------------- /lib/src/features/stopwatch/data/models/timer.dart: -------------------------------------------------------------------------------- 1 | class TimerModel { 2 | const TimerModel(this.timeLeft, this.buttonState); 3 | final String timeLeft; 4 | final ButtonState buttonState; 5 | } 6 | 7 | enum ButtonState { 8 | initial, 9 | started, 10 | paused, 11 | finished, 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/features/stopwatch/data/providers/stopwatch_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | 6 | import '../models/timer.dart'; 7 | 8 | final timerProvider = StateNotifierProvider( 9 | (ref) => TimerNotifier(), 10 | ); 11 | 12 | class TimerNotifier extends StateNotifier { 13 | TimerNotifier() : super(_initialState); 14 | 15 | static const int _initialDuration = 10; 16 | static final _initialState = TimerModel( 17 | _durationString(_initialDuration), 18 | ButtonState.initial, 19 | ); 20 | 21 | final _ticker = Ticker(); 22 | StreamSubscription? _tickerSubscription; 23 | 24 | static String _durationString(int duration) { 25 | final minutes = ((duration / 60) % 60).floor().toString().padLeft(2, '0'); 26 | final seconds = (duration % 60).floor().toString().padLeft(2, '0'); 27 | return '$minutes:$seconds'; 28 | } 29 | 30 | @override 31 | void dispose() { 32 | _tickerSubscription?.cancel(); 33 | super.dispose(); 34 | } 35 | 36 | void start() { 37 | if (state.buttonState == ButtonState.paused) { 38 | _restartTimer(); 39 | } else { 40 | _startTimer(); 41 | } 42 | } 43 | 44 | void _restartTimer() { 45 | _tickerSubscription?.resume(); 46 | state = TimerModel(state.timeLeft, ButtonState.started); 47 | } 48 | 49 | void _startTimer() { 50 | _tickerSubscription?.cancel(); 51 | 52 | _tickerSubscription = 53 | _ticker.tick(ticks: _initialDuration).listen((duration) { 54 | state = TimerModel(_durationString(duration), ButtonState.started); 55 | }); 56 | 57 | _tickerSubscription?.onDone(() { 58 | state = TimerModel(state.timeLeft, ButtonState.finished); 59 | }); 60 | 61 | state = TimerModel(_durationString(_initialDuration), ButtonState.started); 62 | } 63 | 64 | void pause() { 65 | _tickerSubscription?.pause(); 66 | state = TimerModel(state.timeLeft, ButtonState.paused); 67 | } 68 | 69 | void reset() { 70 | _tickerSubscription?.cancel(); 71 | state = _initialState; 72 | } 73 | } 74 | 75 | class Ticker { 76 | Stream tick({required int ticks}) { 77 | return Stream.periodic( 78 | const Duration(seconds: 1), 79 | (x) => ticks - x - 1, 80 | ).take(ticks); 81 | } 82 | } 83 | 84 | // This is a Provider instead of a StateNotifierProvider. 85 | // The reason is that you’re listening to another provider rather than to a StateNotifier. 86 | 87 | // watch only gives updates for unique changes. 88 | //Since _buttonState will be the same for most of the timerProvider changes, 89 | //watching _buttonState in the second provider will only cause rebuilds when _buttonState actually changes. 90 | //Thus, buttonProvider will only be updated when the button state changes. 91 | //You’ve effectively filtered out all of the timeLeft changes. 92 | final buttonProvider = Provider((ref) { 93 | return ref.watch(_buttonState); 94 | }); 95 | 96 | final _buttonState = Provider((ref) { 97 | return ref.watch(timerProvider).buttonState; 98 | }); 99 | 100 | final timeLeftProvider = Provider((ref) { 101 | return ref.watch(_timeLeftProvider); 102 | }); 103 | 104 | final _timeLeftProvider = Provider((ref) { 105 | return ref.watch(timerProvider).timeLeft; 106 | }); 107 | 108 | String generateRandomString(int len) { 109 | var r = Random(); 110 | return String.fromCharCodes( 111 | List.generate(len, (index) => r.nextInt(33) + 89)); 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/enums/difficulty.dart: -------------------------------------------------------------------------------- 1 | enum Difficulty { 2 | any, 3 | easy, 4 | medium, 5 | hard, 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/enums/quiz_status.dart: -------------------------------------------------------------------------------- 1 | enum QuizStatus { initial, correct, incorrect, complete } 2 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/models/failure.dart: -------------------------------------------------------------------------------- 1 | class Failure { 2 | final String message; 3 | 4 | const Failure({required this.message}); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/models/question.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Question extends Equatable { 4 | final String category; 5 | final String difficulty; 6 | final String question; 7 | final String correctAnswer; 8 | final List answers; 9 | 10 | const Question({ 11 | required this.category, 12 | required this.difficulty, 13 | required this.question, 14 | required this.correctAnswer, 15 | required this.answers, 16 | }); 17 | 18 | @override 19 | List get props => 20 | [category, difficulty, question, correctAnswer, answers]; 21 | 22 | factory Question.fromMap(Map map) { 23 | return Question( 24 | category: map['category'] != null ? map['category'].toString() : '', 25 | difficulty: map['difficulty'] != null ? map['difficulty'].toString() : '', 26 | question: map['question'] != null ? map['question'].toString() : '', 27 | correctAnswer: 28 | map['correct_answer'] != null ? map['correct_answer'].toString() : '', 29 | answers: List.from(map['incorrect_answers'] as List) 30 | ..add('${map['correct_answer']}') 31 | ..shuffle(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/models/quiz_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../enums/quiz_status.dart'; 4 | import 'question.dart'; 5 | 6 | class QuizState extends Equatable { 7 | final QuizStatus status; 8 | final String selectedAnswer; 9 | final List correct; // correctly answered questions 10 | final List incorrect; // incorrectly answered questions 11 | 12 | bool get answered => 13 | status == QuizStatus.incorrect || status == QuizStatus.correct; 14 | 15 | const QuizState({ 16 | required this.status, 17 | required this.selectedAnswer, 18 | required this.correct, 19 | required this.incorrect, 20 | }); 21 | 22 | factory QuizState.initial() { 23 | return const QuizState( 24 | status: QuizStatus.initial, 25 | selectedAnswer: '', 26 | correct: [], 27 | incorrect: [], 28 | ); 29 | } 30 | 31 | @override 32 | List get props => [ 33 | status, 34 | correct, 35 | incorrect, 36 | selectedAnswer, 37 | ]; 38 | 39 | QuizState copyWith( 40 | {String? selectedAnswer, 41 | List? correct, 42 | List? incorrect, 43 | QuizStatus? status}) { 44 | return QuizState( 45 | selectedAnswer: selectedAnswer ?? this.selectedAnswer, 46 | correct: correct ?? this.correct, 47 | incorrect: incorrect ?? this.incorrect, 48 | status: status ?? this.status, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/providers/trivia_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import '../enums/difficulty.dart'; 7 | import '../enums/quiz_status.dart'; 8 | import '../models/question.dart'; 9 | import '../models/quiz_state.dart'; 10 | import '../repositories/quiz_repository.dart'; 11 | 12 | final quizNotifierProvider = 13 | StateNotifierProvider.autoDispose( 14 | (ref) => QuizNotifier(QuizState.initial())); 15 | 16 | class QuizNotifier extends StateNotifier { 17 | QuizNotifier(super.state); 18 | 19 | void submitAnswer(Question currentQuestion, String answer) { 20 | if (state.answered) return; 21 | 22 | if (currentQuestion.correctAnswer == answer) { 23 | state = state.copyWith( 24 | selectedAnswer: answer, 25 | correct: [...state.correct, currentQuestion], 26 | status: QuizStatus.correct, 27 | ); 28 | } else { 29 | state = state.copyWith( 30 | selectedAnswer: answer, 31 | incorrect: [...state.incorrect, currentQuestion], 32 | status: QuizStatus.incorrect, 33 | ); 34 | } 35 | } 36 | 37 | void nextQuestion(List questions, int currentIndex) { 38 | state = state.copyWith( 39 | selectedAnswer: '', 40 | status: currentIndex + 1 < questions.length 41 | ? QuizStatus.initial 42 | : QuizStatus.complete, 43 | ); 44 | } 45 | 46 | void reset() { 47 | state = QuizState.initial(); 48 | } 49 | } 50 | 51 | final quizQuiestionsProvider = FutureProvider.autoDispose>( 52 | (ref) => ref.watch(quizRepositoryProvider).getQuestions( 53 | numQuestions: 5, 54 | categoryId: Random().nextInt(24) + 9, 55 | difficulty: Difficulty.any, 56 | ), 57 | ); 58 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/repositories/base_repository.dart: -------------------------------------------------------------------------------- 1 | import '../enums/difficulty.dart'; 2 | import '../models/question.dart'; 3 | 4 | abstract class BaseRepository { 5 | Future> getQuestions({ 6 | required int numQuestions, 7 | required int categoryId, 8 | required Difficulty difficulty, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/features/trivia/data/repositories/quiz_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:enum_to_string/enum_to_string.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | import '../enums/difficulty.dart'; 9 | import '../models/failure.dart'; 10 | import '../models/question.dart'; 11 | import 'base_repository.dart'; 12 | 13 | final dioProvider = Provider((ref) => Dio()); 14 | 15 | final quizRepositoryProvider = 16 | Provider((ref) => QuizRepository(ref.read)); 17 | 18 | class QuizRepository extends BaseRepository { 19 | final _read; 20 | 21 | QuizRepository(this._read); 22 | 23 | @override 24 | Future> getQuestions( 25 | {required int numQuestions, 26 | required int categoryId, 27 | required Difficulty difficulty}) async { 28 | try { 29 | final queryParams = { 30 | 'type': 'multiple', 31 | 'amount': numQuestions, 32 | 'category': categoryId 33 | }; 34 | 35 | if (difficulty != Difficulty.any) { 36 | queryParams 37 | .addAll({'difficulty': EnumToString.convertToString(difficulty)}); 38 | } 39 | 40 | final response = await _read(dioProvider) 41 | .get('https://opentdb.com/api.php', queryParameters: queryParams); 42 | 43 | if (response.statusCode == 200) { 44 | final data = 45 | Map.from(response.data as Map); 46 | final results = List>.from( 47 | data['results'] != null ? data['results'] as List : []); 48 | 49 | if (results.isNotEmpty) { 50 | return results.map((e) => Question.fromMap(e)).toList(); 51 | } 52 | } 53 | 54 | return []; 55 | } on DioError catch (err) { 56 | if (kDebugMode) { 57 | print(err); 58 | } 59 | throw Failure( 60 | message: err.response?.statusMessage ?? 'An error occurred'); 61 | } on SocketException catch (err) { 62 | if (kDebugMode) { 63 | print(err); 64 | } 65 | throw const Failure(message: 'Please check your connection'); 66 | } catch (e) { 67 | if (kDebugMode) { 68 | print(e); 69 | } 70 | throw const Failure(message: 'Formating Error!'); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/answer_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:html_character_entities/html_character_entities.dart'; 4 | 5 | import 'box_shadow.dart'; 6 | 7 | class AnswerCard extends ConsumerWidget { 8 | final String answer; 9 | final bool isSelected; 10 | final bool isCorrect; 11 | final bool isDisplayingAnswer; 12 | final VoidCallback onTap; 13 | 14 | const AnswerCard({ 15 | super.key, 16 | required this.answer, 17 | required this.isSelected, 18 | required this.isCorrect, 19 | required this.isDisplayingAnswer, 20 | required this.onTap, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | return GestureDetector( 26 | onTap: onTap, 27 | child: Container( 28 | margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), 29 | padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), 30 | width: double.infinity, 31 | decoration: BoxDecoration( 32 | color: Colors.white, 33 | boxShadow: boxShadow, 34 | border: Border.all( 35 | color: isDisplayingAnswer 36 | ? isCorrect 37 | ? Colors.green 38 | : isSelected 39 | ? Colors.red 40 | : Colors.white 41 | : Colors.white, 42 | width: 4, 43 | ), 44 | borderRadius: BorderRadius.circular(100), 45 | ), 46 | child: Row( 47 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 48 | children: [ 49 | Flexible( 50 | child: Text( 51 | HtmlCharacterEntities.decode(answer), 52 | style: Theme.of(context).textTheme.headline5!.copyWith( 53 | color: Colors.black, 54 | fontWeight: isDisplayingAnswer && isCorrect 55 | ? FontWeight.bold 56 | : FontWeight.w400, 57 | ), 58 | ), 59 | ), 60 | if (isDisplayingAnswer) 61 | isCorrect 62 | ? const CircularIcon(icon: Icons.check, color: Colors.green) 63 | : isSelected 64 | ? const CircularIcon(icon: Icons.close, color: Colors.red) 65 | : const SizedBox.shrink(), 66 | ], 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | 73 | class CircularIcon extends StatelessWidget { 74 | final IconData icon; 75 | final Color color; 76 | 77 | const CircularIcon({super.key, required this.icon, required this.color}); 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return Container( 82 | height: 24, 83 | width: 24, 84 | decoration: BoxDecoration( 85 | color: color, 86 | shape: BoxShape.circle, 87 | boxShadow: boxShadow, 88 | ), 89 | child: Icon( 90 | icon, 91 | color: Colors.white, 92 | size: 16, 93 | ), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/box_shadow.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final List boxShadow = []; 4 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/confetti_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:confetti/confetti.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ConfettiCard extends StatefulWidget { 5 | const ConfettiCard({super.key}); 6 | 7 | @override 8 | State createState() => _ConfettiCardState(); 9 | } 10 | 11 | class _ConfettiCardState extends State { 12 | late ConfettiController _confettiController; 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | _confettiController = ConfettiController( 18 | duration: const Duration(seconds: 5), 19 | ); 20 | } 21 | 22 | @override 23 | void dispose() { 24 | _confettiController.dispose(); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | _confettiController.play(); 31 | 32 | return Column( 33 | children: [ 34 | // TextButton( 35 | // onPressed: () { 36 | // _confettiController.play(); 37 | // }, 38 | // child: Text('confetti'), 39 | // ), 40 | ConfettiWidget( 41 | confettiController: _confettiController, 42 | blastDirectionality: BlastDirectionality.explosive, 43 | shouldLoop: false, 44 | colors: const [ 45 | Colors.green, 46 | Colors.blue, 47 | Colors.pink, 48 | Colors.orange, 49 | Colors.purple 50 | ], 51 | ), 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/custom_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import 'box_shadow.dart'; 5 | 6 | class CustomButton extends ConsumerWidget { 7 | final String title; 8 | final void Function()? onTap; 9 | 10 | const CustomButton({super.key, required this.title, required this.onTap}); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | return GestureDetector( 15 | onTap: onTap, 16 | child: Container( 17 | margin: const EdgeInsets.all(20), 18 | height: 50, 19 | width: double.infinity, 20 | decoration: BoxDecoration( 21 | color: Colors.yellow[700], 22 | boxShadow: boxShadow, 23 | borderRadius: BorderRadius.circular(25), 24 | ), 25 | alignment: Alignment.center, 26 | child: Text( 27 | title, 28 | style: Theme.of(context).textTheme.headline5!.copyWith( 29 | color: Colors.white, 30 | fontWeight: FontWeight.bold, 31 | ), 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/quiz_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../data/repositories/quiz_repository.dart'; 5 | import 'custom_button.dart'; 6 | 7 | class QuizError extends ConsumerWidget { 8 | final String message; 9 | 10 | const QuizError({super.key, required this.message}); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | return Center( 15 | child: Column( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | crossAxisAlignment: CrossAxisAlignment.center, 18 | children: [ 19 | Text( 20 | message, 21 | style: Theme.of(context).textTheme.headline5!.copyWith( 22 | color: Colors.white, 23 | ), 24 | ), 25 | const SizedBox( 26 | height: 20, 27 | ), 28 | CustomButton( 29 | title: 'Retry', 30 | onTap: () { 31 | ref.refresh(quizRepositoryProvider); 32 | }, 33 | ), 34 | ], 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/quiz_questions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:html_character_entities/html_character_entities.dart'; 4 | 5 | import '../data/models/question.dart'; 6 | import '../data/models/quiz_state.dart'; 7 | import '../data/providers/trivia_provider.dart'; 8 | import 'answer_card.dart'; 9 | 10 | class QuizQuestions extends ConsumerWidget { 11 | final PageController pageController; 12 | final QuizState state; 13 | final List questions; 14 | 15 | const QuizQuestions( 16 | {super.key, 17 | required this.pageController, 18 | required this.state, 19 | required this.questions}); 20 | 21 | @override 22 | Widget build(BuildContext context, WidgetRef ref) { 23 | return PageView.builder( 24 | controller: pageController, 25 | physics: const NeverScrollableScrollPhysics(), 26 | itemCount: questions.length, 27 | itemBuilder: ((context, index) { 28 | final question = questions[index]; 29 | 30 | return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ 31 | Text( 32 | 'Question ${index + 1} of ${questions.length}', 33 | style: Theme.of(context).textTheme.headline5!.copyWith( 34 | color: Colors.white, 35 | fontWeight: FontWeight.bold, 36 | ), 37 | ), 38 | Padding( 39 | padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), 40 | child: Text( 41 | HtmlCharacterEntities.decode(question.question), 42 | style: Theme.of(context).textTheme.headline5!.copyWith( 43 | color: Colors.white, 44 | fontWeight: FontWeight.bold, 45 | ), 46 | ), 47 | ), 48 | Divider( 49 | color: Colors.grey[200], 50 | height: 32, 51 | thickness: 2, 52 | indent: 20, 53 | endIndent: 20, 54 | ), 55 | Column( 56 | children: question.answers 57 | .map( 58 | (e) => AnswerCard( 59 | answer: e, 60 | // ignore: unrelated_type_equality_checks 61 | isSelected: e == state.selectedAnswer, 62 | // ignore: unrelated_type_equality_checks 63 | isCorrect: e == question.correctAnswer, 64 | isDisplayingAnswer: state.answered, 65 | onTap: () { 66 | ref 67 | .read(quizNotifierProvider.notifier) 68 | .submitAnswer(question, e); 69 | }, 70 | ), 71 | ) 72 | .toList(), 73 | ), 74 | ]); 75 | }), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/features/trivia/ui/quiz_results.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../data/models/question.dart'; 5 | import '../data/models/quiz_state.dart'; 6 | import '../data/providers/trivia_provider.dart'; 7 | import '../data/repositories/quiz_repository.dart'; 8 | import 'confetti_card.dart'; 9 | import 'custom_button.dart'; 10 | 11 | class QuizResults extends ConsumerWidget { 12 | final QuizState state; 13 | final List questions; 14 | 15 | const QuizResults({required this.state, required this.questions, super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | return Stack( 20 | children: [ 21 | Align( 22 | alignment: Alignment.center, 23 | child: SizedBox( 24 | width: MediaQuery.of(context).size.width, 25 | height: MediaQuery.of(context).size.height, 26 | child: Center( 27 | child: state.correct.length == questions.length 28 | ? const ConfettiCard() 29 | : Container(), 30 | ), 31 | ), 32 | ), 33 | Column( 34 | mainAxisAlignment: MainAxisAlignment.center, 35 | crossAxisAlignment: CrossAxisAlignment.stretch, 36 | children: [ 37 | Text( 38 | '${state.correct.length} / ${questions.length}', 39 | style: Theme.of(context).textTheme.headline3!.copyWith( 40 | color: Colors.white, 41 | ), 42 | textAlign: TextAlign.center, 43 | ), 44 | Text( 45 | 'CORRECT', 46 | style: Theme.of(context).textTheme.headline3!.copyWith( 47 | color: Colors.white, 48 | ), 49 | textAlign: TextAlign.center, 50 | ), 51 | const SizedBox( 52 | height: 40, 53 | ), 54 | CustomButton( 55 | title: 'New Quiz', 56 | onTap: () { 57 | ref.refresh(quizRepositoryProvider); 58 | ref.read(quizNotifierProvider.notifier).reset(); 59 | }, 60 | ), 61 | ], 62 | ), 63 | ], 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/features/websockets/providers/websockets_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart' show kIsWeb; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:web_socket_channel/io.dart'; 5 | import 'package:web_socket_channel/web_socket_channel.dart'; 6 | 7 | const wsUrl = kIsWeb ? 'ws://127.0.0.1:8080' : 'ws://10.0.2.2:8080'; 8 | 9 | final messagesProvider = StreamProvider.autoDispose((ref) async* { 10 | // open connection 11 | final channel = kIsWeb 12 | ? WebSocketChannel.connect(Uri.parse(wsUrl)) 13 | : IOWebSocketChannel.connect(Uri.parse(wsUrl)); 14 | 15 | // close connectioon when stream is destroyed 16 | ref.onDispose(() => channel.sink.close()); 17 | 18 | channel.sink.add('send_something'); 19 | 20 | // parse value received and emit message instance 21 | await for (final value in channel.stream) { 22 | yield value.toString(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /lib/src/features/websockets/ui/websockets_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../providers/websockets_provider.dart'; 5 | 6 | class WebsocketsScreen extends ConsumerWidget { 7 | const WebsocketsScreen({super.key}); 8 | static const route = '/websockets'; 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final messages = ref.watch(messagesProvider); 13 | 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: const Text('Websockets'), 17 | ), 18 | body: Center( 19 | child: messages.when( 20 | data: (data) { 21 | return Text( 22 | data, 23 | style: Theme.of(context).textTheme.headline5, 24 | ); 25 | }, 26 | error: (error, _) { 27 | return const Text('Probably server not running.'); 28 | }, 29 | loading: () => const CircularProgressIndicator(), 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/utils/async_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | // Bonus: define AsyncValue as a typedef that we can 5 | // reuse across multiple widgets and state notifiers 6 | typedef VoidAsyncValue = AsyncValue; 7 | 8 | extension AsyncValueUI on VoidAsyncValue { 9 | bool get isLoading => this is AsyncLoading; 10 | 11 | void showSnackBarOnError(BuildContext context) => whenOrNull( 12 | error: (error, _) { 13 | ScaffoldMessenger.of(context).showSnackBar( 14 | SnackBar(content: Text(error.toString())), 15 | ); 16 | }, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/utils/contants.dart: -------------------------------------------------------------------------------- 1 | class Constants { 2 | static const appName = 'Riverpod Mobbin Zone!'; 3 | 4 | // storage keys 5 | static const authStorageKey = 'auth'; 6 | static const settingsStorageKey = 'settings'; 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/utils/context_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ContextWarningUI on BuildContext { 4 | void showSnackBar(BuildContext context, 5 | {String message = 'Message', IconData icon = Icons.warning}) => 6 | ScaffoldMessenger.of(context).showSnackBar( 7 | SnackBar( 8 | content: Row( 9 | children: [ 10 | Icon( 11 | icon, 12 | // color: Theme.of(context).snackBarTheme.actionTextColor, 13 | color: Colors.grey.shade100, 14 | ), 15 | const SizedBox( 16 | width: 10, 17 | ), 18 | Text(message), 19 | ], 20 | ), 21 | ), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/utils/list_scroll_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class FetchProvider { 4 | void fetch([int offset]); 5 | } 6 | 7 | mixin ListScrollMixin on State 8 | implements FetchProvider { 9 | static const _scrollDelta = 200.0; 10 | static const _refreshTrigger = -60.0; 11 | ScrollController scrollController = ScrollController(); 12 | late bool canFetch; 13 | late bool canRefresh; 14 | 15 | @override 16 | void initState() { 17 | super.initState(); 18 | canFetch = false; 19 | canRefresh = true; 20 | scrollController.addListener(_handleScroll); 21 | } 22 | 23 | @override 24 | void dispose() { 25 | scrollController.dispose(); 26 | super.dispose(); 27 | } 28 | 29 | void _handleScroll() { 30 | double maxScroll = scrollController.position.maxScrollExtent; 31 | double currentScroll = scrollController.position.pixels; 32 | if (maxScroll - currentScroll <= _scrollDelta && 33 | canFetch && 34 | fetch != null) { 35 | fetch(); 36 | } else if (currentScroll < _refreshTrigger && canRefresh && fetch != null) { 37 | canRefresh = false; 38 | fetch(0); 39 | } else if (currentScroll == 0) { 40 | canRefresh = true; 41 | } 42 | } 43 | } 44 | 45 | // USAGE 46 | // ======================================== 47 | // just add 'with ...' to the class declaration 48 | // class _MyScreenState extends State with ListScrollMixin { 49 | 50 | // remove most of the old code 51 | 52 | // and implement fetch method 53 | // void fetch([int offset]) { 54 | // print('Fetching items, starting at ${offset ?? dummyWinesData.data.length}'); 55 | // ... 56 | // Here can be a direct api call something like api.fetchWines(...) 57 | // or you can dispatch some redux actions --> store.dispatch(FetchWinesAction()) 58 | // etc 59 | 60 | // Also don't forget to lock / canFetch = false; / while loading new data 61 | // to prevent triggering fetch event multiple times 62 | // (you can do it in a reducer if you use redux) 63 | // } 64 | 65 | // @override 66 | // Widget build(BuildContext context) => SomeWidgetWithTheListOfItems() 67 | // } -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 14 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_linux 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import connectivity_plus 9 | import path_provider_macos 10 | import url_launcher_macos 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) 14 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 15 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = learning_flutter_riverpod 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.learningFlutterRiverpod 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:learning_flutter_riverpod/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | learning_flutter_riverpod 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning_flutter_riverpod", 3 | "short_name": "learning_flutter_riverpod", 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 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /web/splash/img/light-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/web/splash/img/light-background.png -------------------------------------------------------------------------------- /web/splash/splash.js: -------------------------------------------------------------------------------- 1 | function removeSplashFromWeb() { 2 | document.getElementById("splash")?.remove(); 3 | document.getElementById("splash-branding")?.remove(); 4 | document.body.style.background = "transparent"; 5 | } 6 | -------------------------------------------------------------------------------- /web/splash/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | height:100%; 4 | background: #000000; 5 | background-image: url("img/light-background.png"); 6 | background-size: 100% 100%; 7 | } 8 | 9 | .center { 10 | margin: 0; 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | -ms-transform: translate(-50%, -50%); 15 | transform: translate(-50%, -50%); 16 | } 17 | 18 | .contain { 19 | display:block; 20 | width:100%; height:100%; 21 | object-fit: contain; 22 | } 23 | 24 | .stretch { 25 | display:block; 26 | width:100%; height:100%; 27 | } 28 | 29 | .cover { 30 | display:block; 31 | width:100%; height:100%; 32 | object-fit: cover; 33 | } 34 | 35 | .bottom { 36 | position: absolute; 37 | bottom: 0; 38 | left: 50%; 39 | -ms-transform: translate(-50%, 0); 40 | transform: translate(-50%, 0); 41 | } 42 | 43 | .bottomLeft { 44 | position: absolute; 45 | bottom: 0; 46 | left: 0; 47 | } 48 | 49 | .bottomRight { 50 | position: absolute; 51 | bottom: 0; 52 | right: 0; 53 | } 54 | 55 | @media (prefers-color-scheme: dark) { 56 | body { 57 | margin:0; 58 | height:100%; 59 | background: #000000; 60 | background-image: url("img/dark-background.png"); 61 | background-size: 100% 100%; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | 12 | void RegisterPlugins(flutter::PluginRegistry* registry) { 13 | ConnectivityPlusWindowsPluginRegisterWithRegistrar( 14 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); 15 | UrlLauncherWindowsRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 17 | } 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | connectivity_plus 7 | url_launcher_windows 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 20 | endforeach(plugin) 21 | 22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "learning_flutter_riverpod" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "learning_flutter_riverpod" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "learning_flutter_riverpod.exe" "\0" 98 | VALUE "ProductName", "learning_flutter_riverpod" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"learning_flutter_riverpod", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxlexx/learning-flutter-riverpod/45180906735bcf0027abd0b15e2998fe114b5c3f/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------