Skip to content
Merged

V2 #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
61 changes: 35 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.

For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).

For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->

<p align="center">
<a href="https://pub.dev/packages/mvi">
<img alt="Pub Version" src="https://img.shields.io/pub/v/mvi">
Expand All @@ -28,45 +15,67 @@ 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

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

Expand Down
6 changes: 0 additions & 6 deletions example/lib/posts/view_model/posts_effect.dart

This file was deleted.

45 changes: 45 additions & 0 deletions examples/counter/.gitignore
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions examples/counter/.metadata
Original file line number Diff line number Diff line change
@@ -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'
37 changes: 37 additions & 0 deletions examples/counter/README.md
Original file line number Diff line number Diff line change
@@ -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.
File renamed without changes.
File renamed without changes.
44 changes: 44 additions & 0 deletions examples/counter/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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 = "../.."
}
45 changes: 45 additions & 0 deletions examples/counter/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="counter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.counter

import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity()
File renamed without changes.
Loading