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
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 2025-09-02 - Memoize M3EThemeData in m3eThemeBridgeBuilder

**经验心得:** `m3eThemeBridgeBuilder` 挂载在 `MaterialApp.builder` 层级,每次根级或 Overlay 重建时均会触发。原实现每次直接调用 `buildM3EThemeData(Theme.of(context))`,造成 `M3EThemeData`、`M3EColorScheme` 和 `mui.TextTheme` 的冗余实例创建。使用 `StatefulWidget` 缓存 `M3EThemeData` 并仅在 `ThemeData` 改变时重建,在不改变应用功能前提下避免了无谓的 GC 压力与 CPU 消耗。

**后续行动:** 在处理挂载于顶级 Builder / Provider 的桥接转换组件时,务必注意入参到转换结果的缓存/记忆化处理。
38 changes: 34 additions & 4 deletions lib/theme/m3e_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,40 @@ M3EThemeData buildM3EThemeData(ThemeData theme) {
/// 真实的主题色。挂在 builder 里是必须的:只有位于 Overlay 之上的祖先,
/// 面板(overlay child)才能通过继承查找到。
Widget m3eThemeBridgeBuilder(BuildContext context, Widget? child) {
return M3ETheme(
data: buildM3EThemeData(Theme.of(context)),
child: child ?? const SizedBox.shrink(),
);
return _M3EThemeBridgeWidget(child: child);
}

/// 内部 Widget,通过缓存 [M3EThemeData] 避免每次父级重建时重复构造等价主题。
class _M3EThemeBridgeWidget extends StatefulWidget {
const _M3EThemeBridgeWidget({this.child});

final Widget? child;

@override
State<_M3EThemeBridgeWidget> createState() => _M3EThemeBridgeWidgetState();
}

class _M3EThemeBridgeWidgetState extends State<_M3EThemeBridgeWidget> {
ThemeData? _cachedTheme;
M3EThemeData? _cachedM3eTheme;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ⚡ Bolt Optimization: Memoize M3EThemeData construction.
// Rebuilding buildM3EThemeData on every widget build allocates new
// M3EColorScheme and M3ETypeScale objects. Caching avoids unnecessary
// allocations when theme properties haven't changed.
if (_cachedTheme != theme || _cachedM3eTheme == null) {
_cachedTheme = theme;
_cachedM3eTheme = buildM3EThemeData(theme);
}

return M3ETheme(
data: _cachedM3eTheme!,
child: widget.child ?? const SizedBox.shrink(),
);
}
}

M3EColorScheme _toM3eColorScheme(ColorScheme cs) {
Expand Down
127 changes: 127 additions & 0 deletions test/unit/theme/m3e_bridge_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:janus/theme/m3e_bridge.dart';
import 'package:material_3_expressive/material_3_expressive.dart';

void main() {
group('M3E Bridge', () {
test(
'buildM3EThemeData builds M3EThemeData matching Flutter ThemeData',
() {
final theme = ThemeData.light();
final m3eThemeData = buildM3EThemeData(theme);

expect(
m3eThemeData.colorScheme.primary,
equals(theme.colorScheme.primary),
);
expect(m3eThemeData.brightness, equals(theme.brightness));
},
);

testWidgets(
'm3eThemeBridgeBuilder memoizes M3EThemeData when theme does not change',
(tester) async {
M3EThemeData? firstM3eTheme;
M3EThemeData? secondM3eTheme;

final theme = ThemeData.light();

await tester.pumpWidget(
Theme(
data: theme,
child: Builder(
builder: (context) {
return m3eThemeBridgeBuilder(
context,
Builder(
builder: (ctx) {
firstM3eTheme = M3ETheme.of(ctx);
return const SizedBox();
},
),
);
},
),
),
);

expect(firstM3eTheme, isNotNull);

// Rebuild with identical theme instance
await tester.pumpWidget(
Theme(
data: theme,
child: Builder(
builder: (context) {
return m3eThemeBridgeBuilder(
context,
Builder(
builder: (ctx) {
secondM3eTheme = M3ETheme.of(ctx);
return const SizedBox();
},
),
);
},
),
),
);

expect(secondM3eTheme, isNotNull);
expect(identical(firstM3eTheme, secondM3eTheme), isTrue);
},
);

testWidgets(
'm3eThemeBridgeBuilder updates M3EThemeData when theme changes',
(tester) async {
M3EThemeData? lightM3eTheme;
M3EThemeData? darkM3eTheme;

await tester.pumpWidget(
Theme(
data: ThemeData.light(),
child: Builder(
builder: (context) {
return m3eThemeBridgeBuilder(
context,
Builder(
builder: (ctx) {
lightM3eTheme = M3ETheme.of(ctx);
return const SizedBox();
},
),
);
},
),
),
);

await tester.pumpWidget(
Theme(
data: ThemeData.dark(),
child: Builder(
builder: (context) {
return m3eThemeBridgeBuilder(
context,
Builder(
builder: (ctx) {
darkM3eTheme = M3ETheme.of(ctx);
return const SizedBox();
},
),
);
},
),
),
);

expect(lightM3eTheme, isNotNull);
expect(darkM3eTheme, isNotNull);
expect(identical(lightM3eTheme, darkM3eTheme), isFalse);
expect(darkM3eTheme!.brightness, equals(Brightness.dark));
},
);
});
}
Loading