Flutter跨平台开发入门教程

教程简介

零基础Flutter跨平台开发完整教程,涵盖Dart语法基础、Widget体系、布局、状态管理、路由导航、网络请求、本地存储、动画、平台通道等核心技能,配有新闻阅读App实战项目,适合移动开发初学者。

Flutter 跨平台开发入门教程

面向零基础读者,从零开始掌握 Flutter 跨平台移动应用开发。


目录

  1. Flutter 是什么
  2. 环境搭建
  3. Dart 语言基础
  4. Flutter Widget 体系
  5. 常用布局 Widget
  6. 状态管理
  7. 路由与导航
  8. 网络请求
  9. 本地存储
  10. 动画
  11. 平台通道(Platform Channel)
  12. 实战项目:新闻阅读 App
  13. 总结与进阶资源

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:

  1. flutter.dev 下载 Flutter SDK zip 包
  2. 解压到目录,如 C:\flutter
  3. C:\flutter\bin 添加到系统环境变量 PATH
  4. 在终端运行 flutter doctor

2.2 配置编辑器

推荐使用 VS CodeAndroid Studio

  • VS Code:安装 FlutterDart 扩展
  • 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

推荐进阶方向

  1. Flutter 渲染原理:深入了解 Widget → Element → RenderObject 三层架构
  2. 自定义绘制:使用 CustomPainter 实现自定义图表、画板
  3. 国际化 (i18n):使用 flutter_localizations 实现多语言支持
  4. 测试:Widget 测试、集成测试、单元测试
  5. CI/CD:使用 Codemagic 或 GitHub Actions 自动化构建发布
  6. 性能优化: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 开发之旅顺利!

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录