Flutter 跨平台开发入门教程
面向零基础读者,从零开始掌握 Flutter 跨平台移动应用开发。
目录
- Flutter 是什么
- 环境搭建
- Dart 语言基础
- Flutter Widget 体系
- 常用布局 Widget
- 状态管理
- 路由与导航
- 网络请求
- 本地存储
- 动画
- 平台通道(Platform Channel)
- 实战项目:新闻阅读 App
- 总结与进阶资源
1. Flutter 是什么
Flutter 是 Google 开源的 UI 开发框架,使用 Dart 语言,一套代码可以同时构建 iOS、Android、Web、Windows、macOS 和 Linux 应用。
核心特点
| 特点 | 说明 |
|---|---|
| 跨平台 | 一套代码,多端运行 |
| 热重载 (Hot Reload) | 保存即刷新,开发效率极高 |
| 自绘引擎 | 自带 Skia 渲染引擎,不依赖平台原生控件,UI 一致性好 |
| 丰富的组件库 | 内置 Material Design 和 Cupertino 风格组件 |
| 高性能 | 编译为原生 ARM 代码,接近原生性能 |
Flutter 与其他方案的对比
- 原生开发 (Swift/Kotlin):性能最好,但需要维护两套代码。
- React Native:使用 JavaScript,通过桥接调用原生组件,性能有损耗。
- Flutter:自绘引擎,无桥接损耗,一套代码多端运行,综合性价比高。
2. 环境搭建
2.1 安装 Flutter SDK
macOS / Linux:
# 下载 Flutter SDK(以 stable 版本为例)
git clone https://github.com/flutter/flutter.git -b stable
# 添加到 PATH
export PATH="$PATH:`pwd`/flutter/bin"
# 验证安装
flutter doctor
Windows:
- 从 flutter.dev 下载 Flutter SDK zip 包
- 解压到目录,如
C:\flutter - 将
C:\flutter\bin添加到系统环境变量 PATH - 在终端运行
flutter doctor
2.2 配置编辑器
推荐使用 VS Code 或 Android Studio:
- VS Code:安装
Flutter和Dart扩展 - Android Studio:安装
Flutter插件(自带 Dart)
2.3 创建第一个项目
flutter create my_first_app
cd my_first_app
flutter run
运行后你会看到默认的计数器 App,恭喜你完成了第一个 Flutter 应用!
3. Dart 语言基础
Flutter 使用 Dart 语言。如果你有 JavaScript、Java 或 Kotlin 基础,上手会很快。
3.1 变量与类型
// 类型推断
var name = 'Flutter'; // 推断为 String
var year = 2024; // 推断为 int
// 显式类型
String greeting = 'Hello';
double pi = 3.14;
bool isAwesome = true;
// 常量与不可变
const maxCount = 100; // 编译时常量
final currentTime = DateTime.now(); // 运行时不可变
// 空安全(Null Safety)
String? nullableName; // 可以为 null
String nonNullName = 'Dart'; // 不能为空
3.2 集合类型
// List(列表)
var fruits = ['苹果', '香蕉', '橘子'];
fruits.add('葡萄');
print(fruits[0]); // 苹果
// Map(键值对)
var scores = {
'数学': 95,
'英语': 88,
'物理': 92,
};
print(scores['数学']); // 95
// Set(集合,元素唯一)
var uniqueNames = {'Alice', 'Bob', 'Alice'};
print(uniqueNames.length); // 2
3.3 函数
// 普通函数
int add(int a, int b) {
return a + b;
}
// 箭头函数(单行表达式)
int multiply(int a, int b) => a * b;
// 可选参数与默认参数
void greet(String name, {String prefix = '你好'}) {
print('$prefix, $name!');
}
greet('小明'); // 你好, 小明!
greet('小明', prefix: '嗨'); // 嗨, 小明!
// 命名参数(用 {} 包裹)
void printUser({required String name, int age = 0}) {
print('$name, $age 岁');
}
3.4 类与面向对象
class Animal {
String name;
int age;
// 构造函数
Animal(this.name, this.age);
// 命名构造函数
Animal.baby(this.name) : age = 0;
void speak() {
print('$name 说:汪汪!');
}
}
// 继承
class Dog extends Animal {
String breed;
Dog(String name, int age, this.breed) : super(name, age);
@override
void speak() {
print('$name ($breed) 说:汪汪!');
}
}
var dog = Dog('旺财', 3, '柴犬');
dog.speak(); // 旺财 (柴犬) 说:汪汪!
3.5 异步编程(Future 与 async/await)
// Future 类似 JavaScript 的 Promise
Future<String> fetchUserName() async {
// 模拟网络请求
await Future.delayed(Duration(seconds: 2));
return '小明';
}
// 使用 async/await
void main() async {
print('获取用户...');
String name = await fetchUserName();
print('用户名: $name');
}
3.6 Lambda 与闭包
var numbers = [1, 2, 3, 4, 5];
// map、where、forEach 等高阶方法
var doubled = numbers.map((n) => n * 2).toList();
// [2, 4, 6, 8, 10]
var evens = numbers.where((n) => n % 2 == 0).toList();
// [2, 4]
4. Flutter Widget 体系
Widget 是 Flutter 的核心概念。 Flutter 中一切皆 Widget——按钮、文字、间距、布局,甚至整个页面都是 Widget。
4.1 StatelessWidget vs StatefulWidget
// StatelessWidget:无状态,不随数据变化刷新
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('你好, $name!');
}
}
// StatefulWidget:有状态,数据变化时可以刷新 UI
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('计数: $_count'),
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text('加 1'),
),
],
);
}
}
4.2 常用基础 Widget
// 文本
Text(
'Hello Flutter',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
)
// 图片
Image.network('https://example.com/photo.jpg')
Image.asset('assets/images/logo.png')
// 图标
Icon(Icons.star, color: Colors.amber, size: 32)
// 按钮
ElevatedButton(
onPressed: () => print('点击了'),
child: Text('普通按钮'),
)
TextButton(
onPressed: () {},
child: Text('文字按钮'),
)
IconButton(
onPressed: () {},
icon: Icon(Icons.favorite),
)
// 输入框
TextField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
border: OutlineInputBorder(),
),
onChanged: (value) => print('输入: $value'),
)
// 卡片
Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(16),
child: Text('这是一个卡片'),
),
)
4.3 Widget 树的概念
Flutter UI 是一棵 Widget 树。build() 方法返回的嵌套结构就是这棵树:
@override
Widget build(BuildContext context) {
return Scaffold( // 页面脚手架
appBar: AppBar( // 顶部导航栏
title: Text('首页'),
),
body: Center( // 居中
child: Column( // 纵向排列
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('欢迎'),
SizedBox(height: 20), // 间距
Icon(Icons.home),
],
),
),
);
}
5. 常用布局 Widget
5.1 Row 与 Column(线性布局)
// Row:水平排列
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.star),
Icon(Icons.star),
Icon(Icons.star),
],
)
// Column:垂直排列
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('第一行'),
Text('第二行'),
Text('第三行'),
],
)
5.2 Stack(层叠布局)
Stack(
children: [
Image.network('https://example.com/cover.jpg'),
Positioned(
bottom: 10,
left: 10,
child: Container(
color: Colors.black54,
padding: EdgeInsets.all(8),
child: Text('标题', style: TextStyle(color: Colors.white)),
),
),
],
)
5.3 ListView(列表)
// 简单列表
ListView(
children: [
ListTile(title: Text('项目 1'), leading: Icon(Icons.star)),
ListTile(title: Text('项目 2'), leading: Icon(Icons.star)),
ListTile(title: Text('项目 3'), leading: Icon(Icons.star)),
],
)
// 动态列表(推荐,自动懒加载)
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('第 $index 项'),
);
},
)
5.4 GridView(网格)
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 每行 2 个
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: 10,
itemBuilder: (context, index) {
return Card(
child: Center(child: Text('卡片 $index')),
);
},
)
5.5 Flex 与 Expanded(弹性布局)
Row(
children: [
Expanded(
flex: 1,
child: Container(color: Colors.red, height: 50),
),
Expanded(
flex: 2,
child: Container(color: Colors.blue, height: 50),
),
Expanded(
flex: 1,
child: Container(color: Colors.green, height: 50),
),
],
)
// 红色占 1/4,蓝色占 2/4,绿色占 1/4
6. 状态管理
当应用复杂度增加,仅靠 setState 不够用时,就需要状态管理方案。
6.1 setState(基础方案)
适合简单的局部状态:
class _MyPageState extends State<MyPage> {
bool _isLoading = false;
void _loadData() async {
setState(() => _isLoading = true);
await Future.delayed(Duration(seconds: 2));
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return _isLoading
? CircularProgressIndicator()
: ElevatedButton(onPressed: _loadData, child: Text('加载'));
}
}
6.2 Provider(推荐的轻量方案)
Provider 是 Flutter 官方推荐的状态管理方案。
第一步:添加依赖
# pubspec.yaml
dependencies:
provider: ^6.1.0
第二步:创建状态类
import 'package:flutter/foundation.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => UnmodifiableListView(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners(); // 通知 UI 刷新
}
void remove(String item) {
_items.remove(item);
notifyListeners();
}
}
第三步:在顶层注入
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: MyApp(),
),
);
}
第四步:在子 Widget 中使用
class CartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 监听变化,自动重建
final cart = context.watch<CartModel>();
return ListView.builder(
itemCount: cart.count,
itemBuilder: (context, index) {
return ListTile(
title: Text(cart.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => cart.remove(cart.items[index]),
),
);
},
);
}
}
class AddItemButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 不监听变化,只读取一次
final cart = context.read<CartModel>();
return ElevatedButton(
onPressed: () => cart.add('新商品'),
child: Text('添加商品'),
);
}
}
6.3 Riverpod(Provider 作者的进阶方案)
Riverpod 解决了 Provider 的一些限制(如无法在顶层读取、类型安全问题),是目前社区主流选择之一:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 定义 Provider
final counterProvider = StateProvider<int>((ref) => 0);
// 使用 ConsumerWidget
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('计数: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('加 1'),
),
],
);
}
}
7. 路由与导航
7.1 基本导航
// 跳转到新页面
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
// 返回上一页
Navigator.pop(context);
// 带参数跳转
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(id: 42),
),
);
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({super.key, required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('详情 #$id')),
body: Center(child: Text('详情内容')),
);
}
}
7.2 命名路由
// main.dart 中定义路由表
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
);
// 使用命名路由跳转
Navigator.pushNamed(context, '/detail');
// 传递参数
Navigator.pushNamed(context, '/detail', arguments: {'id': 42});
// 接收参数
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map;
return Text('ID: ${args['id']}');
}
}
7.3 GoRouter(声明式路由,推荐)
GoRouter 是 Flutter 官方推荐的路由方案,支持深链接、嵌套路由等:
# pubspec.yaml
dependencies:
go_router: ^14.0.0
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/article/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ArticlePage(id: id);
},
),
],
);
// 在 MaterialApp 中使用
MaterialApp.router(routerConfig: router);
// 跳转
context.go('/'); // 替换当前路由
context.push('/article/123'); // 压入新路由
context.pop(); // 返回
8. 网络请求
8.1 使用 http 包
# pubspec.yaml
dependencies:
http: ^1.2.0
import 'dart:convert';
import 'package:http/http.dart' as http;
// GET 请求
Future<List<dynamic>> fetchArticles() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('加载失败: ${response.statusCode}');
}
}
// POST 请求
Future<void> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'title': title,
'body': body,
'userId': 1,
}),
);
if (response.statusCode == 201) {
print('创建成功: ${response.body}');
}
}
8.2 使用 Dio(功能更全的网络库)
dependencies:
dio: ^5.4.0
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
// 添加拦截器(如统一添加 Token)
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer my_token';
handler.next(options);
},
onError: (error, handler) {
print('请求错误: ${error.message}');
handler.next(error);
},
));
// GET
Future<dynamic> getData(String path) async {
final response = await dio.get(path);
return response.data;
}
// POST
Future<dynamic> postData(String path, Map<String, dynamic> data) async {
final response = await dio.post(path, data: data);
return response.data;
}
8.3 JSON 序列化(Model 类)
class Article {
final int id;
final String title;
final String body;
Article({required this.id, required this.title, required this.body});
// 从 JSON 创建对象
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
// 转为 JSON
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'body': body,
};
}
// 使用
final articles = (jsonDecode(response.body) as List)
.map((e) => Article.fromJson(e))
.toList();
9. 本地存储
9.1 SharedPreferences(轻量键值对存储)
适合存储用户设置、登录状态等简单数据。
dependencies:
shared_preferences: ^2.2.0
import 'package:shared_preferences/shared_preferences.dart';
// 写入
Future<void> saveTheme(String theme) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme', theme);
}
// 读取
Future<String> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('theme') ?? 'light';
}
// 删除
Future<void> clearTheme() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('theme');
}
9.2 SQLite(结构化数据存储)
适合存储大量结构化数据,如收藏列表、聊天记录等。
dependencies:
sqflite: ^2.3.0
path: ^1.8.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
Future<Database> get database async {
_database ??= await _initDB();
return _database!;
}
Future<Database> _initDB() async {
final path = join(await getDatabasesPath(), 'news.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL,
savedAt TEXT NOT NULL
)
''');
},
);
}
// 插入
Future<void> insertFavorite(Map<String, dynamic> article) async {
final db = await database;
await db.insert('favorites', article);
}
// 查询
Future<List<Map<String, dynamic>>> getFavorites() async {
final db = await database;
return db.query('favorites', orderBy: 'savedAt DESC');
}
// 删除
Future<void> deleteFavorite(int id) async {
final db = await database;
await db.delete('favorites', where: 'id = ?', whereArgs: [id]);
}
}
10. 动画
10.1 隐式动画(最简单)
Flutter 提供了一系列带 Animated 前缀的 Widget,改变属性时自动过渡:
class AnimatedBox extends StatefulWidget {
@override
State<AnimatedBox> createState() => _AnimatedBoxState();
}
class _AnimatedBoxState extends State<AnimatedBox> {
double _size = 100;
Color _color = Colors.blue;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_size = _size == 100 ? 200 : 100;
_color = _color == Colors.blue ? Colors.red : Colors.blue;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _size,
height: _size,
color: _color,
child: Center(child: Text('点我')),
),
);
}
}
常用隐式动画 Widget:
AnimatedContainer— 通用容器动画AnimatedOpacity— 透明度动画AnimatedPositioned— 位置动画AnimatedCrossFade— 两个 Widget 切换动画
10.2 显式动画(更精细的控制)
使用 AnimationController 精确控制动画:
class SpinAnimation extends StatefulWidget {
@override
State<SpinAnimation> createState() => _SpinAnimationState();
}
class _SpinAnimationState extends State<SpinAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(); // 循环播放
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: FlutterLogo(size: 80),
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
);
}
}
10.3 Hero 动画(页面转场共享元素)
// 页面 A
Hero(
tag: 'article-image-1',
child: Image.network('https://example.com/img.jpg', width: 100),
)
// 页面 B(自动产生过渡动画)
Hero(
tag: 'article-image-1', // 同一个 tag
child: Image.network('https://example.com/img.jpg', width: double.infinity),
)
11. 平台通道(Platform Channel)
当需要调用平台原生功能(如蓝牙、传感器、相机)时,需要通过 Platform Channel 与原生代码通信。
11.1 MethodChannel 示例
Flutter 端:
import 'package:flutter/services.dart';
class BatteryService {
static const _channel = MethodChannel('com.example/battery');
// 调用原生方法
static Future<int> getBatteryLevel() async {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level ?? -1;
}
}
// 使用
final level = await BatteryService.getBatteryLevel();
print('电量: $level%');
Android 端 (Kotlin):
// MainActivity.kt
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "电量不可用", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}
iOS 端 (Swift):
// AppDelegate.swift
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "com.example/battery",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { (call, result) in
if call.method == "getBatteryLevel" {
UIDevice.current.isBatteryMonitoringEnabled = true
let level = Int(UIDevice.current.batteryLevel * 100)
result(level)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
11.2 EventChannel(原生向 Flutter 发送事件流)
适合传感器数据、蓝牙状态等持续推送的场景:
// Flutter 端
class SensorService {
static const _channel = EventChannel('com.example/sensor');
static Stream<double> get accelerometerStream {
return _channel.receiveBroadcastStream().map((event) => event as double);
}
}
// 使用
StreamBuilder<double>(
stream: SensorService.accelerometerStream,
builder: (context, snapshot) {
return Text('加速度: ${snapshot.data?.toStringAsFixed(2) ?? "等待数据"}');
},
)
12. 实战项目:新闻阅读 App
下面我们综合运用以上知识,构建一个简易新闻阅读 App。
12.1 项目结构
lib/
├── main.dart # 入口
├── models/
│ └── article.dart # 数据模型
├── services/
│ ├── api_service.dart # 网络请求
│ └── storage_service.dart # 本地存储
├── providers/
│ └── article_provider.dart # 状态管理
├── pages/
│ ├── home_page.dart # 首页(新闻列表)
│ ├── detail_page.dart # 详情页
│ └── favorites_page.dart # 收藏页
└── widgets/
└── article_card.dart # 文章卡片组件
12.2 数据模型
// lib/models/article.dart
class Article {
final int id;
final String title;
final String body;
final String author;
Article({
required this.id,
required this.title,
required this.body,
required this.author,
});
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'],
title: json['title'],
body: json['body'],
author: json['author'] ?? '佚名',
);
}
}
12.3 网络服务
// lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/article.dart';
class ApiService {
static const _baseUrl = 'https://jsonplaceholder.typicode.com';
static Future<List<Article>> fetchArticles({int page = 1, int limit = 20}) async {
final response = await http.get(
Uri.parse('$_baseUrl/posts?_page=$page&_limit=$limit'),
);
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((e) => Article.fromJson(e)).toList();
} else {
throw Exception('加载失败');
}
}
}
12.4 状态管理
// lib/providers/article_provider.dart
import 'package:flutter/foundation.dart';
import '../models/article.dart';
import '../services/api_service.dart';
class ArticleProvider extends ChangeNotifier {
List<Article> _articles = [];
bool _isLoading = false;
String? _error;
List<Article> get articles => _articles;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> loadArticles() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_articles = await ApiService.fetchArticles();
} catch (e) {
_error = '加载失败,请检查网络';
} finally {
_isLoading = false;
notifyListeners();
}
}
}
12.5 文章卡片组件
// lib/widgets/article_card.dart
import 'package:flutter/material.dart';
import '../models/article.dart';
class ArticleCard extends StatelessWidget {
final Article article;
final VoidCallback onTap;
const ArticleCard({
super.key,
required this.article,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8),
Text(
article.body,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 12),
Row(
children: [
Icon(Icons.person, size: 16, color: Colors.grey),
SizedBox(width: 4),
Text(article.author, style: TextStyle(color: Colors.grey)),
Spacer(),
Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey),
],
),
],
),
),
),
);
}
}
12.6 首页
// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/article_provider.dart';
import '../widgets/article_card.dart';
import 'detail_page.dart';
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
// 首次加载
Future.microtask(() =>
context.read<ArticleProvider>().loadArticles()
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('📰 新闻阅读'),
actions: [
IconButton(
icon: Icon(Icons.favorite),
onPressed: () => Navigator.pushNamed(context, '/favorites'),
),
],
),
body: Consumer<ArticleProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text(provider.error!),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => provider.loadArticles(),
child: Text('重试'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => provider.loadArticles(),
child: ListView.builder(
itemCount: provider.articles.length,
itemBuilder: (context, index) {
final article = provider.articles[index];
return ArticleCard(
article: article,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(article: article),
),
);
},
);
},
),
);
},
),
);
}
}
12.7 详情页(含 Hero 动画)
// lib/pages/detail_page.dart
import 'package:flutter/material.dart';
import '../models/article.dart';
class DetailPage extends StatelessWidget {
final Article article;
const DetailPage({super.key, required this.article});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(article.title),
actions: [
IconButton(
icon: Icon(Icons.share),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('分享功能开发中...')),
);
},
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
article.title,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
// 作者信息
Row(
children: [
CircleAvatar(
child: Text(article.author[0]),
),
SizedBox(width: 8),
Text(
article.author,
style: TextStyle(fontSize: 16, color: Colors.grey[700]),
),
],
),
Divider(height: 32),
// 正文
Text(
article.body,
style: TextStyle(fontSize: 16, height: 1.8),
),
],
),
),
);
}
}
12.8 主入口
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/article_provider.dart';
import 'pages/home_page.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ArticleProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '新闻阅读',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
),
),
home: HomePage(),
);
}
}
12.9 运行项目
cd news_reader_app
flutter pub get
flutter run
你将看到一个带下拉刷新、文章列表、详情页和错误处理的完整新闻阅读 App。
13. 总结与进阶资源
本教程涵盖的知识点
| 章节 | 核心内容 |
|---|---|
| Dart 基础 | 变量、函数、类、异步、集合 |
| Widget 体系 | StatelessWidget、StatefulWidget、常用 Widget |
| 布局 | Row/Column/Stack/ListView/GridView/Expanded |
| 状态管理 | setState、Provider、Riverpod |
| 路由导航 | Navigator、命名路由、GoRouter |
| 网络请求 | http、Dio、JSON 序列化 |
| 本地存储 | SharedPreferences、SQLite |
| 动画 | 隐式动画、显式动画、Hero |
| 平台通道 | MethodChannel、EventChannel |
推荐进阶方向
- Flutter 渲染原理:深入了解 Widget → Element → RenderObject 三层架构
- 自定义绘制:使用
CustomPainter实现自定义图表、画板 - 国际化 (i18n):使用
flutter_localizations实现多语言支持 - 测试:Widget 测试、集成测试、单元测试
- CI/CD:使用 Codemagic 或 GitHub Actions 自动化构建发布
- 性能优化:DevTools 分析、减少重建、图片缓存
学习资源
- 官方文档:https://docs.flutter.dev
- Dart 语言:https://dart.dev
- Flutter Widget 目录:https://docs.flutter.dev/ui/widgets
- pub.dev(包仓库):https://pub.dev
- Flutter GitHub:https://github.com/flutter/flutter
📝 本教程所有代码均为原创示例,可自由使用和修改。祝你 Flutter 开发之旅顺利!