-
Notifications
You must be signed in to change notification settings - Fork 0
LayrzImage
A Material-free image widget that displays raster and vector images from multiple source formats, with intelligent routing to appropriate renderers and graceful error handling.
Metadata
Mirrors: None (new component for layrz_ui)
Phase: M2 (Core primitives)
Domain: Display
Primitive: Image + SvgPicture (from flutter_svg) + Image.network + Image.asset + Image.memory
Status: Confirmed scope.
LayrzImage resolves multiple source formats and automatically routes rendering to the appropriate widget:
-
Network URLs (
http://,https://) →Image.networkorSvgPicture.network -
Data-URIs (
data:...) → decoded bytes →Image.memoryorSvgPicture.memory -
Base64 without prefix (bare base64 string) → decoded bytes →
Image.memory -
Asset paths (anything else) →
Image.assetorSvgPicture.asset
SVG detection: A source is treated as SVG if the path ends with .svg or the data-URI MIME type is image/svg+xml.
- Source format agnostic: Accepts URLs, data-URIs, bare base64, and asset paths with no caller configuration.
-
SVG support: Automatically detects and routes SVG to
SvgPicture(viaflutter_svg). -
Graceful error handling: Malformed base64 or missing assets render
fallbackrather than throwing. - Efficient caching: Base64 payloads are cached by source hash to avoid redundant decoding in lists or repeated renders.
- Placeholder support: Network images can display a loading placeholder while fetching.
class LayrzImage extends StatelessWidget {
/// Image location: an http(s) URL, a `data:` URI, a bare base64 payload, or an asset path.
///
/// Examples:
/// - `https://example.com/image.png`
/// - `data:image/png;base64,iVBORw0KGgo...`
/// - `iVBORw0KGgo...` (bare base64, without `data:` prefix)
/// - `assets/images/avatar.png`
final String source;
/// The width of the displayed image in logical pixels.
///
/// When null, the image width is unconstrained. At least one of [width]
/// or [height] should be specified to avoid unexpected sizing.
final double? width;
/// The height of the displayed image in logical pixels.
///
/// When null, the image height is unconstrained. At least one of [width]
/// or [height] should be specified to avoid unexpected sizing.
final double? height;
/// How the image should be fitted within its bounds.
///
/// Defaults to [BoxFit.cover], which crops the image to fill the space while
/// maintaining aspect ratio. See [BoxFit] for other options.
final BoxFit fit;
/// The alignment of the image within its bounding box.
///
/// Defaults to [Alignment.center]. Affects how the image is positioned when
/// the fit does not fill the entire bounds.
final Alignment alignment;
/// The quality used when resampling the image.
///
/// Defaults to [FilterQuality.medium]. Use [FilterQuality.high] for better
/// quality at the cost of slight performance overhead, or [FilterQuality.low]
/// for faster rendering on low-end devices.
final FilterQuality filterQuality;
/// Widget shown while a network image is loading.
///
/// Only applies to network sources (URLs starting with `http://` or `https://`).
/// Ignored for asset and data-URI sources, which load synchronously or nearly so.
/// When null, a blank area is shown during loading.
final Widget? placeholder;
/// Widget shown when the source cannot be fetched or decoded.
///
/// This includes:
/// - Network errors (404, timeout, connection failure, etc.)
/// - Malformed base64 strings
/// - Missing asset files
/// - Unsupported image formats
///
/// When null, a blank area is shown on error.
final Widget? fallback;
/// Creates a new [LayrzImage].
///
/// The [source] must be one of: an http(s) URL, a data-URI, a bare base64 string,
/// or an asset path. At least one of [width] or [height] should be specified,
/// or the image will expand to fill available space.
const LayrzImage({
super.key,
required this.source,
this.width,
this.height,
this.fit = BoxFit.cover,
this.alignment = Alignment.center,
this.filterQuality = FilterQuality.medium,
this.placeholder,
this.fallback,
});
}The component determines the source type using this logic:
A source is treated as SVG if:
- The path ends with
.svg(works for asset paths and URLs) - The data-URI MIME type is
image/svg+xml
SVG rendering is routed to SvgPicture (from flutter_svg).
A source is treated as a network URL if:
- It starts with
http://orhttps://
Network images are loaded with Image.network (raster) or SvgPicture.network (SVG).
A source is treated as a data-URI if:
- It starts with
data:
Data-URIs are decoded to bytes and rendered with Image.memory (raster) or SvgPicture.memory (SVG).
A source is treated as bare base64 if:
- It does NOT start with
http://,https://, ordata: - It does NOT end with
.svg - It contains only base64-safe characters:
[A-Za-z0-9+/=]
Bare base64 is decoded and rendered with Image.memory (raster).
Any other source is treated as an asset path (e.g., assets/images/avatar.png).
Both full data-URIs and bare base64 strings are supported:
-
Full data-URI:
data:image/png;base64,iVBORw0KGgo... -
Bare base64:
iVBORw0KGgo...(without thedata:prefix)
If a base64 string cannot be decoded, the error is caught internally and fallback is displayed (no exception is raised in build()).
Decoded base64 payloads are cached by source hash to avoid re-decoding identical strings across multiple renders (e.g., in lists or repeated widgets).
Cache bounds: The cache holds a maximum of 50 entries and uses LRU-like eviction — when full, the oldest entry (by insertion order) is evicted.
The placeholder widget is shown while a network image is loading.
- Applies to: Network URLs only (http/https).
- Ignored for: Asset and data-URI sources (which load synchronously or nearly so).
- When null: A blank area is shown during loading.
Example:
placeholder: Center(
child: CircularProgressIndicator(),
)The fallback widget is shown when the source cannot be fetched or decoded.
Triggers fallback rendering:
- Network errors (404, timeout, connection failure)
- Malformed base64 strings
- Missing asset files
- Unsupported image formats
When null: A blank area is shown on error.
Example:
fallback: Icon(
Icons.image_not_supported,
size: 48,
color: Colors.grey,
)SVG images are rendered using SvgPicture from the flutter_svg package. The placeholder and fallback widgets apply to SVG sources the same way as raster images.
SVG detection is automatic — no caller configuration is needed.
- Both
widthandheightare optional. - When either is null, that dimension is unconstrained.
- At least one of
widthorheightshould be specified to avoid unexpected sizing.
Example:
LayrzImage(
source: 'https://example.com/image.png',
width: 100,
height: 100,
)
// Image scales proportionally to fill a 100×100 box with BoxFit.cover-
fit(default:BoxFit.cover): How the image fills its bounds. Other options:BoxFit.contain,BoxFit.fill,BoxFit.fitWidth,BoxFit.fitHeight,BoxFit.scaleDown. -
alignment(default:Alignment.center): Where the image is positioned within its bounding box.
LayrzImage(
source: 'https://example.com/user-avatar.png',
width: 64,
height: 64,
fit: BoxFit.cover,
placeholder: Container(
color: Colors.grey[200],
child: Center(child: CircularProgressIndicator()),
),
fallback: Icon(Icons.person, size: 64),
)LayrzImage(
source: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
width: 48,
height: 48,
)final base64ImageData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
LayrzImage(
source: base64ImageData,
width: 40,
height: 40,
fit: BoxFit.cover,
fallback: Container(color: Colors.grey),
)LayrzImage(
source: 'assets/images/logo.png',
width: 200,
height: 100,
fit: BoxFit.contain,
fallback: Text('Logo not found'),
)LayrzImage(
source: 'https://example.com/icon.svg',
width: 64,
height: 64,
placeholder: SizedBox(
width: 64,
height: 64,
child: CircularProgressIndicator(),
),
)LayrzImage(
source: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAKPC9zdmc+',
width: 100,
height: 100,
)ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: LayrzImage(
source: user.avatarBase64, // Decoded bytes are cached by hash
width: 40,
height: 40,
fit: BoxFit.cover,
fallback: CircleAvatar(child: Text(user.initials)),
),
title: Text(user.name),
);
},
)Decoded base64 bytes are cached internally to avoid redundant decoding when the same base64 string appears multiple times (e.g., in a list of avatars).
- Key: Source string hash code
- Max entries: 50
- Eviction: When full, the oldest entry is removed (FIFO)
- Use case: Lists of user avatars with repeated base64 images
Network images use Flutter's built-in image cache (transparent to the caller).
- Base64 decoding: The first render of a base64 source decodes the bytes and caches them. Subsequent renders with the same source reuse the cached bytes.
- Network images: Placeholder is shown while fetching. Use short loading placeholders for a responsive feel.
-
Filter quality: Default is
FilterQuality.medium. Usehighfor better quality orlowfor faster rendering.
- Raster and vector images are rendered as image content with no automatic semantic labels.
- Wrap with
Semanticswidget if the image conveys semantic information (e.g., a logo, icon).
-
No Material dependencies: Uses only
package:flutter/widgets.dartandpackage:flutter_svg. - Automatic format detection: No caller configuration is needed to distinguish between sources.
-
Error resilience: Malformed base64, missing assets, and network errors all render
fallbackgracefully. - SVG support: Transparent to the caller — SVG is detected and routed automatically.
- LayrzAvatar — Avatar display component that uses LayrzImage for URL and base64 rendering
- Image — Flutter's core image widget
- flutter_svg — SVG support (vendored dependency)
Made with ❤️ by Golden M, Inc.
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput