🎨 08 - StatelessWidget

阶段二:Widget基础 · 第8课 · 预计学习时间:45分钟
✅ 验证通过

📖 StatelessWidget原理

StatelessWidget是Flutter中最简单的Widget类型——它没有内部状态,build()方法的输出完全由构造函数参数决定。

// ✅ 验证通过:Flutter 3.22
// StatelessWidget的核心特征:
// 1. 不可变——所有字段必须是final
// 2. 无状态——没有setState
// 3. 依赖外部数据——通过构造函数传入
// 4. 纯函数式——相同的输入总是产生相同的UI

class GreetingCard extends StatelessWidget {
  // 所有字段必须是final
  final String name;
  final String message;
  final Color accentColor;
  
  // const构造函数——允许编译时创建常量Widget
  const GreetingCard({
    required this.name,
    required this.message,
    this.accentColor = Colors.purple,
    super.key,
  });
  
  // 唯一必须实现的方法
  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: accentColor, width: 2),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'Dear $name,',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: accentColor,
              ),
            ),
            const SizedBox(height: 8),
            Text(message),
          ],
        ),
      ),
    );
  }
}

📖 const优化

// ✅ 验证通过
/// const Widget是Flutter性能优化的关键
/// 当Widget标记为const时,Flutter在重建时会跳过它

// ❌ 不好的写法——每次父组件build都创建新实例
class BadWidget extends StatelessWidget {
  final String title;
  const BadWidget({required this.title, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 这个Text每次都是新实例
        Text(title),
        // 这个也是——即使内容不变
        Text('Static text'),
        // Icon也是
        Icon(Icons.star),
      ],
    );
  }
}

// ✅ 好的写法——尽可能使用const
class GoodWidget extends StatelessWidget {
  final String title;
  const GoodWidget({required this.title, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // title是动态的,不能用const
        Text(title),
        // 静态文本用const——Flutter直接复用
        const Text('Static text'),
        // Icon也可以const
        const Icon(Icons.star),
      ],
    );
  }
}

// const构造函数的完整用法
class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  
  const ProfileCard({
    required this.name,
    required this.role,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16), // const Padding
        child: Row(
          children: [
            const CircleAvatar(
              child: Icon(Icons.person),  // const Icon
            ),
            const SizedBox(width: 16),    // const SizedBox
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name),  // 动态,不能用const
                Text(
                  role,
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

📖 组件提取模式

// ✅ 验证通过
/// 将大build方法拆分为小组件——提升性能和可读性

// ❌ 反模式:巨大的build方法
class MonsterWidget extends StatelessWidget {
  final UserModel user;
  final List<Post> posts;
  final WeatherData weather;
  
  const MonsterWidget({
    required this.user,
    required this.posts,
    required this.weather,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 50行用户信息...
        // 30行帖子列表...
        // 20行天气信息...
        // 任何数据变化 → 整个Column重建
      ],
    );
  }
}

// ✅ 好的模式:提取为独立组件
class UserProfilePage extends StatelessWidget {
  final UserModel user;
  final List<Post> posts;
  final WeatherData weather;
  
  const UserProfilePage({
    required this.user,
    required this.posts,
    required this.weather,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        UserProfileHeader(user: user),       // 独立组件
        UserPostsList(posts: posts),         // 独立组件
        WeatherCard(weather: weather),       // 独立组件
      ],
    );
  }
}

// 每个子组件独立重建,互不影响
class UserProfileHeader extends StatelessWidget {
  final UserModel user;
  const UserProfileHeader({required this.user, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        CircleAvatar(
          backgroundImage: NetworkImage(user.avatarUrl),
        ),
        const SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(user.name, style: Theme.of(context).textTheme.titleMedium),
            Text('@${user.username}', style: TextStyle(color: Colors.grey[600])),
          ],
        ),
      ],
    );
  }
}

class UserPostsList extends StatelessWidget {
  final List<Post> posts;
  const UserPostsList({required this.posts, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, index) => PostCard(post: posts[index]),
      ),
    );
  }
}

class WeatherCard extends StatelessWidget {
  final WeatherData weather;
  const WeatherCard({required this.weather, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Icon(weather.icon),
        title: Text('${weather.temperature}°C'),
        subtitle: Text(weather.description),
      ),
    );
  }
}

// 数据模型
class UserModel {
  final String name;
  final String username;
  final String avatarUrl;
  const UserModel({required this.name, required this.username, required this.avatarUrl});
}

class Post {
  final String title;
  final String content;
  const Post({required this.title, required this.content});
}

class WeatherData {
  final double temperature;
  final String description;
  final IconData icon;
  const WeatherData({required this.temperature, required this.description, required this.icon});
}

💻 实战:个人名片App

// ✅ 验证通过:Flutter 3.22 + Dart 3.4
// 文件:lib/business_card_app.dart

import 'package:flutter/material.dart';

void main() => runApp(const BusinessCardApp());

class BusinessCardApp extends StatelessWidget {
  const BusinessCardApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '个人名片',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const BusinessCardPage(),
    );
  }
}

// 名片数据
class CardData {
  final String name;
  final String title;
  final String company;
  final String email;
  final String phone;
  final String website;
  final String bio;
  final List<Skill> skills;
  final List<SocialLink> socialLinks;
  
  const CardData({
    required this.name,
    required this.title,
    required this.company,
    required this.email,
    required this.phone,
    required this.website,
    required this.bio,
    required this.skills,
    required this.socialLinks,
  });
}

class Skill {
  final String name;
  final double proficiency; // 0.0 - 1.0
  final Color color;
  
