Skip to content

第13章:基础实战

实战1:简易计数器(状态管理基础)

13.1 需求分析

功能需求

  • 实现一个计数器,显示当前计数
  • 提供增加按钮,点击后计数加1
  • 提供减少按钮,点击后计数减1
  • 提供重置按钮,点击后计数重置为0
  • 实时显示计数结果

技术要点

  • StatefulWidget:用于管理可变状态
  • setState():更新状态并触发UI刷新
  • 按钮组件:ElevatedButton
  • 文本组件:Text
  • 布局组件:Column、Row、SizedBox

13.2 核心实现

dart
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

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

  void _decrement() {
    setState(() {
      if (_count > 0) {
        _count--;
      }
    });
  }

  void _reset() {
    setState(() {
      _count = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Count: $_count',
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 40),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrement,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('-'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _reset,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('Reset'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _increment,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('+'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

13.3 实操代码讲解与优化

代码讲解

  1. StatefulWidget:使用 StatefulWidget 来创建计数器页面,因为计数器的状态需要变化。

  2. 状态管理:在 _CounterPageState 类中定义 _count 变量来存储计数。

  3. 状态更新

    • _increment():使用 setState() 方法更新 _count 变量,使其加1
    • _decrement():使用 setState() 方法更新 _count 变量,使其减1(确保不小于0)
    • _reset():使用 setState() 方法将 _count 重置为0
  4. UI 构建

    • 使用 Column 垂直排列组件
    • 使用 Row 水平排列按钮
    • 使用 SizedBox 控制间距
    • 使用 Text 显示计数
    • 使用 ElevatedButton 创建按钮

优化建议

  1. 添加动画效果:为计数器变化添加动画效果,提升用户体验。

  2. 添加边界检查:确保计数不会为负数。

  3. 添加计数限制:可以添加计数的最大值限制。

  4. 添加主题支持:根据应用主题调整颜色。

  5. 添加本地存储:使用 shared_preferences 保存计数状态,重启应用后保持计数。

优化后的代码

dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadCount();
  }

  Future<void> _loadCount() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _count = prefs.getInt('count') ?? 0;
      _isLoading = false;
    });
  }

  Future<void> _saveCount() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt('count', _count);
  }

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

  void _decrement() {
    setState(() {
      if (_count > 0) {
        _count--;
        _saveCount();
      }
    });
  }

  void _reset() {
    setState(() {
      _count = 0;
      _saveCount();
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedContainer(
              duration: const Duration(milliseconds: 300),
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Theme.of(context).primaryColor.withOpacity(0.1),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                'Count: $_count',
                style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
              ),
            ),
            const SizedBox(height: 40),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrement,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('-'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _reset,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('Reset'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _increment,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text('+'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

实战2:登录页面(基础组件+布局实战)

13.4 需求分析

功能需求

  • 实现用户名输入框
  • 实现密码输入框(带密码可见性切换)
  • 实现登录按钮
  • 实现图片展示(如应用 logo)
  • 实现表单校验(用户名和密码不能为空)
  • 实现登录成功后的跳转

技术要点

  • 线性布局:Column、Row
  • 文本组件:Text
  • 输入组件:TextField
  • 按钮组件:ElevatedButton
  • 图片组件:Image
  • 表单校验:Form、GlobalKey
  • 状态管理:StatefulWidget、setState()

13.5 核心实现

dart
import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;
  bool _isLoading = false;

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _login() async {
    if (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = true;
      });

      // 模拟登录请求
      await Future.delayed(const Duration(seconds: 2));

      setState(() {
        _isLoading = false;
      });

      // 登录成功,跳转到首页
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => const HomePage()),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Form(
            key: _formKey,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // Logo
                Image.asset(
                  'assets/logo.png',
                  height: 120,
                  width: 120,
                ),
                const SizedBox(height: 40),

                // Title
                const Text(
                  'Welcome Back',
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  'Sign in to continue',
                  style: TextStyle(
                    fontSize: 16,
                    color: Colors.grey,
                  ),
                ),
                const SizedBox(height: 40),

                // Username Field
                TextFormField(
                  controller: _usernameController,
                  decoration: const InputDecoration(
                    labelText: 'Username',
                    border: OutlineInputBorder(),
                    prefixIcon: Icon(Icons.person),
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please enter username';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 20),

                // Password Field
                TextFormField(
                  controller: _passwordController,
                  obscureText: _obscurePassword,
                  decoration: InputDecoration(
                    labelText: 'Password',
                    border: const OutlineInputBorder(),
                    prefixIcon: const Icon(Icons.lock),
                    suffixIcon: IconButton(
                      icon: Icon(
                        _obscurePassword ? Icons.visibility : Icons.visibility_off,
                      ),
                      onPressed: () {
                        setState(() {
                          _obscurePassword = !_obscurePassword;
                        });
                      },
                    ),
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return 'Please enter password';
                    }
                    if (value.length < 6) {
                      return 'Password must be at least 6 characters';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 30),

                // Login Button
                _isLoading
                    ? const CircularProgressIndicator()
                    : SizedBox(
                        width: double.infinity,
                        child: ElevatedButton(
                          onPressed: _login,
                          style: ElevatedButton.styleFrom(
                            padding: const EdgeInsets.symmetric(vertical: 16),
                            textStyle: const TextStyle(fontSize: 18),
                          ),
                          child: const Text('Login'),
                        ),
                      ),
                const SizedBox(height: 20),

                // Forgot Password
                TextButton(
                  onPressed: () {
                    // 实现忘记密码功能
                  },
                  child: const Text('Forgot Password?'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// Home Page
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: const Center(
        child: Text('Welcome to Home Page!'),
      ),
    );
  }
}

13.6 实操:完成登录页面布局与交互,添加输入校验

步骤 1:创建项目

  1. 打开 Android Studio 或 VS Code
  2. 创建一个新的 Flutter 项目
  3. pubspec.yaml 中添加必要的依赖

步骤 2:添加资源

  1. assets 目录中添加应用 logo 图片
  2. pubspec.yaml 中配置资源路径

步骤 3:实现登录页面

  1. 创建 login_page.dart 文件
  2. 实现登录页面的 UI 和逻辑
  3. 添加表单校验
  4. 实现密码可见性切换
  5. 实现登录功能

步骤 4:创建首页

  1. 创建 home_page.dart 文件
  2. 实现首页的 UI

步骤 5:运行应用

  1. 启动模拟器或连接真机
  2. 运行项目
  3. 测试登录功能
  4. 测试表单校验
  5. 测试密码可见性切换

实战3:列表展示页面(网络请求+列表组件)

13.7 需求分析

功能需求

  • 发送网络请求获取数据
  • 用 ListView 动态展示列表
  • 添加下拉刷新功能
  • 添加加载状态
  • 处理错误状态
  • 点击列表项跳转到详情页

技术要点

  • 网络请求:Dio
  • JSON 解析:手动解析或使用 json_serializable
  • 列表组件:ListView.builder
  • 下拉刷新:RefreshIndicator
  • 状态管理:StatefulWidget、setState()
  • 路由导航:Navigator.push

13.8 核心实现

dart
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

class Post {
  final int id;
  final int userId;
  final String title;
  final String body;

  Post({
    required this.id,
    required this.userId,
    required this.title,
    required this.body,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      userId: json['userId'],
      title: json['title'],
      body: json['body'],
    );
  }
}

class PostListPage extends StatefulWidget {
  const PostListPage({super.key});

  @override
  State<PostListPage> createState() => _PostListPageState();
}

class _PostListPageState extends State<PostListPage> {
  List<Post> _posts = [];
  bool _isLoading = true;
  String? _error;
  final _dio = Dio();

  @override
  void initState() {
    super.initState();
    _fetchPosts();
  }

  Future<void> _fetchPosts() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final response = await _dio.get('https://jsonplaceholder.typicode.com/posts');
      final List<dynamic> data = response.data;
      setState(() {
        _posts = data.map((json) => Post.fromJson(json)).toList();
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = 'Failed to fetch posts: $e';
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Post List'),
      ),
      body: RefreshIndicator(
        onRefresh: _fetchPosts,
        child: _isLoading
            ? const Center(child: CircularProgressIndicator())
            : _error != null
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(_error!),
                        ElevatedButton(
                          onPressed: _fetchPosts,
                          child: const Text('Try Again'),
                        ),
                      ],
                    ),
                  )
                : ListView.builder(
                    itemCount: _posts.length,
                    itemBuilder: (context, index) {
                      final post = _posts[index];
                      return Card(
                        margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                        child: Padding(
                          padding: const EdgeInsets.all(16.0),
                          child: GestureDetector(
                            onTap: () {
                              Navigator.push(
                                context,
                                MaterialPageRoute(
                                  builder: (context) => PostDetailPage(post: post),
                                ),
                              );
                            },
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  post.title,
                                  style: const TextStyle(
                                    fontSize: 18,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                                const SizedBox(height: 8),
                                Text(
                                  post.body,
                                  maxLines: 2,
                                  overflow: TextOverflow.ellipsis,
                                  style: const TextStyle(color: Colors.grey),
                                ),
                              ],
                            ),
                          ),
                        ),
                      );
                    },
                  ),
      ),
    );
  }
}

class PostDetailPage extends StatelessWidget {
  final Post post;

  const PostDetailPage({super.key, required this.post});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Post Detail'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              post.title,
              style: const TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            Text(
              'User ID: ${post.userId}',
              style: const TextStyle(color: Colors.grey),
            ),
            const SizedBox(height: 16),
            Text(
              post.body,
              style: const TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

13.9 实操:完成数据请求、解析与展示,处理异常情况

步骤 1:添加依赖

pubspec.yaml 中添加 Dio 依赖:

yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.3+1

步骤 2:创建数据模型

创建 post.dart 文件,定义 Post 模型类。

步骤 3:实现列表页面

  1. 创建 post_list_page.dart 文件
  2. 实现网络请求逻辑
  3. 实现列表展示
  4. 添加下拉刷新功能
  5. 处理加载和错误状态

步骤 4:实现详情页面

  1. 创建 post_detail_page.dart 文件
  2. 实现详情页的 UI

步骤 5:运行应用

  1. 启动模拟器或连接真机
  2. 运行项目
  3. 测试数据加载
  4. 测试下拉刷新
  5. 测试错误处理
  6. 测试点击跳转

13.10 小结

本章介绍了三个基础实战项目:简易计数器、登录页面和列表展示页面。通过这些实战项目,我们巩固了 Flutter 的核心知识点,包括:

  • 状态管理:使用 StatefulWidget 和 setState() 管理状态
  • 布局组件:使用 Column、Row、SizedBox 等布局组件
  • 基础组件:使用 Text、TextField、ElevatedButton、Image 等基础组件
  • 表单校验:使用 Form 和 GlobalKey 进行表单校验
  • 网络请求:使用 Dio 发送网络请求
  • JSON 解析:手动解析 JSON 数据
  • 列表组件:使用 ListView.builder 展示列表
  • 下拉刷新:使用 RefreshIndicator 实现下拉刷新
  • 路由导航:使用 Navigator 进行页面跳转
  • 错误处理:处理网络请求和其他异常

这些实战项目涵盖了 Flutter 开发中的常见场景,通过实际动手实践,你可以更好地理解和掌握 Flutter 的核心概念和技术。在接下来的章节中,我们将学习更复杂的进阶实战项目,进一步提升 Flutter 开发技能。

© 2026 编程马·菜鸟教程 版权所有