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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
pull_request:
push:
branches: [master]

jobs:
test:
name: Test & Analyze
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: stable

- name: Install dependencies
run: flutter pub get

- name: Analyze
run: flutter analyze lib/ test/ example/

- name: Format check
run: dart format --set-exit-if-changed .

- name: Test
run: flutter test --coverage --test-randomize-ordering-seed random
32 changes: 32 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Publish to pub.dev

on:
push:
tags:
- 'v*'

jobs:
publish:
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC pub.dev publishing

steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: stable

- name: Install dependencies
run: flutter pub get

- name: Analyze
run: flutter analyze lib/ test/

- name: Test
run: flutter test --test-randomize-ordering-seed random

- name: Publish
run: flutter pub publish --force
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## [3.2.0] - [April 9, 2026]

### Bug Fixes
* **Issue #114** — Fixed `showBadge: false` being ignored when a loop animation is active. `didUpdateWidget` now handles `showBadge` changes before any loop-animation guards.
* **Issue #130** — `showBadge: false` now hides the badge immediately. `_animationController.stop()` and `_appearanceController.stop()` are called before `reverse()` so the badge does not wait for the animation to play through.
* **Issue #98** — Fixed border anti-aliasing artifact. Border is now drawn entirely inside the circle via `strokeAlign: BorderSide.strokeAlignInside` in `BoxDecoration`, preventing background-colour bleed at antialiased edges.
* **Issue #115 (partial)** — Key-based animation re-trigger: setting a `Key` on `badgeContent` and changing it now restarts the animation for any widget type, not just `Text` and `Icon`.

