Appearance
第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 实操代码讲解与优化
代码讲解
StatefulWidget:使用
StatefulWidget来创建计数器页面,因为计数器的状态需要变化。状态管理:在
_CounterPageState类中定义_count变量来存储计数。状态更新:
_increment():使用setState()方法更新_count变量,使其加1_decrement():使用setState()方法更新_count变量,使其减1(确保不小于0)_reset():使用setState()方法将_count重置为0
UI 构建:
- 使用
Column垂直排列组件 - 使用
Row水平排列按钮 - 使用
SizedBox控制间距 - 使用
Text显示计数 - 使用
ElevatedButton创建按钮
- 使用
优化建议
添加动画效果:为计数器变化添加动画效果,提升用户体验。
添加边界检查:确保计数不会为负数。
添加计数限制:可以添加计数的最大值限制。
添加主题支持:根据应用主题调整颜色。
添加本地存储:使用
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:创建项目
- 打开 Android Studio 或 VS Code
- 创建一个新的 Flutter 项目
- 在
pubspec.yaml中添加必要的依赖
步骤 2:添加资源
- 在
assets目录中添加应用 logo 图片 - 在
pubspec.yaml中配置资源路径
步骤 3:实现登录页面
- 创建
login_page.dart文件 - 实现登录页面的 UI 和逻辑
- 添加表单校验
- 实现密码可见性切换
- 实现登录功能
步骤 4:创建首页
- 创建
home_page.dart文件 - 实现首页的 UI
步骤 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:实现列表页面
- 创建
post_list_page.dart文件 - 实现网络请求逻辑
- 实现列表展示
- 添加下拉刷新功能
- 处理加载和错误状态
步骤 4:实现详情页面
- 创建
post_detail_page.dart文件 - 实现详情页的 UI
步骤 5:运行应用
- 启动模拟器或连接真机
- 运行项目
- 测试数据加载
- 测试下拉刷新
- 测试错误处理
- 测试点击跳转
13.10 小结
本章介绍了三个基础实战项目:简易计数器、登录页面和列表展示页面。通过这些实战项目,我们巩固了 Flutter 的核心知识点,包括:
- 状态管理:使用 StatefulWidget 和 setState() 管理状态
- 布局组件:使用 Column、Row、SizedBox 等布局组件
- 基础组件:使用 Text、TextField、ElevatedButton、Image 等基础组件
- 表单校验:使用 Form 和 GlobalKey 进行表单校验
- 网络请求:使用 Dio 发送网络请求
- JSON 解析:手动解析 JSON 数据
- 列表组件:使用 ListView.builder 展示列表
- 下拉刷新:使用 RefreshIndicator 实现下拉刷新
- 路由导航:使用 Navigator 进行页面跳转
- 错误处理:处理网络请求和其他异常
这些实战项目涵盖了 Flutter 开发中的常见场景,通过实际动手实践,你可以更好地理解和掌握 Flutter 的核心概念和技术。在接下来的章节中,我们将学习更复杂的进阶实战项目,进一步提升 Flutter 开发技能。
