# Flutter 开发规范

> 基于 Flutter 官方 [Style Guide](https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md) 和最佳实践

## 🎯 核心原则

1. **组合优于继承** - 通过组合构建复杂的 Widget 和逻辑
2. **Widget 即 UI** - Flutter 中一切皆 Widget
3. **不可变 Widget** - Widget(尤其是 StatelessWidget)应该是不可变的
4. **状态分离** - 区分瞬时状态(ephemeral state)和应用状态(app state)
5. **简洁声明式** - 编写简洁的现代声明式代码
6. **性能优先** - 优化 Widget 重建和内存使用

## Widget 设计

### StatelessWidget vs StatefulWidget

```dart
// ✅ 好 - 无状态 Widget,不可变
class UserAvatar extends StatelessWidget {
  const UserAvatar({
    super.key,
    required this.imageUrl,
    this.size = 40,
  });

  final String imageUrl;
  final double size;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      radius: size / 2,
      backgroundImage: NetworkImage(imageUrl),
    );
  }
}

// ✅ 好 - 有状态 Widget,状态清晰
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

// ❌ 坏 - 不必要的 StatefulWidget
class UserAvatar extends StatefulWidget {
  const UserAvatar({super.key, required this.imageUrl});
  
  final String imageUrl;
  
  @override
  State<UserAvatar> createState() => _UserAvatarState();
}

class _UserAvatarState extends State<UserAvatar> {
  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      backgroundImage: NetworkImage(widget.imageUrl),
    );
  }
}
```

### Widget 构造函数

```dart
// ✅ 好 - 构造函数在最前,使用 const
class ProductCard extends StatelessWidget {
  // 1. 默认构造函数首先
  const ProductCard({
    super.key,
    required this.product,
    this.onTap,
  });
  
  // 2. 命名构造函数
  const ProductCard.compact({
    super.key,
    required this.product,
  }) : onTap = null;
  
  // 3. 字段
  final Product product;
  final VoidCallback? onTap;
  
  // 4. build 方法
  @override
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: onTap,
        child: Column(
          children: [
            Image.network(product.imageUrl),
            Text(product.name),
            Text('\$${product.price}'),
          ],
        ),
      ),
    );
  }
}
```

### Widget 组合

```dart
// ✅ 好 - 将大 Widget 拆分成小的可复用组件
class ProductListItem extends StatelessWidget {
  const ProductListItem({super.key, required this.product});
  
  final Product product;
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Row(
        children: [
          _ProductImage(imageUrl: product.imageUrl),
          Expanded(
            child: _ProductInfo(product: product),
          ),
          _ProductActions(product: product),
        ],
      ),
    );
  }
}

class _ProductImage extends StatelessWidget {
  const _ProductImage({required this.imageUrl});
  
  final String imageUrl;
  
  @override
  Widget build(BuildContext context) {
    return Image.network(
      imageUrl,
      width: 80,
      height: 80,
      fit: BoxFit.cover,
    );
  }
}

class _ProductInfo extends StatelessWidget {
  const _ProductInfo({required this.product});
  
  final Product product;
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            product.name,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 4),
          Text(
            product.description,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}

// ❌ 坏 - 单一巨型 Widget
class ProductListItem extends StatelessWidget {
  const ProductListItem({super.key, required this.product});
  
  final Product product;
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Row(
        children: [
          Image.network(
            product.imageUrl,
            width: 80,
            height: 80,
            fit: BoxFit.cover,
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    product.description,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  // ... 更多嵌套代码
                ],
              ),
            ),
          ),
          // ... 更多代码
        ],
      ),
    );
  }
}
```

## 状态管理

### 瞬时状态 (Ephemeral State)

```dart
// ✅ 好 - 使用 setState 管理局部状态
class TabContainer extends StatefulWidget {
  const TabContainer({super.key});

  @override
  State<TabContainer> createState() => _TabContainerState();
}

class _TabContainerState extends State<TabContainer> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TabBar(
          currentIndex: _selectedIndex,
          onTap: (index) {
            setState(() {
              _selectedIndex = index;
            });
          },
        ),
        IndexedStack(
          index: _selectedIndex,
          children: const [
            HomeTab(),
            ProfileTab(),
            SettingsTab(),
          ],
        ),
      ],
    );
  }
}
```

### 应用状态 (App State)

```dart
// ✅ 好 - 使用状态管理方案(如 Provider, Riverpod, Bloc)

// 使用 Provider 示例
class CartProvider extends ChangeNotifier {
  final List<Product> _items = [];
  
  List<Product> get items => List.unmodifiable(_items);
  
  int get itemCount => _items.length;
  
  double get totalPrice => 
    _items.fold(0, (sum, item) => sum + item.price);
  
  void addItem(Product product) {
    _items.add(product);
    notifyListeners();
  }
  
  void removeItem(Product product) {
    _items.remove(product);
    notifyListeners();
  }
  
  void clear() {
    _items.clear();
    notifyListeners();
  }
}

// 在 Widget 中使用
class CartButton extends StatelessWidget {
  const CartButton({super.key});
  
  @override
  Widget build(BuildContext context) {
    final itemCount = context.watch<CartProvider>().itemCount;
    
    return Badge(
      label: Text('$itemCount'),
      child: IconButton(
        icon: const Icon(Icons.shopping_cart),
        onPressed: () => Navigator.pushNamed(context, '/cart'),
      ),
    );
  }
}
```

## 布局最佳实践

### 响应式布局

```dart
// ✅ 好 - 使用 LayoutBuilder 创建响应式布局
class ResponsiveLayout extends StatelessWidget {
  const ResponsiveLayout({super.key, required this.child});
  
  final Widget child;
  
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth > 840) {
          return _DesktopLayout(child: child);
        } else if (constraints.maxWidth > 600) {
          return _TabletLayout(child: child);
        } else {
          return _MobileLayout(child: child);
        }
      },
    );
  }
}

// ✅ 好 - 使用 MediaQuery 获取屏幕信息
class AdaptiveCard extends StatelessWidget {
  const AdaptiveCard({super.key});
  
  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    final isSmallScreen = size.width < 600;
    
    return Card(
      child: Padding(
        padding: EdgeInsets.all(isSmallScreen ? 8 : 16),
        child: Column(
          children: [
            if (!isSmallScreen) const Header(),
            const Content(),
          ],
        ),
      ),
    );
  }
}
```

### 避免溢出

```dart
// ✅ 好 - 使用 Flexible/Expanded 避免溢出
class UserInfo extends StatelessWidget {
  const UserInfo({super.key, required this.user});
  
  final User user;
  
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const CircleAvatar(radius: 24),
        const SizedBox(width: 8),
        Expanded( // 防止文本溢出
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                user.name,
                overflow: TextOverflow.ellipsis,
                maxLines: 1,
              ),
              Text(
                user.email,
                overflow: TextOverflow.ellipsis,
                maxLines: 1,
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
        ),
      ],
    );
  }
}

// ❌ 坏 - 可能导致溢出
class UserInfo extends StatelessWidget {
  const UserInfo({super.key, required this.user});
  
  final User user;
  
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const CircleAvatar(radius: 24),
        const SizedBox(width: 8),
        Column( // 没有限制宽度!
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(user.name), // 可能溢出
            Text(user.email),
          ],
        ),
      ],
    );
  }
}
```

## 主题和样式

### 使用 ThemeData

```dart
// ✅ 好 - 定义完整的主题
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
        textTheme: const TextTheme(
          displayLarge: TextStyle(
            fontSize: 57,
            fontWeight: FontWeight.bold,
          ),
          titleLarge: TextStyle(
            fontSize: 22,
            fontWeight: FontWeight.w600,
          ),
          bodyLarge: TextStyle(
            fontSize: 16,
            height: 1.5,
          ),
        ),
        cardTheme: CardTheme(
          elevation: 2,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      home: const HomePage(),
    );
  }
}

// ✅ 好 - 使用主题值
class MyButton extends StatelessWidget {
  const MyButton({super.key, required this.label});
  
  final String label;
  
  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: theme.colorScheme.primary,
        foregroundColor: theme.colorScheme.onPrimary,
      ),
      onPressed: () {},
      child: Text(label),
    );
  }
}

// ❌ 坏 - 硬编码颜色
class MyButton extends StatelessWidget {
  const MyButton({super.key, required this.label});
  
  final String label;
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.blue, // 硬编码!
        foregroundColor: Colors.white,
      ),
      onPressed: () {},
      child: Text(label),
    );
  }
}
```

### ThemeExtension 扩展主题

```dart
// ✅ 好 - 使用 ThemeExtension 添加自定义主题
@immutable
class CustomColors extends ThemeExtension<CustomColors> {
  const CustomColors({
    required this.success,
    required this.warning,
    required this.danger,
  });
  
  final Color success;
  final Color warning;
  final Color danger;
  
  @override
  CustomColors copyWith({
    Color? success,
    Color? warning,
    Color? danger,
  }) {
    return CustomColors(
      success: success ?? this.success,
      warning: warning ?? this.warning,
      danger: danger ?? this.danger,
    );
  }
  
  @override
  CustomColors lerp(CustomColors? other, double t) {
    if (other is! CustomColors) return this;
    return CustomColors(
      success: Color.lerp(success, other.success, t)!,
      warning: Color.lerp(warning, other.warning, t)!,
      danger: Color.lerp(danger, other.danger, t)!,
    );
  }
}

// 在主题中使用
ThemeData(
  extensions: [
    CustomColors(
      success: Colors.green,
      warning: Colors.orange,
      danger: Colors.red,
    ),
  ],
)

// 访问自定义主题
final customColors = Theme.of(context).extension<CustomColors>()!;
```

## 导航

### 使用现代路由

```dart
// ✅ 好 - 使用 go_router 或 auto_route
import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'profile/:userId',
          builder: (context, state) {
            final userId = state.pathParameters['userId']!;
            return ProfilePage(userId: userId);
          },
        ),
        GoRoute(
          path: 'settings',
          builder: (context, state) => const SettingsPage(),
        ),
      ],
    ),
  ],
);

// 导航
context.go('/profile/123');
context.push('/settings');

// ❌ 坏 - 过时的命名路由
MaterialApp(
  routes: {
    '/': (context) => const HomePage(),
    '/profile': (context) => const ProfilePage(),
  },
)
```

## 性能优化

### 避免不必要的重建

```dart
// ✅ 好 - 使用 const 构造函数
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});
  
  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        Text('Static Text'), // const Widget 不会重建
        Icon(Icons.home),
      ],
    );
  }
}

// ✅ 好 - 提取子 Widget
class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});
  
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _counter = 0;
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('Increment'),
        ),
        const ExpensiveWidget(), // 不会随 counter 变化而重建
      ],
    );
  }
}

class ExpensiveWidget extends StatelessWidget {
  const ExpensiveWidget({super.key});
  
  @override
  Widget build(BuildContext context) {
    // 昂贵的构建逻辑
    return const Text('Expensive Widget');
  }
}
```

### 列表性能

```dart
// ✅ 好 - 使用 ListView.builder 处理长列表
class ProductList extends StatelessWidget {
  const ProductList({super.key, required this.products});
  
  final List<Product> products;
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];
        return ProductListItem(
          key: ValueKey(product.id), // 使用唯一 key
          product: product,
        );
      },
    );
  }
}

// ✅ 好 - 使用 ListView.separated 添加分隔符
ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
  separatorBuilder: (context, index) => const Divider(),
)

// ❌ 坏 - 一次性构建所有项目
ListView(
  children: products.map((p) => ProductListItem(product: p)).toList(),
)
```

### 图片优化

```dart
// ✅ 好 - 使用 cached_network_image
import 'package:cached_network_image/cached_network_image.dart';

class ProductImage extends StatelessWidget {
  const ProductImage({super.key, required this.imageUrl});
  
  final String imageUrl;
  
  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      placeholder: (context, url) => 
        const Center(child: CircularProgressIndicator()),
      errorWidget: (context, url, error) => 
        const Icon(Icons.error),
      fit: BoxFit.cover,
    );
  }
}

// ✅ 好 - 优化图片加载
Image.network(
  imageUrl,
  cacheWidth: 400, // 限制缓存图片宽度
  cacheHeight: 400,
  fit: BoxFit.cover,
)
```

## 测试

### Widget 测试

```dart
// ✅ 好 - 编写 Widget 测试
void main() {
  testWidgets('Counter increments', (tester) async {
    // Arrange
    await tester.pumpWidget(const MaterialApp(home: Counter()));
    
    // Assert initial state
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);
    
    // Act
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    // Assert
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
  
  testWidgets('Product card displays correctly', (tester) async {
    // Arrange
    const product = Product(
      id: '1',
      name: 'Test Product',
      price: 99.99,
    );
    
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: ProductCard(product: product),
        ),
      ),
    );
    
    // Assert
    expect(find.text('Test Product'), findsOneWidget);
    expect(find.text('\$99.99'), findsOneWidget);
  });
}
```

### 集成测试

```dart
// ✅ 好 - 编写集成测试
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  testWidgets('Complete purchase flow', (tester) async {
    // 启动应用
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();
    
    // 浏览商品
    expect(find.text('Products'), findsOneWidget);
    await tester.tap(find.text('Add to Cart').first);
    await tester.pumpAndSettle();
    
    // 查看购物车
    await tester.tap(find.byIcon(Icons.shopping_cart));
    await tester.pumpAndSettle();
    
    // 结账
    await tester.tap(find.text('Checkout'));
    await tester.pumpAndSettle();
    
    // 验证
    expect(find.text('Order Confirmed'), findsOneWidget);
  });
}
```

## 国际化 (i18n)

### 使用 intl 包

```dart
// ✅ 好 - 正确的国际化实现
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart';

class AppLocalizations {
  final Locale locale;
  
  AppLocalizations(this.locale);
  
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
  }
  
  static const LocalizationsDelegate<AppLocalizations> delegate = 
    _AppLocalizationsDelegate();
  
  String get title => Intl.message(
    'My App',
    name: 'title',
    locale: locale.toString(),
  );
  
  String itemCount(int count) => Intl.plural(
    count,
    zero: 'No items',
    one: '1 item',
    other: '$count items',
    name: 'itemCount',
    args: [count],
    locale: locale.toString(),
  );
}

// 在 MaterialApp 中配置
MaterialApp(
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: const [
    Locale('en', ''),
    Locale('zh', ''),
  ],
  home: const HomePage(),
)

// ❌ 坏 - 硬编码文本
Text('Hello World') // 应该使用国际化
```

## 无障碍访问 (Accessibility)

```dart
// ✅ 好 - 提供语义信息
Semantics(
  label: '商品图片',
  child: Image.network(product.imageUrl),
)

// ✅ 好 - 确保足够的对比度
Text(
  'Important Text',
  style: TextStyle(
    color: Colors.black, // 与白色背景对比度 21:1
    fontSize: 16,
  ),
)

// ✅ 好 - 合适的触摸目标大小(至少 48x48)
SizedBox(
  width: 48,
  height: 48,
  child: IconButton(
    icon: const Icon(Icons.add),
    onPressed: () {},
  ),
)
```

## 错误处理

```dart
// ✅ 好 - 使用 ErrorWidget 自定义错误显示
void main() {
  ErrorWidget.builder = (FlutterErrorDetails details) {
    return Material(
      child: Container(
        color: Colors.red[100],
        child: Center(
          child: Text(
            'Error: ${details.exception}',
            style: const TextStyle(color: Colors.red),
          ),
        ),
      ),
    );
  };
  
  runApp(const MyApp());
}

// ✅ 好 - 使用 FutureBuilder 处理异步
class UserProfile extends StatelessWidget {
  const UserProfile({super.key, required this.userId});
  
  final String userId;
  
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: fetchUser(userId),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        
        if (snapshot.hasError) {
          return Center(
            child: Text('Error: ${snapshot.error}'),
          );
        }
        
        if (!snapshot.hasData) {
          return const Center(child: Text('User not found'));
        }
        
        final user = snapshot.data!;
        return UserDetails(user: user);
      },
    );
  }
}
```

## 最佳实践总结

1. **优先使用 const** - 提升性能,减少重建
2. **组合小 Widget** - 保持代码可维护性
3. **合理使用状态管理** - 区分局部和全局状态
4. **响应式布局** - 适配不同屏幕尺寸
5. **使用主题系统** - 避免硬编码样式
6. **性能优化** - 使用 builder、const、key
7. **编写测试** - Widget 测试和集成测试
8. **国际化支持** - 使用 i18n 工具
9. **无障碍访问** - 添加语义信息
10. **错误处理** - 优雅处理异步和错误状态

---

## 📝 TextField 垂直居中规范（重要）

> ⚠️ **此问题曾导致 15+ 轮对话才修复，必须一步到位**

### 问题场景
TextField 中 placeholder、光标、输入内容三者需要在固定高度容器中垂直居中对齐。

### 正确方案（一步到位）

```dart
// ✅ 正确 - Container.alignment + 统一 height + contentPadding.zero
Widget _buildCenteredTextField(String placeholder, TextEditingController controller) {
  return Container(
    height: 36, // 固定容器高度
    alignment: Alignment.center, // 关键1：让 TextField 整体居中
    child: TextField(
      controller: controller,
      textAlign: TextAlign.center,
      style: const TextStyle(
        fontSize: 14,
        height: 1.43, // 关键2：行高 = 期望文本高度 ÷ 字号
      ),
      decoration: InputDecoration(
        hintText: placeholder,
        hintStyle: const TextStyle(
          fontSize: 14,
          height: 1.43, // 关键3：必须与 style.height 完全一致
        ),
        contentPadding: EdgeInsets.zero, // 关键4：清除默认 padding
        isDense: true, // 关键5：移除额外空间
        border: InputBorder.none,
      ),
    ),
  );
}

// ❌ 错误 - 会导致 placeholder 和输入内容位置不一致
TextField(
  textAlignVertical: TextAlignVertical.center, // 只影响输入内容
  style: TextStyle(fontSize: 14, height: 1.43),
  decoration: InputDecoration(
    hintStyle: TextStyle(fontSize: 14), // height 不一致！
    contentPadding: EdgeInsets.symmetric(vertical: 8), // 干扰居中
  ),
)
```

### 行高计算公式

从设计稿获取：
- 容器高度：36px
- 文本 Y 坐标：8px（距顶部）
- 文本高度：20px
- 字号：14px

```
height = 文本高度 ÷ 字号 = 20 ÷ 14 = 1.43
```

### 禁止事项

| 禁止 | 原因 |
|------|------|
| `style.height` ≠ `hintStyle.height` | placeholder 和输入内容位置不一致 |
| 同时用 `textAlignVertical` 和 `height` | 产生冲突，效果不可预测 |
| 用 `strutStyle + forceStrutHeight` | 可能压缩文字 |
| 反复调整 `contentPadding` 试错 | 应先确定行高配置 |

### 调试顺序（遇到问题时）

1. 先从设计稿获取：容器高度、文本 Y 坐标、文本高度
2. 计算 `height = 文本高度 ÷ 字号`
3. 确保 `style.height` = `hintStyle.height`
4. 设置 `Container.alignment: Alignment.center`
5. 设置 `contentPadding: EdgeInsets.zero` + `isDense: true`

### TextField Focus 背景色全局修复

> ⚠️ **TextField 聚焦时出现蓝色半透明背景，需要在 ThemeData 两处同时消除**

Flutter 默认 `focusColor = Color(0x1F2196F3)`（蓝色 12% 透明度），在 TextField focus 时作为 State Layer 叠加在背景上，视觉效果很突兀。

```dart
// ✅ 必须在 ThemeData 和 InputDecorationTheme 两处同时设置
ThemeData(
  focusColor: Colors.transparent,      // 消除 focus state layer
  hoverColor: Colors.transparent,       // 消除 hover state layer
  splashColor: Colors.transparent,      // 消除水波纹
  highlightColor: Colors.transparent,   // 消除高亮
  inputDecorationTheme: InputDecorationTheme(
    filled: true,
    fillColor: Colors.transparent,
    focusColor: Colors.transparent,     // 双重保护
    hoverColor: Colors.transparent,
    border: InputBorder.none,
    enabledBorder: InputBorder.none,
    focusedBorder: InputBorder.none,
  ),
)

// ❌ 错误 - 只改 InputDecorationTheme 不够，ThemeData 级别仍然生效
inputDecorationTheme: InputDecorationTheme(
  focusColor: Colors.transparent, // 单独设置无效
),
```

| 属性 | 控制范围 |
|------|----------|
| `ThemeData.focusColor` | 全局 focus 状态叠加色 |
| `ThemeData.hoverColor` | 全局 hover 状态叠加色 |
| `InputDecorationTheme.focusColor` | TextField 专属 focus 色 |
| `InputDecorationTheme.fillColor` | TextField 填充色 |

---

## 🎨 Sketch/Figma 设计稿还原规范

> ⚠️ **此章节为强制执行规范** - 所有 UI 还原任务必须严格遵循

### 问题根源分析

过去还原设计稿时存在以下问题导致效率低下：

| 问题 | 表现 | 根因 |
|------|------|------|
| 属性读取不完整 | 漏读渐变、圆角、阴影参数 | 只读取部分属性 |
| 假设而非验证 | 假设圆形/颜色/图标 | 未从设计稿验证 |
| 使用近似值 | 用 Material Icons 代替 | 未导出原始 SVG |
| 分散查询 | 多轮对话才获取完整信息 | 每次只查一个属性 |

### 强制执行：一次性完整提取

**在还原任何 UI 元素前，必须一次性提取所有属性（Sketch 示例）：**

```javascript
// 完整样式提取脚本
const sketch = require('sketch');
const page = sketch.getSelectedDocument().selectedPage;

function extractFullStyle(layerName) {
  const layer = sketch.find(`[name="${layerName}"]`, page)[0];
  if (!layer) return console.log(`Layer "${layerName}" not found`);

  console.log('=== 基本信息 ===');
  console.log(`Name: ${layer.name} (${layer.type})`);
  console.log(`Frame: ${layer.frame.width}x${layer.frame.height}`);

  const style = layer.style;

  // 1. 填充（颜色/渐变）
  console.log('=== 填充 ===');
  (style.fills || []).filter(f => f.enabled).forEach((fill, i) => {
    console.log(`Fill ${i}: Type=${fill.fillType}`);
    if (fill.fillType === 'Color') {
      console.log(`  Color: ${fill.color}`);
    } else if (fill.fillType === 'Gradient') {
      console.log(`  Gradient: ${fill.gradient.gradientType}`);
      fill.gradient.stops.forEach((stop, j) => {
        console.log(`  Stop ${j}: ${stop.color} @ ${stop.position}`);
      });
    }
  });

  // 2. 阴影
  console.log('=== 阴影 ===');
  (style.shadows || []).filter(s => s.enabled).forEach((s, i) => {
    console.log(`Shadow ${i}: Color=${s.color}, Offset=(${s.x}, ${s.y}), Blur=${s.blur}, Spread=${s.spread}`);
  });

  // 3. 内阴影
  (style.innerShadows || []).filter(s => s.enabled).forEach((s, i) => {
    console.log(`InnerShadow ${i}: Color=${s.color}, Offset=(${s.x}, ${s.y}), Blur=${s.blur}, Spread=${s.spread}`);
  });

  // 4. 边框
  console.log('=== 边框 ===');
  (style.borders || []).filter(b => b.enabled).forEach((b, i) => {
    console.log(`Border ${i}: Color=${b.color}, Width=${b.thickness}`);
  });
}

extractFullStyle('Layer Name');
```

### SVG 图标还原规范

> ⚠️ **禁止使用 Material Icons 或其他近似图标，必须从设计稿导出原始 SVG**

#### SVG 导出规范

**导出时保留完整 viewBox 和坐标**：

```javascript
// 从 Sketch 导出 SVG
const sketch = require('sketch');
const layer = sketch.find('[name="Icon Name"]', sketch.getSelectedDocument().selectedPage)[0];
if (layer) {
  sketch.export(layer, { 
    formats: 'svg', 
    output: '/path/to/assets/icons/' 
  });
}
```

```xml
<!-- ❌ 错误 - 导出最小 viewBox -->
<!-- viewBox="0 0 6 3" 放在 12x12 容器中需要额外居中处理 -->

<!-- ✅ 正确 - 导出完整容器 viewBox -->
<svg viewBox="0 0 12 12">
  <!-- 保留元素在容器中的精确位置 -->
  <polygon fill="#1C2B45" fill-opacity="0.7" points="3.5 5 6 7.5 8.5 5"/>
</svg>
```

#### SVG 使用规范

```dart
// ❌ 错误 - 强制覆盖颜色（会丢失透明度）
SvgPicture.asset(
  'assets/icons/dropdown_arrow.svg',
  colorFilter: ColorFilter.mode(
    someColor,           // 覆盖了 SVG 原有颜色
    BlendMode.srcIn,     // 覆盖了 SVG 原有透明度
  ),
)

// ✅ 正确 - 保留 SVG 原有样式
SvgPicture.asset(
  'assets/icons/dropdown_arrow.svg',
  width: 12,
  height: 12,
  // 不使用 colorFilter，保留 SVG 原有颜色和透明度
  // 仅在外部明确指定颜色时才覆盖
  colorFilter: customColor != null
      ? ColorFilter.mode(customColor, BlendMode.srcIn)
      : null,
)
```

#### Bitmap 图层处理规范

Sketch 将 Bitmap（位图）图层"导出为 SVG"时，**并不生成矢量路径**，而是嵌入 base64 PNG 数据：

```xml
<!-- Bitmap 导出 SVG 的实际内容 - 只有 base64 图片，无矢量路径 -->
<svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
  <image xlink:href="data:image/png;base64,iVBORw0KGgo..." width="12" height="12"/>
</svg>
```

**正确处理方式：识别 Bitmap 图层后，改为 PNG 导出，Flutter 使用 `Image.asset`**：

```javascript
// 判断图层是否为 Bitmap
const isBitmap = layer.type === 'Image';

// Bitmap 图层导出 PNG（@3x 高清）
if (isBitmap) {
  sketch.export(layer, { formats: 'png', scales: '3', output: '/path/to/assets/icons/' });
}
```

```dart
// ✅ 正确 - Bitmap 图层用 Image.asset，不用 SvgPicture.asset
Image.asset(
  'assets/icons/ic_example.png',
  width: 12,
  height: 12,
  color: AppColors.textDarkSecondary,  // 按需染色
  colorBlendMode: BlendMode.srcIn,
)

// ❌ 错误 - 对 Bitmap SVG 用 SvgPicture 会显示模糊的 base64 位图
SvgPicture.asset('assets/icons/ic_example.svg') // 实际是嵌套 PNG，非矢量
```

#### 颜色透明度转换

设计稿颜色格式：`#RRGGBBAA`（最后两位是透明度）

```
Sketch: #1c2b45b3 → R:28 G:43 B:69 A:70%
Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
```

常用透明度对照：

