Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Controls in Python are now defined as plain dataclasses
* Unified diffing algorithm supports both imperative and declarative styles
* Refactored Flutter layer using inherited widgets and `Provider`
* Added a Shimmer control for building skeleton loaders and animated placeholders.
* Added `FletApp.appErrorMessage` template to customize loading screen errors.
* See the list of [breaking changes](https://github.com/flet-dev/flet/issues/5238)

Expand Down
8 changes: 8 additions & 0 deletions client/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shimmer:
dependency: transitive
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
Expand Down
2 changes: 1 addition & 1 deletion packages/flet/lib/src/controls/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class SemanticsControl extends StatelessWidget {
hint: control.getString("hint"),
onTapHint: control.getString("on_tap_hint"),
onLongPressHint: control.getString("on_long_press_hint"),
container: control.getBool("container")!,
container: control.getBool("container", false)!,
liveRegion: control.getBool("live_region"),
obscured: control.getBool("obscured"),
multiline: control.getBool("multiline"),
Expand Down
73 changes: 73 additions & 0 deletions packages/flet/lib/src/controls/shimmer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';

import '../extensions/control.dart';
import '../models/control.dart';
import '../utils/colors.dart';
import '../utils/gradient.dart';
import '../utils/numbers.dart';
import '../utils/time.dart';
import '../widgets/error.dart';
import 'base_controls.dart';

class ShimmerControl extends StatelessWidget {
final Control control;

const ShimmerControl({super.key, required this.control});

@override
Widget build(BuildContext context) {
debugPrint("Shimmer build: ${control.id}");

final content = control.buildWidget("content");
if (content == null) {
return const ErrorControl("Shimmer.content must be specified");
}

final gradient = control.getGradient("gradient", Theme.of(context));
final baseColor = control.getColor("base_color", context);
final highlightColor = control.getColor("highlight_color", context);

if (gradient == null && (baseColor == null || highlightColor == null)) {
return const ErrorControl(
"Shimmer requires either gradient or base/highlight colors");
}

final direction = _parseDirection(control.getString("direction"));
final period =
control.getDuration("period", const Duration(milliseconds: 1500))!;
final loop = control.getInt("loop", 0)!;

final shimmerWidget = gradient != null
? Shimmer(
gradient: gradient,
direction: direction,
period: period,
loop: loop,
enabled: !control.disabled,
child: content,
)
: Shimmer.fromColors(
baseColor: baseColor!,
highlightColor: highlightColor!,
direction: direction,
period: period,
loop: loop,
enabled: !control.disabled,
child: content,
);

return LayoutControl(control: control, child: shimmerWidget);
}
}

ShimmerDirection _parseDirection(String? value,
[ShimmerDirection defaultValue = ShimmerDirection.ltr]) {
if (value == null) {
return defaultValue;
}
return ShimmerDirection.values.firstWhereOrNull(
(dir) => dir.name.toLowerCase() == value.toLowerCase()) ??
defaultValue;
}
3 changes: 3 additions & 0 deletions packages/flet/lib/src/flet_core_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import 'controls/segmented_button.dart';
import 'controls/selection_area.dart';
import 'controls/semantics.dart';
import 'controls/shader_mask.dart';
import 'controls/shimmer.dart';
import 'controls/snack_bar.dart';
import 'controls/stack.dart';
import 'controls/submenu_button.dart';
Expand Down Expand Up @@ -325,6 +326,8 @@ class FletCoreExtension extends FletExtension {
return SemanticsControl(key: key, control: control);
case "ShaderMask":
return ShaderMaskControl(key: key, control: control);
case "Shimmer":
return ShimmerControl(key: key, control: control);
case "Slider":
return AdaptiveSliderControl(key: key, control: control);
case "SnackBar":
Expand Down
1 change: 1 addition & 0 deletions packages/flet/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies:
screenshot: ^3.0.0
sensors_plus: ^6.1.1
shared_preferences: 2.5.3
shimmer: ^3.0.0
url_launcher: 6.3.2
vector_math: ^2.2.0
web: ^1.1.1
Expand Down
15 changes: 15 additions & 0 deletions sdk/python/examples/controls/file_picker/pick_and_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ class State:


def main(page: ft.Page):
if not page.web:
page.add(
ft.Text(
"This example is only available in Flet Web mode.\n"
"\n"
"Run this example with:\n"
" export FLET_SECRET_KEY=<some_secret_key>\n"
" flet run --web "
"examples/controls/file_picker/pick_and_upload.py",
color=ft.Colors.RED,
selectable=True,
)
)
return

prog_bars: dict[str, ft.ProgressRing] = {}

def on_upload_progress(e: ft.FilePickerUploadEvent):
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/examples/controls/haptic_feedback/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


def main(page: ft.Page):
page.overlay.append(hf := ft.HapticFeedback())
hf = ft.HapticFeedback()
Copy link
Contributor

@ndonkoHenri ndonkoHenri Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document Service base control, as we discussed, to explain the difference between Services we have, so users understand and know which approach to choose for each Service, and why.

Also, the description/docstring of our custom services should explain or point (through a link) to the approach to be used, explained in the base Service.


async def heavy_impact():
await hf.heavy_impact()
Expand Down
6 changes: 2 additions & 4 deletions sdk/python/examples/controls/shake_detector/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@


def main(page: ft.Page):
# just need hold a reference to ShakeDetector in the session store
page.session.store.set(
"shake_detector",
page.services.append(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Will this work for Audio and AudioRec too?

Copy link
Contributor

@ndonkoHenri ndonkoHenri Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, similar to page.add(), should we have a page.services.add()? so we dont have to call page.update() ourselves.

edit: ah, it wont be easily possible, since page.services is a list.

ft.ShakeDetector(
minimum_shake_count=2,
shake_slop_time_ms=300,
shake_count_reset_time_ms=1000,
on_shake=lambda _: page.add(ft.Text("Shake detected!")),
),
)
)

page.add(ft.Text("Shake your device!"))
Expand Down
Empty file.
21 changes: 21 additions & 0 deletions sdk/python/examples/controls/shimmer/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import flet as ft


def main(page: ft.Page):
page.add(
ft.Shimmer(
base_color=ft.Colors.with_opacity(0.3, ft.Colors.GREY_400),
highlight_color=ft.Colors.WHITE,
content=ft.Column(
controls=[
ft.Container(height=80, bgcolor=ft.Colors.GREY_300),
ft.Container(height=80, bgcolor=ft.Colors.GREY_300),
ft.Container(height=80, bgcolor=ft.Colors.GREY_300),
],
),
)
)


if __name__ == "__main__":
ft.run(main)
68 changes: 68 additions & 0 deletions sdk/python/examples/controls/shimmer/basic_placeholder.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add integration test for this example?

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import flet as ft


def _line(width: int, height: int = 12) -> ft.Control:
return ft.Container(
width=width,
height=height,
bgcolor=ft.Colors.GREY_400,
border_radius=ft.BorderRadius.all(height),
)


def _placeholder_tile() -> ft.Control:
return ft.Container(
padding=ft.Padding.all(16),
bgcolor=ft.Colors.with_opacity(0.3, ft.Colors.WHITE),
border_radius=ft.BorderRadius.all(20),
content=ft.Row(
spacing=16,
vertical_alignment=ft.CrossAxisAlignment.START,
controls=[
ft.Container(
width=48,
height=48,
bgcolor=ft.Colors.with_opacity(0.5, ft.Colors.GREY_400),
border_radius=ft.BorderRadius.all(24),
content=ft.Icon(ft.Icons.PERSON, color=ft.Colors.GREY_500),
),
ft.Column(
expand=True,
spacing=10,
controls=[
_line(160),
_line(120),
ft.Row(
spacing=10,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[_line(70, 10), _line(90, 10)],
),
],
),
ft.Container(
width=32,
height=32,
bgcolor=ft.Colors.GREY_200,
border_radius=ft.BorderRadius.all(16),
),
],
),
)


def main(page: ft.Page):
page.title = "Shimmer - loading placeholders"

page.add(
ft.Shimmer(
base_color=ft.Colors.with_opacity(0.3, ft.Colors.GREY_400),
highlight_color=ft.Colors.WHITE,
content=ft.Column(
controls=[_placeholder_tile() for _ in range(3)],
),
),
)


if __name__ == "__main__":
ft.run(main)
69 changes: 69 additions & 0 deletions sdk/python/examples/controls/shimmer/custom_gradient.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add integration test for this example?

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import flet as ft


def _stat_block(title: str, subtitle: str) -> ft.Control:
def metric(width: int, height: int = 14) -> ft.Control:
return ft.Container(
width=width,
height=height,
bgcolor=ft.Colors.WHITE,
opacity=0.6,
border_radius=ft.BorderRadius.all(height),
)

return ft.Container(
width=200,
padding=ft.Padding.all(20),
bgcolor=ft.Colors.with_opacity(0.1, ft.Colors.BLACK),
border_radius=ft.BorderRadius.all(24),
content=ft.Column(
spacing=16,
controls=[
metric(140),
ft.Row(spacing=10, controls=[metric(60, 10), metric(90, 10)]),
ft.Container(
border_radius=ft.BorderRadius.all(16),
bgcolor=ft.Colors.WHITE,
opacity=0.35,
),
ft.Column(spacing=8, controls=[metric(120, 12), metric(160, 12)]),
ft.Text(title, weight=ft.FontWeight.W_600),
ft.Text(subtitle, size=12),
],
),
)


def main(page: ft.Page):
page.title = "Shimmer - custom gradients"
page.bgcolor = "#0e0e18"
accent = ft.LinearGradient(
begin=ft.Alignment(-1.0, -0.5),
end=ft.Alignment(1.0, 0.5),
colors=[
ft.Colors.PURPLE,
ft.Colors.PURPLE,
ft.Colors.AMBER_200,
ft.Colors.PURPLE,
ft.Colors.PURPLE,
],
stops=[0.0, 0.35, 0.5, 0.65, 1.0],
)

cards = ft.Row(
wrap=True,
controls=[
ft.Shimmer(
gradient=accent,
direction=ft.ShimmerDirection.TTB,
period=2200,
content=_stat_block("Recent activity", "Smooth top-to-bottom sweep"),
),
],
)

page.add(cards)


if __name__ == "__main__":
ft.run(main)
35 changes: 35 additions & 0 deletions sdk/python/packages/flet/docs/controls/shimmer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
class_name: flet.Shimmer
examples: ../../examples/controls/shimmer
example_images: ../test-images/examples/core/golden/macos/shimmer
---

{{ class_summary(class_name, example_images + "/image_for_docs.gif", image_caption="Basic shimmer") }}

## Examples

### Basic

```python
--8<-- "{{ examples }}/basic.py"
```

{{ image(example_images + "/image_for_docs.gif", alt="custom-label", width="50%") }}

### Skeleton list placeholders

```python
--8<-- "{{ examples }}/basic_placeholder.py"
```

{{ image(example_images + "/basic_placeholder.png", alt="custom-label", width="50%") }}

### Custom gradients and directions

```python
--8<-- "{{ examples }}/custom_gradient.py"
```

{{ image(example_images + "/custom_gradient.png", alt="custom-label", width="50%") }}

{{ class_members(class_name) }}
1 change: 1 addition & 0 deletions sdk/python/packages/flet/integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def create_flet_app(request):
flutter_app_dir=(Path(__file__).parent / "../../../../../client").resolve(),
test_path=request.fspath,
flet_app_main=params.get("flet_app_main"),
skip_pump_and_settle=params.get("skip_pump_and_settle", False),
assets_dir=Path(__file__).resolve().parent / "assets",
)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading