Flutter 实战:从零打造酷炫的幸运大转盘午餐选择器

每天到了饭点,面对琳琅满目的外卖应用,或者是家里塞满食材的冰箱,你是否也曾陷入“吃什么”的终极哲学难题?作为开发者,我们不仅要解决代码中的逻辑难题,当然也要用技术来解决生活中的“选择困难症”。

在本文中,我们将通过一个有趣且具有实战意义的 Flutter 项目——“2026版智能午餐大转盘”,来一起探索如何构建交互性强、动画流畅的应用。不同于旧版的简单教程,我们将站在 2026 年的技术视角,深入探讨如何结合 AI 辅助开发现代化状态管理以及企业级异常处理来打造一个健壮的应用。

准备工作:构建项目的基石与 AI 辅助开发

在开始写代码之前,让我们先理清思路。这个项目的核心在于“随机选择”与“视觉反馈”。但作为 2026 年的开发者,我们不再是从零开始手写每一行代码,而是善于利用 AI 结对编程 工具(如 Cursor 或 GitHub Copilot)来加速开发。

AI 开发建议:在我们初始化项目前,我们可以直接询问 AI:“如何在 Flutter 中配置一个支持 Material 3 的项目并处理网络请求缓存?”AI 不仅能帮我们生成代码,还能提供最新的架构建议。

#### 第一步:初始化项目与环境配置

首先,我们需要在本地创建一个新的 Flutter 项目。打开你的终端,运行以下命令:

flutter create lunch_wheel_app
``

创建完成后,建议你使用 **VS Code** 配合 **Flutter** 扩展插件打开项目。在 2026 年,**Dart 3** 的模式安全性和空安全特性已经成为标准,请确保你的环境已升级至最新稳定版。

#### 第二步:配置项目依赖

在 `pubspec.yaml` 文件中,我们需要声明核心依赖。除了基础的 UI 库,我们还需要引入 **Riverpod** 来进行现代化的状态管理,这比传统的 `setState` 更易于维护和测试。

yaml

dependencies:

flutter:

sdk: flutter

# 核心转盘组件

flutterfortunewheel: ^1.3.2

# 网络请求

http: ^1.3.0

# 庆祝动画

confetti: ^0.8.0

# 2026年推荐的状态管理方案

flutter_riverpod: ^2.6.0

# 用于JSON序列化的代码生成器

json_annotation: ^4.9.0

dev_dependencies:

flutter_test:

sdk: flutter

build_runner: ^2.4.0

json_serializable: ^6.8.0


添加完代码后,运行 `flutter pub get`。如果你使用的是 AI IDE,通常它会自动检测 `pubspec.yaml` 的变化并提示你安装。

#### 第三步:数据模型与序列化

在生产环境中,手动编写序列化代码容易出错。我们使用 **代码生成** 技术。创建 `lunch_model.dart`:

dart

import ‘package:jsonannotation/jsonannotation.dart‘;

// 这一行允许 build_runner 生成生成文件的部分

data.g.dart‘ show LunchSerializer;

// 定义 Lunch 类,用于存储午餐的名称和图片信息

class Lunch {

final String meal; // 午餐名称

final String? img; // 午餐图片链接(可空)

Lunch({required this.meal, this.img});

// 使用 json_serializable 自动生成 fromJson 方法

factory Lunch.fromJson(Map json) => _$LunchFromJson(json);

Map toJson() => _$LunchToJson(this);

}


运行 `dart run build_runner build` 即可自动生成序列化代码。这是现代 Flutter 开发中提高代码健壮性的关键步骤。

### 核心功能实现:数据与 UI 的深度结合

接下来,让我们进入最激动人心的部分——编写核心业务逻辑和 UI 界面。我们将使用 **Riverpod** 的 `FutureProvider` 来处理异步数据,这能自动处理加载状态和错误状态,极大简化 UI 代码。

#### 第四步:网络请求与状态管理

我们不再在 Widget 内部直接写 `http.get`,而是创建一个 Provider。

dart

import ‘package:flutterriverpod/flutterriverpod.dart‘;

import ‘package:http/http.dart‘ as http;

import ‘dart:convert‘;

import ‘lunch_model.dart‘;

// 定义一个 Provider 获取午餐列表