| 百分比 | Hex | 示例 |
|--------|-----|------|
| 100% | FF | #FFFFFFFF |
| 70% | B3 | #1C2B45B3 |
| 50% | 80 | #00000080 |
| 15% | 26 | #1C2B4526 |

### 字重（FontWeight）还原规范

> ⚠️ **Sketch `style.fontWeight` 返回的是内部索引值，≠ Flutter FontWeight 数值**（高频错误）

#### 错误根因

Sketch JS API 的 `layer.style.fontWeight` 返回 5、6、8、9 等内部索引，与 CSS/Flutter 的字重体系（400、500、600、700）**没有直接倍数关系**，不能 `×100` 使用。

| Sketch `fontWeight` 值 | 实际字体面 | Flutter 正确值 | 常见错误推断 |
|---|---|---|---|
| 5 | PingFangSC-Regular | `FontWeight.w400` | ~~w500~~（直接 ×100） |
| 6 | PingFangSC-Medium | `FontWeight.w500` | ~~w600~~ |
| 8 | PingFangSC-Semibold | `FontWeight.w600` | ~~w800~~（最常见错误！）|
| 9 | PingFangSC-Bold | `FontWeight.w700` | ~~w900~~ |

#### 正确获取方式（必须用 native API）

```javascript
// ❌ 错误 - fontWeight 数字不等于 Flutter FontWeight 值
const weight = layer.style.fontWeight; // 返回 8，不代表 w800

// ✅ 正确 - 获取字体面完整名称，再按规则映射
const fontName = String(layer.sketchObject.font().fontName());
// "PingFangSC-Regular"  → FontWeight.w400
// "PingFangSC-Medium"   → FontWeight.w500
// "PingFangSC-Semibold" → FontWeight.w600
// "PingFangSC-Bold"     → FontWeight.w700

// 批量提取 Text 图层字重的完整脚本
function getLayerFontWeight(layer) {
  const fontName = String(layer.sketchObject.font().fontName());
  const map = {
    'Regular':  'FontWeight.w400',
    'Medium':   'FontWeight.w500',
    'Semibold': 'FontWeight.w600',
    'Bold':     'FontWeight.w700',
  };
  const variant = Object.keys(map).find(k => fontName.includes(k)) || 'Regular';
  return { fontName, fontWeight: map[variant] };
}
```

#### 字重与 UI 元素对照（PingFangSC 典型规律）

| UI 元素 | 常用字体面 | Flutter FontWeight |
|--------|--------|-------------------|
| 说明文字、普通标签 | PingFangSC-Regular | `w400` |
| 次标题、中等强调 | PingFangSC-Medium | `w500` |
| 重要数值、价格金额 | PingFangSC-Semibold | `w600` |
| 页面主标题 | PingFangSC-Bold | `w700` |

### 问题速查表

> ⚠️ **修改代码前，先检查是否属于已知问题类型**

| 问题特征 | 问题 ID | 快速方案 |
|----------|---------|----------|
| 半透明容器颜色偏暗 | #1 阴影透出 | `HollowShadowPainter` 挖空阴影 |
| 元素位置/间距不对 | #2 布局偏移 | 固定宽度 + 精确坐标 |
| 选中项阴影模糊一片 | #3 裁剪问题 | `clipBehavior: Clip.none` |
| focus 时出现蓝框 | #4 边框异常 | 全局 + 组件级移除边框 |
| 形状错误（圆形vs圆角） | #5 shape 冲突 | 检查 `shape` vs `borderRadius` |
| Row 内 Gap 间距无效 | #6 Gap 方向错误 | `SizedBox(width:)` 或 `Gap.h()` |
| **SVG 颜色比设计稿浅** | #7 ColorFilter 覆盖 | **移除 ColorFilter，保留 SVG 原有样式** |
| **SVG 图标未居中** | #8 viewBox 不匹配 | **SVG viewBox 与使用尺寸一致** |
| **字重比设计稿更粗/更细** | #9 fontWeight 数字误读 | **用 `font().fontName()` 获取字体面名称映射** |
| **SVG 图标模糊/非矢量** | #10 Bitmap 图层误用 | **位图图层改用 PNG 导出 + `Image.asset`** |
| **TextField 聚焦出现蓝色背景** | #11 Focus 颜色未清除 | **ThemeData + InputDecorationTheme 两处设 transparent** |
| **Obx 不触发 RxSet 刷新** | #12 GetX 引用未订阅 | **Obx 内用 `Set.from(rxSet)` 创建新集合触发订阅** |

### 还原检查清单

在还原任何 UI 元素前，必须确认以下所有属性：

