Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ indent_size = 2

[*.md]
trim_trailing_whitespace = false

[packages/flutwire/test/fixtures/**]
# Bytes that a test compares exactly, with no final newline to add or trailing
# space to trim.
insert_final_newline = false
trim_trailing_whitespace = false
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The fixtures are bytes that tests compare exactly, so git never converts them.
packages/flutwire/test/fixtures/** -text
42 changes: 42 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,34 @@ jobs:
echo "::endgroup::"
done

# A version number is a promise about the public API, and nothing else checks
# it. Each package is compared with its latest release on pub.dev, and the job
# fails when the API breaks and the pubspec has not moved to a version that
# allows it: from 1.0 a removed or changed member needs the next major.
# STABILITY.md says what each kind of change means.
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: dart pub global activate dart_apitool 0.23.2
- name: Compare each package with its latest release
run: |
export PATH="$PATH:$HOME/.pub-cache/bin"
status=0
for pkg in flutwire flutwire_dio flutwire_http flutwire_cloud_functions; do
latest=$(curl -fsS "https://pub.dev/api/packages/$pkg" | jq -r .latest.version)
echo "::group::$pkg against $latest"
dart-apitool diff --old "pub://$pkg/$latest" --new "packages/$pkg" \
--version-check-mode onlyBreakingChanges || status=1
echo "::endgroup::"
done
exit $status

# The plugin has native halves that `flutter analyze` never reads. Building
# the example app is what compiles them, so a Kotlin or Swift error fails
# here and not on a stranger's machine.
Expand All @@ -114,6 +142,20 @@ jobs:
- run: flutter build apk --debug
working-directory: packages/flutwire/example

# The web has no dart:io, so it is where the adapters do the recording and
# where the example is compiled by a different compiler than anywhere else.
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build web
working-directory: packages/flutwire/example

# The Swift halves can only be compiled on a Mac.
apple:
runs-on: macos-latest
Expand Down
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,37 @@ board can be cross-checked against what actually shipped.

## [Unreleased]

## [1.0.0] - 2026-10-24

### Added

- A stability policy, `STABILITY.md`. It says what is public, what a version bump means for the API, the history on disk and the JSON export, how long a deprecated member stays, which versions get fixes, and which platforms and toolchains are supported. It also holds three promises about behaviour: recording does not change traffic unless `interception` is turned on, a release build records nothing unless it is asked to, and the default redaction masks credentials (M25)
- The history on disk records its layout in a file called `layout`. A folder with none is a new one, or one that 0.1.0 or 0.2.0 wrote, and is read as layout 1. A folder with a layout this release does not know is left exactly as it is while the history is kept in memory, where an older release would have deleted every summary it could not read (M40)
- A second tour in the README, with a 74 second video, and four more screenshots on the pub.dev page of `flutwire`, for what 0.2.0 added: two calls compared, the statistics, the dock, and the mocks and breakpoints with the marks they leave on the list. The page now shows ten screenshots, with the light list and the XML body making room (M37)

### Changed

- Every export of a package names what it makes public. `parseFormEncoded` and `prettyXml` were exported with the body model by accident and no longer are: `FlutwireBody.formFields` and `FlutwireBody.prettyText` are how to reach what they do (M25)
- `FlutwireCallRecorder.observeRequest` is removed. Nothing called it, and a client that has a request body as a stream reports it with `requestBody` (M25)
- `FlutwireStore`, `MemoryFlutwireStore` and `FlutwirePreferences` are `base` classes, so none can be implemented outside the package, and `FlutwireClient` in `flutwire_http` is `final`. None was meant to be implemented or extended by an app, and each can now gain a member without breaking one. `FlutwireBodyDecoder` stays an interface to implement. The API comparison does not see a change of class modifier, so it is listed here (M25)
- `FlutwireExportFormat.render` writes this package's version as the creator of a HAR where it wrote `0` (M25)
- On a screen narrower than 600 logical pixels the docked inspector always sits under the app, and its menu does not offer the right side there, because beside the app it left neither of them a usable width (M39)

### Fixed

- A Protobuf enum with a negative value showed as a number instead of its name, because a descriptor writes such a value as ten bytes and the reader dropped it (M38)
- A Protobuf float showed the 17 digits of the double it widens to, `0.10000000149011612` for `0.1`. It shows the shortest decimal that reads back as the same float (M38)
- The four numbers above the list ran into each other in a narrow panel and at a large text size. They stand in two rows of two when four do not fit, and the title of a selection shrinks to the room the buttons leave instead of being cut off (M39)

### Internal

- CI compares the public API of each package with its latest release on pub.dev, and fails when the API breaks and the pubspec has not moved to a version that allows it (M41)
- CI builds the example for the web, which no job did before (M39)
- The Protobuf decoder is tested against a descriptor set and messages that `protoc` and the code it generates wrote. Its other tests build their input by hand (M38)
- A history written by the 0.2.0 code, with a summary in the 0.1.0 shape, is read back in a test (M40)
- `integration_test/features_smoke_test.dart` records a call through `dart:io` and opens the comparison, the statistics and the dock on the platform it is started on. It was run on macOS and on an iOS simulator, and the example was run by hand on the web (M39)
- The example adds no button of its own where Flutwire floats one, so on the web the two no longer overlap (M39)

## [0.2.0] - 2026-10-24

### Added
Expand Down Expand Up @@ -66,6 +97,7 @@ board can be cross-checked against what actually shipped.
- The screens of the inspector work on an injected session instead of reading a global, which lets them be tested against a fake (M33)
- Analysis rules for open subscriptions and sinks, dropped futures, catch clauses that name no type, positional booleans and dynamic calls (M33)

[Unreleased]: https://github.com/NaCode-Studios/Flutwire/compare/v0.2.0...HEAD
[Unreleased]: https://github.com/NaCode-Studios/Flutwire/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/NaCode-Studios/Flutwire/compare/v0.2.0...v1.0.0
[0.2.0]: https://github.com/NaCode-Studios/Flutwire/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/NaCode-Studios/Flutwire/releases/tag/v0.1.0
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,16 @@ flutter analyze --fatal-infos
(cd packages/flutwire_cloud_functions && flutter test)
```

The Android and Apple halves are compiled by building the example app: `flutter build apk --debug`, `flutter build ios --simulator --no-codesign` and `flutter build macos --debug`, each from `packages/flutwire/example`.
The Android and Apple halves are compiled by building the example app: `flutter build apk --debug`, `flutter build ios --simulator --no-codesign` and `flutter build macos --debug`, each from `packages/flutwire/example`. The web has no native half, and `flutter build web` from the same folder is what compiles the example for it.

How the JSON tree behaves on a big response is measured on a device, in profile mode, by `integration_test/json_tree_performance_test.dart` in the example app. Its first lines say how to run it. CI does not run it, because it needs a device.
The public API is checked against the latest release on pub.dev, and a breaking change needs a new major version in the pubspec. To see what a change does to it:

```bash
dart pub global activate dart_apitool 0.23.2
dart-apitool diff --old pub://flutwire/<latest> --new packages/flutwire --version-check-mode onlyBreakingChanges
```

How the JSON tree behaves on a big response is measured on a device, in profile mode, by `integration_test/json_tree_performance_test.dart` in the example app, and `integration_test/features_smoke_test.dart` runs the comparison, the statistics and the dock on the platform it is started on. Their first lines say how to run them. CI runs neither, because they need a device.

## Commits and branches

Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ MaterialApp(builder: Flutwire.builder); // a button where there is no
</picture>
</p>

The inspector also sets two calls side by side, adds up the numbers of a history and sits in a panel beside the app. With interception turned on, a rule answers a request with a mock or holds it at a breakpoint.

<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="docs/media/flutwire-beyond-light.png">
<img src="docs/media/flutwire-beyond-dark.png" alt="Two calls side by side with each difference marked, the error rate and the slowest calls, the inspector docked under the app with a request held at a breakpoint, and a list where mocks and breakpoints mark the calls they touched" width="100%">
</picture><br>
<sub><a href="docs/media/flutwire-beyond.mp4">Watch it work</a>, 74 seconds on an Android emulator.</sub>
</p>

This is what the notification lists, and what a call becomes when it is shared as a command:

```text
Expand All @@ -66,7 +76,7 @@ Flutwire reads traffic and changes it only through the mocks and breakpoints the

In a release build `Flutwire.init` does nothing: no call is recorded, no file is written and no notification is posted. The plugin's native code still ships in the app, like any other plugin's, and stays idle until Flutwire starts: on iOS and macOS it leaves the app's notification delegate alone, and on Android its manifest adds the notification permission to the merged one.

> **Status — `0.2`, alpha.** Minor versions may still break the API until `1.0`. The plan is on [the project board](https://github.com/orgs/NaCode-Studios/projects/13).
> **Status — `1.0`, stable.** The public API follows [Semantic Versioning](STABILITY.md): a breaking change needs a new major version, and a deprecated member stays for at least two minor releases. The plan is on [the project board](https://github.com/orgs/NaCode-Studios/projects/13).

## Why Flutwire

Expand Down Expand Up @@ -121,11 +131,13 @@ Flutwire

## Roadmap

**Shipped (`1.0.0`).** A review of the public API, so that every export names what it makes public and the types that were never meant to be implemented are closed to it, a written [stability policy](STABILITY.md) with its deprecation window, a layout number on the history kept on disk, and a Protobuf decoder checked against what `protoc` writes. [`CHANGELOG.md`](CHANGELOG.md) has the detail.

**Shipped (`0.2.0`).** Editing a call and sending it again, the messages of a WebSocket and of an event stream, comparing two calls, statistics and a timeline, a dock beside the app, mocks and breakpoints, calls made in other isolates, Protobuf and MessagePack bodies, and a note above the list when calls may have gone unseen. [`CHANGELOG.md`](CHANGELOG.md) has the detail.

**Shipped (`0.1.0`).** Capture, the history on disk, the notification on Android, iOS and macOS, the inspector with the body viewer, export, the adapters and the example app.

**Next, `1.0.0`: stabilisation.** A review of the public API so that every public member is documented or made private, and a written stability policy with its deprecation window. gRPC capture comes after `1.0`. The ideas that were turned down, with the reason, are on the board too.
**Next.** gRPC capture. gRPC in Dart opens its own sockets, so the `HttpClient` wrapper cannot see it, and it needs an interceptor for the `grpc` package. It has no date yet. The ideas that were turned down, with the reason, are on the board too.

The plan lives on [the project board](https://github.com/orgs/NaCode-Studios/projects/13) and is mirrored as [milestones](https://github.com/NaCode-Studios/Flutwire/milestones).

Expand Down
65 changes: 65 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Stability policy

Flutwire follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). This document says what that means in practice: what is covered, what is not, and how long a deprecated API survives. It complements [the project board](https://github.com/orgs/NaCode-Studios/projects/13), where the project is going, and [`CHANGELOG.md`](CHANGELOG.md), which says what has shipped.

The four packages, `flutwire`, `flutwire_dio`, `flutwire_http` and `flutwire_cloud_functions`, are released together under one version number, and each adapter asks for the major version of `flutwire` that it was released with.

## What is public

The public API is what a package exports from its main library: `package:flutwire/flutwire.dart`, `package:flutwire_dio/flutwire_dio.dart`, `package:flutwire_http/flutwire_http.dart` and `package:flutwire_cloud_functions/flutwire_cloud_functions.dart`. Every export names what it makes public, so nothing is public only because it shares a file with something that is.

Anything under a `src/` folder that those libraries do not export may change in any release, and so may a member annotated `@internal`, `@visibleForTesting` or `@protected`. The `@protected` ones are hooks for the subclasses inside the package, not for yours. Three more things are not covered, because they are made to be looked at and not called:

- The look, the wording and the languages of the inspector, and what the notification says.
- The example app, and the native code behind the notification.
- The exact text of the plain text and cURL exports. A cURL command that stops repeating the call it describes is a bug, but how it is quoted may change.

CI compares each package with its latest release on pub.dev and fails when the public API breaks and the version in the pubspec does not allow it. The comparison does not see a change of class modifier, such as a class that stops being extendable, or a change in behaviour, so those are held to this document and to review.

### How the types may be used

`FlutwireBodyDecoder` is the one interface meant to be implemented: write one to show a format the inspector does not know. It will not gain a member before the next major.

Everything else is for reading and constructing, not for implementing or extending. The model classes and the configuration are `final`, and `FlutwireStore` and `FlutwirePreferences` are `base`, so a minor release can add a member to any of them without breaking a program that compiled against the one before. A new value may be added to an enum in a minor release, so a `switch` over one wants a `default` case.

## What a version bump means

| Change | Bump |
| --- | --- |
| A public member is removed or renamed, a parameter or a type changes incompatibly, a class stops being usable the way it was, or a promise on this page stops holding | major |
| A public member is added, an option is added with a default that keeps today's behaviour, a new kind of call is recorded | minor |
| A fix that brings behaviour back to what was documented | patch |

Some promises are about behaviour and not about names. Recording does not change traffic: the calls an app makes and the answers it gets are the same with Flutwire and without it. Mocks and breakpoints are the one exception, and they stay off until `FlutwireConfig.interception` is turned on, which no release will do by default. The only requests Flutwire makes itself are the ones a person edits and sends again. A release build records nothing unless `allowInRelease` is set, and no release will change that default. With the default redaction, the values of `authorization`, `proxy-authorization`, `cookie` and `set-cookie` are masked when a call is recorded. A release may add a field to `FlutwireRedaction.defaultHeaders`, and none will take one out before the next major.

### What is written to disk and to a file

The history on disk has a layout, a number kept in a file called `layout` in its folder, and it is `1`. A folder with no such file is a new one, or one that 0.1.0 or 0.2.0 wrote, which has layout `1` as well.

A release reads every layout up to its own. A minor or a patch does not change the layout: a field may be added to a summary, because a reader ignores what it does not know, and a field that a reader needs is not removed. A new layout comes only with a major release, which reads the layout of the one before it. A release that finds a layout newer than its own leaves the folder exactly as it is and keeps the history in memory, so going back to an older release never costs the history of a newer one.

The Flutwire JSON export carries `"flutwire": 1` at its top. A field may be added to it in a minor release, and none is removed or renamed before the next major, which is also the only release that changes that number. The HAR export follows HAR 1.2.

## Deprecation

A deprecated API survives at least two minor releases before removal, carries a deprecation notice naming its replacement, and is listed under `Deprecated` in [`CHANGELOG.md`](CHANGELOG.md) in the release that deprecates it. It is removed only in a major release.

## Supported versions

The latest minor release of the latest major receives fixes. An older minor does not: the answer to a bug in `1.2` is `1.3`.

## Platform and toolchain support

The packages need Dart 3.9 and Flutter 3.35, as their pubspecs say. CI runs the stable channel and, separately, the lowest versions of every dependency the constraints allow.

| Platform | What it gets |
| --- | --- |
| Android 7 (API 24) and later | The notification and the inspector |
| iOS 15 and later | The notification and the inspector |
| macOS 11 and later | The notification and the inspector |
| Windows and Linux | The inspector, opened by a floating button and a keyboard shortcut |
| Web | The same, with no `dart:io`, so calls are recorded by an adapter, and sending a call again, mocks and breakpoints are not offered |

The example app is built for Android, iOS, macOS and the web on every pull request. Windows and Linux need no native code, and their builds are not run.

Raising the minimum version of Dart, Flutter or one of those platforms is a minor release, and it is listed under `Changed` in [`CHANGELOG.md`](CHANGELOG.md).
Binary file added docs/media/flutwire-beyond-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/flutwire-beyond-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/flutwire-beyond.mp4
Binary file not shown.
4 changes: 4 additions & 0 deletions packages/flutwire/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.0.0

The first stable release. The public API follows [Semantic Versioning](https://github.com/NaCode-Studios/Flutwire/blob/main/STABILITY.md), every export names what it makes public, and the history kept on disk records its layout, so that a release that does not know a layout leaves it alone. A Protobuf enum with a negative value shows by name, and a float shows its shortest digits. The [repository changelog](https://github.com/NaCode-Studios/Flutwire/blob/main/CHANGELOG.md#100---2026-10-24) lists what it contains, and what was removed or closed to implementing.

## 0.2.0

Adds editing a call and sending it again, the messages of a WebSocket and of an event stream, comparing two calls, statistics and a timeline, a dock beside the app, mocks and breakpoints, recording of the calls made in other isolates, and Protobuf and MessagePack bodies. On iOS and macOS the plugin no longer takes over the app's notification delegate in a build that never starts Flutwire. The [repository changelog](https://github.com/NaCode-Studios/Flutwire/blob/main/CHANGELOG.md#020---2026-10-24) lists what it contains.
Expand Down
Loading
Loading