🌟 场景题 1:实现一个“下拉刷新 + 上拉加载更多”的商品列表
需求:
- 商品列表支持下拉刷新。
- 滑动到底部自动加载更多。
- 加载中显示“加载中...”Footer。
- 加载失败显示“点击重试”按钮。
- 网络请求失败有重试机制。
✅ 高级回答要点:
dart
深色版本
class ProductList extends StatefulWidget {
@override
_ProductListState createState() => _ProductListState();
}
class _ProductListState extends State<ProductList> {
final ProductBloc _bloc = ProductBloc();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
_bloc.fetchProducts();
_scrollController.addListener(_onScroll);
super.initState();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_bloc.loadMore();
}
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () => _bloc.refresh(),
child: BlocBuilder<ProductBloc, ProductState>(
bloc: _bloc,
builder: (context, state) {
if (state is ProductLoading && state.products.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
controller: _scrollController,
itemCount: state.products.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.products.length) {
if (state.loadFailed) {
return ElevatedButton(
onPressed: _bloc.retryLoadMore,
child: const Text('点击重试'),
);
}
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return ProductItem(product: state.products[index]);
},
);
},
),
);
}
@override
void dispose() {
_bloc.dispose();
_scrollController.dispose();
super.dispose();
}
}
💡 考察点:
- 状态管理:使用
Bloc
或Riverpod
管理复杂状态(加载中、失败、是否有更多)。 - 性能:
ListView.builder
懒加载。 - 用户体验:Footer 显示加载/失败状态。
- 内存管理:
ScrollController
和Bloc
正确释放。 - 可维护性:逻辑与 UI 分离。
🌟 场景题 2:实现一个“登录页”,支持手机号、密码、验证码登录,且表单验证
需求:
- 手机号输入框(带格式化:138 **** ****)。
- 密码或验证码输入。
- “获取验证码”按钮(60秒倒计时)。
- 表单验证(手机号格式、密码长度、验证码6位)。
- 提交按钮禁用状态控制。
✅ 高级回答要点:
dart
深色版本
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _phoneController = TextEditingController();
final _codeController = TextEditingController();
bool _isCodeLogin = true;
bool _isSubmitting = false;
int _countdown = 0;
@override
void initState() {
super.initState();
_phoneController.addListener(_formatPhone);
}
void _formatPhone() {
// 实现手机号格式化:138****1234
}
void _startCountdown() {
setState(() => _countdown = 60);
Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown <= 1) {
timer.cancel();
setState(() => _countdown = 0);
} else {
setState(() => _countdown--);
}
});
}
void _submit() async {
if (_formKey.currentState?.validate() ?? false) {
setState(() => _isSubmitting = true);
try {
await login(_phoneController.text, _codeController.text);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => Home()));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败: $e')));
} finally {
setState(() => _isSubmitting = false);
}
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: '手机号'),
validator: (value) => isValidPhone(value) ? null : '请输入正确的手机号',
),
if (_isCodeLogin) ...[
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: '验证码',
suffixIcon: _countdown > 0
? Text('$_countdowns')
: TextButton(
onPressed: _countdown == 0 ? _startCountdown : null,
child: const Text('获取验证码'),
),
),
validator: (value) => value?.length == 6 ? null : '验证码为6位',
),
] else ...[
// 密码输入框
],
ElevatedButton(
onPressed: (_formKey.currentState?.isValid ?? false) && !_isSubmitting ? _submit : null,
child: _isSubmitting ? const CircularProgressIndicator() : const Text('登录'),
),
],
),
);
}
}
💡 考察点:
- 表单管理:
GlobalKey<FormState>
+TextFormField
。 - 输入控制:
TextEditingController
监听和格式化。 - 状态反馈:倒计时、提交中 loading 状态。
- 用户体验:按钮禁用、错误提示。
- 可扩展性:支持密码/验证码切换。
🌟 场景题 3:实现一个“聊天界面”,支持文本、图片、语音消息
需求:
- 消息列表(左:别人,右:自己)。
- 支持文本、图片、语音消息。
- 图片可点击放大。
- 语音消息可播放/暂停。
- 输入框支持文字输入和发送。
✅ 高级回答要点:
dart
深色版本
// 1. 定义消息类型
sealed class ChatMessage {
final String sender;
final DateTime timestamp;
}
class TextMessage extends ChatMessage {
final String text;
TextMessage({required this.text, required super.sender, required super.timestamp});
}
class ImageMessage extends ChatMessage {
final String imageUrl;
ImageMessage({required this.imageUrl, required super.sender, required super.timestamp});
}
class VoiceMessage extends ChatMessage {
final String audioUrl;
final int duration;
VoiceMessage({required this.audioUrl, required this.duration, required super.sender, required super.timestamp});
}
// 2. 消息项 Widget
Widget buildMessage(ChatMessage message) {
final isMe = message.sender == 'me';
return Row(
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: isMe ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: _buildMessageContent(message),
),
],
);
}
Widget _buildMessageContent(ChatMessage message) {
if (message is TextMessage) {
return Text(message.text, style: TextStyle(color: message.sender == 'me' ? Colors.white : Colors.black));
} else if (message is ImageMessage) {
return GestureDetector(
onTap: () => showImageDialog(message.imageUrl),
child: Image.network(message.imageUrl, width: 150),
);
} else if (message is VoiceMessage) {
return VoicePlayer(audioUrl: message.audioUrl, duration: message.duration);
}
return const SizedBox();
}
💡 考察点:
- 数据建模:使用
sealed class
(Dart 3.0)或enum + factory
区分消息类型。 - UI 复用:
_buildMessageContent
分类型渲染。 - 交互:图片点击放大(
showDialog
+PhotoView
)。 - 第三方库:
audioplayers
播放语音。 - 性能:图片懒加载、缓存。
🌟 场景题 4:实现一个“商品详情页”,包含轮播图、标题、价格、规格选择、购买按钮
需求:
- 顶部轮播图(支持自动播放、指示器)。
- 商品标题、价格、销量。
- 规格选择(颜色、尺寸),选择后更新价格。
- “加入购物车”和“立即购买”按钮。
- 页面滑动时,标题栏颜色渐变。
✅ 高级回答要点:
dart
深色版本
class ProductDetailPage extends StatefulWidget {
@override
_ProductDetailPageState createState() => _ProductDetailPageState();
}
class _ProductDetailPageState extends State<ProductDetailPage> {
final ScrollController _scrollController = ScrollController();
double _appBarOpacity = 0;
@override
void initState() {
_scrollController.addListener(() {
final offset = _scrollController.offset;
setState(() {
_appBarOpacity = offset.clamp(0.0, 1.0);
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
pinned: true,
backgroundColor: Colors.white.withOpacity(_appBarOpacity),
title: Text('商品详情', style: TextStyle(color: _appBarOpacity > 0.5 ? Colors.black : Colors.white)),
),
SliverToBoxAdapter(
child: Column(
children: [
CarouselSlider(...), // 轮播图
ProductInfo(), // 标题、价格
SpecSelector(), // 规格选择
BuyButtons(), // 购买按钮
],
),
),
],
),
],
),
);
}
}
💡 考察点:
- 复杂布局:
CustomScrollView
+SliverAppBar
实现渐变标题。 - 状态联动:规格选择 → 价格更新(
Provider
或Bloc
)。 - 第三方库:
carousel_slider
实现轮播。 - 用户体验:平滑滚动、视觉反馈。
🌟 场景题 5:实现一个“后台定位服务”(Android/iOS)
需求:
- 应用在后台时持续获取用户位置。
- 位置变化超过 100 米上报一次。
- 低功耗模式(不影响电池)。
- 前台服务显示通知(Android)。
✅ 高级回答要点:
- 插件选择:
geolocator
+workmanager
(定时任务)或flutter_background_service
。 - 权限:
- Android:
ACCESS_FINE_LOCATION
,ACCESS_BACKGROUND_LOCATION
- iOS:
NSLocationAlwaysAndWhenInUseUsageDescription
- Android:
- 实现:
深色版本
final service = FlutterBackgroundService(); service.startForeground( onStart: (service) { service.on('location').listen((data) { Geolocator.getPositionStream( distanceFilter: 100, // 100米变化 desiredAccuracy: LocationAccuracy.low, ).listen((position) { // 上报位置 sendToServer(position); }); }); }, );
- 保活:Android 前台服务 + 通知,iOS
BGTaskScheduler
。
💡 考察点:
- 平台特性:了解 Android/iOS 后台限制。
- 插件能力:选择合适的插件。
- 功耗优化:低精度、距离过滤。
- 合规性:隐私权限、用户提示。
总结:如何应对 Flutter 场景题?
步骤 | 说明 |
---|---|
1. 理解需求 | 问清楚边界条件、异常情况 |
2. 设计数据模型 | 定义 class 、enum 、state |
3. 选择状态管理 | Provider 、Bloc 、Riverpod |
4. 构建 UI 结构 | Widget 拆分、复用 |
5. 处理交互 | 手势、动画、表单 |
6. 考虑性能 | const 、ListView.builder 、Isolate |
7. 异常与加载 | loading 、error 、retry |
8. 内存与生命周期 | dispose() 、controller 释放 |
记住:面试官想看到的是你的思考过程,边说边写,展示架构思维和工程素养。