| 属性 | 检查项 | Flutter 对应 |
|------|--------|--------------|
| **尺寸** | width, height | `width`, `height` |
| **填充类型** | Color / Gradient / Image | `color` / `gradient` / `DecorationImage` |
| **渐变细节** | stops, from, to, type | `LinearGradient`, `RadialGradient` |
| **圆角** | cornerRadius (4个角) | `borderRadius` / `BoxShape.circle` |
| **阴影** | color, x, y, blur, spread | `boxShadow: [BoxShadow(...)]` |
| **内阴影** | 同上 | 需要特殊处理（Flutter 不原生支持） |
| **边框** | color, thickness, position | `border: Border.all(...)` |
| **不透明度** | opacity (颜色末尾两位) | 颜色 alpha 或 `Opacity` widget |
| **图标** | SVG path, fill color, opacity | `SvgPicture.asset` |
| **字重** | fontFace 名称（非 fontWeight 数字） | `FontWeight.w400/w500/w600/w700` |
| **图标类型** | 矢量(Shape) vs 位图(Image) | Shape→SVG；Image→PNG+`Image.asset` |

### 禁止事项

1. ❌ **禁止假设形状** - 必须从设计稿读取 `cornerRadius`
2. ❌ **禁止假设颜色** - 必须读取完整的 `fills` 数组
3. ❌ **禁止使用近似图标** - 必须导出 SVG
4. ❌ **禁止分散查询** - 必须一次性获取所有属性
5. ❌ **禁止遗漏阴影参数** - 必须读取全部 5 个参数
6. ❌ **禁止忽略透明度** - 颜色 `#RRGGBBAA` 最后两位是透明度
7. ❌ **禁止 ColorFilter 覆盖 SVG** - 除非明确需要改变颜色
8. ❌ **禁止用 Sketch `fontWeight` 数字推断 Flutter FontWeight** - 必须通过字体面名称（`font().fontName()`）映射
9. ❌ **禁止对 Bitmap 图层使用 SvgPicture.asset** - 必须导出 PNG，用 `Image.asset`
10. ❌ **禁止只在 InputDecorationTheme 修复 focus 背景色** - ThemeData 级别的 focusColor 也必须设置
11. ❌ **禁止在 Obx 外传入 RxSet 引用** - 必须在 Obx lambda 内用 `Set.from()` 触发响应式订阅

---

**参考资源:**
- [Flutter Documentation](https://flutter.dev/docs)
- [Flutter Style Guide](https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md)
- [Effective Dart](https://dart.dev/effective-dart)
- [Material Design 3](https://m3.material.io/)
- [Flutter Performance Best Practices](https://flutter.dev/docs/perf/best-practices)

---

## 📎 关联规范（v2.1.0 新增）

以下规范提供了 Flutter 开发中常见问题的跨框架深度指导，建议按需加载：

### 国际化检查清单

> `get_standard_by_id({ id: "i18n" })` → Flutter ARB 章节

- 所有用户可见文本使用 `S.of(context).keyName` 或 `S.current.keyName`
- 新增翻译 key 同时更新 `app_en.arb` 和 `app_zh.arb`
- 禁止在代码中硬编码中文字符串
- 领域层使用 `S.of(Get.context!)` 或通过参数传递

### 硬编码防范检查清单

> `get_standard_by_id({ id: "hardcoding-prevention" })` → Flutter 章节

- 颜色使用 `AppColors.xxx` 或 `$c.xxx`，禁止 `Color(0x...)`
- 字号使用 `DesignFontSizes.fXX` 或 `$t.xxx`，禁止 `fontSize: 数字`
- 圆角使用 `AppRadius.rXX` 或 `$r.xxx`，禁止 `BorderRadius.circular(数字)`
- 间距使用 `$s.xxx` 或 `Gap(...)`
- 透明度使用 `.withValues(alpha: ...)` 而非 `.withOpacity(...)`

### 错误处理统一

> `get_standard_by_id({ id: "error-handling-unification" })` → Flutter 章节

- 所有通知通过 `AppToast.success/error/warning` 发出
- 禁止直接调用 `Get.snackbar` 或 `ScaffoldMessenger`
- API 错误通过中间层统一处理

### Mock 数据

> `get_standard_by_id({ id: "mock-data" })` → Flutter 章节

- 设计稿还原使用 `Model.mock()` 工厂方法，禁止硬编码假字符串
- Mock 数据结构必须与真实 API 返回一致
- 覆盖边界场景：空列表、超长文本、多语言

### 代码文件拆分

> `get_standard_by_id({ id: "code-file-splitting" })` → Flutter 策略

- 文件超 500 行应拆分，超 800 行强制拆分
- 私有 Widget → `part / part of`
- 公有组件 → barrel export
- 创建新组件前必须项目搜索去重
