Convert expressive_loading_indicator_custom from submodule to normal folder

This commit is contained in:
Pranshul committed 2025-09-12 15:51:01 +05:30
1 parent 91a9f9d8ad
commit b4e69a3ee2
13 files changed
+938 -2

No files matched your search

@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "edada7c56edf4a183c1735310e123c7f923584f1"
channel: "stable"
project_type: package
@@ -0,0 +1,3 @@
## 0.0.1
* Initial release of the ExpressiveLoadingIndicator package
@@ -0,0 +1,8 @@
Copyright 2025 Tamim Arafat
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,20 @@
This product includes software derived from the Android Open Source Project.
Specifically, the following files are Dart ports of AOSP Kotlin components:
- lib/expressive_loading_indicator.dart (derived from compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt)
The Android Open Source Project is licensed under the Apache License, Version 2.0.
You may obtain a copy of the License at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Copyright 2024 The Android Open Source Project
Additionally, this product includes software developed by Tamim Arafat
and licensed under the MIT License.
@@ -0,0 +1,75 @@
# Expressive Loading Indicator
> Archival notice: Refer to the implementation at [ksokolovskyi/material_loading_indicator](https://github.com/ksokolovskyi/material_loading_indicator).
[![Pub](https://img.shields.io/pub/v/expressive_loading_indicator.svg)](https://pub.dartlang.org/packages/expressive_loading_indicator)
Material 3 Expressive loading indicator ported to Flutter.
[![Expressive Loading Indicator Demo](https://github.com/user-attachments/assets/d5ee926f-7cae-4824-8a37-e844b7ebee4f)](https://github.com/user-attachments/assets/d5ee926f-7cae-4824-8a37-e844b7ebee4f)
## Package
[`expressive_loading_indicator` on pub.dev](https://pub.dev/packages/expressive_loading_indicator).
## Usage
```dart
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
ExpressiveLoadingIndicator(
// Custom color
color: Colors.purple,
// Custom size constraints
constraints: BoxConstraints(
minWidth: 64.0,
minHeight: 64.0,
maxWidth: 64.0,
maxHeight: 64.0,
),
// Custom polygon shapes
polygons: [
MaterialShapes.softBurst,
MaterialShapes.pentagon,
MaterialShapes.pill,
],
// Accessibility
semanticsLabel: 'Loading',
semanticsValue: 'In progress',
)
```
## TODO
- [ ] Add support for ContainedLoadingIndicator - A version that shows the loading indicator inside a container with a background
- [ ] Add support for DeterminateLoadingIndicator - A version that morphs depending on the progress value
## Credits
- [Kostia Sokolovskyi](https://github.com/ksokolovskyi) for their initial implementation of [material_shapes](https://github.com/ksokolovskyi/material_shapes).
## External Links
- [Loading Indicator](https://m3.material.io/components/loading-indicator/overview) on Material Design.
- [Kotlin source implementation](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt) on Android Code Search.
## License & Attribution
This package is available under the [MIT License](LICENSE).
This package contains Dart ports of components from the Android Open Source Project (AOSP). The original Android components are licensed under the Apache License 2.0.
See [NOTICE](NOTICE) for complete attribution and license details.
## Contributing
Contributions are welcome! Please feel free to submit an Issue or Pull Request.
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/amazing-feature`)
3. Commit your Changes (`git commit -m 'Add some amazing feature'`)
4. Push to the Branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expressive Loading Indicator Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expressive Loading Indicator Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Default Loading Indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
// Default loading indicator
const ExpressiveLoadingIndicator(),
const SizedBox(height: 32),
const Text(
'Custom Color Loading Indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
// Customized with color
const ExpressiveLoadingIndicator(color: Colors.orange),
const SizedBox(height: 32),
const Text(
'Custom Size Loading Indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
// Customized with size
const ExpressiveLoadingIndicator(
constraints: BoxConstraints(
minWidth: 72.0,
minHeight: 72.0,
maxWidth: 72.0,
maxHeight: 72.0,
),
),
const SizedBox(height: 32),
const Text(
'Custom Shapes Loading Indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
// Customized with shapes
ExpressiveLoadingIndicator(
polygons: [
MaterialShapes.softBurst,
MaterialShapes.pill,
MaterialShapes.pentagon,
],
),
],
),
),
);
}
}
@@ -0,0 +1,337 @@
// Port of Android's LoadingIndicator
// Source: androidx/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
// Copyright (c) 2024 The Android Open Source Project
// Licensed under the Apache License, Version 2.0
//
// Dart port by Tamim Arafat
// Copyright (c) 2025 Tamim Arafat
// Licensed under MIT License
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/semantics.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
/// A Material Design loading indicator.
///
/// This version of the loading indicator morphs between its [polygons] shapes.
/// ![Loading indicator image](https://developer.android.com/images/reference/androidx/compose/material3/loading-indicator.png)
class ExpressiveLoadingIndicator extends ProgressIndicator {
/// A list of [RoundedPolygon]s for the sequence of shapes this loading indicator
/// will morph between. The loading indicator expects at least two items in that list.
final List<RoundedPolygon>? polygons;
/// Defines minimum and maximum sizes for an [ExpressiveLoadingIndicator].
/// If null, then the [ProgressIndicatorThemeData.constraints] will be used. Otherwise, defaults to a minimum width and height of 48 pixels.
final BoxConstraints? constraints;
final double activeSize;
const ExpressiveLoadingIndicator({
super.key,
super.color,
this.polygons,
this.activeSize = 38.0,
this.constraints,
super.semanticsLabel,
super.semanticsValue,
}) : assert(polygons != null ? polygons.length > 1 : true);
@override
State<ExpressiveLoadingIndicator> createState() =>
_ExpressiveLoadingIndicatorState();
}
class _ExpressiveLoadingIndicatorState extends State<ExpressiveLoadingIndicator>
with TickerProviderStateMixin {
static final List<RoundedPolygon> _defaultPolygons = [
MaterialShapes.softBurst,
MaterialShapes.cookie9Sided,
MaterialShapes.pentagon,
MaterialShapes.pill,
MaterialShapes.sunny,
MaterialShapes.cookie4Sided,
MaterialShapes.oval,
];
static final BoxConstraints _defaultConstraints = BoxConstraints(
minWidth: 48.0,
minHeight: 48.0,
maxWidth: 48.0,
maxHeight: 48.0,
); // default from kotlin source
late final List<RoundedPolygon> _polygons;
static const int _globalRotationDurationMs = 4666;
static const int _morphIntervalMs = 650;
static const double _fullRotation = 360.0;
static const double _quarterRotation = _fullRotation / 4;
static const double _activeSize = 48; // based on source spec
late final List<Morph> _morphSequence;
late final AnimationController _morphController;
late final AnimationController _globalRotationController;
int _currentMorphIndex = 0;
double _morphRotationTargetAngle = _quarterRotation;
Timer? _morphTimer;
final _morphAnimationSpec = SpringSimulation(
SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0),
0.0,
1.0,
5.0,
snapToEnd: true,
);
late BoxConstraints _constraints;
late Color _color;
@override
Widget build(BuildContext context) {
final indicatorTheme = ProgressIndicatorTheme.of(context);
_color =
widget.color ??
indicatorTheme.color ??
Theme.of(context).colorScheme.primary;
_constraints =
widget.constraints ?? indicatorTheme.constraints ?? _defaultConstraints;
final activeIndicatorScale =
widget.activeSize /
math.min(_constraints.maxWidth, _constraints.maxHeight);
final shapesScaleFactor =
_calculateScaleFactor(_polygons) * activeIndicatorScale;
return Semantics.fromProperties(
properties: SemanticsProperties(
label: widget.semanticsLabel,
value: widget.semanticsValue,
),
child: RepaintBoundary(
child: ConstrainedBox(
constraints: _constraints,
child: AspectRatio(
aspectRatio: 1.0,
child: AnimatedBuilder(
animation: Listenable.merge([
_morphController,
_globalRotationController,
]),
builder: (context, child) {
final morphProgress = _morphController.value.clamp(0.0, 1.0);
final globalRotationDegrees =
_globalRotationController.value * _fullRotation;
// calculate total rotation (clockwise, matching Kotlin implementation)
final totalRotationDegrees =
morphProgress * _quarterRotation +
_morphRotationTargetAngle +
globalRotationDegrees;
final totalRotationRadians =
totalRotationDegrees * (math.pi / 180.0);
return Transform.rotate(
angle: totalRotationRadians,
child: CustomPaint(
painter: _MorphPainter(
morph: _morphSequence[_currentMorphIndex],
progress: morphProgress,
color: _color,
scaleFactor: shapesScaleFactor,
repaint: Listenable.merge([
_morphController,
_globalRotationController,
]),
),
child: const SizedBox.expand(),
),
);
},
),
),
),
),
);
}
@override
void dispose() {
_morphTimer?.cancel();
_morphController.dispose();
_globalRotationController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_polygons = widget.polygons ?? _defaultPolygons;
_morphSequence = _createMorphSequence(_polygons, circularSequence: true);
_morphController = AnimationController.unbounded(vsync: this);
// continuous linear rotation
_globalRotationController = AnimationController(
duration: const Duration(milliseconds: _globalRotationDurationMs),
vsync: this,
);
_startAnimations();
}
List<Morph> _createMorphSequence(
List<RoundedPolygon> polygons, {
required bool circularSequence,
}) {
final morphs = <Morph>[];
for (int i = 0; i < polygons.length; i++) {
if (i + 1 < polygons.length) {
morphs.add(Morph(polygons[i], polygons[i + 1]));
} else if (circularSequence) {
// morph from last shape back to first shape
morphs.add(Morph(polygons[i], polygons[0]));
}
}
return morphs;
}
/// Calculates a scale factor that will be used when scaling the provided [RoundedPolygon]s into a
/// specified sized container.
///
/// Since the polygons may rotate, a simple [RoundedPolygon.calculateBounds] is not enough to
/// determine the size the polygon will occupy as it rotates. Using the simple bounds calculation may
/// result in a clipped shape.
///
/// This function calculates and returns a scale factor by utilizing the
/// [RoundedPolygon.calculateMaxBounds] and comparing its result to the
/// [RoundedPolygon.calculateBounds]. The scale factor can later be used when calling [processPath].
///
/// Port of Kotlin implementation.
double _calculateScaleFactor(List<RoundedPolygon> polygons) {
var scaleFactor = 1.0;
for (final polygon in polygons) {
final bounds = polygon.calculateBounds();
final maxBounds = polygon.calculateMaxBounds();
final boundsWidth = bounds[2] - bounds[0];
final boundsHeight = bounds[3] - bounds[1];
final maxBoundsWidth = maxBounds[2] - maxBounds[0];
final maxBoundsHeight = maxBounds[3] - maxBounds[1];
final scaleX = boundsWidth / maxBoundsWidth;
final scaleY = boundsHeight / maxBoundsHeight;
// We use max(scaleX, scaleY) to handle cases like a pill-shape that can throw off the
// entire calculation.
scaleFactor = math.min(scaleFactor, math.max(scaleX, scaleY));
}
return scaleFactor;
}
void _startAnimations() {
// infinite global rotation
_globalRotationController.repeat();
// periodic morph cycle
_morphTimer = Timer.periodic(
const Duration(milliseconds: _morphIntervalMs),
(_) => _startMorphCycle(),
);
_startMorphCycle();
}
void _startMorphCycle() {
if (!mounted) return;
// move to next morph in sequence
_currentMorphIndex = (_currentMorphIndex + 1) % _morphSequence.length;
// accumulate rotation target
_morphRotationTargetAngle =
(_morphRotationTargetAngle + _quarterRotation) % _fullRotation;
// Reset and start morph animation
_morphController
..value = 0.0
..animateWith(_morphAnimationSpec);
}
}
class _MorphPainter extends CustomPainter {
final Morph morph;
final double progress;
final Color color;
/// A scale factor that will be taken into account uniformly when the [path] is
/// scaled (i.e. the scaleX would be the [size] width x the scale factor, and the scaleY would be
/// the [size] height x the scale factor)
final double scaleFactor;
_MorphPainter({
required this.morph,
required this.progress,
required this.color,
this.scaleFactor = 1.0,
super.repaint,
});
@override
void paint(Canvas canvas, Size size) {
final path = morph.toPath(progress: progress);
final processedPath = _processPath(path, size);
canvas.drawPath(
processedPath,
Paint()
..style = PaintingStyle.fill
..color = color,
);
}
@override
bool shouldRepaint(_MorphPainter oldDelegate) {
return oldDelegate.morph != morph ||
oldDelegate.progress != progress ||
oldDelegate.color != color ||
oldDelegate.scaleFactor != scaleFactor;
}
/// Process a given path to scale it and center it inside the given size.
///
/// [path] takes a [Path] that was generated by a _normalized_ [Morph] or [RoundedPolygon].
/// [size] takes a [Size] that the provided [path] is going to be scaled and centered into.
Path _processPath(Path path, Size size) {
// a [Matrix] that would be used to apply the scaling. Note that any provided
// matrix will be reset in this function.
final Matrix4 scaleMatrix = Matrix4.diagonal3Values(
size.width * scaleFactor,
size.height * scaleFactor,
1,
);
final Path scaledPath = path.transform(scaleMatrix.storage);
// Translate the path so that its center aligns with the center of the container.
final Rect bounds = scaledPath.getBounds();
final Offset translation =
Offset(size.width / 2, size.height / 2) - bounds.center;
final Path finalPath = scaledPath.shift(translation);
return finalPath;
}
}
@@ -0,0 +1,55 @@
name: expressive_loading_indicator
description: Material Design 3 Expressive loading indicator port for Flutter
version: 0.0.1
homepage: https://github.com/arafatamim/expressive_loading_indicator
environment:
sdk: ^3.8.1
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
material_new_shapes: ^1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package
@@ -0,0 +1,302 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
void main() {
group('ExpressiveLoadingIndicator', () {
testWidgets('renders with default polygons', (WidgetTester tester) async {
// Build our widget
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
),
);
// Verify the widget renders
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// Let the animation run for a bit
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
// No errors should occur during animation
expect(tester.takeException(), isNull);
});
testWidgets('renders with custom polygons', (WidgetTester tester) async {
final customPolygons = [
MaterialShapes.pill,
MaterialShapes.pentagon,
MaterialShapes.oval,
];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ExpressiveLoadingIndicator(polygons: customPolygons),
),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(tester.takeException(), isNull);
});
testWidgets('renders with custom color', (WidgetTester tester) async {
const customColor = Colors.red;
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(child: ExpressiveLoadingIndicator(color: customColor)),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// Extract the widget to verify its properties
final ExpressiveLoadingIndicator widget = tester.widget(
find.byType(ExpressiveLoadingIndicator),
);
expect(widget.color, equals(customColor));
});
testWidgets('renders with custom constraints', (WidgetTester tester) async {
const customConstraints = BoxConstraints(
minWidth: 100.0,
minHeight: 100.0,
maxWidth: 100.0,
maxHeight: 100.0,
);
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: ExpressiveLoadingIndicator(constraints: customConstraints),
),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// Find the ConstrainedBox widget
final ConstrainedBox constrainedBox = tester.widget(
find.descendant(
of: find.byType(ExpressiveLoadingIndicator),
matching: find.byType(ConstrainedBox),
),
);
expect(constrainedBox.constraints, equals(customConstraints));
});
testWidgets('applies semantics properties correctly', (
WidgetTester tester,
) async {
const String semanticsLabel = 'Loading';
const String semanticsValue = '50%';
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: ExpressiveLoadingIndicator(
semanticsLabel: semanticsLabel,
semanticsValue: semanticsValue,
),
),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// Verify semantics are applied to the widget
final semanticsWidget = find.byType(Semantics);
expect(semanticsWidget, findsWidgets);
});
testWidgets('throws assertion error with single polygon', (
WidgetTester tester,
) async {
expect(
() => ExpressiveLoadingIndicator(polygons: [MaterialShapes.pill]),
throwsAssertionError,
);
});
testWidgets('renders in theme with correct color', (
WidgetTester tester,
) async {
const Color themeColor = Colors.green;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: themeColor,
),
),
home: const Scaffold(
body: Center(child: ExpressiveLoadingIndicator()),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
// The widget should use the theme color
// This test verifies the widget respects the theme
final ExpressiveLoadingIndicator widget = tester.widget(
find.byType(ExpressiveLoadingIndicator),
);
expect(widget.color, isNull); // Widget itself doesn't have a color set
});
testWidgets('renders in theme with custom constraints', (
WidgetTester tester,
) async {
const BoxConstraints themeConstraints = BoxConstraints(
minWidth: 80.0,
minHeight: 80.0,
maxWidth: 80.0,
maxHeight: 80.0,
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
progressIndicatorTheme: const ProgressIndicatorThemeData(
constraints: themeConstraints,
),
),
home: const Scaffold(
body: Center(child: ExpressiveLoadingIndicator()),
),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// The ConstrainedBox should use theme constraints
final ConstrainedBox constrainedBox = tester.widget(
find.descendant(
of: find.byType(ExpressiveLoadingIndicator),
matching: find.byType(ConstrainedBox),
),
);
expect(constrainedBox.constraints, equals(themeConstraints));
});
testWidgets('disposes resources properly', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
// Replace the widget with something else
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: Text('No indicator'))),
),
);
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
expect(find.text('No indicator'), findsOneWidget);
// No errors should occur during disposal
expect(tester.takeException(), isNull);
});
testWidgets('handles widget rebuilds during animation', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
),
);
// Let animation start
await tester.pump(const Duration(milliseconds: 100));
// Rebuild with the same widget
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
),
);
// Continue animation
await tester.pump(const Duration(milliseconds: 100));
// No exceptions should occur
expect(tester.takeException(), isNull);
});
testWidgets('handles rapid animation frames', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
),
);
// Rapid succession of small animation steps
for (int i = 0; i < 5; i++) {
await tester.pump(const Duration(milliseconds: 16)); // ~60fps
}
// Widget should handle rapid animation frames without issues
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('handles navigation away during animation', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const Scaffold(
body: Center(child: ExpressiveLoadingIndicator()),
),
'/next': (context) =>
const Scaffold(body: Center(child: Text('Next Page'))),
},
),
);
// Start animation
await tester.pump(const Duration(milliseconds: 100));
// Navigate away
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Center(child: Text('Next Page'))),
),
);
// No exceptions should occur
expect(tester.takeException(), isNull);
});
});
}
+1 -1
View File
@@ -164,7 +164,7 @@ packages:
expressive_loading_indicator:
dependency: "direct main"
description:
path: expressive_loading_indicator_custom
path: expressive_loading_indicator_modified
relative: true
source: path
version: "0.0.1"
+1 -1
View File
@@ -71,7 +71,7 @@ dependencies:
path: ./settings_tiles_custom
expressive_loading_indicator:
path: ./expressive_loading_indicator_custom
path: ./expressive_loading_indicator_modified
dev_dependencies:
flutter_test: