diff --git a/.vscode/launch.json b/.vscode/launch.json index 4566494..bc7b1b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,24 +5,16 @@ "version": "0.2.0", "configurations": [ { - "name": "example", - "cwd": "example", + "name": "example_login", + "cwd": "examples/login_posts_list", "request": "launch", "type": "dart" }, { - "name": "example (profile mode)", - "cwd": "example", + "name": "counter", + "cwd": "examples/counter", "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "example (release mode)", - "cwd": "example", - "request": "launch", - "type": "dart", - "flutterMode": "release" + "type": "dart" } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1b311..e21948a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +## 2.0.0 + +**🚨 BREAKING CHANGES - Complete Architecture Rewrite** + +This is a major rewrite of the MVI package with significant breaking changes. + +### New Architecture + +* **Replaced signals with ValueListenable**: Now uses Flutter's built-in ValueListenable and ValueNotifier for reactive state management +* **Dual ViewModel approach**: + - `SimpleViewModel` for basic state management without effects + - `ViewModel` for full MVI pattern with effects support +* **New mixins**: + - `SimpleViewModelMixin` for connecting widgets to SimpleViewModels + - `ViewModelMixin` for connecting widgets to ViewModels with effects +* **Method rename**: `provideViewModel()` renamed to `createViewModel()` for better clarity + +### Migration Guide + +This version is not backward compatible. To migrate: + +1. Replace `BaseViewModel` with either `SimpleViewModel` or `ViewModel` +2. Update your mixins to use `SimpleViewModelMixin` or `ViewModelMixin` +3. Rename `provideViewModel()` to `createViewModel()` +4. Replace signals-based state observation with ValueListenableBuilder + +### Examples + +* Added complete counter example demonstrating basic MVI pattern +* Updated login/posts example with new architecture + ## 1.0.6 * Log information about the ViewModel when debugLabel is not null diff --git a/README.md b/README.md index ffcc93b..1da90a8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ - -

Pub Version @@ -28,28 +15,41 @@ and the Flutter guide for # MVI - Model-View-Intent for Flutter -An implementation of the MVI (Model-View-Intent) pattern for Flutter that uses the `signals` package for managing state. +A clean and efficient implementation of the MVI (Model-View-Intent) pattern for Flutter using ValueListenable for reactive state management. -This package provides a basic structure to implement the MVI pattern in your Flutter projects, helping you to build reactive and predictable user interfaces. +This package provides a robust architecture to implement the MVI pattern in your Flutter projects, helping you build reactive, predictable, and testable user interfaces. ## What is MVI? -MVI is a unidirectional data flow architecture pattern that helps in managing the state of your application in a more predictable way. It is composed of three main components: +MVI is a unidirectional data flow architecture pattern that helps manage application state predictably. It consists of three main components: - **Model**: Represents the state of the application. It's an immutable object that holds all the data needed for the view. - **View**: The user interface (UI) that displays the state. In Flutter, this would be your widgets. - **Intent**: Represents an intention to change the state. These are usually triggered by user interactions with the UI. -## Core Concepts +Additionally, this implementation supports **Effects** for handling one-time side effects like navigation, showing snackbars, or other UI actions that don't affect state. + +## Core Components + +### Base Classes -This implementation is built around a few core components: +- **`BaseState`**: Abstract base class for all state objects. States should be immutable. +- **`BaseEvent`**: Abstract base class for all events (intents) that can trigger state changes. +- **`BaseEffect`**: Abstract base class for all effects (one-time side effects). -- `BaseViewModel`: A class that holds the business logic. It receives intents, processes them, and emits new states. -- `ViewModelMixin`: A mixin that can be used with a `StatefulWidget`'s `State` to automatically listen to state changes from a `BaseViewModel` and rebuild the UI. +### ViewModels -## State Management with Signals +- **`SimpleViewModel`**: For state management without effects. +- **`ViewModel`**: For state management with effects support. -This MVI implementation uses the [signals](https://pub.dev/packages/signals) package to manage the state. The state of a `BaseViewModel` is a `Signal` that can be observed by the UI. When a new state is emitted, the UI is automatically rebuilt. +### Mixins + +- **`SimpleViewModelMixin`**: Mixin for connecting widgets to SimpleViewModels. +- **`ViewModelMixin`**: Mixin for connecting widgets to ViewModels with effects support. + +## State Management with ValueListenable + +This MVI implementation uses Flutter's built-in `ValueListenable` and `ValueNotifier` for reactive state management. The state is observable and automatically triggers UI rebuilds when changed. ## Getting Started @@ -57,16 +57,25 @@ Add the package to your `pubspec.yaml`: ```yaml dependencies: - mvi: ^1.0.0 + mvi: ^2.0.0 ``` Then, run `flutter pub get`. -## Usage +## Example + +Check the examples folder for complete implementations and tests: + +- [`examples/counter/`](examples/counter/) - Simple counter app demonstrating basic MVI pattern +- [`examples/login_posts_list/`](examples/login_posts_list/) - Complete app with login flow and data fetching -Check the example folder for a detailed implementation with tests. +## Features -**Note**: The architecture in this example is designed solely for simplicity and demonstration purposes. +- ✅ **Reactive State Management**: Built on Flutter's ValueListenable +- ✅ **Effects Support**: Handle side effects like navigation and dialogs +- ✅ **Performance Optimized**: Selectors prevent unnecessary rebuilds +- ✅ **Debug Support**: Built-in logging for development +- ✅ **Testable**: Easy to unit test ViewModels and business logic ## Contributing diff --git a/example/lib/posts/view_model/posts_effect.dart b/example/lib/posts/view_model/posts_effect.dart deleted file mode 100644 index f71b5cc..0000000 --- a/example/lib/posts/view_model/posts_effect.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:mvi/mvi.dart'; - -sealed class PostsEffect extends BaseEffect { - @override - String toString() => 'PostsEffect'; -} diff --git a/examples/counter/.gitignore b/examples/counter/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/examples/counter/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +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 +.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 diff --git a/examples/counter/.metadata b/examples/counter/.metadata new file mode 100644 index 0000000..da04d09 --- /dev/null +++ b/examples/counter/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/counter/README.md b/examples/counter/README.md new file mode 100644 index 0000000..b360bac --- /dev/null +++ b/examples/counter/README.md @@ -0,0 +1,37 @@ +# Counter MVI Example + +A simple counter app demonstrating the MVI pattern using the `mvi` package. + +## Features + +- Increment and decrement counter +- Uses `SimpleViewModel` for basic state management without effects +- Demonstrates the MVI pattern with clean separation of concerns + +## Structure + +``` +lib/ +├── counter/ +│ ├── view_model/ +│ │ ├── counter_state.dart # State definition +│ │ ├── counter_event.dart # Events (Increment/Decrement) +│ │ └── counter_view_model.dart # Business logic +│ └── counter_page.dart # UI implementation +└── main.dart # App entry point +``` + +## Key Components + +- **CounterState**: Holds the counter value +- **CounterEvent**: Defines increment and decrement actions +- **CounterViewModel**: Handles business logic and state updates +- **CounterPage**: UI that connects to the ViewModel using `SimpleViewModelMixin` + +## Running + +```bash +flutter run +``` + +This example demonstrates the basic MVI pattern without effects, making it perfect for understanding the core concepts. diff --git a/example/analysis_options.yaml b/examples/counter/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to examples/counter/analysis_options.yaml diff --git a/example/android/.gitignore b/examples/counter/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to examples/counter/android/.gitignore diff --git a/examples/counter/android/app/build.gradle.kts b/examples/counter/android/app/build.gradle.kts new file mode 100644 index 0000000..26e4842 --- /dev/null +++ b/examples/counter/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.counter" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.counter" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + 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.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/examples/counter/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to examples/counter/android/app/src/debug/AndroidManifest.xml diff --git a/examples/counter/android/app/src/main/AndroidManifest.xml b/examples/counter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..edb73c9 --- /dev/null +++ b/examples/counter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt b/examples/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt new file mode 100644 index 0000000..5e4daaa --- /dev/null +++ b/examples/counter/android/app/src/main/kotlin/com/example/counter/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.counter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/examples/counter/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable-v21/launch_background.xml rename to examples/counter/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/examples/counter/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to examples/counter/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to examples/counter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to examples/counter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to examples/counter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to examples/counter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to examples/counter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values-night/styles.xml b/examples/counter/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from example/android/app/src/main/res/values-night/styles.xml rename to examples/counter/android/app/src/main/res/values-night/styles.xml diff --git a/example/android/app/src/main/res/values/styles.xml b/examples/counter/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to examples/counter/android/app/src/main/res/values/styles.xml diff --git a/example/android/app/src/profile/AndroidManifest.xml b/examples/counter/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/android/app/src/profile/AndroidManifest.xml rename to examples/counter/android/app/src/profile/AndroidManifest.xml diff --git a/example/android/build.gradle.kts b/examples/counter/android/build.gradle.kts similarity index 100% rename from example/android/build.gradle.kts rename to examples/counter/android/build.gradle.kts diff --git a/example/android/gradle.properties b/examples/counter/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to examples/counter/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/examples/counter/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to examples/counter/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/settings.gradle.kts b/examples/counter/android/settings.gradle.kts similarity index 100% rename from example/android/settings.gradle.kts rename to examples/counter/android/settings.gradle.kts diff --git a/example/ios/.gitignore b/examples/counter/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to examples/counter/ios/.gitignore diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/examples/counter/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to examples/counter/ios/Flutter/AppFrameworkInfo.plist diff --git a/example/ios/Flutter/Debug.xcconfig b/examples/counter/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to examples/counter/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/examples/counter/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to examples/counter/ios/Flutter/Release.xcconfig diff --git a/examples/counter/ios/Runner.xcodeproj/project.pbxproj b/examples/counter/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dc7c363 --- /dev/null +++ b/examples/counter/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy 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 */ + 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; 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 = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + 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 */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + 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 */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + 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 = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 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 */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 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 */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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 */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency 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; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.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.counter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.counter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.counter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.counter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.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; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.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.counter; + 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.counter; + 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 */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 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/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/counter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to examples/counter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/counter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to examples/counter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/counter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to examples/counter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/counter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to examples/counter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/counter/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to examples/counter/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/counter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to examples/counter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/counter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to examples/counter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.swift b/examples/counter/ios/Runner/AppDelegate.swift similarity index 100% rename from example/ios/Runner/AppDelegate.swift rename to examples/counter/ios/Runner/AppDelegate.swift diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to examples/counter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/counter/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to examples/counter/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/examples/counter/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to examples/counter/ios/Runner/Base.lproj/Main.storyboard diff --git a/examples/counter/ios/Runner/Info.plist b/examples/counter/ios/Runner/Info.plist new file mode 100644 index 0000000..d9ea807 --- /dev/null +++ b/examples/counter/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Counter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + counter + 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 + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/examples/counter/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/ios/Runner/Runner-Bridging-Header.h rename to examples/counter/ios/Runner/Runner-Bridging-Header.h diff --git a/example/ios/RunnerTests/RunnerTests.swift b/examples/counter/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from example/ios/RunnerTests/RunnerTests.swift rename to examples/counter/ios/RunnerTests/RunnerTests.swift diff --git a/examples/counter/lib/counter/counter_page.dart b/examples/counter/lib/counter/counter_page.dart new file mode 100644 index 0000000..99ae129 --- /dev/null +++ b/examples/counter/lib/counter/counter_page.dart @@ -0,0 +1,65 @@ +import 'package:counter/counter/view_model/counter_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:mvi/mvi.dart'; + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State + with + SimpleViewModelMixin< + CounterPage, + CounterState, + CounterEvent, + CounterViewModel + > { + @override + CounterViewModel createViewModel() => CounterViewModel(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Counter'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + ValueListenableBuilder( + valueListenable: viewModel.state, + builder: (context, state, child) { + return Text( + '${state.count}', + style: Theme.of(context).textTheme.headlineMedium, + ); + }, + ), + ], + ), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: () => addEvent(Increment()), + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + const SizedBox(height: 8), + FloatingActionButton( + onPressed: () => addEvent(Decrement()), + tooltip: 'Decrement', + child: const Icon(Icons.remove), + ), + ], + ), + ); + } +} diff --git a/examples/counter/lib/counter/view_model/counter_event.dart b/examples/counter/lib/counter/view_model/counter_event.dart new file mode 100644 index 0000000..54d968d --- /dev/null +++ b/examples/counter/lib/counter/view_model/counter_event.dart @@ -0,0 +1,13 @@ +import 'package:mvi/mvi.dart'; + +sealed class CounterEvent extends BaseEvent {} + +final class Increment extends CounterEvent { + @override + String toString() => 'Increment'; +} + +final class Decrement extends CounterEvent { + @override + String toString() => 'Decrement'; +} diff --git a/examples/counter/lib/counter/view_model/counter_state.dart b/examples/counter/lib/counter/view_model/counter_state.dart new file mode 100644 index 0000000..c51925c --- /dev/null +++ b/examples/counter/lib/counter/view_model/counter_state.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; +import 'package:mvi/mvi.dart'; + +final class CounterState extends BaseState with EquatableMixin { + const CounterState({this.count = 0}); + + final int count; + + CounterState copyWith({int? count}) => + CounterState(count: count ?? this.count); + + @override + List get props => [count]; + + @override + String toString() => 'CounterState(count: $count)'; +} diff --git a/examples/counter/lib/counter/view_model/counter_view_model.dart b/examples/counter/lib/counter/view_model/counter_view_model.dart new file mode 100644 index 0000000..cde252c --- /dev/null +++ b/examples/counter/lib/counter/view_model/counter_view_model.dart @@ -0,0 +1,28 @@ +import 'package:counter/counter/view_model/counter_event.dart'; +import 'package:counter/counter/view_model/counter_state.dart'; +import 'package:mvi/mvi.dart'; + +export 'counter_event.dart'; +export 'counter_state.dart'; + +final class CounterViewModel + extends SimpleViewModel { + CounterViewModel() + : super(const CounterState(), debugLabel: 'CounterViewModel'); + + @override + void onEvent(CounterEvent event) => switch (event) { + Increment() => _onIncrement(), + Decrement() => _onDecrement(), + }; + + void _onIncrement() { + final currentState = state.value; + updateState(currentState.copyWith(count: currentState.count + 1)); + } + + void _onDecrement() { + final currentState = state.value; + updateState(currentState.copyWith(count: currentState.count - 1)); + } +} diff --git a/examples/counter/lib/main.dart b/examples/counter/lib/main.dart new file mode 100644 index 0000000..2713264 --- /dev/null +++ b/examples/counter/lib/main.dart @@ -0,0 +1,22 @@ +import 'package:counter/counter/counter_page.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Counter MVI Example', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const CounterPage(), + ); + } +} diff --git a/examples/counter/pubspec.lock b/examples/counter/pubspec.lock new file mode 100644 index 0000000..9946c99 --- /dev/null +++ b/examples/counter/pubspec.lock @@ -0,0 +1,228 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mvi: + dependency: "direct main" + description: + path: "../.." + relative: true + source: path + version: "1.0.6" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" +sdks: + dart: ">=3.8.1 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/examples/counter/pubspec.yaml b/examples/counter/pubspec.yaml new file mode 100644 index 0000000..e885c83 --- /dev/null +++ b/examples/counter/pubspec.yaml @@ -0,0 +1,93 @@ +name: counter +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + + mvi: + path: ../../ + equatable: ^2.0.7 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/examples/counter/test/widget_test.dart b/examples/counter/test/widget_test.dart new file mode 100644 index 0000000..d7d88a0 --- /dev/null +++ b/examples/counter/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:counter/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/example/.gitignore b/examples/login_posts_list/.gitignore similarity index 100% rename from example/.gitignore rename to examples/login_posts_list/.gitignore diff --git a/example/.metadata b/examples/login_posts_list/.metadata similarity index 100% rename from example/.metadata rename to examples/login_posts_list/.metadata diff --git a/example/README.md b/examples/login_posts_list/README.md similarity index 100% rename from example/README.md rename to examples/login_posts_list/README.md diff --git a/examples/login_posts_list/analysis_options.yaml b/examples/login_posts_list/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/examples/login_posts_list/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/examples/login_posts_list/android/.gitignore b/examples/login_posts_list/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/examples/login_posts_list/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle.kts b/examples/login_posts_list/android/app/build.gradle.kts similarity index 100% rename from example/android/app/build.gradle.kts rename to examples/login_posts_list/android/app/build.gradle.kts diff --git a/examples/login_posts_list/android/app/src/debug/AndroidManifest.xml b/examples/login_posts_list/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/examples/login_posts_list/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/examples/login_posts_list/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to examples/login_posts_list/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/examples/login_posts_list/android/app/src/main/kotlin/com/example/example/MainActivity.kt similarity index 100% rename from example/android/app/src/main/kotlin/com/example/example/MainActivity.kt rename to examples/login_posts_list/android/app/src/main/kotlin/com/example/example/MainActivity.kt diff --git a/examples/login_posts_list/android/app/src/main/res/drawable-v21/launch_background.xml b/examples/login_posts_list/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/examples/login_posts_list/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/examples/login_posts_list/android/app/src/main/res/drawable/launch_background.xml b/examples/login_posts_list/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/examples/login_posts_list/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/examples/login_posts_list/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/login_posts_list/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/examples/login_posts_list/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/login_posts_list/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/login_posts_list/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/examples/login_posts_list/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/login_posts_list/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/login_posts_list/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/examples/login_posts_list/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/login_posts_list/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/login_posts_list/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/examples/login_posts_list/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/login_posts_list/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/login_posts_list/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/examples/login_posts_list/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/login_posts_list/android/app/src/main/res/values-night/styles.xml b/examples/login_posts_list/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/examples/login_posts_list/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/login_posts_list/android/app/src/main/res/values/styles.xml b/examples/login_posts_list/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/examples/login_posts_list/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/login_posts_list/android/app/src/profile/AndroidManifest.xml b/examples/login_posts_list/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/examples/login_posts_list/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/examples/login_posts_list/android/build.gradle.kts b/examples/login_posts_list/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/examples/login_posts_list/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/examples/login_posts_list/android/gradle.properties b/examples/login_posts_list/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/examples/login_posts_list/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/examples/login_posts_list/android/gradle/wrapper/gradle-wrapper.properties b/examples/login_posts_list/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/examples/login_posts_list/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-8.12-all.zip diff --git a/examples/login_posts_list/android/settings.gradle.kts b/examples/login_posts_list/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/examples/login_posts_list/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/examples/login_posts_list/ios/.gitignore b/examples/login_posts_list/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/examples/login_posts_list/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/login_posts_list/ios/Flutter/AppFrameworkInfo.plist b/examples/login_posts_list/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/examples/login_posts_list/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 + 12.0 + + diff --git a/examples/login_posts_list/ios/Flutter/Debug.xcconfig b/examples/login_posts_list/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/examples/login_posts_list/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/examples/login_posts_list/ios/Flutter/Release.xcconfig b/examples/login_posts_list/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/examples/login_posts_list/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/examples/login_posts_list/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to examples/login_posts_list/ios/Runner.xcodeproj/project.pbxproj diff --git a/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/login_posts_list/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/login_posts_list/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/login_posts_list/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/login_posts_list/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/examples/login_posts_list/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/login_posts_list/ios/Runner/AppDelegate.swift b/examples/login_posts_list/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/examples/login_posts_list/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@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/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/examples/login_posts_list/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/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/examples/login_posts_list/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/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/login_posts_list/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/examples/login_posts_list/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/login_posts_list/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/login_posts_list/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/examples/login_posts_list/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/login_posts_list/ios/Runner/Base.lproj/Main.storyboard b/examples/login_posts_list/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/examples/login_posts_list/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/examples/login_posts_list/ios/Runner/Info.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to examples/login_posts_list/ios/Runner/Info.plist diff --git a/examples/login_posts_list/ios/Runner/Runner-Bridging-Header.h b/examples/login_posts_list/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/examples/login_posts_list/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/examples/login_posts_list/ios/RunnerTests/RunnerTests.swift b/examples/login_posts_list/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/examples/login_posts_list/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/login/login_page.dart b/examples/login_posts_list/lib/login/login_page.dart similarity index 57% rename from example/lib/login/login_page.dart rename to examples/login_posts_list/lib/login/login_page.dart index ad973e9..f5f1850 100644 --- a/example/lib/login/login_page.dart +++ b/examples/login_posts_list/lib/login/login_page.dart @@ -31,13 +31,13 @@ class _LoginPageState extends State > { @override // Creates and provides the ViewModel instance for this widget - LoginViewModel provideViewModel() => widget.viewModel(); + LoginViewModel createViewModel() => widget.viewModel(); - // Uses signals to observe only the isAuthenticating part of the state + // Uses selector to observe only the isAuthenticating part of the state // and automatically dispose when the widget is disposed late final _isAuthenticating = viewModel.select( (state) => state.isAuthenticating, - debugLabel: 'LoginPage: isAuthenticating', + debugLabel: 'isAuthenticating', ); @override @@ -86,62 +86,62 @@ class _LoginPageState extends State appBar: AppBar(title: const Text('MVI Example')), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - // Watch widget from signals_flutter automatically rebuilds this subtree + // ValueListenableBuilder automatically rebuilds this subtree // whenever isAuthenticating changes - child: Watch(debugLabel: 'LoginPage: Login Form', (context) { - // Get the value of the isAuthenticating signal - final isAuthenticating = _isAuthenticating.value; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextField( - enabled: !isAuthenticating, - decoration: InputDecoration( - labelText: 'Email', - hintText: 'admin@admin.com', - prefixIcon: Icon( - Icons.email, - color: Theme.of(context).primaryColor, + child: ValueListenableBuilder( + valueListenable: _isAuthenticating, + builder: (context, isAuthenticating, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextField( + enabled: !isAuthenticating, + decoration: InputDecoration( + labelText: 'Email', + hintText: 'admin@admin.com', + prefixIcon: Icon( + Icons.email, + color: Theme.of(context).primaryColor, + ), + ), + // Dispatches EmailChanged events to update the ViewModel state + onChanged: (value) => addEvent(EmailChanged(value)), + ), + const SizedBox(height: 8), + TextField( + enabled: !isAuthenticating, + decoration: InputDecoration( + labelText: 'Password', + hintText: '123456', + prefixIcon: Icon( + Icons.lock, + color: Theme.of(context).primaryColor, + ), ), + obscureText: true, + // Dispatches PasswordChanged events to update the ViewModel state + onChanged: (value) => addEvent(PasswordChanged(value)), ), - // Dispatches EmailChanged events to update the ViewModel state - onChanged: (value) => addEvent(EmailChanged(value)), - ), - const SizedBox(height: 8), - TextField( - enabled: !isAuthenticating, - decoration: InputDecoration( - labelText: 'Password', - hintText: '123456', - prefixIcon: Icon( - Icons.lock, - color: Theme.of(context).primaryColor, + const SizedBox(height: 16), + OutlinedButton( + onPressed: isAuthenticating ? null : () => _onLogin(context), + child: const Text( + 'LOGIN', + style: TextStyle(fontWeight: FontWeight.bold), ), ), - obscureText: true, - // Dispatches PasswordChanged events to update the ViewModel state - onChanged: (value) => addEvent(PasswordChanged(value)), - ), - const SizedBox(height: 16), - OutlinedButton( - onPressed: isAuthenticating ? null : () => _onLogin(context), - child: const Text( - 'LOGIN', - style: TextStyle(fontWeight: FontWeight.bold), + const SizedBox(height: 16), + SizedBox( + height: 50, + child: isAuthenticating + ? const Center(child: CircularProgressIndicator()) + : null, ), - ), - const SizedBox(height: 16), - SizedBox( - height: 50, - child: isAuthenticating - ? const Center(child: CircularProgressIndicator()) - : null, - ), - ], - ); - }), + ], + ); + }, + ), ), ); } diff --git a/example/lib/login/view_model/login_effect.dart b/examples/login_posts_list/lib/login/view_model/login_effect.dart similarity index 100% rename from example/lib/login/view_model/login_effect.dart rename to examples/login_posts_list/lib/login/view_model/login_effect.dart diff --git a/example/lib/login/view_model/login_event.dart b/examples/login_posts_list/lib/login/view_model/login_event.dart similarity index 100% rename from example/lib/login/view_model/login_event.dart rename to examples/login_posts_list/lib/login/view_model/login_event.dart diff --git a/example/lib/login/view_model/login_state.dart b/examples/login_posts_list/lib/login/view_model/login_state.dart similarity index 100% rename from example/lib/login/view_model/login_state.dart rename to examples/login_posts_list/lib/login/view_model/login_state.dart diff --git a/example/lib/login/view_model/login_state.g.dart b/examples/login_posts_list/lib/login/view_model/login_state.g.dart similarity index 100% rename from example/lib/login/view_model/login_state.g.dart rename to examples/login_posts_list/lib/login/view_model/login_state.g.dart diff --git a/example/lib/login/view_model/login_view_model.dart b/examples/login_posts_list/lib/login/view_model/login_view_model.dart similarity index 95% rename from example/lib/login/view_model/login_view_model.dart rename to examples/login_posts_list/lib/login/view_model/login_view_model.dart index 839537f..4e038d6 100644 --- a/example/lib/login/view_model/login_view_model.dart +++ b/examples/login_posts_list/lib/login/view_model/login_view_model.dart @@ -8,7 +8,7 @@ export 'login_event.dart'; export 'login_state.dart'; final class LoginViewModel - extends BaseViewModel { + extends ViewModel { LoginViewModel() : super(const LoginState(), debugLabel: 'LoginViewModel'); @override diff --git a/example/lib/main.dart b/examples/login_posts_list/lib/main.dart similarity index 89% rename from example/lib/main.dart rename to examples/login_posts_list/lib/main.dart index 873e4f9..8865461 100644 --- a/example/lib/main.dart +++ b/examples/login_posts_list/lib/main.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:example/login/login_page.dart'; import 'package:example/login/view_model/login_view_model.dart'; -import 'package:mvi/mvi.dart'; void main() { - SignalsObserver.instance = null; runApp(const MyApp()); } diff --git a/example/lib/posts/data/post_error.dart b/examples/login_posts_list/lib/posts/data/post_error.dart similarity index 100% rename from example/lib/posts/data/post_error.dart rename to examples/login_posts_list/lib/posts/data/post_error.dart diff --git a/example/lib/posts/data/post_model.dart b/examples/login_posts_list/lib/posts/data/post_model.dart similarity index 100% rename from example/lib/posts/data/post_model.dart rename to examples/login_posts_list/lib/posts/data/post_model.dart diff --git a/example/lib/posts/data/posts_repository.dart b/examples/login_posts_list/lib/posts/data/posts_repository.dart similarity index 100% rename from example/lib/posts/data/posts_repository.dart rename to examples/login_posts_list/lib/posts/data/posts_repository.dart diff --git a/example/lib/posts/posts_screen.dart b/examples/login_posts_list/lib/posts/posts_screen.dart similarity index 70% rename from example/lib/posts/posts_screen.dart rename to examples/login_posts_list/lib/posts/posts_screen.dart index 2807ad6..87d1541 100644 --- a/example/lib/posts/posts_screen.dart +++ b/examples/login_posts_list/lib/posts/posts_screen.dart @@ -17,17 +17,16 @@ class _PostsScreenState extends State with // The mixin provides the connection between widget and ViewModel // It handles the lifecycle, state updates, and event dispatching - ViewModelMixin< + SimpleViewModelMixin< PostsScreen, PostsState, PostsEvent, - PostsEffect, PostsViewModel > { @override // Creates the ViewModel and immediately triggers a FetchPosts event // This is a good example of handling initial data loading in MVI - PostsViewModel provideViewModel() => + PostsViewModel createViewModel() => widget.viewModel()..addEvent(FetchPosts()); @override @@ -37,22 +36,23 @@ class _PostsScreenState extends State body: RefreshIndicator( // Event dispatching on user interaction (pull-to-refresh) onRefresh: () async => addEvent(FetchPosts()), - // Watch widget from signals_flutter automatically rebuilds this subtree - // whenever any signal it depends on changes (in this case, viewModel.state) - // This ensures our UI stays in sync with the ViewModel's state efficiently - child: Watch( - debugLabel: 'Posts Screen Content', - // AnimatedSwitcher provides smooth transitions between different states - (context) => AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - // Pattern matching on the state to render the appropriate UI - // This is a key concept in MVI where the UI is a pure function of state - child: switch (viewModel.state.value) { - PostsLoading() => const _LoadingIndicator(), - PostsLoaded(posts: final posts) => _PostList(posts: posts), - PostsError(error: final error) => _ErrorMessage(error: error), - }, - ), + // Using ViewModelListener to listen to state changes + // This is similar to Riverpod's Consumer or Signals' Watch widget + child: ValueListenableBuilder( + valueListenable: viewModel.state, + builder: (context, state, child) { + // AnimatedSwitcher provides smooth transitions between different states + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + // Pattern matching on the state to render the appropriate UI + // This is a key concept in MVI where the UI is a pure function of state + child: switch (state) { + PostsLoading() => const _LoadingIndicator(), + PostsLoaded(posts: final posts) => _PostList(posts: posts), + PostsError(error: final error) => _ErrorMessage(error: error), + }, + ); + }, ), ), ); diff --git a/example/lib/posts/view_model/posts_event.dart b/examples/login_posts_list/lib/posts/view_model/posts_event.dart similarity index 100% rename from example/lib/posts/view_model/posts_event.dart rename to examples/login_posts_list/lib/posts/view_model/posts_event.dart diff --git a/example/lib/posts/view_model/posts_state.dart b/examples/login_posts_list/lib/posts/view_model/posts_state.dart similarity index 100% rename from example/lib/posts/view_model/posts_state.dart rename to examples/login_posts_list/lib/posts/view_model/posts_state.dart diff --git a/example/lib/posts/view_model/posts_view_model.dart b/examples/login_posts_list/lib/posts/view_model/posts_view_model.dart similarity index 83% rename from example/lib/posts/view_model/posts_view_model.dart rename to examples/login_posts_list/lib/posts/view_model/posts_view_model.dart index 48ed6c8..05eb0c7 100644 --- a/example/lib/posts/view_model/posts_view_model.dart +++ b/examples/login_posts_list/lib/posts/view_model/posts_view_model.dart @@ -1,15 +1,12 @@ import 'package:example/posts/data/posts_repository.dart'; -import 'package:example/posts/view_model/posts_effect.dart'; import 'package:example/posts/view_model/posts_event.dart'; import 'package:example/posts/view_model/posts_state.dart'; import 'package:mvi/mvi.dart'; -export 'posts_effect.dart'; export 'posts_event.dart'; export 'posts_state.dart'; -final class PostsViewModel - extends BaseViewModel { +final class PostsViewModel extends SimpleViewModel { PostsViewModel({required PostsRepository postsRepository}) : _postsRepository = postsRepository, super(const PostsLoading(), debugLabel: 'PostsViewModel'); diff --git a/example/pubspec.yaml b/examples/login_posts_list/pubspec.yaml similarity index 99% rename from example/pubspec.yaml rename to examples/login_posts_list/pubspec.yaml index 2584f52..d2b73ca 100644 --- a/example/pubspec.yaml +++ b/examples/login_posts_list/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: flutter: sdk: flutter mvi: - path: ../ + path: ../../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/example/test/login/login_view_model_test.dart b/examples/login_posts_list/test/login/login_view_model_test.dart similarity index 100% rename from example/test/login/login_view_model_test.dart rename to examples/login_posts_list/test/login/login_view_model_test.dart diff --git a/example/test/posts/posts_view_model_test.dart b/examples/login_posts_list/test/posts/posts_view_model_test.dart similarity index 100% rename from example/test/posts/posts_view_model_test.dart rename to examples/login_posts_list/test/posts/posts_view_model_test.dart diff --git a/lib/mvi.dart b/lib/mvi.dart index 7ffdad4..f4d4039 100644 --- a/lib/mvi.dart +++ b/lib/mvi.dart @@ -1,6 +1,4 @@ -export 'src/base_view_model.dart'; +export 'src/mvi_base.dart'; +export 'src/view_models.dart'; export 'src/view_model_creator_type.dart'; export 'src/view_model_mixin.dart'; - -export 'package:signals_flutter/signals_flutter.dart' - show Watch, SignalsObserver; diff --git a/lib/src/base_view_model.dart b/lib/src/base_view_model.dart deleted file mode 100644 index 1c0f102..0000000 --- a/lib/src/base_view_model.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:signals_flutter/signals_flutter.dart'; - -/// Base class for all state objects in the application. -/// States should be immutable and contain all the data needed to render a UI. -/// In MVI, the state represents the Model component. -abstract class BaseState { - const BaseState(); - - /// Cast this state to a specific type. - T cast() => this as T; -} - -/// Base class for all events that can be dispatched to the ViewModel. -/// Events represent user actions or system events that should trigger state changes. -/// In MVI, events are Intents that express the user's intention. -abstract class BaseEvent { - const BaseEvent(); -} - -/// Base class for all effects that can be emitted by the ViewModel. -/// Effects represent one-time side effects like navigation, showing a snackbar, etc. -/// These are part of the MVI pattern for handling UI actions that don't affect state. -abstract class BaseEffect { - const BaseEffect(); -} - -/// Abstract base class for all ViewModels in the application. -/// Implements the MVI (Model-View-Intent) pattern with unidirectional data flow: -/// - Intent: User actions are captured as Events -/// - Model: State represents the UI state at any point in time -/// - View: UI observes the state and renders accordingly -/// -/// Generic parameters: -/// - S: The state type that extends [BaseState] (Model) -/// - E: The event type that extends [BaseEvent] (Intent) -/// - F: The effect type that extends [BaseEffect] (Side Effects) -abstract class BaseViewModel< - S extends BaseState, - E extends BaseEvent, - F extends BaseEffect -> { - /// Creates a new ViewModel with the given [initialState]. - /// If [debugLabel] is not null, it will be used to log information about the ViewModel. - BaseViewModel(S initialState, {String? debugLabel}) - : _state = Signal(initialState, debugLabel: debugLabel), - _debugLabel = debugLabel { - _eventsSubscription = _events.stream.listen(onEvent); - - if (_debugLabel != null) { - log('$_debugLabel: [Created]'); - } - - onInit(); - } - - /// Debug label for logging purposes. - final String? _debugLabel; - - /// Whether this ViewModel has been disposed. - bool _isDisposed = false; - bool get isDisposed => _isDisposed; - - /// The internal mutable state signal. - late final Signal _state; - - /// The public read-only state signal that UI components (View) can observe. - ReadonlySignal get state => _state.readonly(); - - /// Creates a derived signal that computes a value from the current state. - /// Use this to observe specific parts of the state. - ReadonlySignal select( - R Function(S state) selector, { - bool autoDispose = true, - String? debugLabel, - }) { - return computed( - () => selector(_state.value), - autoDispose: autoDispose, - debugLabel: debugLabel, - ); - } - - /// Stream controller for events (Intents) dispatched to this ViewModel. - final _events = StreamController(); - late final StreamSubscription _eventsSubscription; - - /// Stream controller for effects emitted by this ViewModel. - final _effects = StreamController(); - - /// Public stream of effects that UI components can listen to. - late final Stream effects = _effects.stream; - - /// Handle an event (Intent) dispatched to this ViewModel. - /// Subclasses must implement this method to process events and update state. - /// This is where the business logic resides that transforms intents into state changes. - @protected - void onEvent(E event); - - /// Updates the state (Model) of this ViewModel. - /// Throws an error if the ViewModel has been disposed. - @protected - // ignore: use_setters_to_change_properties - @visibleForTesting - void updateState(S newState) { - if (_isDisposed) { - throw StateError('Cannot update state of a disposed ViewModel'); - } - - if (_debugLabel != null && _state.value != newState) { - log('$_debugLabel: [Previous State] => ${_state.value}'); - log('$_debugLabel: [Current State] => $newState'); - } - - _state.value = newState; - } - - /// Dispatches an event (Intent) to this ViewModel. - /// The event will be processed by [onEvent]. - void addEvent(E event) { - if (_isDisposed) { - throw StateError('Cannot add event to a disposed ViewModel'); - } - - if (_debugLabel != null) { - log('$_debugLabel: [Event] => $event'); - } - - _events.add(event); - } - - /// Emits an effect from this ViewModel. - /// Effects are one-time occurrences like navigation or showing dialogs - /// that don't affect the state but trigger UI actions. - @protected - void addEffect(F effect) { - if (_isDisposed) { - throw StateError('Cannot add effect to a disposed ViewModel'); - } - - if (_debugLabel != null) { - log('$_debugLabel: [Effect] => $effect'); - } - - _effects.add(effect); - } - - /// Called when the ViewModel is initialized. - /// Override this method to perform initialization logic. - @protected - void onInit() { - // Override this method for initialization logic - } - - /// Called when the ViewModel is about to be disposed. - /// Override this method to perform cleanup logic. - @protected - void onDispose() { - // Override this method for cleanup logic - } - - /// Adds an error to the effect stream. This is only used for testing. - @protected - @visibleForTesting - void addEffectError(Object error) { - _effects.addError(error); - } - - /// Disposes the ViewModel, releasing all resources. - /// This cancels all subscriptions and closes all streams. - void dispose() { - onDispose(); - _state.dispose(); - _eventsSubscription.cancel(); - _events.close(); - _effects.close(); - _isDisposed = true; - - if (_debugLabel != null) { - log('$_debugLabel: [Disposed]'); - } - } -} diff --git a/lib/src/mvi_base.dart b/lib/src/mvi_base.dart new file mode 100644 index 0000000..d24ceae --- /dev/null +++ b/lib/src/mvi_base.dart @@ -0,0 +1,23 @@ +/// Base class for all state objects in the application. +/// States should be immutable and contain all the data needed to render a UI. +/// In MVI, the state represents the Model component. +abstract class BaseState { + const BaseState(); + + /// Cast this state to a specific type. + T cast() => this as T; +} + +/// Base class for all events that can be dispatched to the ViewModel. +/// Events represent user actions or system events that should trigger state changes. +/// In MVI, events are Intents that express the user's intention. +abstract class BaseEvent { + const BaseEvent(); +} + +/// Base class for all effects that can be emitted by the ViewModel. +/// Effects represent one-time side effects like navigation, showing a snackbar, etc. +/// These are part of the MVI pattern for handling UI actions that don't affect state. +abstract class BaseEffect { + const BaseEffect(); +} diff --git a/lib/src/view_model_creator_type.dart b/lib/src/view_model_creator_type.dart index bfdad10..b472768 100644 --- a/lib/src/view_model_creator_type.dart +++ b/lib/src/view_model_creator_type.dart @@ -1,4 +1,4 @@ -import 'base_view_model.dart'; +import 'view_models.dart'; /// A function type that creates and returns a new ViewModel instance. typedef ViewModelCreator = VM Function(); diff --git a/lib/src/view_model_mixin.dart b/lib/src/view_model_mixin.dart index 0ccdd13..6a1fbdb 100644 --- a/lib/src/view_model_mixin.dart +++ b/lib/src/view_model_mixin.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'base_view_model.dart'; +import 'mvi_base.dart'; +import 'view_models.dart'; /// A mixin that implements the View part of the MVI pattern for StatefulWidget classes. /// @@ -19,7 +20,7 @@ mixin ViewModelMixin< S extends BaseState, E extends BaseEvent, F extends BaseEffect, - VM extends BaseViewModel + VM extends ViewModel > on State { /// The ViewModel instance that manages state and business logic @@ -28,11 +29,11 @@ mixin ViewModelMixin< /// Subscription to the effects stream from the ViewModel late final StreamSubscription _effectSubscription; - /// Override this method to provide a ViewModel instance for this widget + /// Override this method to create a ViewModel instance for this widget /// /// This is where you should initialize and return your ViewModel @protected - VM provideViewModel(); + VM createViewModel(); /// Override this method to handle effects (side effects) from the ViewModel /// @@ -54,7 +55,7 @@ mixin ViewModelMixin< void initState() { super.initState(); // Initialize the ViewModel - viewModel = provideViewModel(); + viewModel = createViewModel(); // Subscribe to the effects stream _effectSubscription = viewModel.effects.listen( (effect) { @@ -84,3 +85,53 @@ mixin ViewModelMixin< viewModel.addEvent(event); } } + +/// A mixin that implements the View part of the MV pattern for StatefulWidget classes. +/// +/// This mixin handles the connection between a Flutter widget and its SimpleViewModel, +/// automatically subscribing to state changes. Use this for simple state management without effects. +/// +/// Generic parameters: +/// - T: The StatefulWidget type this mixin is applied to +/// - S: The state type that extends BaseState +/// - E: The event type that extends BaseEvent +/// - VM: The SimpleViewModel type that extends SimpleViewModel +mixin SimpleViewModelMixin< + T extends StatefulWidget, + S extends BaseState, + E extends BaseEvent, + VM extends SimpleViewModel +> + on State { + /// The ViewModel instance that manages state and business logic + late final VM viewModel; + + /// Override this method to create a ViewModel instance for this widget + /// + /// This is where you should initialize and return your ViewModel + @protected + VM createViewModel(); + + @override + void initState() { + super.initState(); + // Initialize the ViewModel + viewModel = createViewModel(); + } + + @override + void dispose() { + // Clean up resources when the widget is disposed + viewModel.dispose(); + super.dispose(); + } + + /// Dispatches an event to the ViewModel + /// + /// Use this method to send user actions or other events to the ViewModel + /// for processing. The ViewModel will update its state in response. + void addEvent(E event) { + if (!mounted || viewModel.isDisposed) return; + viewModel.addEvent(event); + } +} diff --git a/lib/src/view_models.dart b/lib/src/view_models.dart new file mode 100644 index 0000000..7f1ec0e --- /dev/null +++ b/lib/src/view_models.dart @@ -0,0 +1,260 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; + +import 'mvi_base.dart'; + +/// Base interface for all ViewModels. +abstract interface class BaseViewModel< + S extends BaseState, + E extends BaseEvent +> { + ValueListenable get state; + ValueListenable select( + R Function(S state) selector, { + String? debugLabel, + }); + void addEvent(E event); + bool get isDisposed; + void dispose(); +} + +/// Simple ViewModel without effects for basic state management. +/// Implements the MV (Model-View) pattern with unidirectional data flow: +/// - Model: State represents the UI state at any point in time +/// - View: UI observes the state and renders accordingly +/// +/// Generic parameters: +/// - S: The state type that extends [BaseState] (Model) +/// - E: The event type that extends [BaseEvent] (Intent) +abstract class SimpleViewModel + implements BaseViewModel { + /// Creates a new SimpleViewModel with the given [initialState]. + /// If [debugLabel] is not null, it will be used to log information about the ViewModel. + SimpleViewModel(S initialState, {String? debugLabel}) + : _state = ValueNotifier(initialState), + _debugLabel = debugLabel { + _eventsSubscription = _events.stream.listen(onEvent); + + if (_debugLabel != null) { + log('$_debugLabel: Created'); + } + + onInit(); + } + + /// Debug label for logging purposes. + final String? _debugLabel; + + /// Debug label for logging purposes. + @protected + String? get debugLabel => _debugLabel; + + /// Whether this ViewModel has been disposed. + bool _isDisposed = false; + + @override + bool get isDisposed => _isDisposed; + + /// The internal state holder that manages the reactive state. + late final ValueNotifier _state; + + /// Keeps track of all created selectors to ensure they're disposed. + final _selectors = <_DerivedValueListenable>[]; + + /// The public read-only state that UI components (View) can observe. + @override + ValueListenable get state => _state; + + /// Creates a ValueListenable that computes a value from the current state. + /// Use this to observe specific parts of the state. + /// The selector function will be called whenever the state changes. + @override + ValueListenable select( + R Function(S state) selector, { + String? debugLabel, + }) { + if (_isDisposed) { + throw StateError('Cannot select from a disposed ViewModel'); + } + + final derived = _DerivedValueListenable( + _state, + selector, + debugLabel: '$_debugLabel ($debugLabel)', + ); + + _selectors.add(derived); + + return derived; + } + + /// Stream controller for events (Intents) dispatched to this ViewModel. + final _events = StreamController(); + + /// Subscription to the events stream. + late final StreamSubscription _eventsSubscription; + + /// Handle an event (Intent) dispatched to this ViewModel. + /// Subclasses must implement this method to process events and update state. + /// This is where the business logic resides that transforms intents into state changes. + @protected + void onEvent(E event); + + /// Updates the state (Model) of this ViewModel. + /// Throws an error if the ViewModel has been disposed. + @protected + // ignore: use_setters_to_change_properties + @visibleForTesting + void updateState(S newState) { + if (_isDisposed) { + throw StateError('Cannot update state of a disposed ViewModel'); + } + + if (_debugLabel != null && _state.value != newState) { + log('$_debugLabel: Previous State => ${_state.value}'); + log('$_debugLabel: Current State => $newState'); + } + + _state.value = newState; + } + + /// Dispatches an event (Intent) to this ViewModel. + /// The event will be processed by [onEvent]. + @override + void addEvent(E event) { + if (_isDisposed) { + throw StateError('Cannot add event to a disposed ViewModel'); + } + + if (_debugLabel != null) { + log('$_debugLabel: Event => $event'); + } + + _events.add(event); + } + + /// Called when the ViewModel is initialized. + /// Override this method to perform initialization logic. + @protected + void onInit() { + // Override this method for initialization logic + } + + /// Called when the ViewModel is about to be disposed. + /// Override this method to perform cleanup logic. + @protected + void onDispose() { + // Override this method for cleanup logic + } + + /// Disposes the ViewModel, releasing all resources. + /// This cancels all subscriptions and closes all streams. + @override + void dispose() { + onDispose(); + + // Dispose all created selectors + for (final selector in _selectors) { + selector.dispose(); + } + _selectors.clear(); + + _state.dispose(); + _eventsSubscription.cancel(); + _events.close(); + _isDisposed = true; + + if (_debugLabel != null) { + log('$_debugLabel: Disposed'); + } + } +} + +/// Full-featured ViewModel with effects for complex state management. +/// Extends SimpleViewModel and adds effects support for the full MVI pattern. +/// +/// Generic parameters: +/// - S: The state type that extends [BaseState] (Model) +/// - E: The event type that extends [BaseEvent] (Intent) +/// - F: The effect type that extends [BaseEffect] (Side Effects) +abstract class ViewModel< + S extends BaseState, + E extends BaseEvent, + F extends BaseEffect +> + extends SimpleViewModel { + /// Creates a new ViewModel with the given [initialState]. + /// If [debugLabel] is not null, it will be used to log information about the ViewModel. + ViewModel(super.initialState, {super.debugLabel}); + + /// Stream controller for effects emitted by this ViewModel. + final _effects = StreamController(); + + /// Public stream of effects that UI components can listen to. + late final Stream effects = _effects.stream; + + /// Emits an effect from this ViewModel. + /// Effects are one-time occurrences like navigation or showing dialogs + /// that don't affect the state but trigger UI actions. + @protected + void addEffect(F effect) { + if (isDisposed) { + throw StateError('Cannot add effect to a disposed ViewModel'); + } + + if (debugLabel != null) { + log('$debugLabel: Effect => $effect'); + } + + _effects.add(effect); + } + + /// Adds an error to the effect stream. This is only used for testing. + @protected + @visibleForTesting + void addEffectError(Object error) { + _effects.addError(error); + } + + /// Disposes the ViewModel, releasing all resources. + /// This cancels all subscriptions and closes all streams. + @override + void dispose() { + _effects.close(); + super.dispose(); + } +} + +/// A ValueListenable that derives its value from another Listenable using a selector function. +class _DerivedValueListenable extends ValueNotifier { + _DerivedValueListenable(this._source, this._selector, {String? debugLabel}) + : _debugLabel = debugLabel, + super(_selector(_source.value)) { + _source.addListener(_onSourceChanged); + log('$_debugLabel: Created'); + } + + final ValueNotifier _source; + final R Function(S) _selector; + final String? _debugLabel; + + void _onSourceChanged() { + final newValue = _selector(_source.value); + if (_debugLabel != null && value != newValue) { + log('$_debugLabel: Previous State => $value'); + log('$_debugLabel: Current State => $newValue'); + } + value = newValue; + } + + @override + void dispose() { + _source.removeListener(_onSourceChanged); + super.dispose(); + if (_debugLabel != null) { + log('$_debugLabel: Disposed'); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 7e4499f..76b7eb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mvi description: "A Flutter package that applies the MVI architectural pattern to help manage app state and logic in a structured and maintainable way." -version: 1.0.6 +version: 2.0.0 homepage: https://github.com/devfelipereis/flutter-mvi environment: @@ -10,7 +10,6 @@ environment: dependencies: flutter: sdk: flutter - signals_flutter: ^6.0.2 dev_dependencies: flutter_test: diff --git a/test/view_model_mixin_test.dart b/test/view_model_mixin_test.dart index d946ba4..063a023 100644 --- a/test/view_model_mixin_test.dart +++ b/test/view_model_mixin_test.dart @@ -30,7 +30,7 @@ final class MessageEffect extends TestEffect { final String message; } -class TestViewModel extends BaseViewModel { +class TestViewModel extends ViewModel { TestViewModel() : super(const TestState()); bool eventHandled = false; @@ -94,7 +94,7 @@ class TestWidgetState extends State final List errors = []; @override - TestViewModel provideViewModel() => widget.viewModelCreator(); + TestViewModel createViewModel() => widget.viewModelCreator(); @override void onEffect(TestEffect effect, BuildContext context) { @@ -126,7 +126,9 @@ void main() { }); tearDown(() { - testViewModel.dispose(); + if (!testViewModel.isDisposed) { + testViewModel.dispose(); + } }); group('initialization', () { diff --git a/test/base_view_model_test.dart b/test/view_models_test.dart similarity index 90% rename from test/base_view_model_test.dart rename to test/view_models_test.dart index b862653..647b548 100644 --- a/test/base_view_model_test.dart +++ b/test/view_models_test.dart @@ -24,7 +24,7 @@ final class CounterEffect extends TestEffect { final String message; } -class TestViewModel extends BaseViewModel { +class TestViewModel extends ViewModel { TestViewModel() : super(const TestState(), debugLabel: 'TestViewModel'); bool initCalled = false; @@ -94,7 +94,7 @@ void main() { }); }); - group('BaseViewModel', () { + group('ViewModel', () { late TestViewModel viewModel; setUp(() { @@ -102,16 +102,18 @@ void main() { }); tearDown(() { - viewModel.dispose(); + if (!viewModel.isDisposed) { + viewModel.dispose(); + } }); test('should initialize with the provided initial state', () { const initialState = TestState(); final viewModel = TestViewModel(); - final state = viewModel.state.value; + final state = viewModel.state; - expect(state.value, equals(initialState.value)); + expect(state.value.value, equals(initialState.value)); expect(viewModel.initCalled, isTrue); }); @@ -120,14 +122,14 @@ void main() { await pumpEventQueue(); - final state = viewModel.state.value; - expect(state.value, equals(1)); + final state = viewModel.state; + expect(state.value.value, equals(1)); viewModel.decrement(); await pumpEventQueue(); - final newState = viewModel.state.value; - expect(newState.value, equals(0)); + final newState = viewModel.state; + expect(newState.value.value, equals(0)); }); test('should emit effects', () async {