diff --git a/docs/pages/docs/avatar.mdx b/docs/pages/docs/avatar.mdx
index be032f404..39476361a 100644
--- a/docs/pages/docs/avatar.mdx
+++ b/docs/pages/docs/avatar.mdx
@@ -6,14 +6,31 @@ An image element with a fallback for representing the user.
-
+
```dart
- FAvatar(
- image: const NetworkImage('https://picsum.photos/250?image=9'),
- placeholderBuilder: (_) => const Text('MN'),
- ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // With Valid image.
+ FAvatar(
+ image: const NetworkImage('https://raw.githubusercontent.com/forus-labs/forui/main/samples/assets/avatar.png'),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+
+ // With Invalid image and fallback.
+ FAvatar(
+ image: const NetworkImage(''),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+
+ // With Invalid image without fallback.
+ FAvatar(image: const NetworkImage('')),
+ ],
+ );
```
@@ -24,9 +41,78 @@ An image element with a fallback for representing the user.
```dart
FAvatar(
- image: const NetworkImage('https://picsum.photos/250?image=9'),
- size: 60,
- placeholderBuilder: (_) => const Text('MN'),
-),
+ image: const NetworkImage('https://raw.githubusercontent.com/forus-labs/forui/main/samples/assets/avatar.png'),
+ fallback: const Text('MN'),
+);
+```
+
+### `FAvatar.raw(...)`
+
+```dart
+// Raw constructor - with icon
+FAvatar.raw(
+ child: FAssets.icons.baby(
+ colorFilter: ColorFilter.mode(theme.colorScheme.mutedForeground, BlendMode.srcIn),
+ ),
+);
+
+// Raw constructor - with text
+FAvatar.raw(child: const Text('MN'));
```
+## Examples
+
+### Invalid image
+
+
+
+
+
+ ```dart
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // With fallback widget.
+ FAvatar(
+ image: const AssetImage(''),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+
+ // Without fallback widget.
+ FAvatar(image: const AssetImage('')),
+ ],
+ );
+ ```
+
+
+
+### Without fallback
+
+
+
+
+
+ ```dart
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Raw constructor - without child.
+ FAvatar.raw(),
+ const SizedBox(width: 10),
+
+ // Raw constructor - with child.
+ FAvatar.raw(
+ child: FAssets.icons.baby(
+ colorFilter: ColorFilter.mode(theme.colorScheme.mutedForeground, BlendMode.srcIn),
+ ),
+ ),
+ const SizedBox(width: 10),
+
+ // Raw constructor - with text.
+ FAvatar.raw(child: const Text('MN')),
+ ],
+ );
+ ```
+
+
diff --git a/forui/lib/src/widgets/avatar.dart b/forui/lib/src/widgets/avatar/avatar.dart
similarity index 55%
rename from forui/lib/src/widgets/avatar.dart
rename to forui/lib/src/widgets/avatar/avatar.dart
index f1a5c0830..2dee94eb2 100644
--- a/forui/lib/src/widgets/avatar.dart
+++ b/forui/lib/src/widgets/avatar/avatar.dart
@@ -5,47 +5,59 @@ import 'package:meta/meta.dart';
import 'package:forui/forui.dart';
+part 'avatar_content.dart';
+
/// An image element with a fallback for representing the user.
///
+/// use image property to provide a profile image displayed within the circle.
/// Typically used with a user's profile image. If the image fails to load,
-/// [placeholderBuilder] is used instead, which usually displays the user's initials.
+/// the fallback widget is used instead, which usually displays the user's initials.
///
-/// If the user's profile has no image, use [placeholderBuilder] to provide
+/// If the user's profile has no image, use the fallback property to provide
/// the initials using a [Text] widget styled with [FAvatarStyle.backgroundColor].
class FAvatar extends StatelessWidget {
/// The style. Defaults to [FThemeData.avatarStyle].
final FAvatarStyle? style;
- /// The profile image displayed within the circle.
- ///
- /// If the user's initials are used, use [placeholderBuilder] instead.
- final ImageProvider image;
-
/// The circle's size.
final double size;
- /// The fallback widget displayed if [image] fails to load.
+ /// The fallback widget displayed if image parameter fails to load.
///
/// Typically used to display the user's initials using a [Text] widget
/// styled with [FAvatarStyle.backgroundColor].
///
- /// Use [image] to display an image; use [placeholderBuilder] for initials.
- final Widget Function(BuildContext)? placeholderBuilder;
+ /// Use image parameter to display an image; use [fallback] for initials.
+ final Widget fallback;
/// Creates an [FAvatar].
- const FAvatar({
- required this.image,
+ FAvatar({
+ required ImageProvider image,
this.style,
this.size = 40.0,
- this.placeholderBuilder,
+ Widget? fallback,
super.key,
- });
+ }) : fallback = _AvatarContent(
+ image: image,
+ style: style,
+ size: size,
+ fallback: fallback,
+ );
+
+ /// Creates a [FAvatar] without a fallback.
+ FAvatar.raw({
+ Widget? child,
+ this.style,
+ this.size = 40.0,
+ super.key,
+ }) : fallback = child ?? _Placeholder(style: style, size: size);
@override
Widget build(BuildContext context) {
final style = this.style ?? context.theme.avatarStyle;
return Container(
+ alignment: Alignment.center,
height: size,
width: size,
decoration: BoxDecoration(
@@ -53,39 +65,9 @@ class FAvatar extends StatelessWidget {
shape: BoxShape.circle,
),
clipBehavior: Clip.hardEdge,
- child: Center(
- child: Image(
- filterQuality: FilterQuality.medium,
- image: image,
- errorBuilder: (context, exception, stacktrace) => DefaultTextStyle(
- style: style.text,
- child: placeholderBuilder != null ? placeholderBuilder!(context) : _Placeholder(size: size),
- ),
- frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
- if (wasSynchronouslyLoaded) {
- return child;
- }
- return AnimatedSwitcher(
- duration: const Duration(milliseconds: 500),
- child: frame == null
- ? DefaultTextStyle(
- style: style.text,
- child: placeholderBuilder != null ? placeholderBuilder!(context) : _Placeholder(size: size),
- )
- : child,
- );
- },
- loadingBuilder: (context, child, loadingProgress) {
- if (loadingProgress == null) {
- return child;
- }
- return DefaultTextStyle(
- style: style.text,
- child: placeholderBuilder != null ? placeholderBuilder!(context) : _Placeholder(size: size),
- );
- },
- fit: BoxFit.cover,
- ),
+ child: DefaultTextStyle(
+ style: style.text,
+ child: fallback,
),
);
}
@@ -94,27 +76,29 @@ class FAvatar extends StatelessWidget {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
- ..add(DiagnosticsProperty('image', image))
..add(DoubleProperty('size', size))
- ..add(DiagnosticsProperty('style', style))
- ..add(ObjectFlagProperty.has('placeholderBuilder', placeholderBuilder));
+ ..add(DiagnosticsProperty('style', style));
}
}
/// [FAvatar]'s style.
final class FAvatarStyle with Diagnosticable {
- /// The placeholder's background color.
+ /// The fallback's background color.
final Color backgroundColor;
+ /// The fallback's color.
+ final Color foregroundColor;
+
/// Duration for the transition animation.
final Duration fadeInDuration;
- /// The text style for the placeholder text.
+ /// The text style for the fallback text.
final TextStyle text;
/// Creates a [FAvatarStyle].
- FAvatarStyle({
+ const FAvatarStyle({
required this.backgroundColor,
+ required this.foregroundColor,
required this.fadeInDuration,
required this.text,
});
@@ -122,6 +106,7 @@ final class FAvatarStyle with Diagnosticable {
/// Creates a [FCardStyle] that inherits its properties from [colorScheme] and [typography].
FAvatarStyle.inherit({required FColorScheme colorScheme, required FTypography typography})
: backgroundColor = colorScheme.muted,
+ foregroundColor = colorScheme.mutedForeground,
fadeInDuration = const Duration(milliseconds: 500),
text = typography.base.copyWith(
color: colorScheme.mutedForeground,
@@ -144,11 +129,13 @@ final class FAvatarStyle with Diagnosticable {
@useResult
FAvatarStyle copyWith({
Color? backgroundColor,
+ Color? foregroundColor,
Duration? fadeInDuration,
TextStyle? text,
}) =>
FAvatarStyle(
backgroundColor: backgroundColor ?? this.backgroundColor,
+ foregroundColor: foregroundColor ?? this.foregroundColor,
fadeInDuration: fadeInDuration ?? this.fadeInDuration,
text: text ?? this.text,
);
@@ -158,6 +145,7 @@ final class FAvatarStyle with Diagnosticable {
super.debugFillProperties(properties);
properties
..add(ColorProperty('backgroundColor', backgroundColor))
+ ..add(ColorProperty('foregroundColor', foregroundColor))
..add(DiagnosticsProperty('fadeInDuration', fadeInDuration))
..add(DiagnosticsProperty('text', text));
}
@@ -168,31 +156,10 @@ final class FAvatarStyle with Diagnosticable {
other is FAvatarStyle &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor &&
+ foregroundColor == other.foregroundColor &&
fadeInDuration == other.fadeInDuration &&
text == other.text;
@override
- int get hashCode => backgroundColor.hashCode ^ fadeInDuration.hashCode ^ text.hashCode;
-}
-
-class _Placeholder extends StatelessWidget {
- final double size;
-
- const _Placeholder({required this.size});
-
- @override
- Widget build(BuildContext context) {
- final style = context.theme;
-
- return FAssets.icons.userRound(
- height: size / 2,
- colorFilter: ColorFilter.mode(style.colorScheme.mutedForeground, BlendMode.srcIn),
- );
- }
-
- @override
- void debugFillProperties(DiagnosticPropertiesBuilder properties) {
- super.debugFillProperties(properties);
- properties.add(DoubleProperty('size', size));
- }
+ int get hashCode => backgroundColor.hashCode ^ foregroundColor.hashCode ^ fadeInDuration.hashCode ^ text.hashCode;
}
diff --git a/forui/lib/src/widgets/avatar/avatar_content.dart b/forui/lib/src/widgets/avatar/avatar_content.dart
new file mode 100644
index 000000000..e9e9096c8
--- /dev/null
+++ b/forui/lib/src/widgets/avatar/avatar_content.dart
@@ -0,0 +1,79 @@
+part of 'avatar.dart';
+
+class _AvatarContent extends StatelessWidget {
+ final ImageProvider image;
+ final double size;
+ final FAvatarStyle? style;
+ final Widget? fallback;
+
+ const _AvatarContent({
+ required this.image,
+ required this.size,
+ this.style,
+ this.fallback,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final style = this.style ?? context.theme.avatarStyle;
+
+ final fallback = this.fallback ?? _Placeholder(style: style, size: size);
+
+ return Image(
+ height: size,
+ width: size,
+ filterQuality: FilterQuality.medium,
+ image: image,
+ errorBuilder: (context, exception, stacktrace) => fallback,
+ frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
+ if (wasSynchronouslyLoaded) {
+ return child;
+ }
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 500),
+ child: frame == null ? fallback : child,
+ );
+ },
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) {
+ return child;
+ }
+ return fallback;
+ },
+ fit: BoxFit.cover,
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DiagnosticsProperty('image', image))
+ ..add(DoubleProperty('size', size))
+ ..add(DiagnosticsProperty('style', style));
+ }
+}
+
+class _Placeholder extends StatelessWidget {
+ final double size;
+ final FAvatarStyle? style;
+
+ const _Placeholder({required this.size, this.style});
+
+ @override
+ Widget build(BuildContext context) {
+ final style = this.style ?? context.theme.avatarStyle;
+ return FAssets.icons.userRound(
+ height: size / 2,
+ colorFilter: ColorFilter.mode(style.foregroundColor, BlendMode.srcIn),
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties
+ ..add(DoubleProperty('size', size))
+ ..add(DiagnosticsProperty('style', style));
+ }
+}
diff --git a/forui/lib/widgets.dart b/forui/lib/widgets.dart
index 985949896..88561dc9c 100644
--- a/forui/lib/widgets.dart
+++ b/forui/lib/widgets.dart
@@ -19,7 +19,7 @@ library forui.widgets;
import 'package:forui/forui.dart';
export 'src/widgets/alert/alert.dart' hide Variant;
-export 'src/widgets/avatar.dart';
+export 'src/widgets/avatar/avatar.dart';
export 'src/widgets/badge/badge.dart' hide Variant;
export 'src/widgets/bottom_navigation_bar/bottom_navigation_bar.dart';
export 'src/widgets/button/button.dart' hide Variant;
diff --git a/forui/test/golden/avatar/zinc-dark-raw-content.png b/forui/test/golden/avatar/zinc-dark-raw-content.png
new file mode 100644
index 000000000..a8fb93399
Binary files /dev/null and b/forui/test/golden/avatar/zinc-dark-raw-content.png differ
diff --git a/forui/test/golden/avatar/zinc-dark-with-image.png b/forui/test/golden/avatar/zinc-dark-with-image.png
index c9fe7e488..0b7db1fb4 100644
Binary files a/forui/test/golden/avatar/zinc-dark-with-image.png and b/forui/test/golden/avatar/zinc-dark-with-image.png differ
diff --git a/forui/test/golden/avatar/zinc-light-raw-content.png b/forui/test/golden/avatar/zinc-light-raw-content.png
new file mode 100644
index 000000000..eca73164d
Binary files /dev/null and b/forui/test/golden/avatar/zinc-light-raw-content.png differ
diff --git a/forui/test/golden/avatar/zinc-light-with-image.png b/forui/test/golden/avatar/zinc-light-with-image.png
index 858fcc52e..86cfa19ec 100644
Binary files a/forui/test/golden/avatar/zinc-light-with-image.png and b/forui/test/golden/avatar/zinc-light-with-image.png differ
diff --git a/forui/test/src/widgets/avatar_golden_test.dart b/forui/test/src/widgets/avatar_golden_test.dart
index d4b4759e6..4c90f00b5 100644
--- a/forui/test/src/widgets/avatar_golden_test.dart
+++ b/forui/test/src/widgets/avatar_golden_test.dart
@@ -19,12 +19,9 @@ void main() {
final testWidget = MaterialApp(
home: TestScaffold(
data: theme,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: FAvatar(
- image: FileImage(File('./test/resources/pante.jpg')),
- placeholderBuilder: (_) => const Text('MN'),
- ),
+ child: FAvatar(
+ image: FileImage(File('./test/resources/pante.jpg')),
+ fallback: const Text('MN'),
),
),
);
@@ -48,6 +45,27 @@ void main() {
/// We will not be testing for the fallback behavior due to this issue on flutter
/// https://github.com/flutter/flutter/issues/107416
+
+ testWidgets('$name with raw content', (tester) async {
+ await tester.pumpWidget(
+ TestScaffold(
+ data: theme,
+ child: FAvatar.raw(
+ child: Padding(
+ padding: const EdgeInsets.all(10),
+ child: FAssets.icons.baby(
+ colorFilter: ColorFilter.mode(theme.colorScheme.mutedForeground, BlendMode.srcIn),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await expectLater(
+ find.byType(TestScaffold),
+ matchesGoldenFile('avatar/$name-raw-content.png'),
+ );
+ });
}
},
);
diff --git a/samples/lib/main.dart b/samples/lib/main.dart
index fc7c6e686..70b41ba9e 100644
--- a/samples/lib/main.dart
+++ b/samples/lib/main.dart
@@ -47,6 +47,14 @@ class _AppRouter extends $_AppRouter {
path: '/avatar/default',
page: AvatarRoute.page,
),
+ AutoRoute(
+ path: '/avatar/raw',
+ page: AvatarRawRoute.page,
+ ),
+ AutoRoute(
+ path: '/avatar/invalid',
+ page: AvatarInvalidRoute.page,
+ ),
AutoRoute(
path: '/badge/default',
page: BadgeRoute.page,
diff --git a/samples/lib/widgets/avatar.dart b/samples/lib/widgets/avatar.dart
index 5ada65882..30a253906 100644
--- a/samples/lib/widgets/avatar.dart
+++ b/samples/lib/widgets/avatar.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
@@ -5,6 +6,8 @@ import 'package:forui/forui.dart';
import 'package:forui_samples/sample_scaffold.dart';
+String path(String str) => kIsWeb ? 'assets/$str' : str;
+
@RoutePage()
class AvatarPage extends SampleScaffold {
AvatarPage({
@@ -12,13 +15,66 @@ class AvatarPage extends SampleScaffold {
});
@override
- Widget child(BuildContext context) => Column(
+ Widget child(BuildContext context) => Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FAvatar(
+ image: AssetImage(path('avatar.png')),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+ FAvatar(
+ image: const AssetImage(''),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+ FAvatar(
+ image: const AssetImage(''),
+ ),
+ ],
+ );
+}
+
+@RoutePage()
+class AvatarRawPage extends SampleScaffold {
+ AvatarRawPage({
+ @queryParam super.theme,
+ });
+
+ @override
+ Widget child(BuildContext context) => Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FAvatar.raw(),
+ const SizedBox(width: 10),
+ FAvatar.raw(
+ child: FAssets.icons.baby(
+ colorFilter: ColorFilter.mode(theme.colorScheme.mutedForeground, BlendMode.srcIn),
+ ),
+ ),
+ const SizedBox(width: 10),
+ FAvatar.raw(child: const Text('MN')),
+ ],
+ );
+}
+
+@RoutePage()
+class AvatarInvalidPage extends SampleScaffold {
+ AvatarInvalidPage({
+ @queryParam super.theme,
+ });
+
+ @override
+ Widget child(BuildContext context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FAvatar(
- size: 70,
- image: const AssetImage('avatar.png'),
- placeholderBuilder: (_) => const Text('MN'),
+ image: const AssetImage(''),
+ fallback: const Text('MN'),
+ ),
+ const SizedBox(width: 10),
+ FAvatar(
+ image: const AssetImage(''),
),
],
);