diff --git a/examples/now_in_dart_flutter/.gitignore b/examples/now_in_dart_flutter/.gitignore new file mode 100644 index 00000000..4be7d820 --- /dev/null +++ b/examples/now_in_dart_flutter/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Isar specific +libisar.so diff --git a/examples/now_in_dart_flutter/LICENSE b/examples/now_in_dart_flutter/LICENSE new file mode 100644 index 00000000..64a93f2d --- /dev/null +++ b/examples/now_in_dart_flutter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Biplab Dutta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/now_in_dart_flutter/README.md b/examples/now_in_dart_flutter/README.md new file mode 100644 index 00000000..1b923a9c --- /dev/null +++ b/examples/now_in_dart_flutter/README.md @@ -0,0 +1,73 @@ +# now_in_dart_flutter + +This app serves the purpose of staying up-to-date with the latest changes across Dart and Flutter ecosystem. The way this app works is by fetching markdowns from Dart and Flutter's official Github repo via REST API and rendering the information in a `WebView` widget. + +## Features + +- Display Dart CHANGELOG +- Display Flutter What's New +- Display Flutter Release Notes +- Offline Support + +## Application Demo + +
+ + + + + +
+ + + +
+
+ +## Architecture + +The app follows a simple but effective architecture. It relies on a feature-driven architecture with some sub-features. + +Inside [lib/features/](./lib/features/) directory, you can find two sub-features: `detail` and `home`. + +The [home](./lib/features/home/) sub-feature is responsible for showing a scaffold with a bottom navigation bar. It also contains logic for maintaining bottom navigation bar's state. + +The [detail](./lib/features/detail/) feature is divided into two sub-features: [dart_detail](./lib/features/detail/dart_detail/) and [flutter_detail](./lib/features/detail/flutter_detail/). Each of these sub-features has similar structure and is divided into three layers. + +- **Application Layer**: Contains state management logic and acts as a mediator between the presentation and the data layer. +- **Data Layer**: Responsible for making all the necessary API calls and local cache operations. +- **Presentation Layer**: Associated with the UI + +To check the `fpdart` implementation, consider taking a look at the `data` layer of each of the sub-features. + +## State Management +The project uses [flutter_bloc](https://pub.dev/packages/flutter_bloc) for managing the app's state. + +## Storage +The app stores fetched data locally on user's device for offline support. The project uses the latest [isar](https://pub.dev/packages/isar) plugin for local storage. + +## Test +The unit test has been written based on the `fpdart` refactoring and can be found in the [test](./test/) directory. +- [detail_local_service_test.dart](./test/features/detail/core/data/detail_local_service_test.dart) +- [detail_remote_service_test.dart](./test/features/detail/core/data/detail_remote_service_test.dart) + +Dependencies +The project makes use of few third-party packages for rapid development. Some of them are listed below: +- [dio](https://pub.dev/packages/dio) (To perform network calls) +- [equatable](https://pub.dev/packages/equatable) (To achieve value equality) +- [flash](https://pub.dev/packages/flash) (To display customizable toast) +- [flutter_bloc](https://pub.dev/packages/flutter_bloc) (State Management) +- [fpdart](https://pub.dev/packages/fpdart) (Functional Programming) +- [isar](https://pub.dev/packages/isar) (Local Storage) +- [url_launcher](https://pub.dev/packages/url_launcher) (To display information from hyperlinks in a browser interface within the app) +- [webview_flutter](https://pub.dev/packages/webview_flutter) (To display markdowns) +- [mocktail](https://pub.dev/packages/mocktail) (As a mocking library) + +## Types used from `fpdart` +- `TaskEither`: Used instead of `Future` to make async request that may fail +- `IOEither`: Used to represent a synchronous computation that may fail +- `Do` Notation: Used to write functional code that looks like normal imperative code and to avoid methods chaining \ No newline at end of file diff --git a/examples/now_in_dart_flutter/analysis_options.yaml b/examples/now_in_dart_flutter/analysis_options.yaml new file mode 100644 index 00000000..1aa2aeb9 --- /dev/null +++ b/examples/now_in_dart_flutter/analysis_options.yaml @@ -0,0 +1,14 @@ +include: package:very_good_analysis/analysis_options.yaml + +linter: + rules: + public_member_api_docs: false + avoid_private_typedef_functions: false + library_private_types_in_public_api: false + sort_pub_dependencies: false + avoid_multiple_declarations_per_line: false + +analyzer: + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' diff --git a/examples/now_in_dart_flutter/android/.gitignore b/examples/now_in_dart_flutter/android/.gitignore new file mode 100644 index 00000000..6f568019 --- /dev/null +++ b/examples/now_in_dart_flutter/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/examples/now_in_dart_flutter/android/app/build.gradle b/examples/now_in_dart_flutter/android/app/build.gradle new file mode 100644 index 00000000..dda07d6f --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 33 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.now_in_dart_flutter" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 19 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/examples/now_in_dart_flutter/android/app/src/debug/AndroidManifest.xml b/examples/now_in_dart_flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..13fc6968 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/examples/now_in_dart_flutter/android/app/src/main/AndroidManifest.xml b/examples/now_in_dart_flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..99a0572a --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/now_in_dart_flutter/android/app/src/main/kotlin/com/example/now_in_dart_flutter/MainActivity.kt b/examples/now_in_dart_flutter/android/app/src/main/kotlin/com/example/now_in_dart_flutter/MainActivity.kt new file mode 100644 index 00000000..e27d2369 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/kotlin/com/example/now_in_dart_flutter/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.now_in_dart_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/examples/now_in_dart_flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/drawable/launch_background.xml b/examples/now_in_dart_flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/examples/now_in_dart_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/values-night/styles.xml b/examples/now_in_dart_flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/now_in_dart_flutter/android/app/src/main/res/values/styles.xml b/examples/now_in_dart_flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/now_in_dart_flutter/android/app/src/profile/AndroidManifest.xml b/examples/now_in_dart_flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..13fc6968 --- /dev/null +++ b/examples/now_in_dart_flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/examples/now_in_dart_flutter/android/build.gradle b/examples/now_in_dart_flutter/android/build.gradle new file mode 100644 index 00000000..e394c2eb --- /dev/null +++ b/examples/now_in_dart_flutter/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.8.22' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/examples/now_in_dart_flutter/android/gradle.properties b/examples/now_in_dart_flutter/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/examples/now_in_dart_flutter/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/examples/now_in_dart_flutter/android/gradle/wrapper/gradle-wrapper.properties b/examples/now_in_dart_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..cb24abda --- /dev/null +++ b/examples/now_in_dart_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/examples/now_in_dart_flutter/android/settings.gradle b/examples/now_in_dart_flutter/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/examples/now_in_dart_flutter/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/examples/now_in_dart_flutter/assets/icon_dart.svg.vec b/examples/now_in_dart_flutter/assets/icon_dart.svg.vec new file mode 100644 index 00000000..3f06a445 Binary files /dev/null and b/examples/now_in_dart_flutter/assets/icon_dart.svg.vec differ diff --git a/examples/now_in_dart_flutter/assets/icon_flutter.svg.vec b/examples/now_in_dart_flutter/assets/icon_flutter.svg.vec new file mode 100644 index 00000000..c0ebafea Binary files /dev/null and b/examples/now_in_dart_flutter/assets/icon_flutter.svg.vec differ diff --git a/examples/now_in_dart_flutter/ios/.gitignore b/examples/now_in_dart_flutter/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/examples/now_in_dart_flutter/ios/Flutter/AppFrameworkInfo.plist b/examples/now_in_dart_flutter/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..9625e105 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/examples/now_in_dart_flutter/ios/Flutter/Debug.xcconfig b/examples/now_in_dart_flutter/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/examples/now_in_dart_flutter/ios/Flutter/Release.xcconfig b/examples/now_in_dart_flutter/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/examples/now_in_dart_flutter/ios/Podfile b/examples/now_in_dart_flutter/ios/Podfile new file mode 100644 index 00000000..88359b22 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/examples/now_in_dart_flutter/ios/Podfile.lock b/examples/now_in_dart_flutter/ios/Podfile.lock new file mode 100644 index 00000000..3d0a47dc --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - Flutter (1.0.0) + - isar_flutter_libs (1.0.0): + - Flutter + - url_launcher_ios (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + isar_flutter_libs: + :path: ".symlinks/plugins/isar_flutter_libs/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + isar_flutter_libs: bfb66f35a1fa9db9ec96b93539a03329ce147738 + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.11.3 diff --git a/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.pbxproj b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a28ac905 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,549 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8CF41CD242281DA4B22F6178 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1538B71A50E98E1AAA59DA4E /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 061D9865A4F0F2A2FEAE1D5A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1538B71A50E98E1AAA59DA4E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 93E0263D59E1A3A8EBE471F6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BA5EF43BCF85EA570255BAD4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8CF41CD242281DA4B22F6178 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4C9024B4174EB08FFC557A57 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1538B71A50E98E1AAA59DA4E /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 784D6B34FE9CF29AD488A34D /* Pods */ = { + isa = PBXGroup; + children = ( + 061D9865A4F0F2A2FEAE1D5A /* Pods-Runner.debug.xcconfig */, + 93E0263D59E1A3A8EBE471F6 /* Pods-Runner.release.xcconfig */, + BA5EF43BCF85EA570255BAD4 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 784D6B34FE9CF29AD488A34D /* Pods */, + 4C9024B4174EB08FFC557A57 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 6972F1D59FBB2DB0C8839758 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 28C32D7B14E8C18C921C5CC3 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 28C32D7B14E8C18C921C5CC3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 6972F1D59FBB2DB0C8839758 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.nowInDartFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.nowInDartFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.nowInDartFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..c87d15a3 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/now_in_dart_flutter/ios/Runner/AppDelegate.swift b/examples/now_in_dart_flutter/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/examples/now_in_dart_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/now_in_dart_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/now_in_dart_flutter/ios/Runner/Base.lproj/Main.storyboard b/examples/now_in_dart_flutter/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/now_in_dart_flutter/ios/Runner/Info.plist b/examples/now_in_dart_flutter/ios/Runner/Info.plist new file mode 100644 index 00000000..a239fa00 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Now In Dart Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + now_in_dart_flutter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/examples/now_in_dart_flutter/ios/Runner/Runner-Bridging-Header.h b/examples/now_in_dart_flutter/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/examples/now_in_dart_flutter/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/examples/now_in_dart_flutter/lib/app/app.dart b/examples/now_in_dart_flutter/lib/app/app.dart new file mode 100644 index 00000000..d50de291 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/app/app.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_repository.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_repository.dart'; +import 'package:now_in_dart_flutter/features/home/home.dart'; + +class App extends StatelessWidget { + const App({ + required DartDetailRepository dartDetailRepository, + required FlutterDetailRepository flutterDetailRepository, + super.key, + }) : _dartDetailRepository = dartDetailRepository, + _flutterDetailRepository = flutterDetailRepository; + + final DartDetailRepository _dartDetailRepository; + final FlutterDetailRepository _flutterDetailRepository; + + @override + Widget build(BuildContext context) { + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: _dartDetailRepository), + RepositoryProvider.value(value: _flutterDetailRepository), + ], + child: MaterialApp( + darkTheme: ThemeData.dark(), + home: const HomePage(), + ), + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/app/app_bloc_observer.dart b/examples/now_in_dart_flutter/lib/app/app_bloc_observer.dart new file mode 100644 index 00000000..15b4508b --- /dev/null +++ b/examples/now_in_dart_flutter/lib/app/app_bloc_observer.dart @@ -0,0 +1,17 @@ +import 'dart:developer'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AppBlocObserver extends BlocObserver { + @override + void onChange(BlocBase bloc, Change change) { + super.onChange(bloc, change); + log('onChange(${bloc.runtimeType}, change)'); + } + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + log('onError(${bloc.runtimeType}, error, stackTrace)'); + super.onError(bloc, error, stackTrace); + } +} diff --git a/examples/now_in_dart_flutter/lib/bootstrap.dart b/examples/now_in_dart_flutter/lib/bootstrap.dart new file mode 100644 index 00000000..c6bcbf42 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/bootstrap.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'dart:developer' as dev show log; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:now_in_dart_flutter/app/app_bloc_observer.dart'; +import 'package:now_in_dart_flutter/core/data/data.dart'; + +typedef _BootstrapBuilder = Widget Function(Dio dio); + +void bootstrap(_BootstrapBuilder builder) { + Bloc.observer = AppBlocObserver(); + + FlutterError.onError = (details) { + dev.log( + details.exceptionAsString(), + stackTrace: details.stack, + ); + }; + + runZonedGuarded( + () async { + WidgetsFlutterBinding.ensureInitialized(); + await IsarDatabase().init(); + final dio = Dio() + ..options = BaseOptions( + baseUrl: 'https://api.github.com/', + headers: {'Accept': 'application/vnd.github.html+json'}, + responseType: ResponseType.plain, + validateStatus: (status) { + return status != null && status >= 200 && status < 400; + }, + ); + + runApp(builder(dio)); + }, + (error, stackTrace) => dev.log( + error.toString(), + stackTrace: stackTrace, + ), + ); +} diff --git a/examples/now_in_dart_flutter/lib/core/data/data.dart b/examples/now_in_dart_flutter/lib/core/data/data.dart new file mode 100644 index 00000000..b9d4d1ac --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/data.dart @@ -0,0 +1,6 @@ +export 'dio_extension.dart'; +export 'github_header.dart'; +export 'github_header_cache.dart'; +export 'isar_database.dart'; +export 'remote_response.dart'; +export 'utils.dart'; diff --git a/examples/now_in_dart_flutter/lib/core/data/dio_extension.dart b/examples/now_in_dart_flutter/lib/core/data/dio_extension.dart new file mode 100644 index 00000000..0aa8779f --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/dio_extension.dart @@ -0,0 +1,7 @@ +import 'package:dio/dio.dart'; + +extension DioExceptionExtension on DioException { + bool get isNoConnectionError { + return type == DioExceptionType.connectionError; + } +} diff --git a/examples/now_in_dart_flutter/lib/core/data/github_header.dart b/examples/now_in_dart_flutter/lib/core/data/github_header.dart new file mode 100644 index 00000000..66143ddc --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/github_header.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:isar/isar.dart'; + +part 'github_header.g.dart'; + +@Collection(inheritance: false) +class GithubHeader extends Equatable { + const GithubHeader({ + required this.id, + required this.eTag, + required this.path, + }); + + factory GithubHeader.parse(int id, Response response, String path) { + return GithubHeader( + id: id, + eTag: response.headers.map['ETag']![0], + path: path, + ); + } + + final String eTag; + + // We are only making `path` a property of this class because we want to make + // a query using path value. If Isar supports key-value storage mechanism too + // in the future, then the `path` property can be removed from this file. + + @Index(unique: true) + final String path; + + final Id id; + + @ignore + @override + List get props => [id, eTag, path]; +} diff --git a/examples/now_in_dart_flutter/lib/core/data/github_header_cache.dart b/examples/now_in_dart_flutter/lib/core/data/github_header_cache.dart new file mode 100644 index 00000000..ef85cf29 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/github_header_cache.dart @@ -0,0 +1,38 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:isar/isar.dart'; +import 'package:now_in_dart_flutter/core/data/github_header.dart'; +import 'package:now_in_dart_flutter/core/data/isar_database.dart'; + +abstract class HeaderCache { + Task saveHeader(GithubHeader header); + Task getHeader(String path); +} + +class GithubHeaderCache implements HeaderCache { + GithubHeaderCache({ + IsarDatabase? isarDb, + }) : _isarDb = isarDb ?? IsarDatabase(); + + final IsarDatabase _isarDb; + + Isar get _isar => _isarDb.instance; + + IsarCollection get _githubHeaders => _isar.githubHeaders; + + @override + Task saveHeader(GithubHeader header) { + final txn = _isar.writeTxn( + () async { + await _githubHeaders.put(header); + return unit; + }, + silent: true, + ); + return Task(() => txn); + } + + @override + Task getHeader(String path) { + return Task(() => _githubHeaders.getByPath(path)); + } +} diff --git a/examples/now_in_dart_flutter/lib/core/data/id.dart b/examples/now_in_dart_flutter/lib/core/data/id.dart new file mode 100644 index 00000000..2eea5d48 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/id.dart @@ -0,0 +1,5 @@ +abstract class EntityId { + static const dartChangelogDetail = 1; + static const flutterWhatsNewDetail = 2; + static const flutterReleaseNotesDetail = 3; +} diff --git a/examples/now_in_dart_flutter/lib/core/data/isar_database.dart b/examples/now_in_dart_flutter/lib/core/data/isar_database.dart new file mode 100644 index 00000000..4ba5ae57 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/isar_database.dart @@ -0,0 +1,27 @@ +import 'package:isar/isar.dart'; +import 'package:now_in_dart_flutter/core/data/github_header.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:path_provider/path_provider.dart'; + +class IsarDatabase { + factory IsarDatabase() => _isarDatabase; + IsarDatabase._internal(); + + static final _isarDatabase = IsarDatabase._internal(); + + late Isar _instance; + + Isar get instance => _instance; + + /// Initializes the isar database. + /// + /// This method needs to be called before accessing any isar-specific APIs. + Future init() async { + final dir = await getApplicationDocumentsDirectory(); + if (Isar.instanceNames.isNotEmpty) return; + _instance = await Isar.open( + [GithubHeaderSchema, DetailDTOSchema], + directory: dir.path, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/core/data/remote_response.dart b/examples/now_in_dart_flutter/lib/core/data/remote_response.dart new file mode 100644 index 00000000..745fad63 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/remote_response.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +sealed class RemoteResponse extends Equatable { + const RemoteResponse(); + + @override + List get props => []; +} + +class NoConnectionRemoteResponse extends RemoteResponse { + const NoConnectionRemoteResponse(); +} + +class UnModifiedRemoteResponse extends RemoteResponse { + const UnModifiedRemoteResponse(); +} + +class ModifiedRemoteResponse extends RemoteResponse { + const ModifiedRemoteResponse(this.data); + + final T data; + + @override + List get props => [data]; +} + +extension RemoteResponseExt on RemoteResponse { + A when({ + required A Function() noConnection, + required A Function() unmodified, + required A Function(T data) modified, + }) { + return switch (this) { + NoConnectionRemoteResponse() => noConnection(), + UnModifiedRemoteResponse() => unmodified(), + ModifiedRemoteResponse(:final data) => modified(data), + }; + } +} diff --git a/examples/now_in_dart_flutter/lib/core/data/utils.dart b/examples/now_in_dart_flutter/lib/core/data/utils.dart new file mode 100644 index 00000000..57627e26 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/data/utils.dart @@ -0,0 +1,13 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; + +IOEither uriParser(String uri) { + return IOEither.tryCatch( + () => Uri.parse(uri), + (e, stackTrace) => UriParserFailure( + 'Invalid Uri string', + errorObject: e, + stackTrace: stackTrace, + ), + ); +} diff --git a/examples/now_in_dart_flutter/lib/core/domain/failure.dart b/examples/now_in_dart_flutter/lib/core/domain/failure.dart new file mode 100644 index 00000000..ba47e75f --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/domain/failure.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; + +sealed class Failure extends Equatable { + const Failure(this.message, {this.code, this.stackTrace, this.errorObject}); + + final String message; + final int? code; + final Object? errorObject; + final StackTrace? stackTrace; +} + +class ApiFailure extends Failure { + const ApiFailure( + super.message, { + super.code, + super.stackTrace, + super.errorObject, + }); + + @override + List get props => [message, code, stackTrace, errorObject]; +} + +class UriParserFailure extends Failure { + const UriParserFailure(super.message, {super.errorObject, super.stackTrace}); + + @override + List get props => [message, errorObject, stackTrace]; +} diff --git a/examples/now_in_dart_flutter/lib/core/domain/fresh.dart b/examples/now_in_dart_flutter/lib/core/domain/fresh.dart new file mode 100644 index 00000000..5053890e --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/domain/fresh.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; + +class Fresh extends Equatable { + const Fresh._({this.entity, this.isFresh}); + + /// Factory for [WhenFresh] + const factory Fresh.yes({ + required T entity, + }) = WhenFresh._; + + /// Factory for [WhenNotFresh] + const factory Fresh.no({ + required T entity, + }) = WhenNotFresh._; + + /// Entity whose freshness is to be checked. + final T? entity; + + /// Determines if the entity is fresh or not. + final bool? isFresh; + + @override + List get props => [entity, isFresh]; +} + +/// Represents that the entity is fresh. +class WhenFresh extends Fresh { + const WhenFresh._({ + required T super.entity, + }) : super._(isFresh: true); + + @override + String toString() { + return 'WhenFresh(entity: $entity, isFresh: true)'; + } +} + +/// Represents that the entity is not fresh. +class WhenNotFresh extends Fresh { + const WhenNotFresh._({ + required T super.entity, + }) : super._(isFresh: false); + + @override + String toString() { + return 'WhenNotFresh(entity: $entity, isFresh: false)'; + } +} diff --git a/examples/now_in_dart_flutter/lib/core/presentation/assets_path.dart b/examples/now_in_dart_flutter/lib/core/presentation/assets_path.dart new file mode 100644 index 00000000..40465e61 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/presentation/assets_path.dart @@ -0,0 +1,7 @@ +abstract class AssetsPath { + /// assets/icon_dart.svg.vec + static const dartIcon = 'assets/icon_dart.svg.vec'; + + /// assets/icon_flutter.svg.vec + static const flutterIcon = 'assets/icon_flutter.svg.vec'; +} diff --git a/examples/now_in_dart_flutter/lib/core/presentation/lazy_indexed_stack.dart b/examples/now_in_dart_flutter/lib/core/presentation/lazy_indexed_stack.dart new file mode 100644 index 00000000..315c867c --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/presentation/lazy_indexed_stack.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +/// IndexedStack but lazy. +/// +/// Source code credit: [marcossevilla](https://github.com/marcossevilla/lazy_indexed_stack/blob/main/lib/src/flutter_lazy_indexed_stack.dart) +class LazyIndexedStack extends StatefulWidget { + const LazyIndexedStack({ + super.key, + this.index = 0, + this.children = const [], + }); + + final int index; + + final List children; + + @override + State createState() => _LazyIndexedStackState(); +} + +class _LazyIndexedStackState extends State { + late final List _activatedChildren; + + @override + void initState() { + super.initState(); + _activatedChildren = List.generate( + widget.children.length, + (i) => i == widget.index, + ); + } + + @override + void didUpdateWidget(LazyIndexedStack oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.index != widget.index) _activateChild(widget.index); + } + + void _activateChild(int? index) { + if (index == null) return; + + if (!_activatedChildren[index]) _activatedChildren[index] = true; + } + + List get children { + return List.generate( + widget.children.length, + (i) { + return _activatedChildren[i] + ? widget.children[i] + : const SizedBox.shrink(); + }, + ); + } + + @override + Widget build(BuildContext context) { + return IndexedStack( + index: widget.index, + children: children, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/core/presentation/no_connection_toast.dart b/examples/now_in_dart_flutter/lib/core/presentation/no_connection_toast.dart new file mode 100644 index 00000000..56cba06a --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/presentation/no_connection_toast.dart @@ -0,0 +1,30 @@ +import 'package:flash/flash.dart'; +import 'package:flutter/material.dart'; + +Future showNoConnectionToast( + String message, + BuildContext context, +) async { + await showFlash( + context: context, + duration: const Duration(seconds: 2), + builder: (context, controller) { + return FlashBar( + controller: controller, + backgroundColor: Colors.black.withOpacity(0.7), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + margin: const EdgeInsets.all(8), + content: Padding( + padding: const EdgeInsets.all(8), + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + ); + }, + ); +} diff --git a/examples/now_in_dart_flutter/lib/core/presentation/no_results_display.dart b/examples/now_in_dart_flutter/lib/core/presentation/no_results_display.dart new file mode 100644 index 00000000..342ee301 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/presentation/no_results_display.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class NoResultsDisplay extends StatelessWidget { + const NoResultsDisplay({ + required this.message, + super.key, + }); + + final String message; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.hourglass_empty, + size: 96, + ), + Text( + message, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/core/presentation/responsive.dart b/examples/now_in_dart_flutter/lib/core/presentation/responsive.dart new file mode 100644 index 00000000..31ea69c7 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/core/presentation/responsive.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class Responsive extends StatelessWidget { + const Responsive({ + required this.mobile, + required this.tabletOrDesktop, + super.key, + }); + + final Widget mobile, tabletOrDesktop; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 650) { + return mobile; + } else { + return tabletOrDesktop; + } + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/data/constants.dart b/examples/now_in_dart_flutter/lib/features/detail/core/data/constants.dart new file mode 100644 index 00000000..fde2fc7b --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/data/constants.dart @@ -0,0 +1,10 @@ +/// repos/flutter/website/contents/src/release/whats-new.md +const flutterWhatsNewPath = + 'repos/flutter/website/contents/src/release/whats-new.md'; + +/// repos/flutter/website/contents/src/release/release-notes/index.md +const flutterReleaseNotesPath = + 'repos/flutter/website/contents/src/release/release-notes/index.md'; + +/// repos/dart-lang/sdk/contents/CHANGELOG.md +const dartChangelogPath = 'repos/dart-lang/sdk/contents/CHANGELOG.md'; diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_dto.dart b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_dto.dart new file mode 100644 index 00000000..9bde4ebe --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_dto.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; +import 'package:isar/isar.dart'; +import 'package:now_in_dart_flutter/features/detail/core/domain/detail.dart'; + +part 'detail_dto.g.dart'; + +@Collection(inheritance: false) +class DetailDTO extends Equatable { + const DetailDTO({required this.id, required this.html}); + + /// The parser that parses the received html data. + /// + /// The markdowns in the flutter's github repo has some data in the format + /// `%7B%7Bsite.url%7D%7D` which actually is `{{site.url}}`. But WebView will + /// not be able to take us to relevant web page if `%7B%7Bsite.url%7D%7D` + /// isn't parsed. So, we need to convert `%7B%7Bsite.url%7D%7D` to + /// `https://docs.flutter.dev`. + /// + /// The mappings will have to be done in accordance to [_mappings]. + factory DetailDTO.parseHtml(int id, String html) { + final parsedHtml = _mappings.entries.fold( + html, + (str, map) => str.replaceAll(map.key, map.value), + ); + return DetailDTO(id: id, html: parsedHtml); + } + + final Id id; + final String html; + + Detail toDomain() => Detail(html: html); + + static const _mappings = { + '%7B%7Bsite.url%7D%7D': 'https://docs.flutter.dev', + '%7B%7Bsite.medium%7D%7D': 'https://medium.com', + '%7B%7Bsite.github%7D%7D': 'https://github.com', + '%7B%7Bsite.groups%7D%7D': 'https://groups.google.com', + '%7B%7Bsite.dart-site%7D%7D': 'https://dart.dev', + '%7B%7Bsite.main-url%7D%7D': 'https://flutter.dev', + '%7B%7Bsite.codelabs%7D%7D': 'https://codelabs.developers.google.com', + '%7B%7Bsite.youtube-site%7D%7D': 'https://youtube.com', + '%7B%7Bsite.flutter-medium%7D%7D': 'https://medium.com/flutter', + '%7B%7Bsite.repo.this%7D%7D': 'https://github.com/flutter/website', + '%7B%7Bsite.firebase%7D%7D': 'https://firebase.google.com', + '%7B%7Bsite.google-blog%7D%7D': 'https://developers.googleblog.com', + '%7B%7Bsite.pub%7D%7D': 'https://pub.dev', + '%7B%7Bsite.api%7D%7D': 'https://api.flutter.dev', + '%7B%7Bsite.repo.flutter%7D%7D': 'https://github.com/flutter/flutter', + }; + + @ignore + @override + List get props => [id, html]; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_local_service.dart b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_local_service.dart new file mode 100644 index 00000000..04794e3b --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_local_service.dart @@ -0,0 +1,37 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; +import 'package:now_in_dart_flutter/core/data/data.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; + +abstract class DetailLocalService { + DetailLocalService({ + IsarDatabase? isarDb, + }) : _isarDb = isarDb ?? IsarDatabase(); + + final IsarDatabase _isarDb; + + Isar get _isar => _isarDb.instance; + + @protected + @visibleForTesting + Task upsertDetail(DetailDTO detailDTO) { + return Task( + () { + return _isar.writeTxn( + () async { + await _isar.detailDTOs.put(detailDTO); + return unit; + }, + silent: true, + ); + }, + ); + } + + @protected + @visibleForTesting + Task getDetail(int id) { + return Task(() => _isar.detailDTOs.get(id)); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_remote_service.dart b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_remote_service.dart new file mode 100644 index 00000000..cb665932 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/data/detail_remote_service.dart @@ -0,0 +1,86 @@ +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:meta/meta.dart'; +import 'package:now_in_dart_flutter/core/data/data.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; + +typedef _FailureOrRemoteResponse = TaskEither>; + +abstract class DetailRemoteService { + DetailRemoteService({ + Dio? dio, + HeaderCache? headerCache, + }) : _dio = dio ?? Dio(), + _headerCache = headerCache ?? GithubHeaderCache(); + + final Dio _dio; + final HeaderCache _headerCache; + + @protected + @visibleForTesting + _FailureOrRemoteResponse getDetail( + int id, + String fullPathToMarkdownFile, + ) { + return TaskEither.Do( + (_) async { + final requestUri = await _( + uriParser(fullPathToMarkdownFile).toTaskEither(), + ); + + final cachedHeader = await _( + _headerCache.getHeader(fullPathToMarkdownFile).toTaskEither(), + ); + + return _( + TaskEither>.tryCatch( + () => _dio.getUri( + requestUri, + options: Options( + headers: { + 'If-None-Match': cachedHeader?.eTag ?? '', + }, + ), + ), + (e, stackTrace) { + return ApiFailure( + 'Error on network request', + errorObject: e, + stackTrace: stackTrace, + ); + }, + ).flatMap( + (response) { + return TaskEither>( + () async { + if (response.statusCode == 200) { + final header = GithubHeader.parse( + id, + response, + fullPathToMarkdownFile, + ); + + await _(_headerCache.saveHeader(header).toTaskEither()); + + final html = response.data ?? ''; + return right(ModifiedRemoteResponse(html)); + } + + return right(const UnModifiedRemoteResponse()); + }, + ); + }, + ).orElse( + (failure) { + final error = failure.errorObject; + if (error is DioException && error.isNoConnectionError) { + return TaskEither.right(const NoConnectionRemoteResponse()); + } + return TaskEither.left(failure); + }, + ), + ); + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/domain/detail.dart b/examples/now_in_dart_flutter/lib/features/detail/core/domain/detail.dart new file mode 100644 index 00000000..8c42391c --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/domain/detail.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +class Detail extends Equatable { + const Detail({required this.html}); + + final String html; + + /// An empty detail used to represent null detail. + + // This pattern helps us to work with concrete domain level entities and + // avoid nulls. + static const empty = Detail(html: ''); + + /// Convenience getter to determine whether the current detail is empty. + bool get isEmpty => this == Detail.empty; + + @override + List get props => [html]; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/core/presentation/widget/detail_webview.dart b/examples/now_in_dart_flutter/lib/features/detail/core/presentation/widget/detail_webview.dart new file mode 100644 index 00000000..e2550542 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/core/presentation/widget/detail_webview.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class DetailWebView extends StatelessWidget { + const DetailWebView({ + required String html, + super.key, + }) : _html = html; + + final String _html; + + @override + Widget build(BuildContext context) { + final url = Uri.dataFromString( + ''' + + + + + $_html + + $_css + ''', + mimeType: 'text/html', + encoding: utf8, + ).toString(); + return WebViewWidget( + controller: WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (navReq) { + if (navReq.url == url) { + return NavigationDecision.navigate; + } + launchUrl(Uri.parse(navReq.url)); + return NavigationDecision.prevent; + }, + ), + ) + ..loadRequest(Uri.parse(url)) + ..enableZoom(false), + gestureRecognizers: const { + Factory( + VerticalDragGestureRecognizer.new, + ), + }, + ); + } + + static const _css = """ + + """; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_bloc.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_bloc.dart new file mode 100644 index 00000000..813ba3bf --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_bloc.dart @@ -0,0 +1,63 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/core/domain/fresh.dart'; +import 'package:now_in_dart_flutter/features/detail/core/domain/detail.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_repository.dart'; + +part 'dart_detail_event.dart'; +part 'dart_detail_state.dart'; + +class DartDetailBloc extends Bloc { + DartDetailBloc({ + required DartDetailRepository repository, + }) : _repository = repository, + super(const DartDetailState()) { + on( + (event, emit) async { + await event.when( + changelogDetailRequested: (id) { + return _onDartChangelogDetailRequested(emit, id); + }, + ); + }, + ); + } + + final DartDetailRepository _repository; + + Future _onDartChangelogDetailRequested( + Emitter emit, + int id, + ) async { + emit(state.copyWith(status: () => DartDetailStatus.loading)); + final failureOrSuccessDetail = await _repository.getDartDetail(id).run(); + return failureOrSuccessDetail.match( + (failure) { + emit( + state.copyWith( + status: () => DartDetailStatus.failure, + failureMessage: () { + return switch (failure) { + ApiFailure() => failure.message, + UriParserFailure() => failure.message, + }; + }, + ), + ); + return unit; + }, + (detail) { + emit( + state.copyWith( + status: () => DartDetailStatus.success, + detail: () => detail, + failureMessage: () => null, + ), + ); + return unit; + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_event.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_event.dart new file mode 100644 index 00000000..1f490f3b --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_event.dart @@ -0,0 +1,19 @@ +part of 'dart_detail_bloc.dart'; + +sealed class DartDetailEvent { + const DartDetailEvent(); +} + +class DartChangelogDetailRequested extends DartDetailEvent { + const DartChangelogDetailRequested(this.id); + + final int id; +} + +extension DartDetailEventExt on DartDetailEvent { + A when({required A Function(int) changelogDetailRequested}) { + return switch (this) { + DartChangelogDetailRequested(:final id) => changelogDetailRequested(id), + }; + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_state.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_state.dart new file mode 100644 index 00000000..49b40840 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/application/dart_detail_state.dart @@ -0,0 +1,31 @@ +part of 'dart_detail_bloc.dart'; + +enum DartDetailStatus { initial, loading, success, failure } + +class DartDetailState extends Equatable { + const DartDetailState({ + this.status = DartDetailStatus.initial, + this.detail = const Fresh.yes(entity: Detail.empty), + this.failureMessage, + }); + + final DartDetailStatus status; + final Fresh detail; + final String? failureMessage; + + DartDetailState copyWith({ + DartDetailStatus Function()? status, + Fresh Function()? detail, + String? Function()? failureMessage, + }) { + return DartDetailState( + status: status != null ? status() : this.status, + detail: detail != null ? detail() : this.detail, + failureMessage: + failureMessage != null ? failureMessage() : this.failureMessage, + ); + } + + @override + List get props => [status, detail, failureMessage]; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_local_service.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_local_service.dart new file mode 100644 index 00000000..d5f5b7ac --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_local_service.dart @@ -0,0 +1,13 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_local_service.dart'; + +class DartDetailLocalService extends DetailLocalService { + DartDetailLocalService({super.isarDb}); + + Task upsertDartDetail(DetailDTO detailDTO) { + return super.upsertDetail(detailDTO); + } + + Task getDartDetail(int id) => super.getDetail(id); +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_remote_service.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_remote_service.dart new file mode 100644 index 00000000..243e142a --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_remote_service.dart @@ -0,0 +1,18 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/data/remote_response.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/constants.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_remote_service.dart'; + +typedef _DartDetail = TaskEither>; + +class DartDetailRemoteService extends DetailRemoteService { + DartDetailRemoteService({ + super.dio, + super.headerCache, + }); + + _DartDetail getDartChangelogDetail(int id) { + return super.getDetail(id, dartChangelogPath); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_repository.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_repository.dart new file mode 100644 index 00000000..0ef296e6 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/data/dart_detail_repository.dart @@ -0,0 +1,49 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/data/data.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/core/domain/fresh.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:now_in_dart_flutter/features/detail/core/domain/detail.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_local_service.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_remote_service.dart'; + +typedef _DartDetailOrFailure = TaskEither>; + +class DartDetailRepository { + DartDetailRepository({ + DartDetailLocalService? localService, + DartDetailRemoteService? remoteService, + }) : _localService = localService ?? DartDetailLocalService(), + _remoteService = remoteService ?? DartDetailRemoteService(); + + final DartDetailLocalService _localService; + final DartDetailRemoteService _remoteService; + + _DartDetailOrFailure getDartDetail(int id) { + return TaskEither.Do( + (_) async { + final remoteResponse = await _( + _remoteService.getDartChangelogDetail(id), + ); + + return remoteResponse.when( + noConnection: () async { + final dto = await _(_localService.getDartDetail(id).toTaskEither()); + return Fresh.no(entity: dto?.toDomain() ?? Detail.empty); + }, + unmodified: () async { + final cachedData = await _( + _localService.getDartDetail(id).toTaskEither(), + ); + return Fresh.yes(entity: cachedData?.toDomain() ?? Detail.empty); + }, + modified: (data) async { + final dto = DetailDTO.parseHtml(id, data); + await _(_localService.upsertDartDetail(dto).toTaskEither()); + return Fresh.yes(entity: dto.toDomain()); + }, + ); + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/dart_detail/presentation/view/dart_changelog_page.dart b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/presentation/view/dart_changelog_page.dart new file mode 100644 index 00000000..2aa2cfa3 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/dart_detail/presentation/view/dart_changelog_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:now_in_dart_flutter/core/data/id.dart'; +import 'package:now_in_dart_flutter/core/presentation/no_connection_toast.dart'; +import 'package:now_in_dart_flutter/core/presentation/no_results_display.dart'; +import 'package:now_in_dart_flutter/features/detail/core/presentation/widget/detail_webview.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/application/dart_detail_bloc.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_repository.dart'; + +class DartChangelogPage extends StatelessWidget { + const DartChangelogPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Dart')), + body: BlocProvider( + create: (context) { + const id = EntityId.dartChangelogDetail; + return DartDetailBloc( + repository: context.read(), + )..add(const DartChangelogDetailRequested(id)); + }, + child: const DartChangelogView(), + ), + ); + } +} + +class DartChangelogView extends StatefulWidget { + const DartChangelogView({super.key}); + + @override + State createState() => _DartChangelogViewState(); +} + +class _DartChangelogViewState extends State { + bool _hasAlreadyShownNoConnectionToast = false; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (!state.detail.isFresh! && !_hasAlreadyShownNoConnectionToast) { + _hasAlreadyShownNoConnectionToast = true; + showNoConnectionToast('No Internet Connection!!!', context); + } + }, + builder: (context, state) { + switch (state.status) { + case DartDetailStatus.initial: + return const SizedBox.shrink(); + + case DartDetailStatus.loading: + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + + case DartDetailStatus.success: + final receivedDetail = state.detail; + if (receivedDetail.entity!.isEmpty) { + return const NoResultsDisplay( + message: "Sorry. There's nothing to display ☚ī¸", + ); + } + return DetailWebView( + html: receivedDetail.entity!.html, + ); + + case DartDetailStatus.failure: + return NoResultsDisplay(message: state.failureMessage!); + } + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_bloc.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_bloc.dart new file mode 100644 index 00000000..a033aac6 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_bloc.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/core/domain/fresh.dart'; +import 'package:now_in_dart_flutter/features/detail/core/domain/detail.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_repository.dart'; + +part 'flutter_detail_event.dart'; +part 'flutter_detail_state.dart'; + +typedef _DetailFailureOrSuccess = TaskEither>; + +class FlutterDetailBloc extends Bloc { + FlutterDetailBloc({ + required FlutterDetailRepository repository, + }) : _repository = repository, + super(const FlutterDetailState()) { + on( + (event, emit) async { + await event.when( + flutterWhatsNewDetailRequested: (id) { + return _onFlutterWhatsNewDetailRequested(emit, id); + }, + flutterReleaseNotesDetailRequested: (id) { + return _onFlutterReleaseNotesDetailRequested(emit, id); + }, + ); + }, + ); + } + + final FlutterDetailRepository _repository; + + Future _onFlutterWhatsNewDetailRequested( + Emitter emit, + int id, + ) { + return _onFlutterDetailRequested( + _repository.getWhatsNewFlutterDetail, + emit, + id, + ); + } + + Future _onFlutterReleaseNotesDetailRequested( + Emitter emit, + int id, + ) { + return _onFlutterDetailRequested( + _repository.getFlutterReleaseNotesDetail, + emit, + id, + ); + } + + Future _onFlutterDetailRequested( + _DetailFailureOrSuccess Function(int) caller, + Emitter emit, + int id, + ) async { + emit(state.copyWith(status: () => FlutterDetailStatus.loading)); + final failureOrSuccessDetail = await caller(id).run(); + return failureOrSuccessDetail.match( + (failure) { + emit( + state.copyWith( + status: () => FlutterDetailStatus.failure, + failureMessage: () { + return switch (failure) { + ApiFailure() => failure.message, + UriParserFailure() => failure.message, + }; + }, + ), + ); + return unit; + }, + (detail) { + emit( + state.copyWith( + status: () => FlutterDetailStatus.success, + detail: () => detail, + failureMessage: () => null, + ), + ); + return unit; + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_event.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_event.dart new file mode 100644 index 00000000..8fcff240 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_event.dart @@ -0,0 +1,31 @@ +part of 'flutter_detail_bloc.dart'; + +sealed class FlutterDetailEvent { + const FlutterDetailEvent(); +} + +class FlutterWhatsNewDetailRequested extends FlutterDetailEvent { + const FlutterWhatsNewDetailRequested(this.id); + + final int id; +} + +class FlutterReleaseNotesDetailRequested extends FlutterDetailEvent { + const FlutterReleaseNotesDetailRequested(this.id); + + final int id; +} + +extension FlutterDetailEventExt on FlutterDetailEvent { + A when({ + required A Function(int) flutterWhatsNewDetailRequested, + required A Function(int) flutterReleaseNotesDetailRequested, + }) { + return switch (this) { + FlutterWhatsNewDetailRequested(:final id) => + flutterWhatsNewDetailRequested(id), + FlutterReleaseNotesDetailRequested(:final id) => + flutterReleaseNotesDetailRequested(id), + }; + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_state.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_state.dart new file mode 100644 index 00000000..cce07e20 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/application/flutter_detail_state.dart @@ -0,0 +1,31 @@ +part of 'flutter_detail_bloc.dart'; + +enum FlutterDetailStatus { initial, loading, success, failure } + +class FlutterDetailState extends Equatable { + const FlutterDetailState({ + this.status = FlutterDetailStatus.initial, + this.detail = const Fresh.yes(entity: Detail.empty), + this.failureMessage, + }); + + final FlutterDetailStatus status; + final Fresh detail; + final String? failureMessage; + + FlutterDetailState copyWith({ + FlutterDetailStatus Function()? status, + Fresh Function()? detail, + String? Function()? failureMessage, + }) { + return FlutterDetailState( + status: status != null ? status() : this.status, + detail: detail != null ? detail() : this.detail, + failureMessage: + failureMessage != null ? failureMessage() : this.failureMessage, + ); + } + + @override + List get props => [status, detail, failureMessage]; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_local_service.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_local_service.dart new file mode 100644 index 00000000..c869eb92 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_local_service.dart @@ -0,0 +1,13 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_local_service.dart'; + +class FlutterDetailLocalService extends DetailLocalService { + FlutterDetailLocalService({super.isarDb}); + + Task upsertFlutterDetail(DetailDTO detailDTO) { + return super.upsertDetail(detailDTO); + } + + Task getFlutterDetail(int id) => super.getDetail(id); +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_remote_service.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_remote_service.dart new file mode 100644 index 00000000..28d420e0 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_remote_service.dart @@ -0,0 +1,22 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/data/remote_response.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/constants.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_remote_service.dart'; + +typedef _FlutterDetail = TaskEither>; + +class FlutterDetailRemoteService extends DetailRemoteService { + FlutterDetailRemoteService({ + super.dio, + super.headerCache, + }); + + _FlutterDetail getWhatsNewFlutterDetail(int id) { + return super.getDetail(id, flutterWhatsNewPath); + } + + _FlutterDetail getFlutterReleaseNotesDetail(int id) { + return super.getDetail(id, flutterReleaseNotesPath); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_repository.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_repository.dart new file mode 100644 index 00000000..968a28ef --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/data/flutter_detail_repository.dart @@ -0,0 +1,60 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:now_in_dart_flutter/core/data/remote_response.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:now_in_dart_flutter/core/domain/fresh.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:now_in_dart_flutter/features/detail/core/domain/detail.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_local_service.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_remote_service.dart'; + +typedef _FlutterDetailOrFailure = TaskEither>; + +class FlutterDetailRepository { + FlutterDetailRepository({ + FlutterDetailLocalService? localService, + FlutterDetailRemoteService? remoteService, + }) : _localService = localService ?? FlutterDetailLocalService(), + _remoteService = remoteService ?? FlutterDetailRemoteService(); + + final FlutterDetailLocalService _localService; + final FlutterDetailRemoteService _remoteService; + + _FlutterDetailOrFailure getWhatsNewFlutterDetail(int id) { + return _getFlutterDetail(id, _remoteService.getWhatsNewFlutterDetail); + } + + _FlutterDetailOrFailure getFlutterReleaseNotesDetail(int id) { + return _getFlutterDetail(id, _remoteService.getFlutterReleaseNotesDetail); + } + + _FlutterDetailOrFailure _getFlutterDetail( + int id, + TaskEither> Function(int) caller, + ) { + return TaskEither.Do( + (_) async { + final remoteResponse = await _(caller(id)); + + return remoteResponse.when( + noConnection: () async { + final dto = await _( + _localService.getFlutterDetail(id).toTaskEither(), + ); + return Fresh.no(entity: dto?.toDomain() ?? Detail.empty); + }, + unmodified: () async { + final cachedData = await _( + _localService.getFlutterDetail(id).toTaskEither(), + ); + return Fresh.yes(entity: cachedData?.toDomain() ?? Detail.empty); + }, + modified: (data) async { + final dto = DetailDTO.parseHtml(id, data); + await _(_localService.upsertFlutterDetail(dto).toTaskEither()); + return Fresh.yes(entity: dto.toDomain()); + }, + ); + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_common.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_common.dart new file mode 100644 index 00000000..1cefc06b --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_common.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:now_in_dart_flutter/core/presentation/no_connection_toast.dart'; +import 'package:now_in_dart_flutter/core/presentation/no_results_display.dart'; +import 'package:now_in_dart_flutter/features/detail/core/presentation/widget/detail_webview.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/application/flutter_detail_bloc.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_repository.dart'; + +class FlutterDetailCommonPage extends StatelessWidget { + const FlutterDetailCommonPage({ + required FlutterDetailEvent event, + super.key, + }) : _event = event; + + final FlutterDetailEvent _event; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return FlutterDetailBloc( + repository: context.read(), + )..add(_event); + }, + child: const FlutterDetailCommonView(), + ); + } +} + +class FlutterDetailCommonView extends StatefulWidget { + const FlutterDetailCommonView({super.key}); + + @override + State createState() => + _FlutterDetailCommonViewState(); +} + +class _FlutterDetailCommonViewState extends State { + bool _hasAlreadyShownNoConnectionToast = false; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (!state.detail.isFresh! && !_hasAlreadyShownNoConnectionToast) { + _hasAlreadyShownNoConnectionToast = true; + showNoConnectionToast('No Internet Connection!!!', context); + } + }, + builder: (context, state) { + switch (state.status) { + case FlutterDetailStatus.initial: + return const SizedBox.shrink(); + + case FlutterDetailStatus.loading: + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + + case FlutterDetailStatus.success: + final receivedDetail = state.detail; + if (receivedDetail.entity!.isEmpty) { + return const NoResultsDisplay( + message: "Sorry. There's nothing to display ☚ī¸", + ); + } + return DetailWebView(html: receivedDetail.entity!.html); + + case FlutterDetailStatus.failure: + return NoResultsDisplay(message: state.failureMessage!); + } + }, + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_page.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_page.dart new file mode 100644 index 00000000..71f2d810 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_detail_page.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/presentation/view/flutter_release_notes_page.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/presentation/view/flutter_whats_new_page.dart'; + +class FlutterDetailPage extends StatelessWidget { + const FlutterDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + const tabs = [ + Tab(text: "What's new 🆕"), + Tab(text: 'Release Notes 🗒ī¸'), + ]; + return DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar( + title: const Text('Flutter'), + bottom: const TabBar( + tabs: tabs, + ), + ), + body: const TabBarView( + children: [ + FlutterWhatsNewPage(), + FlutterReleaseNotesPage(), + ], + ), + ), + ); + } +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_release_notes_page.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_release_notes_page.dart new file mode 100644 index 00000000..33454a6a --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_release_notes_page.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:now_in_dart_flutter/core/data/id.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/application/flutter_detail_bloc.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/presentation/view/flutter_detail_common.dart'; + +class FlutterReleaseNotesPage extends StatefulWidget { + const FlutterReleaseNotesPage({super.key}); + + @override + State createState() => + _FlutterReleaseNotesPageState(); +} + +class _FlutterReleaseNotesPageState extends State + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return const FlutterDetailCommonPage( + event: FlutterReleaseNotesDetailRequested( + EntityId.flutterReleaseNotesDetail, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_whats_new_page.dart b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_whats_new_page.dart new file mode 100644 index 00000000..fa448795 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/detail/flutter_detail/presentation/view/flutter_whats_new_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:now_in_dart_flutter/core/data/id.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/application/flutter_detail_bloc.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/presentation/view/flutter_detail_common.dart'; + +class FlutterWhatsNewPage extends StatefulWidget { + const FlutterWhatsNewPage({super.key}); + + @override + State createState() => _FlutterWhatsNewPageState(); +} + +class _FlutterWhatsNewPageState extends State + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return const FlutterDetailCommonPage( + event: FlutterWhatsNewDetailRequested( + EntityId.flutterWhatsNewDetail, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/examples/now_in_dart_flutter/lib/features/home/cubit/home_cubit.dart b/examples/now_in_dart_flutter/lib/features/home/cubit/home_cubit.dart new file mode 100644 index 00000000..30ec9dea --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/home/cubit/home_cubit.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'home_state.dart'; + +class HomeCubit extends Cubit { + HomeCubit() : super(const HomeState()); + + void setTab(int index) => emit(HomeState(index: index)); +} diff --git a/examples/now_in_dart_flutter/lib/features/home/cubit/home_state.dart b/examples/now_in_dart_flutter/lib/features/home/cubit/home_state.dart new file mode 100644 index 00000000..873e598a --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/home/cubit/home_state.dart @@ -0,0 +1,12 @@ +part of 'home_cubit.dart'; + +class HomeState extends Equatable { + const HomeState({ + this.index = 0, + }); + + final int index; + + @override + List get props => [index]; +} diff --git a/examples/now_in_dart_flutter/lib/features/home/home.dart b/examples/now_in_dart_flutter/lib/features/home/home.dart new file mode 100644 index 00000000..0e9281ae --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/home/home.dart @@ -0,0 +1,2 @@ +export 'cubit/home_cubit.dart'; +export 'view/view.dart'; diff --git a/examples/now_in_dart_flutter/lib/features/home/view/home_page.dart b/examples/now_in_dart_flutter/lib/features/home/view/home_page.dart new file mode 100644 index 00000000..d44b1d30 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/home/view/home_page.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:now_in_dart_flutter/core/presentation/assets_path.dart'; +import 'package:now_in_dart_flutter/core/presentation/lazy_indexed_stack.dart'; +import 'package:now_in_dart_flutter/core/presentation/responsive.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/presentation/view/dart_changelog_page.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/presentation/view/flutter_detail_page.dart'; +import 'package:now_in_dart_flutter/features/home/cubit/home_cubit.dart'; +import 'package:vector_graphics/vector_graphics.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => HomeCubit(), + child: const HomeView(), + ); + } +} + +class HomeView extends StatelessWidget { + const HomeView({super.key}); + + @override + Widget build(BuildContext context) { + return const Responsive( + mobile: MobileView(), + tabletOrDesktop: TabletOrDesktopView(), + ); + } +} + +class MobileView extends StatelessWidget { + const MobileView({super.key}); + + @override + Widget build(BuildContext context) { + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + const pages = [DartChangelogPage(), FlutterDetailPage()]; + + final selectedTabIndex = context.select( + (HomeCubit cubit) => cubit.state.index, + ); + return Scaffold( + body: LazyIndexedStack( + index: selectedTabIndex, + children: pages, + ), + bottomNavigationBar: NavigationBar( + destinations: _destinations, + selectedIndex: selectedTabIndex, + onDestinationSelected: context.read().setTab, + ), + ); + } +} + +class TabletOrDesktopView extends StatelessWidget { + const TabletOrDesktopView({super.key}); + + @override + Widget build(BuildContext context) { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + const pages = [DartChangelogPage(), FlutterDetailPage()]; + + final selectedTabIndex = context.select( + (HomeCubit cubit) => cubit.state.index, + ); + return Scaffold( + body: Row( + children: [ + NavigationRail( + destinations: _railDestinations, + selectedIndex: selectedTabIndex, + useIndicator: true, + labelType: NavigationRailLabelType.selected, + groupAlignment: 0, + onDestinationSelected: context.read().setTab, + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: LazyIndexedStack( + index: selectedTabIndex, + children: pages, + ), + ), + ], + ), + ); + } +} + +final _destinations = [ + const NavigationDestination( + icon: SvgPicture( + AssetBytesLoader(AssetsPath.dartIcon), + width: 24, + height: 24, + ), + label: 'Dart', + ), + const NavigationDestination( + icon: SvgPicture( + AssetBytesLoader(AssetsPath.flutterIcon), + width: 24, + height: 24, + ), + label: 'Flutter', + ), +]; + +final _railDestinations = [ + NavigationRailDestination( + icon: SvgPicture.asset( + AssetsPath.dartIcon, + width: 24, + height: 24, + ), + label: const Text('Dart'), + ), + NavigationRailDestination( + icon: SvgPicture.asset( + AssetsPath.flutterIcon, + width: 24, + height: 24, + ), + label: const Text('Flutter'), + ), +]; diff --git a/examples/now_in_dart_flutter/lib/features/home/view/view.dart b/examples/now_in_dart_flutter/lib/features/home/view/view.dart new file mode 100644 index 00000000..e4ff2696 --- /dev/null +++ b/examples/now_in_dart_flutter/lib/features/home/view/view.dart @@ -0,0 +1 @@ +export 'home_page.dart'; diff --git a/examples/now_in_dart_flutter/lib/main.dart b/examples/now_in_dart_flutter/lib/main.dart new file mode 100644 index 00000000..c15f6c0f --- /dev/null +++ b/examples/now_in_dart_flutter/lib/main.dart @@ -0,0 +1,25 @@ +import 'package:now_in_dart_flutter/app/app.dart'; +import 'package:now_in_dart_flutter/bootstrap.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_remote_service.dart'; +import 'package:now_in_dart_flutter/features/detail/dart_detail/data/dart_detail_repository.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_remote_service.dart'; +import 'package:now_in_dart_flutter/features/detail/flutter_detail/data/flutter_detail_repository.dart'; + +void main() { + bootstrap( + (dio) { + final dartDetailRepository = DartDetailRepository( + remoteService: DartDetailRemoteService(dio: dio), + ); + + final flutterDetailRepository = FlutterDetailRepository( + remoteService: FlutterDetailRemoteService(dio: dio), + ); + + return App( + dartDetailRepository: dartDetailRepository, + flutterDetailRepository: flutterDetailRepository, + ); + }, + ); +} diff --git a/examples/now_in_dart_flutter/pubspec.yaml b/examples/now_in_dart_flutter/pubspec.yaml new file mode 100644 index 00000000..bfae3d0b --- /dev/null +++ b/examples/now_in_dart_flutter/pubspec.yaml @@ -0,0 +1,39 @@ +name: now_in_dart_flutter +description: A simple app that uses WebView under the hood to display all the news and updates regarding Dart and Flutter. The information is retrieved using GitHub's API and the github repository used as source is that of Flutter and Dart's. + +publish_to: 'none' + +version: 2.0.0 + +environment: + sdk: '>=3.1.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + dio: ^5.3.2 + equatable: ^2.0.5 + flash: ^3.0.5+1 + flutter_bloc: ^8.1.3 + flutter_svg: ^2.0.7 + fpdart: ^1.1.0 + isar: ^3.1.0+1 + isar_flutter_libs: ^3.1.0+1 + meta: ^1.9.1 + url_launcher: ^6.1.12 + webview_flutter: ^4.2.3 + path_provider: ^2.1.0 + vector_graphics: ^1.1.7 + +dev_dependencies: + build_runner: ^2.4.6 + flutter_lints: ^2.0.2 + mocktail: ^1.0.0 + test: ^1.24.6 + very_good_analysis: ^5.0.0+1 + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/examples/now_in_dart_flutter/test/features/detail/core/data/detail_local_service_test.dart b/examples/now_in_dart_flutter/test/features/detail/core/data/detail_local_service_test.dart new file mode 100644 index 00000000..de6bfc00 --- /dev/null +++ b/examples/now_in_dart_flutter/test/features/detail/core/data/detail_local_service_test.dart @@ -0,0 +1,77 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_dto.dart'; +import 'package:test/test.dart'; + +import '../../../../helpers/mocks.dart'; + +void main() { + final isar = MockIsar(); + final isarDb = MockIsarDatabase(); + final isarCollection = MockIsarCollection(); + final detailLocalService = MockDetailLocalService(isarDb: isarDb); + + group( + 'DetailLocalService |', + () { + const fakeDetailDTO = DetailDTO(id: 1, html: 'html'); + + setUpAll(() => when(() => isarDb.instance).thenReturn(isar)); + + test( + 'should instantiate IsarDatabase() when not injected', + () => expect(MockDetailLocalService(), isNotNull), + ); + + test( + 'The method `upsertDetail` should either update or insert the passed ' + 'DetailDTO', + () async { + when( + () => isar.writeTxn( + any(that: isA()), + silent: any(named: 'silent', that: isA()), + ), + ).thenAnswer((_) async => unit); + + final result = + await detailLocalService.upsertDetail(fakeDetailDTO).run(); + + expect(result, isA()); + }, + ); + + group( + 'The method `getDetail`', + () { + setUpAll( + () => when(() => isar.detailDTOs).thenReturn(isarCollection), + ); + test( + 'should return DetailDTO object for the passed id', + () async { + when(() => isarCollection.get(any(that: isA()))) + .thenAnswer((_) async => fakeDetailDTO); + + final result = await detailLocalService.getDetail(1).run(); + + expect(result, fakeDetailDTO); + }, + ); + + test( + 'should return null if invalid id is passed', + () async { + when(() => isarCollection.get(any(that: isA()))) + .thenAnswer((_) async => null); + + final result = await detailLocalService.getDetail(1).run(); + + expect(result, isNull); + }, + ); + }, + ); + }, + ); +} diff --git a/examples/now_in_dart_flutter/test/features/detail/core/data/detail_remote_service_test.dart b/examples/now_in_dart_flutter/test/features/detail/core/data/detail_remote_service_test.dart new file mode 100644 index 00000000..29311d23 --- /dev/null +++ b/examples/now_in_dart_flutter/test/features/detail/core/data/detail_remote_service_test.dart @@ -0,0 +1,174 @@ +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:now_in_dart_flutter/core/data/github_header.dart'; +import 'package:now_in_dart_flutter/core/data/remote_response.dart'; +import 'package:now_in_dart_flutter/core/domain/failure.dart'; +import 'package:test/test.dart'; + +import '../../../../helpers/mocks.dart'; +import '../../../../helpers/register_multiple_fallback_values.dart'; + +void main() { + final dio = MockDio(); + final response = MockResponse(); + final headers = MockHeaders(); + final headerCache = MockHeaderCache(); + final detailRemoteService = MockDetailRemoteService( + dio: dio, + headerCache: headerCache, + ); + + setUpAll(() { + registerMultipleFallbackValues([ + FakeUri(), + FakeOptions(), + FakeGithubHeader(), + ]); + }); + + group( + 'DetailRemoteService |', + () { + const fakeGithubHeader = GithubHeader( + id: 1, + eTag: '12345', + path: '/path', + ); + + test( + 'should instantiate Dio() and HeaderCache() when not injected', + () => expect(MockDetailRemoteService(), isNotNull), + ); + group( + 'The method `getDetail`', + () { + setUpAll( + () => when( + () => headerCache.getHeader(any(that: isA())), + ).thenReturn(Task(() async => fakeGithubHeader)), + ); + test( + 'should return right of TaskEither>' + ' i.e. ModifiedRemoteResponse if the status code is 200', + () async { + when(() => headers.map).thenReturn( + { + 'ETag': ['12345'], + }, + ); + + when(() => response.statusCode).thenReturn(200); + when(() => response.data).thenReturn('html'); + when(() => response.headers).thenReturn(headers); + + when( + () => dio.getUri( + any(that: isA()), + options: any(named: 'options', that: isA()), + ), + ).thenAnswer((_) async => response); + + when( + () => headerCache.saveHeader(any(that: isA())), + ).thenReturn(Task(() async => unit)); + + final result = + await detailRemoteService.getDetail(1, '/path').run(); + + expect( + result, + right>( + const ModifiedRemoteResponse('html'), + ), + ); + }, + ); + + test( + 'should return right of TaskEither>' + ' i.e. UnModifiedRemoteResponse if the status code is 304', + () async { + when(() => response.statusCode).thenReturn(304); + + when( + () => dio.getUri( + any(that: isA()), + options: any(named: 'options', that: isA()), + ), + ).thenAnswer((_) async => response); + + final result = + await detailRemoteService.getDetail(1, '/path').run(); + + expect( + result, + right>( + const UnModifiedRemoteResponse(), + ), + ); + }, + ); + + test( + 'should return right of TaskEither>' + ' i.e. NoConnectionRemoteResponse if DioException.connectionError ' + 'is thrown', + () async { + when( + () => dio.getUri( + any(that: isA()), + options: any(named: 'options', that: isA()), + ), + ).thenThrow( + DioException.connectionError( + requestOptions: RequestOptions(), + reason: '', + ), + ); + + final result = + await detailRemoteService.getDetail(1, '/path').run(); + + expect( + result, + right>( + const NoConnectionRemoteResponse(), + ), + ); + }, + ); + + test( + 'should return left of TaskEither>' + ' i.e. ApiFailure if network request is unsuccessful', + () async { + const errorMessage = 'Error on network request'; + + when( + () => dio.getUri( + any(that: isA()), + options: any(named: 'options', that: isA()), + ), + ).thenThrow(Exception()); + + final result = + await detailRemoteService.getDetail(1, '/path').run(); + + expect( + result.match( + (failure) { + final isApiFailure = failure is ApiFailure; + return isApiFailure && failure.message == errorMessage; + }, + (_) => false, + ), + isTrue, + ); + }, + ); + }, + ); + }, + ); +} diff --git a/examples/now_in_dart_flutter/test/helpers/fakes.dart b/examples/now_in_dart_flutter/test/helpers/fakes.dart new file mode 100644 index 00000000..aff7c535 --- /dev/null +++ b/examples/now_in_dart_flutter/test/helpers/fakes.dart @@ -0,0 +1,7 @@ +part of 'mocks.dart'; + +class FakeUri extends Fake implements Uri {} + +class FakeOptions extends Fake implements Options {} + +class FakeGithubHeader extends Fake implements GithubHeader {} diff --git a/examples/now_in_dart_flutter/test/helpers/mocks.dart b/examples/now_in_dart_flutter/test/helpers/mocks.dart new file mode 100644 index 00000000..1c32059f --- /dev/null +++ b/examples/now_in_dart_flutter/test/helpers/mocks.dart @@ -0,0 +1,30 @@ +import 'package:dio/dio.dart'; +import 'package:isar/isar.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:now_in_dart_flutter/core/data/data.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_local_service.dart'; +import 'package:now_in_dart_flutter/features/detail/core/data/detail_remote_service.dart'; + +part 'fakes.dart'; + +class MockDio extends Mock implements Dio {} + +class MockResponse extends Mock implements Response {} + +class MockHeaders extends Mock implements Headers {} + +class MockHeaderCache extends Mock implements HeaderCache {} + +class MockDetailRemoteService extends DetailRemoteService { + MockDetailRemoteService({super.dio, super.headerCache}); +} + +class MockDetailLocalService extends DetailLocalService { + MockDetailLocalService({super.isarDb}); +} + +class MockIsarDatabase extends Mock implements IsarDatabase {} + +class MockIsar extends Mock implements Isar {} + +class MockIsarCollection extends Mock implements IsarCollection {} diff --git a/examples/now_in_dart_flutter/test/helpers/register_multiple_fallback_values.dart b/examples/now_in_dart_flutter/test/helpers/register_multiple_fallback_values.dart new file mode 100644 index 00000000..2e990d44 --- /dev/null +++ b/examples/now_in_dart_flutter/test/helpers/register_multiple_fallback_values.dart @@ -0,0 +1,7 @@ +import 'package:mocktail/mocktail.dart'; + +void registerMultipleFallbackValues(List values) { + for (final value in values) { + registerFallbackValue(value); + } +} diff --git a/packages/fpdart/README.md b/packages/fpdart/README.md index 7ed80a98..bf63c6d4 100644 --- a/packages/fpdart/README.md +++ b/packages/fpdart/README.md @@ -51,6 +51,7 @@ fpdart is inspired by [fp-ts](https://gcanti.github.io/fp-ts/), [cats](https://t - [🧑‍đŸĢ Getting started with functional programming](#-getting-started-with-functional-programming) - [đŸ’ģ Installation](#-installation) - [✨ Examples](#-examples) + - [`now_in_dart_flutter`](#now_in_dart_flutter) - [`fpdart` + `riverpod`](#fpdart--riverpod) - [Pokeapi](#pokeapi) - [Open Meteo API](#open-meteo-api) @@ -128,6 +129,9 @@ dependencies: ## ✨ Examples +### [`now_in_dart_flutter`](https://github.com/Biplab-Dutta/now-in-dart-flutter/tree/master/lib) +A simple app that uses WebView under the hood to display all the news and updates regarding Dart and Flutter. The information is retrieved using GitHub's API and the github repository used as source is that of Flutter and Dart's. The project heavily relies on `fpdart` and its latest APIs (Either, Do, Task, IO, flatmap, etc). It uses `flutter_bloc` for state management. + ### [`fpdart` + `riverpod`](https://www.sandromaglione.com/course/fpdart-riverpod-develop-flutter-app) Step by step course on how to build a safe, maintainable, and testable Flutter app using `fpdart` and `riverpod`.