### New Features
* **`BadgeStyle.copyWith`** — Returns a copy of a `BadgeStyle` with overridden fields.
* **`BadgePosition.centerStart`** — New named constructor: badge vertically centered on the start (left) side.
* **`BadgePosition.centerEnd`** — New named constructor: badge vertically centered on the end (right) side.
* **`BadgeState.animationController` / `appearanceController` getters** — Public access to the internal animation controllers for advanced use cases (PR #128).
* **`_BadgeVisual` private widget** — Extracted badge visual from an inner closure into a proper `StatelessWidget` so Flutter's element tree can cache it and avoid rebuilding the full subtree on every opacity tick (PR #120).
* **Minimum-square badge sizing** — Single-character text and small-icon badges now maintain correct circular/square proportions (PR #111).
* **`BadgeGradient.gradient()` asserts** — Added `assert` statements documenting constructor invariants instead of silently force-unwrapping nullable fields.
* **Controller duration updates** — `didUpdateWidget` now updates `AnimationController` durations when `badgeAnimation` duration properties change.

### Example App & Docs
* Fixed all lint warnings in `example/` (`use_super_parameters`, `prefer_final_fields`, `curly_braces_in_flow_control_structures`, `avoid_print`, `deprecated_member_use`).
* README: fixed showcase GIF height (`600px` → `400px`).
* README: added `hide Badge` import pattern (issue #123).
* Bumped `flutter_lints` to `^6.0.0`.

## [3.1.2] - [August 28, 2023]
* Update Dart SDK version. Update readme.

Expand Down
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Get dependencies
flutter pub get

# Run all tests (with randomized ordering)
flutter test --coverage --test-randomize-ordering-seed random

# Run a single test file
flutter test test/badges_test.dart

# Lint
flutter analyze .

# Format (CI enforces this — run before committing)
flutter format .
```

## Architecture

This is a Flutter package (`badges`) that provides a single `Badge` widget for overlaying notification-style badges on any widget.

### Public API (`lib/badges.dart`)

All public exports go through `lib/badges.dart`. Consumers import as:
```dart
import 'package:badges/badges.dart' as badges;
```
The `as badges` alias is required because Flutter 3.7+ added a `Badge` widget to Material that conflicts.

### Core widget (`lib/src/badge.dart`)

`Badge` is a `StatefulWidget` with `TickerProviderStateMixin`. It manages two `AnimationController`s:
- `_animationController` — drives the content-change animation (slide, scale, fade, size, rotation)
- `_appearanceController` — drives the fade in/out when `showBadge` toggles

The `didUpdateWidget` override is where animation re-triggering logic lives: it watches for changes to `badgeContent` (Text/Icon data), `badgeColor`, `showBadge`, and `loopAnimation`.

When `child` is null, the badge renders standalone. When `child` is provided, it uses a `Stack` with `BadgePositioned` to overlay the badge. When `onTap` is set, extra padding is added to the child and the position is recalculated via `CalculationUtils` to keep the full badge tappable.

### Configuration objects

- **`BadgeStyle`** — visual properties: shape, color, gradient, border, padding, elevation
- **`BadgeAnimation`** — named constructors per animation type (`.slide()`, `.fade()`, `.scale()`, `.size()`, `.rotation()`); each sets the `animationType` field and relevant defaults
- **`BadgePosition`** — named constructors (`topEnd`, `topStart`, `bottomEnd`, `bottomStart`, `center`) plus custom offsets
- **`BadgeGradient`** — named constructors (`.linear()`, `.radial()`, `.sweep()`) wrapping Flutter gradient types
- **`BadgeShape`** — enum: `circle`, `square`, `twitter`, `instagram`

### Custom shapes

`BadgeShape.twitter` and `BadgeShape.instagram` bypass the `Material`/`AnimatedContainer` path and use `CustomPaint` with their respective painters in `lib/src/painters/`. The `DrawingUtils.drawBadgeShape()` helper selects the correct painter.

### Internal utilities (not exported)

- `lib/src/utils/calculation_utils.dart` — computes padding and position adjustments for tappable badges
- `lib/src/utils/gradient_utils.dart` — gradient rendering helpers
- `lib/src/badge_border_gradient.dart` — custom `BoxBorder` subclass that paints a gradient border
- `lib/src/badge_gradient_type.dart` — internal enum used by `BadgeGradient`

### Tests (`test/`)

`badges_test.dart` is the entry point; it imports and calls group functions from `test/badge_animations_tests/`. Tests use `flutter_test` and wrap widgets in `MaterialApp` + `Scaffold`. Animation tests pump specific durations and check `hasRunningAnimations` to assert controller state.
126 changes: 126 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# flutter_badges — Renovation Plan

> Track progress across sessions. Check off items as they are completed.
> Current Flutter: **3.41.6** | Dart: **3.11.4** | Package version: **3.2.0**

---

## Phase 1 — Dependency & Tooling Updates ✅ Completed

- [x] **1.1** Bumped `flutter_lints` `^2.0.1` → `^6.0.0` in main package
- [x] **1.2** Example app: replaced `pedantic ^1.11.1` (discontinued) with `flutter_lints ^6.0.0`, removed unused `integration_test` dep, bumped `cupertino_icons ^1.0.4` → `^1.0.8`
- [x] **1.3** Updated flutter SDK lower bound `">=0.2.5"` → `">=3.10.0"` in both pubspec files (Flutter 3.10 is where Dart 3.0 landed)
- [x] **1.4** Ran `flutter pub upgrade`; lockfiles are gitignored so not committed
- [x] Fixed 3 `use_super_parameters` lint warnings introduced by the new lint rules (`Badge`, `BadgePositioned`, `TestWidgetScreen`)

---

## Phase 2 — Bug Fixes (Open Issues) ✅ Completed

- [x] **2.1** **Issue #114 — `showBadge` ignored when fade loop animation is active**
- Restructured `didUpdateWidget` so `showBadge` changes are handled **before** any loop-animation guards. The early-return that blocked `_appearanceController.reverse()` is gone.

- [x] **2.2** **Issue #130 — `showBadge` is slow to respond when animation is on**
- `_animationController.stop()` + `_appearanceController.stop()` are now called before `reverse()` when `showBadge` flips to false, so hiding is immediate.

- [x] **2.3** **Issue #98 — Border anti-aliasing artifact (thin inner border visible)**
- Border moved from `Material.shape` into `BoxDecoration` with `strokeAlign: BorderSide.strokeAlignInside`. Material shape uses `CircleBorder()` / `RoundedRectangleBorder()` with no side (elevation shadow only). `gradientBorder` → `boxBorder` typed as `BoxBorder?`.

- [x] **2.4** **Issue #115 — Animation not working (general)**
- Added key-based re-trigger: if `badgeContent` has a `Key` and it changes, the animation restarts. Handles all widget types beyond `Text` and `Icon`.

---

## Phase 3 — Open PRs Review ✅ Completed

- [x] **3.1** **PR #122 — Fix for issue #114 (showBadge + loop fade)**
- Our Phase 2.1 fix supersedes this PR. Cleaner restructuring of `didUpdateWidget`.

- [x] **3.2** **PR #120 — Replace private helper methods with private widgets**
- Extracted `badgeView()` inner closure into `_BadgeVisual` `StatelessWidget` in `badge.dart`. Flutter's element tree now caches it across appearance controller ticks.

- [x] **3.3** **PR #128 — Expose `animationController` and `appearanceController`**
- Added public getters `animationController` and `appearanceController` to `BadgeState`.

- [x] **3.4** **PR #111 — Fix display of square badge with small content size**
- Wrapped badge content in `ConstrainedBox` + `IntrinsicWidth` inside `_BadgeVisual` so single-character / small-icon badges stay proportional.

---

## Phase 4 — Code Quality & Improvements ✅ Completed

- [x] **4.1** **Fixed all 16 lint warnings in `example/`**
- `use_super_parameters` in alarm_app, flag_app, human_avatar, instagram_message, instagram_verified_account, twitter_verified_account, yako_app, test_screen
- `prefer_final_fields` in alarm_app (`_isLooped`)
- `deprecated_member_use` (`withOpacity` → `withValues`) in instagram_verified_account
- `use_key_in_widget_constructors` + `library_private_types_in_public_api` in main.dart
- `curly_braces_in_flow_control_structures` in test_screen.dart
- `avoid_print` in yako_app.dart (removed print)

- [x] **4.2** **Replaced `badgeView()` helper method with `_BadgeVisual` widget class** — covered by 3.2

- [x] **4.3** **Audited `didUpdateWidget`** — fully restructured with clear priority ordering; covered by 2.1

- [x] **4.4** **Controller getters** — covered by 3.3 (public `animationController` + `appearanceController` getters on `BadgeState`)

- [x] **4.5** **`BadgePosition` — added `centerStart` and `centerEnd` named constructors**

- [x] **4.6** **`BadgeStyle.copyWith`** — added `copyWith` method covering all 8 fields

- [x] **4.7** **`BadgeGradient.gradient()` asserts** — added `assert` statements in each `switch` case documenting constructor invariants instead of silently force-unwrapping

---

## Phase 5 — Example App ✅ Completed

- [x] **5.1** Fixed all 16 lint issues in `example/` — see 4.1 above
- [x] **5.2** Example app builds cleanly with no analyzer warnings

---

## Phase 6 — Tests ✅ Completed

**175 tests passing, 0 skipped.**

Changes made:
- **Rewrote `content_change_badge_animation_tests.dart`**: removed illegal direct calls to `state.didUpdateWidget()` — tests now use `TestWidgetScreen` with `setState` so the framework calls `didUpdateWidget` naturally.
- **Fixed `show_hide_badge_animation_tests.dart`**: renamed duplicate test name.
- **Cleaned `utils_tests.dart`**: removed unnecessary `async` keyword from unit tests.
- **Added new test groups to `badges_test.dart`**:
- `Badge without child`
- `showBadge false at initial render`
- `BadgePosition factory defaults`
- `Icon content change triggers animation`
- `Non-Text/non-Icon content change does not re-trigger animation`
- `Issue #114 regression` — now passing (was previously skipped)
- **Removed `skip: true`** from the issue #114 regression test after Phase 2 fix.

---

## Phase 7 — Documentation & README ✅ Completed

- [x] **7.1** Fixed README main GIF height: `600px` → `400px`
- [x] **7.2** Added `hide Badge` import pattern (Option 2) to README
- [x] **7.3** Bumped version: `3.1.2` → `3.2.0` in `pubspec.yaml` and README
- [x] **7.4** Updated `CHANGELOG.md` with all changes for 3.2.0

---

## Phase 8 — Release

- [x] **8.1** Version bump: `3.2.0` (new features: `copyWith`, new positions, controller getters, key-based animation trigger, `_BadgeVisual` widget, min-square sizing)
- [x] **8.2** All 175 tests passing
- [x] **8.3** `flutter analyze lib/ test/ example/` — 0 issues
- [x] **8.4** `dart format .` — all files formatted
- [ ] **8.5** Publish: `flutter pub publish` — intentionally skipped per task instructions

---

## Notes & Decisions Log

| Date | Decision |
|------|----------|
| 2026-04-09 | PR #122 superseded by cleaner Phase 2.1 restructure of `didUpdateWidget` |
| 2026-04-09 | PR #111 triangle shape deferred; only size-fix implemented |
| 2026-04-09 | `BadgeController` abstraction (issue #127 / PR #128) deferred; raw controller getters added as simpler API |
| 2026-04-09 | `copyWith` added to `BadgeStyle` only (not `BadgeAnimation` / `BadgePosition` — lower demand) |
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,29 @@
<img src="https://github.com/yako-dev/flutter_badges/blob/master/images/readme_header.png?raw=true">
</p>
<p align="center">
<img src="https://github.com/yako-dev/flutter_badges/blob/master/images/showcase.gif?raw=true" height="600px">
<img src="https://github.com/yako-dev/flutter_badges/blob/master/images/showcase.gif?raw=true" height="400px">
</p>


## Installing:
In your pubspec.yaml
```yaml
dependencies:
badges: ^3.1.2
badges: ^3.2.0
```
Attention! In Flutter 3.7 the Badge widget was introduced in the Material library, so to escape the ambiguous imports you need to import the package like this:

**Option 1: namespace prefix**
```dart
import 'package:badges/badges.dart' as badges;
```
and then use the "badges.Badge" widget instead of the "Badge" widget. The same for all the classes from this package.

**Option 2: hide Flutter's Material Badge widget**
```dart
import 'package:badges/badges.dart';
import 'package:flutter/material.dart' hide Badge;
```
<br>
<br>

Expand Down
6 changes: 2 additions & 4 deletions example/lib/alarm_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';

class AlarmApp extends StatefulWidget {
const AlarmApp({
Key? key,
}) : super(key: key);
const AlarmApp({super.key});

@override
State<AlarmApp> createState() => _AlarmAppState();
}

class _AlarmAppState extends State<AlarmApp> {
bool _isLooped = true;
final bool _isLooped = true;
int counter = 1;

@override
Expand Down
4 changes: 1 addition & 3 deletions example/lib/flag_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';

class FlagApp extends StatelessWidget {
const FlagApp({
Key? key,
}) : super(key: key);
const FlagApp({super.key});

@override
Widget build(BuildContext context) {
Expand Down
4 changes: 1 addition & 3 deletions example/lib/human_avatar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:badges/badges.dart' as badges;

class HumanAvatar extends StatefulWidget {
const HumanAvatar({
Key? key,
}) : super(key: key);
const HumanAvatar({super.key});

@override
State<HumanAvatar> createState() => _HumanAvatarState();
Expand Down
3 changes: 1 addition & 2 deletions example/lib/instagram_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import 'package:flutter/material.dart';

class InstagramMessage extends StatefulWidget {
const InstagramMessage(
{Key? key, required this.text, required this.emojiReaction})
: super(key: key);
{super.key, required this.text, required this.emojiReaction});

final String text;
final String emojiReaction;
Expand Down
4 changes: 2 additions & 2 deletions example/lib/instagram_verified_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';

class InstagramVerifiedAccount extends StatelessWidget {
const InstagramVerifiedAccount({Key? key}) : super(key: key);
const InstagramVerifiedAccount({super.key});

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: Colors.grey.withOpacity(0.2),
backgroundColor: Colors.grey.withValues(alpha: 0.2),
radius: 24,
child: CircleAvatar(
radius: 23,
Expand Down
Loading
Loading