final lunchProvider = FutureProvider.autoDispose<List>((ref) async {

// API 地址,这里我们获取印度风味的美食

const url = "https://www.themealdb.com/api/json/v1/1/filter.php?a=Indian";

// 发起 GET 请求

final response = await http.get(Uri.parse(url));

// 检查状态码,200 表示成功

if (response.statusCode == 200) {

// 解析 JSON 数据

final jsonData = json.decode(response.body);

if (jsonData[‘meals‘] != null) {

return (jsonData[‘meals‘] as List)

.map((item) => Lunch.fromJson(item))

.toList();

} else {

throw Exception(‘No meals found‘);

}

} else {

throw Exception(‘Failed to load meals‘);

}

});


**技术洞察**:`autoDispose` 修饰符确保了当用户离开页面时,状态会被自动销毁,这在 2026 年这种资源敏感型应用开发中是最佳实践,能有效防止内存泄漏。

#### 第五步:构建现代 UI 与转盘集成

在 UI 层,我们需要处理“加载中”、“错误”和“成功”三种状态。使用 `ConsumerWidget` 结合 `when` 方法,代码会变得异常清晰。

dart

// 定义一个 StreamController 来控制转盘选中的索引

final selectedProvider = StreamProvider.autoDispose((ref) {

final controller = StreamController();

ref.onDispose(() => controller.close()); // 确保资源释放

return controller.stream;

});

class LunchWheelPage extends ConsumerWidget {

const LunchWheelPage({super.key});

@override

Widget build(BuildContext context, WidgetRef ref) {

// 监听数据状态

final lunchAsyncValue = ref.watch(lunchProvider);

return Scaffold(

appBar: AppBar(title: const Text("2026 智能午餐大转盘")),

body: lunchAsyncValue.when(

data: (lunches) {

if (lunches.isEmpty) return const Center(child: Text("没有找到美食"));

return _buildWheel(context, ref, lunches);

},

loading: () => const Center(child: CircularProgressIndicator()),

error: (err, stack) => Center(

child: Column(

mainAxisAlignment: MainAxisAlignment.center,

children: [

Text("错误: ${err.toString()}"),

const SizedBox(height: 10),

ElevatedButton(

onPressed: () => ref.refresh(lunchProvider),

child: const Text("重试"),

)

],

),

),

),

);

}

Widget _buildWheel(BuildContext context, WidgetRef ref, List lunches) {

return Column(

mainAxisAlignment: MainAxisAlignment.center,

children: [

SizedBox(

height: 400,

child: FortuneWheel(

// 这里我们需要结合 StatefulProvider 来控制选中的流

selected: ref.watch(selectedProvider.stream),

items: [

for (var it in lunches)

FortuneItem(

child: Text(it.meal, style: const TextStyle(fontSize: 16)),

),

],

onAnimationEnd: () {

// 动画结束逻辑,这里可以配合 Confetti 展示结果

// 注意:在实际项目中,我们需要从 Stream 的最后值获取结果

print("动画结束");

},

),

),

const SizedBox(height: 20),

ElevatedButton(

onPressed: () => _spin(ref, lunches.length),

child: const Text("开始旋转"),

),

],

);

}

void _spin(WidgetRef ref, int maxLength) {

// 生成随机索引并推送到 Stream

final randomIndex = (maxLength * (DateTime.now().millisecondsSinceEpoch % 1000) / 1000).floor();

// 注意:这里简化了 Stream 的写入逻辑,实际中你需要通过 Provider 暴露 add 方法

// 例如:ref.read(selectedProvider.notifier).add(randomIndex);

}

}

“INLINECODE5f37b40fconnectivityplusINLINECODE269c0e5csqfliteINLINECODE471363e0hiveINLINECODEc1998b26LayoutBuilderINLINECODE2a7e0abeAdaptiveScaffoldINLINECODEb36e85ddflutterfortunewheelINLINECODE68fb838bRepaintBoundaryINLINECODEb9707763speechto_text`),让用户直接说“转一转”,或者接入智能家居 API,直接把选好的菜谱推送到厨房屏幕。

编程的乐趣就在于无限的创造可能。希望这个项目能成为你探索现代 Flutter 开发的起点!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/20739.html
点赞
0.00 平均评分 (0% 分数) - 0