Skip to content

Commit 133d463

Browse files
committed
feat: implement deferred example loading to reduce initial bundle size and improve performance; update embed handling and usage
1 parent c9d2b0a commit 133d463

File tree

8 files changed

+301
-162
lines changed

8 files changed

+301
-162
lines changed
Lines changed: 132 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,158 @@
11
import 'package:flutter/material.dart';
22

3+
import 'design_kit/theme.dart';
34
import 'example_model.dart';
45

5-
/// Minimal wrapper for embedding examples in iframes.
6+
/// Widget that handles deferred loading of examples.
67
///
7-
/// Shows the example with minimal chrome - just the example content
8-
/// and optionally a small header with title and source link.
9-
class EmbedWrapper extends StatelessWidget {
8+
/// Shows a loading indicator while the example loads, then displays it.
9+
class DeferredExampleLoader extends StatefulWidget {
1010
final Example example;
11-
final bool showHeader;
12-
final VoidCallback? onSourceTap;
1311

14-
const EmbedWrapper({
15-
super.key,
16-
required this.example,
17-
this.showHeader = true,
18-
this.onSourceTap,
19-
});
12+
const DeferredExampleLoader({super.key, required this.example});
13+
14+
@override
15+
State<DeferredExampleLoader> createState() => _DeferredExampleLoaderState();
16+
}
17+
18+
class _DeferredExampleLoaderState extends State<DeferredExampleLoader> {
19+
late Future<Widget> _loadFuture;
20+
21+
@override
22+
void initState() {
23+
super.initState();
24+
_loadFuture = widget.example.load(context);
25+
}
26+
27+
@override
28+
void didUpdateWidget(DeferredExampleLoader oldWidget) {
29+
super.didUpdateWidget(oldWidget);
30+
if (oldWidget.example.id != widget.example.id) {
31+
_loadFuture = widget.example.load(context);
32+
}
33+
}
2034

2135
@override
2236
Widget build(BuildContext context) {
23-
final theme = Theme.of(context);
37+
// If already loaded, build immediately
38+
if (widget.example.isLoaded) {
39+
return widget.example.build(context);
40+
}
2441

25-
return Scaffold(
26-
backgroundColor: theme.colorScheme.surface,
27-
body: Column(
42+
// Otherwise show loading indicator while loading
43+
return FutureBuilder<Widget>(
44+
future: _loadFuture,
45+
builder: (context, snapshot) {
46+
if (snapshot.connectionState == ConnectionState.done) {
47+
if (snapshot.hasError) {
48+
return _ErrorView(error: snapshot.error.toString());
49+
}
50+
return snapshot.data!;
51+
}
52+
return const _LoadingView();
53+
},
54+
);
55+
}
56+
}
57+
58+
class _LoadingView extends StatelessWidget {
59+
const _LoadingView();
60+
61+
@override
62+
Widget build(BuildContext context) {
63+
return Center(
64+
child: Column(
65+
mainAxisSize: MainAxisSize.min,
2866
children: [
29-
// Minimal header for embedded view
30-
if (showHeader)
31-
Container(
32-
height: 40,
33-
padding: const EdgeInsets.symmetric(horizontal: 12),
34-
decoration: BoxDecoration(
35-
color: theme.colorScheme.surfaceContainerHighest,
36-
border: Border(
37-
bottom: BorderSide(
38-
color: theme.colorScheme.outlineVariant,
39-
width: 1,
40-
),
41-
),
42-
),
43-
child: Row(
44-
children: [
45-
if (example.icon != null) ...[
46-
Icon(
47-
example.icon,
48-
size: 16,
49-
color: theme.colorScheme.primary,
50-
),
51-
const SizedBox(width: 8),
52-
],
53-
Expanded(
54-
child: Text(
55-
example.title,
56-
style: theme.textTheme.titleSmall?.copyWith(
57-
fontWeight: FontWeight.w600,
58-
color: theme.colorScheme.onSurface,
59-
),
60-
maxLines: 1,
61-
overflow: TextOverflow.ellipsis,
62-
),
63-
),
64-
if (onSourceTap != null)
65-
TextButton.icon(
66-
onPressed: onSourceTap,
67-
icon: Icon(
68-
Icons.code,
69-
size: 16,
70-
color: theme.colorScheme.primary,
71-
),
72-
label: Text(
73-
'Source',
74-
style: TextStyle(
75-
fontSize: 12,
76-
color: theme.colorScheme.primary,
77-
),
78-
),
79-
style: TextButton.styleFrom(
80-
padding: const EdgeInsets.symmetric(horizontal: 8),
81-
minimumSize: Size.zero,
82-
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
83-
),
84-
),
85-
],
86-
),
67+
SizedBox(
68+
width: 24,
69+
height: 24,
70+
child: CircularProgressIndicator(
71+
strokeWidth: 2,
72+
color: DemoTheme.accent,
8773
),
88-
// Example content
89-
Expanded(child: example.builder(context)),
74+
),
75+
const SizedBox(height: 12),
76+
Text(
77+
'Loading example...',
78+
style: Theme.of(
79+
context,
80+
).textTheme.bodySmall?.copyWith(color: context.textSecondaryColor),
81+
),
9082
],
9183
),
9284
);
9385
}
9486
}
9587

96-
/// Embed wrapper without any header - just the raw example.
97-
class EmbedWrapperMinimal extends StatelessWidget {
88+
class _ErrorView extends StatelessWidget {
89+
final String error;
90+
91+
const _ErrorView({required this.error});
92+
93+
@override
94+
Widget build(BuildContext context) {
95+
return Center(
96+
child: Column(
97+
mainAxisSize: MainAxisSize.min,
98+
children: [
99+
Icon(Icons.error_outline, size: 32, color: DemoTheme.error),
100+
const SizedBox(height: 12),
101+
Text(
102+
'Failed to load example',
103+
style: Theme.of(context).textTheme.titleSmall,
104+
),
105+
const SizedBox(height: 4),
106+
Text(
107+
error,
108+
style: Theme.of(
109+
context,
110+
).textTheme.bodySmall?.copyWith(color: context.textSecondaryColor),
111+
textAlign: TextAlign.center,
112+
),
113+
],
114+
),
115+
);
116+
}
117+
}
118+
119+
/// Context that indicates whether the app is running in embed mode.
120+
///
121+
/// When in embed mode, certain UI elements like the navigation drawer
122+
/// and control panel should be hidden.
123+
class EmbedContext extends InheritedWidget {
124+
/// Whether embed mode is active.
125+
final bool isEmbed;
126+
127+
const EmbedContext({super.key, required this.isEmbed, required super.child});
128+
129+
/// Returns true if currently in embed mode.
130+
static bool of(BuildContext context) {
131+
final widget = context.dependOnInheritedWidgetOfExactType<EmbedContext>();
132+
return widget?.isEmbed ?? false;
133+
}
134+
135+
@override
136+
bool updateShouldNotify(EmbedContext oldWidget) =>
137+
isEmbed != oldWidget.isEmbed;
138+
}
139+
140+
/// Wrapper for embedding examples in iframes or documentation.
141+
///
142+
/// Shows just the raw example content without any navigation,
143+
/// control panel, or header chrome. Handles deferred loading automatically.
144+
class EmbedWrapper extends StatelessWidget {
98145
final Example example;
99146

100-
const EmbedWrapperMinimal({super.key, required this.example});
147+
const EmbedWrapper({super.key, required this.example});
101148

102149
@override
103150
Widget build(BuildContext context) {
104-
return Scaffold(body: example.builder(context));
151+
return Scaffold(
152+
body: EmbedContext(
153+
isEmbed: true,
154+
child: DeferredExampleLoader(example: example),
155+
),
156+
);
105157
}
106158
}

packages/demo/lib/example_detail_view.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22

3+
import 'embed_wrapper.dart';
34
import 'example_model.dart';
45

56
/// Provides example metadata to child widgets via InheritedWidget
@@ -40,7 +41,11 @@ class ExampleDetailView extends StatelessWidget {
4041

4142
// No header - the header will be shown in the right panel
4243
// Wrap with ExampleContext so child widgets can access example metadata
43-
return ExampleContext(example: example!, child: example!.builder(context));
44+
// Use DeferredExampleLoader to handle async loading
45+
return ExampleContext(
46+
example: example!,
47+
child: DeferredExampleLoader(example: example!),
48+
);
4449
}
4550

4651
Widget _buildEmptyState(BuildContext context) {

packages/demo/lib/example_model.dart

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
import 'package:flutter/material.dart';
22

3-
/// Represents a single example
3+
/// Function type for deferred example loading.
4+
/// Returns a widget builder after loading the deferred library.
5+
typedef ExampleLoader = Future<Widget Function(BuildContext)> Function();
6+
7+
/// Represents a single example with deferred loading support.
48
class Example {
59
final String id;
610
final String title;
711
final String description;
812
final IconData? icon;
9-
final Widget Function(BuildContext context) builder;
1013

11-
const Example({
14+
/// Loader function that loads the deferred library and returns the builder.
15+
final ExampleLoader loader;
16+
17+
/// Cached builder after loading.
18+
Widget Function(BuildContext)? _cachedBuilder;
19+
20+
Example({
1221
required this.id,
1322
required this.title,
1423
required this.description,
1524
this.icon,
16-
required this.builder,
25+
required this.loader,
1726
});
27+
28+
/// Whether this example has been loaded.
29+
bool get isLoaded => _cachedBuilder != null;
30+
31+
/// Loads the example and returns the widget.
32+
/// Caches the builder after first load.
33+
Future<Widget> load(BuildContext context) async {
34+
_cachedBuilder ??= await loader();
35+
return _cachedBuilder!(context);
36+
}
37+
38+
/// Builds the widget synchronously if already loaded.
39+
/// Throws if not loaded - use [load] first or [ExampleLoader] widget.
40+
Widget build(BuildContext context) {
41+
if (_cachedBuilder == null) {
42+
throw StateError('Example "$id" not loaded. Call load() first.');
43+
}
44+
return _cachedBuilder!(context);
45+
}
1846
}
1947

2048
/// Represents a category of examples with its examples

0 commit comments

Comments
 (0)