From 4b85fb74ad0bc0a343827c92a0e8879e86c534b9 Mon Sep 17 00:00:00 2001 From: hunghd Date: Fri, 21 Aug 2026 11:33:48 +0700 Subject: [PATCH 1/3] Harden shimmer animation lifecycle and expand docs and tests. Keep period, enabled, and infinite loops in sync at runtime, and replace the single construction smoke test with coverage plus usage docs. Co-authored-by: Cursor --- .github/workflows/flutter.yml | 22 +-- CHANGELOG.md | 9 + README.md | 127 +++++++++++-- analysis_options.yaml | 8 +- docs/lessons/widget-tests.md | 16 ++ docs/optimization.md | 69 +++++++ docs/overview.md | 49 +++++ example/README.md | 29 +-- lib/main.dart | 125 ------------- lib/shimmer.dart | 121 ++++++++----- pubspec.yaml | 7 +- test/shimmer_test.dart | 328 +++++++++++++++++++++++++++++++++- 12 files changed, 692 insertions(+), 218 deletions(-) create mode 100644 docs/lessons/widget-tests.md create mode 100644 docs/optimization.md create mode 100644 docs/overview.md delete mode 100644 lib/main.dart diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 25690b3..9dbdd02 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -1,19 +1,15 @@ name: unit test -on: [push] +on: [push, pull_request] jobs: - build: - + test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 - with: - channel: beta - - run: flutter pub get - - run: flutter test + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: flutter analyze + - run: flutter test diff --git a/CHANGELOG.md b/CHANGELOG.md index 4575f67..3066823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ +## Unreleased + +* Keep the animation duration in sync when `period` changes, and restart cleanly when `enabled` is toggled back on +* Start infinite loops with `AnimationController.repeat()` instead of a first `forward()` cycle +* Treat `direction` updates as paint work, not layout work +* Expand widget tests for construction, animation, looping, and highlight geometry +* Refresh README, example README, and add `docs/` for architecture and optimization notes +* Remove leftover `lib/main.dart` counter app from the package + ## 3.0.0 * upgrade sdk constraint to support Dart 3 diff --git a/README.md b/README.md index 63be96f..39bfcbd 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,129 @@ # Shimmer -[![pub package](https://img.shields.io/pub/v/shimmer.svg)](https://pub.dartlang.org/packages/shimmer) ![](https://github.com/hnvn/flutter_shimmer/workflows/unit%20test/badge.svg) +[![pub package](https://img.shields.io/pub/v/shimmer.svg)](https://pub.dev/packages/shimmer) +![unit test](https://github.com/hnvn/flutter_shimmer/workflows/unit%20test/badge.svg) -A package provides an easy way to add shimmer effect in Flutter project +A lightweight Flutter widget that paints a moving highlight over placeholder +UI. Typical uses are skeleton screens while data loads, and a sliding highlight +on a call to action.