  const Skill({required this.name, required this.proficiency, required this.color});
}

class SocialLink {
  final IconData icon;
  final String label;
  final String url;
  
  const SocialLink({required this.icon, required this.label, required this.url});
}

// 示例数据
const myCard = CardData(
  name: '张三',
  title: '高级Flutter开发工程师',
  company: 'FutureTech Inc.',
  email: 'zhangsan@futuretech.com',
  phone: '+86 138-0000-0000',
  website: 'zhangsan.dev',
  bio: '热爱移动开发,5年Flutter实战经验。开源爱好者,技术博主。',
  skills: [
    Skill(name: 'Flutter', proficiency: 0.95, color: Colors.blue),
    Skill(name: 'Dart', proficiency: 0.9, color: Colors.cyan),
    Skill(name: 'Firebase', proficiency: 0.8, color: Colors.orange),
    Skill(name: 'UI/UX', proficiency: 0.7, color: Colors.purple),
  ],
  socialLinks: [
    SocialLink(icon: Icons.code, label: 'GitHub', url: 'github.com/zhangsan'),
    SocialLink(icon: Icons.article, label: 'Blog', url: 'zhangsan.dev/blog'),
    SocialLink(icon: Icons.wechat, label: 'WeChat', url: 'zhangsan_wx'),
  ],
);

// 主页面
class BusinessCardPage extends StatelessWidget {
  const BusinessCardPage({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // 顶部大图
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Theme.of(context).colorScheme.primary,
                      Theme.of(context).colorScheme.tertiary,
                    ],
                  ),
                ),
                child: const Center(
                  child: Icon(Icons.person, size: 80, color: Colors.white54),
                ),
              ),
            ),
          ),
          
          // 内容区域
          SliverToBoxAdapter(
            child: Column(
              children: [
                const _ProfileHeader(),
                const _BioSection(),
                const _SkillsSection(),
                const _ContactSection(),
                const _SocialSection(),
                const SizedBox(height: 80),
              ],
            ),
          ),
        ],
      ),
      
      // 底部分享按钮
      bottomNavigationBar: BottomAppBar(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            FilledButton.icon(
              onPressed: () {},
              icon: const Icon(Icons.share),
              label: const Text('分享名片'),
            ),
            OutlinedButton.icon(
              onPressed: () {},
              icon: const Icon(Icons.qr_code),
              label: const Text('二维码'),
            ),
          ],
        ),
      ),
    );
  }
}

// 头部信息
class _ProfileHeader extends StatelessWidget {
  const _ProfileHeader();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            myCard.name,
            style: Theme.of(context).textTheme.headlineMedium?.copyWith(
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            myCard.title,
            style: Theme.of(context).textTheme.titleMedium?.copyWith(
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            myCard.company,
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Colors.grey[600],
            ),
          ),
        ],
      ),
    );
  }
}

// 简介区域
class _BioSection extends StatelessWidget {
  const _BioSection();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20),
      child: Text(
        myCard.bio,
        style: Theme.of(context).textTheme.bodyMedium,
      ),
    );
  }
}

// 技能区域
class _SkillsSection extends StatelessWidget {
  const _SkillsSection();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('技能', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 12),
          ...myCard.skills.map((skill) => _SkillBar(
            skill: skill,
          )),
        ],
      ),
    );
  }
}

class _SkillBar extends StatelessWidget {
  final Skill skill;
  
  const _SkillBar({required this.skill});
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(skill.name),
              Text('${(skill.proficiency * 100).toInt()}%'),
            ],
          ),
          const SizedBox(height: 4),
          ClipRRect(
            borderRadius: BorderRadius.circular(4),
            child: LinearProgressIndicator(
              value: skill.proficiency,
              minHeight: 8,
              backgroundColor: skill.color.withOpacity(0.2),
              valueColor: AlwaysStoppedAnimation(skill.color),
            ),
          ),
        ],
      ),
    );
  }
}

// 联系方式
class _ContactSection extends StatelessWidget {
  const _ContactSection();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('联系方式', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 12),
          _ContactItem(icon: Icons.email, label: myCard.email),
          _ContactItem(icon: Icons.phone, label: myCard.phone),
          _ContactItem(icon: Icons.language, label: myCard.website),
        ],
      ),
    );
  }
}

class _ContactItem extends StatelessWidget {
  final IconData icon;
  final String label;
  
  const _ContactItem({required this.icon, required this.label});
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Row(
        children: [
          Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
          const SizedBox(width: 12),
          Text(label),
        ],
      ),
    );
  }
}

// 社交链接
class _SocialSection extends StatelessWidget {
  const _SocialSection();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('社交', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 12),
          Row(
            children: myCard.socialLinks.map((link) => Padding(
              padding: const EdgeInsets.only(right: 12),
              child: ActionChip(
                avatar: Icon(link.icon, size: 18),
                label: Text(link.label),
                onPressed: () {},
              ),
            )).toList(),
          ),
        ],
      ),
    );
  }
}

🏋️ 练习

练习1:const审计 🔍

检查你的项目代码,找出所有应该用const但没用的地方。目标:至少优化5处。

练习2:组件拆分 ✂️

找一个超过100行的build方法,拆分为3-5个独立的StatelessWidget。

练习3:设计系统组件 🎨

创建一套可复用的设计系统组件:

// AppButton — 支持 filled/outlined/text 三种样式
// AppCard — 统一的卡片样式
// AppTextField — 带验证的输入框
// AppBadge — 标签/徽章

🏆 本课成就

📚 下节预告

下一课:StatefulWidget——学习有状态组件,理解setState、State生命周期和状态管理模式。