-## How to use +## Install + +```yaml +dependencies: + shimmer: ^3.0.0 +``` ```dart import 'package:shimmer/shimmer.dart'; +``` + +## Usage + +### Skeleton placeholder + +`Shimmer.fromColors` is the usual constructor. Build the child from solid +shapes (`Container`, `Row`, `Column`). The gradient replaces those colors; +transparent pixels stay transparent. + +```dart +Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Column( + children: [ + Container(height: 200, color: Colors.white), + const SizedBox(height: 16), + Container(height: 12, color: Colors.white), + const SizedBox(height: 8), + Container(height: 12, width: 200, color: Colors.white), + ], + ), +); +``` + +Dark theme: +```dart +Shimmer.fromColors( + baseColor: Colors.grey.shade800, + highlightColor: Colors.grey.shade600, + child: placeholder, +); ``` +### Custom gradient + +Use the default constructor when you need a `RadialGradient`, `SweepGradient`, +or a `LinearGradient` that follows `Theme`. + ```dart -SizedBox( - width: 200.0, - height: 100.0, - child: Shimmer.fromColors( - baseColor: Colors.red, - highlightColor: Colors.yellow, - child: Text( - 'Shimmer', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 40.0, - fontWeight: - FontWeight.bold, - ), - ), +Shimmer( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceContainerHighest, + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceContainerHighest, + ], + stops: const [0.35, 0.5, 0.65], ), + child: placeholder, ); +``` + +### Direction, speed, and loops +```dart +Shimmer.fromColors( + direction: ShimmerDirection.rtl, + period: const Duration(milliseconds: 1200), + loop: 0, // 0 = forever + enabled: isLoading, + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: placeholder, +); ``` + +| Parameter | Default | Role | +|-------------|----------------|------| +| `child` | required | Opaque area the highlight is blended onto | +| `gradient` | required\* | Highlight colors (`fromColors` builds this for you) | +| `direction` | `ltr` | `ltr`, `rtl`, `ttb`, `btt` | +| `period` | `1500ms` | Duration of one pass | +| `loop` | `0` | Passes before stopping; `0` repeats forever | +| `enabled` | `true` | `false` pauses the animation | + +\*Required on `Shimmer(...)`. `Shimmer.fromColors` takes `baseColor` and +`highlightColor` instead. + +## Performance + +- Wrap a **list of placeholders in one `Shimmer`**, not one `Shimmer` per row. +- Keep `child` simple and static. Fancy widgets (images, text with decoration, + elevation) often look wrong because the shader replaces their colors. +- Toggle `enabled` to `false` when loading finishes so the ticker stops. + +## Example + +The `example/` app shows a loading list and a “slide to unlock” highlight. +From the repository root: + +```bash +cd example && flutter run +``` + +## How it works + +`Shimmer` drives an `AnimationController` and paints a `ShaderMaskLayer` over +the child (`BlendMode.srcIn`). The highlight rectangle is three times the +child size so the band can travel fully across the widget. + +Project internals, tests, and further optimization notes live in +[`docs/overview.md`](docs/overview.md) and +[`docs/optimization.md`](docs/optimization.md). diff --git a/analysis_options.yaml b/analysis_options.yaml index 1c64a96..ad478b6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -29,9 +29,15 @@ analyzer: # Ignore analyzer hints for updating pubspecs when using Future or # Stream and not importing dart:async # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore exclude: - bin/cache/** + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** linter: rules: diff --git a/docs/lessons/widget-tests.md b/docs/lessons/widget-tests.md new file mode 100644 index 0000000..e3618e7 --- /dev/null +++ b/docs/lessons/widget-tests.md @@ -0,0 +1,16 @@ +# Lessons + +## Widget tests need Directionality for Text + +Pumping `Text` (or any `RichText`) under `Shimmer` alone fails with +`No Directionality widget found`. Wrap the widget under test in +`Directionality` (or `MaterialApp`) inside the test helper, even when +the production widget does not require it. + +## Do not assert animation completion at exactly `period` + +`AnimationController` may still be running after `pump(period)` because +the ticker starts on a later frame. For finite `loop` tests, use +`pumpAndSettle()`. To prove `period` updates without restarting, lengthen +the duration near the end of a cycle and assert it finishes in the +remaining fraction of the **new** duration. diff --git a/docs/optimization.md b/docs/optimization.md new file mode 100644 index 0000000..60fde07 --- /dev/null +++ b/docs/optimization.md @@ -0,0 +1,69 @@ +# Optimization notes + +Changes already applied in this pass are listed first. Items below that are +suggestions: they improve the package but change behavior, pub constraints, or +the public API enough to confirm before shipping. + +## Applied + +- Start infinite loops with `AnimationController.repeat()` instead of one + `forward()` cycle then `repeat()`. +- Update `controller.duration` when `period` changes. +- Restart cleanly when `enabled` goes from `false` to `true`, including after + a finite `loop` has finished. +- `direction` calls `markNeedsPaint()` rather than `markNeedsLayout()`. +- Extract `shimmerHighlightRect` so highlight travel can be unit-tested. +- Remove leftover `lib/main.dart` (default Flutter counter, not part of the + package API). + +## Rendering + +1. **One shimmer per screen, not per row.** The example already does this. + Document it in app code reviews: each `Shimmer` owns a ticker and a + compositing layer. +2. **Optional `RepaintBoundary` around `Shimmer`.** Isolates the shader + animation from ancestors. Apps can wrap it; adding it inside the package + can surprise layout that relies on parent layer merging. +3. **Reuse the `Shader` when only `percent` is unchanged.** Today + `createShader` runs every paint, which is required because the rect moves. + If `percent` and `size` are unchanged, skip shader creation (the existing + setters already avoid extra paints). +4. **Avoid `saveLayer` elsewhere in the child.** `BlendMode.srcIn` already + composites; extra opacity/save layers on the child increase GPU cost. + +## Animation + +5. **`loop == 1` vs `loop == 0`.** Finite loops still use `forward` plus a + status listener. That is correct; do not switch finite loops to `repeat` + with a counter unless you also handle `enabled` mid-cycle. +6. **Reset `_count` when `loop` shrinks** while the widget is still mounted + and enabled. Current code only restarts when the controller is idle. +7. **Honor `Duration.zero` / negative `loop`.** Guard with asserts so bad + values fail in debug instead of hanging a ticker. + +## API (confirm before adding) + +8. **Theme-aware defaults.** A `Shimmer.theme(context)` helper that reads + `ColorScheme` would reduce boilerplate for skeleton screens, but it is a + new constructor. +9. **`Semantics` / accessibility.** Announce “Loading” while `enabled` is + true. Must be opt-in so existing trees do not get duplicate semantics. +10. **`fromColors` diagonal gradient.** `begin: topLeft` and + `end: centerRight` is slightly diagonal. A true horizontal band would + use `Alignment.centerLeft` → `Alignment.centerRight`. Changing it would + visually break apps that depend on the current slant. + +## Project hygiene + +11. **CI** still used `actions/checkout@v1`, Java 12, and the Flutter beta + channel. Unit tests for this package do not need Android toolchains. +12. **`analysis_options.yaml`** is a dated Flutter-repo snapshot. Rules such + as `iterable_contains_unrelated_type` were removed from the linter. + Prefer `package:flutter_lints` once you are ready to fix new findings. +13. **`example` pubspec** is still named `new_example` with a default + description. Rename for pub.dev example scoring if you republish. +14. **LICENSE** text is the Dart project BSD header (Google Inc., 2013), not + a project-specific copyright. Confirm with the maintainer before editing. +15. **Minimum Flutter SDK** is `>=1.9.1`, which is far below what current + `super.key` / Material 3 examples need. Raising it documents reality and + unlocks newer Dart syntax. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..a1026e4 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,49 @@ +# Project overview + +Context for working on [`shimmer`](https://pub.dev/packages/shimmer), a Flutter +package that paints a moving highlight over placeholder UI. + +## Layout + +| Path | Role | +|------|------| +| `lib/shimmer.dart` | Entire public API (`Shimmer`, `ShimmerDirection`, `Shimmer.fromColors`) | +| `test/shimmer_test.dart` | Widget tests plus geometry tests for `shimmerHighlightRect` | +| `example/` | Sample app: loading list and slide-to-unlock | +| `example/lib/placeholders.dart` | Skeleton blocks used by the loading-list demo | + +There is no plugin/platform code. Painting is done with a `ShaderMaskLayer`. + +## Widget tree + +``` +Shimmer (StatefulWidget + AnimationController) + └── AnimatedBuilder + └── _Shimmer (SingleChildRenderObjectWidget) + └── _ShimmerFilter (RenderProxyBox) + └── ShaderMaskLayer (BlendMode.srcIn) +``` + +`AnimatedBuilder` receives `child: widget.child` so the placeholder subtree +is not rebuilt every tick. `_ShimmerFilter` only `markNeedsPaint()`s when +`percent`, `gradient`, or `direction` change. + +## Public API + +- `Shimmer(...)` — caller supplies a `Gradient`. +- `Shimmer.fromColors(...)` — builds a five-stop `LinearGradient` from + `baseColor` and `highlightColor`. +- `ShimmerDirection` — `ltr`, `rtl`, `ttb`, `btt`. +- `enabled` pauses the ticker; `loop: 0` repeats forever. + +Geometry for the sliding highlight is in `shimmerHighlightRect` (marked +`@visibleForTesting`). + +## Conventions + +- Match the existing style in `lib/shimmer.dart` (explicit types, named + arguments, short dartdoc on the public widget). +- Keep the public surface small. Do not add new widgets or dependencies + without an explicit request. +- SDK constraint is `>=2.17.0 <4.0.0`. Avoid Dart 3-only syntax in + `lib/` (no switch expressions, no records). diff --git a/example/README.md b/example/README.md index a82e13e..56af8f1 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,25 @@ -# new_example +# Shimmer example -A new Flutter project. +Demo app for the [`shimmer`](https://pub.dev/packages/shimmer) package. -## Getting Started +## Screens -This project is a starting point for a Flutter application. +- **Loading List** — one `Shimmer.fromColors` wrapping a column of skeleton + placeholders (`BannerPlaceholder`, `TitlePlaceholder`, `ContentPlaceholder`). +- **Slide To Unlock** — a highlight passing over a call-to-action row. -A few resources to get you started if this is your first Flutter project: +## Run -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +From this directory: -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +```bash +flutter pub get +flutter run +``` + +The app depends on the package via a path dependency in `pubspec.yaml`: + +```yaml +shimmer: + path: .. +``` diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index dda5554..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a blue toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } -} diff --git a/lib/shimmer.dart b/lib/shimmer.dart index 159e63a..574de7a 100644 --- a/lib/shimmer.dart +++ b/lib/shimmer.dart @@ -1,9 +1,9 @@ /// /// A package provides an easy way to add shimmer effect to Flutter application /// - library shimmer; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -104,7 +104,7 @@ class Shimmer extends StatefulWidget { ]); @override - _ShimmerState createState() => _ShimmerState(); + State createState() => _ShimmerState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -128,30 +128,54 @@ class _ShimmerState extends State with SingleTickerProviderStateMixin { void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.period) - ..addStatusListener((AnimationStatus status) { - if (status != AnimationStatus.completed) { - return; - } - _count++; - if (widget.loop <= 0) { - _controller.repeat(); - } else if (_count < widget.loop) { - _controller.forward(from: 0.0); - } - }); + ..addStatusListener(_onStatus); if (widget.enabled) { - _controller.forward(); + _start(); + } + } + + void _onStatus(AnimationStatus status) { + if (status != AnimationStatus.completed) { + return; + } + _count++; + if (widget.loop <= 0) { + _controller.repeat(); + } else if (_count < widget.loop) { + _controller.forward(from: 0.0); + } + } + + void _start() { + _count = 0; + if (widget.loop <= 0) { + _controller.repeat(); + } else { + _controller.forward(from: 0.0); } } @override void didUpdateWidget(Shimmer oldWidget) { - if (widget.enabled) { - _controller.forward(); - } else { + super.didUpdateWidget(oldWidget); + if (widget.period != oldWidget.period) { + _controller.duration = widget.period; + } + if (!widget.enabled) { _controller.stop(); + return; + } + if (!oldWidget.enabled) { + if (_controller.value == 0.0 || _controller.value == 1.0) { + _start(); + } else { + _controller.forward(); + } + return; + } + if (!_controller.isAnimating && widget.loop != oldWidget.loop) { + _start(); } - super.didUpdateWidget(oldWidget); } @override @@ -235,35 +259,18 @@ class _ShimmerFilter extends RenderProxyBox { return; } _direction = newDirection; - markNeedsLayout(); + markNeedsPaint(); } @override void paint(PaintingContext context, Offset offset) { if (child != null) { assert(needsCompositing); - - final double width = child!.size.width; - final double height = child!.size.height; - Rect rect; - double dx, dy; - if (_direction == ShimmerDirection.rtl) { - dx = _offset(width, -width, _percent); - dy = 0.0; - rect = Rect.fromLTWH(dx - width, dy, 3 * width, height); - } else if (_direction == ShimmerDirection.ttb) { - dx = 0.0; - dy = _offset(-height, height, _percent); - rect = Rect.fromLTWH(dx, dy - height, width, 3 * height); - } else if (_direction == ShimmerDirection.btt) { - dx = 0.0; - dy = _offset(height, -height, _percent); - rect = Rect.fromLTWH(dx, dy - height, width, 3 * height); - } else { - dx = _offset(-width, width, _percent); - dy = 0.0; - rect = Rect.fromLTWH(dx - width, dy, 3 * width, height); - } + final Rect rect = shimmerHighlightRect( + size: child!.size, + direction: _direction, + percent: _percent, + ); layer ??= ShaderMaskLayer(); layer! ..shader = _gradient.createShader(rect) @@ -274,8 +281,36 @@ class _ShimmerFilter extends RenderProxyBox { layer = null; } } +} - double _offset(double start, double end, double percent) { - return start + (end - start) * percent; +/// Sliding rectangle used as the shader bounds for the highlight. +/// +/// The band is three times the child's width or height so the highlight can +/// travel fully across the child while [percent] goes from `0.0` to `1.0`. +@visibleForTesting +Rect shimmerHighlightRect({ + required Size size, + required ShimmerDirection direction, + required double percent, +}) { + final double width = size.width; + final double height = size.height; + switch (direction) { + case ShimmerDirection.rtl: + final double dx = _offset(width, -width, percent); + return Rect.fromLTWH(dx - width, 0.0, 3 * width, height); + case ShimmerDirection.ttb: + final double dy = _offset(-height, height, percent); + return Rect.fromLTWH(0.0, dy - height, width, 3 * height); + case ShimmerDirection.btt: + final double dy = _offset(height, -height, percent); + return Rect.fromLTWH(0.0, dy - height, width, 3 * height); + case ShimmerDirection.ltr: + final double dx = _offset(-width, width, percent); + return Rect.fromLTWH(dx - width, 0.0, 3 * width, height); } } + +double _offset(double start, double end, double percent) { + return start + (end - start) * percent; +} diff --git a/pubspec.yaml b/pubspec.yaml index 6d43fe5..2f9783d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,15 @@ name: shimmer -description: A package provides an easy way to add shimmer effect in Flutter project +description: A lightweight Flutter widget that paints a moving highlight over skeleton placeholders and call-to-action text. version: 3.0.0 repository: https://github.com/hnvn/flutter_shimmer issue_tracker: https://github.com/hnvn/flutter_shimmer/issues contributors: Gregor Weber , Vasilliy Ditsyask homepage: https://github.com/hnvn/flutter_shimmer +topics: + - animation + - loading + - shimmer + - ui environment: sdk: '>=2.17.0 <4.0.0' diff --git a/test/shimmer_test.dart b/test/shimmer_test.dart index 3114794..0154a24 100644 --- a/test/shimmer_test.dart +++ b/test/shimmer_test.dart @@ -1,16 +1,330 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shimmer/shimmer.dart'; void main() { - testWidgets('Shimmer.fromColors() can be constructed', - (WidgetTester tester) async { - await tester.pumpWidget(Shimmer.fromColors( - child: Container( + const Color baseColor = Color(0xFFFF0000); + const Color highlightColor = Color(0xFFFFFF00); + + Widget buildShimmer({ + Key? key, + Widget? child, + Duration period = const Duration(milliseconds: 1500), + ShimmerDirection direction = ShimmerDirection.ltr, + int loop = 0, + bool enabled = true, + Gradient? gradient, + }) { + final Widget content = child ?? + const SizedBox( + key: Key('shimmer-child'), width: 100.0, height: 100.0, - ), - baseColor: Colors.red, - highlightColor: Colors.yellow)); + ); + final Widget shimmer = gradient != null + ? Shimmer( + key: key, + gradient: gradient, + period: period, + direction: direction, + loop: loop, + enabled: enabled, + child: content, + ) + : Shimmer.fromColors( + key: key, + baseColor: baseColor, + highlightColor: highlightColor, + period: period, + direction: direction, + loop: loop, + enabled: enabled, + child: content, + ); + return Directionality( + textDirection: TextDirection.ltr, + child: shimmer, + ); + } + + testWidgets('Shimmer.fromColors() can be constructed', + (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer()); + expect(find.byType(Shimmer), findsOneWidget); + }); + + testWidgets('default constructor accepts a custom gradient', + (WidgetTester tester) async { + const LinearGradient gradient = LinearGradient( + colors: [Colors.black, Colors.white], + ); + + await tester.pumpWidget(buildShimmer(gradient: gradient)); + + final Shimmer shimmer = tester.widget(find.byType(Shimmer)); + expect(shimmer.gradient, gradient); + }); + + testWidgets('fromColors builds a five-stop linear gradient', + (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer()); + + final Shimmer shimmer = tester.widget(find.byType(Shimmer)); + expect(shimmer.gradient, isA()); + final LinearGradient gradient = shimmer.gradient as LinearGradient; + expect(gradient.colors, const [ + baseColor, + baseColor, + highlightColor, + baseColor, + baseColor, + ]); + expect(gradient.stops, const [0.0, 0.35, 0.5, 0.65, 1.0]); + }); + + testWidgets('renders the provided child', (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer( + child: const Text('Loading'), + )); + + expect(find.text('Loading'), findsOneWidget); + }); + + testWidgets('uses expected defaults', (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer()); + + final Shimmer shimmer = tester.widget(find.byType(Shimmer)); + expect(shimmer.direction, ShimmerDirection.ltr); + expect(shimmer.period, const Duration(milliseconds: 1500)); + expect(shimmer.loop, 0); + expect(shimmer.enabled, isTrue); + }); + + testWidgets('animates when enabled', (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer()); + await tester.pump(); + + expect(tester.hasRunningAnimations, isTrue); }); + + testWidgets('does not animate when disabled', (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer(enabled: false)); + await tester.pump(); + + expect(tester.hasRunningAnimations, isFalse); + }); + + testWidgets('pauses and resumes when enabled changes', + (WidgetTester tester) async { + const Key key = Key('shimmer'); + await tester.pumpWidget(buildShimmer(key: key)); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + + await tester.pumpWidget(buildShimmer(key: key, enabled: false)); + await tester.pump(); + expect(tester.hasRunningAnimations, isFalse); + + await tester.pumpWidget(buildShimmer(key: key)); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + }); + + testWidgets('keeps running after the first cycle when loop is 0', + (WidgetTester tester) async { + await tester.pumpWidget( + buildShimmer(period: const Duration(milliseconds: 50)), + ); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + + await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + }); + + testWidgets('stops after the requested number of loops', + (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer( + period: const Duration(milliseconds: 50), + loop: 2, + )); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + + await tester.pumpAndSettle(); + expect(tester.hasRunningAnimations, isFalse); + }); + + testWidgets('restarts a finished loop when enabled is toggled back on', + (WidgetTester tester) async { + const Key key = Key('shimmer'); + await tester.pumpWidget(buildShimmer( + key: key, + loop: 1, + period: const Duration(milliseconds: 50), + )); + await tester.pumpAndSettle(); + expect(tester.hasRunningAnimations, isFalse); + + await tester.pumpWidget(buildShimmer( + key: key, + loop: 1, + period: const Duration(milliseconds: 50), + enabled: false, + )); + await tester.pumpWidget(buildShimmer( + key: key, + loop: 1, + period: const Duration(milliseconds: 50), + )); + await tester.pump(); + expect(tester.hasRunningAnimations, isTrue); + }); + + testWidgets('applies a new period without restarting the animation', + (WidgetTester tester) async { + const Key key = Key('shimmer'); + await tester.pumpWidget(buildShimmer( + key: key, + loop: 1, + period: const Duration(milliseconds: 500), + )); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + expect(tester.hasRunningAnimations, isTrue); + + await tester.pumpWidget(buildShimmer( + key: key, + loop: 1, + period: const Duration(milliseconds: 1000), + )); + await tester.pump(const Duration(milliseconds: 250)); + expect(tester.hasRunningAnimations, isFalse); + }); + + testWidgets('does not rebuild the child on every animation tick', + (WidgetTester tester) async { + final _BuildCounter counter = _BuildCounter(); + + await tester.pumpWidget(buildShimmer(child: _BuildTracker(counter: counter))); + await tester.pump(); + final int buildsAfterMount = counter.builds; + + await tester.pump(const Duration(milliseconds: 100)); + expect(counter.builds, buildsAfterMount); + }); + + testWidgets('debugFillProperties includes shimmer configuration', + (WidgetTester tester) async { + await tester.pumpWidget(buildShimmer( + direction: ShimmerDirection.rtl, + loop: 3, + enabled: false, + )); + + final Shimmer shimmer = tester.widget(find.byType(Shimmer)); + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + shimmer.debugFillProperties(builder); + + final Map values = { + for (final DiagnosticsNode node in builder.properties) + if (node.name != null) node.name!: node.toDescription(), + }; + + expect(values['direction'], contains('rtl')); + expect(values['loop'], '3'); + expect(values['enabled'], 'false'); + expect(values['period'], isNotNull); + expect(values['gradient'], isNotNull); + }); + + group('shimmerHighlightRect', () { + const Size size = Size(100.0, 40.0); + + test('ltr travels from left to right', () { + final Rect start = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.ltr, + percent: 0.0, + ); + final Rect end = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.ltr, + percent: 1.0, + ); + + expect(start, const Rect.fromLTWH(-200.0, 0.0, 300.0, 40.0)); + expect(end, const Rect.fromLTWH(0.0, 0.0, 300.0, 40.0)); + expect(end.left, greaterThan(start.left)); + }); + + test('rtl travels from right to left', () { + final Rect start = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.rtl, + percent: 0.0, + ); + final Rect end = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.rtl, + percent: 1.0, + ); + + expect(start, const Rect.fromLTWH(0.0, 0.0, 300.0, 40.0)); + expect(end, const Rect.fromLTWH(-200.0, 0.0, 300.0, 40.0)); + expect(end.left, lessThan(start.left)); + }); + + test('ttb travels from top to bottom', () { + final Rect start = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.ttb, + percent: 0.0, + ); + final Rect end = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.ttb, + percent: 1.0, + ); + + expect(start, const Rect.fromLTWH(0.0, -80.0, 100.0, 120.0)); + expect(end, const Rect.fromLTWH(0.0, 0.0, 100.0, 120.0)); + expect(end.top, greaterThan(start.top)); + }); + + test('btt travels from bottom to top', () { + final Rect start = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.btt, + percent: 0.0, + ); + final Rect end = shimmerHighlightRect( + size: size, + direction: ShimmerDirection.btt, + percent: 1.0, + ); + + expect(start, const Rect.fromLTWH(0.0, 0.0, 100.0, 120.0)); + expect(end, const Rect.fromLTWH(0.0, -80.0, 100.0, 120.0)); + expect(end.top, lessThan(start.top)); + }); + }); +} + +class _BuildCounter { + int builds = 0; +} + +class _BuildTracker extends StatelessWidget { + const _BuildTracker({required this.counter}); + + final _BuildCounter counter; + + @override + Widget build(BuildContext context) { + counter.builds++; + return const SizedBox(width: 100.0, height: 100.0); + } } From ad7cd7d514366883db0a35933d84c011987c7e7b Mon Sep 17 00:00:00 2001 From: hunghd Date: Fri, 21 Aug 2026 11:38:56 +0700 Subject: [PATCH 2/3] Migrate to the standalone material_ui package. Flutter 3.47 decoupled Material from the SDK; this major bump requires Flutter 3.44+ so consumers track the new design library. Co-authored-by: Cursor --- CHANGELOG.md | 3 +- README.md | 6 +- analysis_options.yaml | 4 - docs/lessons/material-ui-migration.md | 18 ++++ docs/optimization.md | 5 +- docs/overview.md | 8 +- example/README.md | 3 +- example/lib/main.dart | 2 +- example/lib/placeholders.dart | 2 +- example/pubspec.lock | 117 +++++++++++++++++++------- example/pubspec.yaml | 8 +- lib/shimmer.dart | 2 +- pubspec.yaml | 7 +- test/shimmer_test.dart | 2 +- 14 files changed, 131 insertions(+), 56 deletions(-) create mode 100644 docs/lessons/material-ui-migration.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3066823..b509f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -## Unreleased +## 4.0.0 +* **BREAKING:** Use the standalone [`material_ui`](https://pub.dev/packages/material_ui) package instead of `package:flutter/material.dart`. Requires Flutter `>=3.44.0` and Dart `^3.12.0`. * Keep the animation duration in sync when `period` changes, and restart cleanly when `enabled` is toggled back on * Start infinite loops with `AnimationController.repeat()` instead of a first `forward()` cycle * Treat `direction` updates as paint work, not layout work diff --git a/README.md b/README.md index 39bfcbd..158e5d4 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,14 @@ on a call to action. ```yaml dependencies: - shimmer: ^3.0.0 + shimmer: ^4.0.0 + material_ui: ^1.0.1 ``` +`shimmer` 4.0 uses Flutter's standalone [`material_ui`](https://pub.dev/packages/material_ui) package (Flutter 3.44+). Apps that still import `package:flutter/material.dart` can wrap those subtrees in `MaterialUiCompatibilityBridge`. + ```dart +import 'package:material_ui/material_ui.dart'; import 'package:shimmer/shimmer.dart'; ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index ad478b6..f491756 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -58,7 +58,6 @@ linter: - avoid_field_initializers_in_const_classes - avoid_function_literals_in_foreach_calls - avoid_init_to_null - - avoid_null_checks_in_equality_operators - avoid_private_typedef_functions - avoid_relative_lib_imports - avoid_renaming_method_parameters @@ -93,12 +92,10 @@ linter: - hash_and_equals - implementation_imports # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 - - iterable_contains_unrelated_type # - join_return_with_assignment # not yet tested - library_names - library_prefixes # - lines_longer_than_80_chars # not yet tested - - list_remove_unrelated_type # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 - no_adjacent_strings_in_list - no_duplicate_case_values @@ -108,7 +105,6 @@ linter: # - one_member_abstracts # too many false positives # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 - overridden_fields - - package_api_docs - package_names - package_prefixed_library_names # - parameter_assignments # we do this commonly diff --git a/docs/lessons/material-ui-migration.md b/docs/lessons/material-ui-migration.md new file mode 100644 index 0000000..ca2bad0 --- /dev/null +++ b/docs/lessons/material-ui-migration.md @@ -0,0 +1,18 @@ +# Lessons: material_ui migration + +## Add the package before trusting dart fix pubspec edits + +`dart fix --apply --code=migrate_design_widgets` rewrites imports correctly, but +it may add `material_ui: any` to `example/pubspec.yaml`. Replace that with a +semver range (`^1.0.1`) after `flutter pub add material_ui`. + +## Import order + +The fix inserts `package:material_ui/material_ui.dart` where +`package:flutter/material.dart` was. Re-sort so `package:flutter/...` imports +stay together above `material_ui` (`directives_ordering`). + +## Package major version + +Treat the move off SDK Material as a breaking release. Consumers on Flutter +older than 3.44 cannot resolve `material_ui` 1.x. diff --git a/docs/optimization.md b/docs/optimization.md index 60fde07..4768f96 100644 --- a/docs/optimization.md +++ b/docs/optimization.md @@ -64,6 +64,5 @@ the public API enough to confirm before shipping. description. Rename for pub.dev example scoring if you republish. 14. **LICENSE** text is the Dart project BSD header (Google Inc., 2013), not a project-specific copyright. Confirm with the maintainer before editing. -15. **Minimum Flutter SDK** is `>=1.9.1`, which is far below what current - `super.key` / Material 3 examples need. Raising it documents reality and - unlocks newer Dart syntax. +15. **Minimum Flutter SDK** was raised to `>=3.44.0` (Dart `^3.12.0`) with + the `material_ui` 4.0.0 migration. diff --git a/docs/overview.md b/docs/overview.md index a1026e4..efaca82 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -14,6 +14,8 @@ package that paints a moving highlight over placeholder UI. There is no plugin/platform code. Painting is done with a `ShaderMaskLayer`. +Design widgets come from the standalone [`material_ui`](https://pub.dev/packages/material_ui) package (`package:material_ui/material_ui.dart`), not `package:flutter/material.dart`. + ## Widget tree ``` @@ -45,5 +47,7 @@ Geometry for the sliding highlight is in `shimmerHighlightRect` (marked arguments, short dartdoc on the public widget). - Keep the public surface small. Do not add new widgets or dependencies without an explicit request. -- SDK constraint is `>=2.17.0 <4.0.0`. Avoid Dart 3-only syntax in - `lib/` (no switch expressions, no records). +- SDK constraint is Dart `^3.12.0` and Flutter `>=3.44.0` (required by + `material_ui` 1.x). +- Import Material from `package:material_ui/material_ui.dart`. Do not add + `package:flutter/material.dart` back. diff --git a/example/README.md b/example/README.md index 56af8f1..47508f9 100644 --- a/example/README.md +++ b/example/README.md @@ -17,7 +17,8 @@ flutter pub get flutter run ``` -The app depends on the package via a path dependency in `pubspec.yaml`: +The demo imports Material from `package:material_ui/material_ui.dart` (Flutter +3.44+). The app depends on the package via a path dependency in `pubspec.yaml`: ```yaml shimmer: diff --git a/example/lib/main.dart b/example/lib/main.dart index e6f8ded..bb28dbb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:shimmer/shimmer.dart'; import 'placeholders.dart'; diff --git a/example/lib/placeholders.dart b/example/lib/placeholders.dart index 46c0e7f..2decdfe 100644 --- a/example/lib/placeholders.dart +++ b/example/lib/placeholders.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; class BannerPlaceholder extends StatelessWidget { const BannerPlaceholder({Key? key}) : super(key: key); diff --git a/example/pubspec.lock b/example/pubspec.lock index 3fd0b9b..45a282b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,26 +21,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -49,14 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + cupertino_ui: + dependency: transitive + description: + name: cupertino_ui + sha256: "7ed8ce4159d342eec4c65f4ea6eec57adaf9365404378541f38efc1da20a5b3d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -70,19 +78,48 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - js: + intl: + dependency: transitive + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "3.0.2" lints: dependency: transitive description: @@ -95,46 +132,54 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.20" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.13.0" + material_ui: + dependency: "direct main" + description: + name: material_ui + sha256: "4f3f38b9953df0a87d6bf5f21880029f77c47048487d5339410c39936be4683b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.19.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.1" shimmer: dependency: "direct main" description: path: ".." relative: true source: path - version: "3.0.0" + version: "4.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -147,18 +192,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -179,18 +224,26 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.7.12" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.4.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" sdks: - dart: ">=3.0.1 <4.0.0" - flutter: ">=1.9.1" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 31bae2e..ce47ff9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,17 +5,15 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.0.1 <4.0.0' + sdk: ^3.12.0 dependencies: + cupertino_icons: ^1.0.2 flutter: sdk: flutter - - cupertino_icons: ^1.0.2 - + material_ui: ^1.0.1 shimmer: path: .. - dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/shimmer.dart b/lib/shimmer.dart index 574de7a..bc31d73 100644 --- a/lib/shimmer.dart +++ b/lib/shimmer.dart @@ -4,8 +4,8 @@ library shimmer; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:material_ui/material_ui.dart'; /// /// An enum defines all supported directions of shimmer effect diff --git a/pubspec.yaml b/pubspec.yaml index 2f9783d..529cc8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shimmer description: A lightweight Flutter widget that paints a moving highlight over skeleton placeholders and call-to-action text. -version: 3.0.0 +version: 4.0.0 repository: https://github.com/hnvn/flutter_shimmer issue_tracker: https://github.com/hnvn/flutter_shimmer/issues contributors: Gregor Weber , Vasilliy Ditsyask @@ -12,12 +12,13 @@ topics: - ui environment: - sdk: '>=2.17.0 <4.0.0' - flutter: '>=1.9.1' + sdk: ^3.12.0 + flutter: '>=3.44.0' dependencies: flutter: sdk: flutter + material_ui: ^1.0.1 dev_dependencies: flutter_test: diff --git a/test/shimmer_test.dart b/test/shimmer_test.dart index 0154a24..181a6a7 100644 --- a/test/shimmer_test.dart +++ b/test/shimmer_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:shimmer/shimmer.dart'; void main() { From d3d2b20a424e60ab579ab622222eb7dc724ed380 Mon Sep 17 00:00:00 2001 From: hunghd Date: Fri, 21 Aug 2026 11:41:07 +0700 Subject: [PATCH 3/3] Align the example iOS project with Flutter 3.47. Raise the iOS deployment target, adopt UIScene, and ignore generated SwiftPM/build paths so the demo matches the new SDK floor. Co-authored-by: Cursor --- example/.gitignore | 2 ++ example/analysis_options.yaml | 9 ++++++ example/ios/Flutter/AppFrameworkInfo.plist | 2 -- example/ios/Runner.xcodeproj/project.pbxproj | 8 ++--- .../xcshareddata/xcschemes/Runner.xcscheme | 5 +++- example/ios/Runner/AppDelegate.swift | 11 ++++--- example/ios/Runner/Info.plist | 29 ++++++++++++++++--- 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/example/.gitignore b/example/.gitignore index 24476c5..6c31954 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..5bee4ff 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,6 +7,15 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9625e10..391a902 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 11.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index b53e5b7..2b24ec4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -344,7 +344,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -472,7 +472,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -521,7 +521,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e42adcb..e3773d4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..c30b367 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,16 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index cf6e68e..db3cb66 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -43,9 +68,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents -