一、Dart 语言基础
1. Dart 中 final 和 const 的区别是什么?
答案:
| 特性 | final |
const |
|---|---|---|
| 赋值时机 | 运行时赋值,只能赋值一次 | 编译时常量 |
| 内存 | 每次创建新对象 | 相同值复用同一对象(规范化) |
| 可用于 | 实例变量、局部变量 | 顶层变量、静态变量、局部变量 |
| 实例变量 | ✅ 可以 | ❌ 不可以(需加 static) |
final time = DateTime.now(); // ✅ 运行时确定
// const time = DateTime.now(); // ❌ 编译时无法确定
const list1 = [1, 2, 3];
const list2 = [1, 2, 3];
print(identical(list1, list2)); // true,指向同一对象
final list3 = [1, 2, 3];
final list4 = [1, 2, 3];
print(identical(list3, list4)); // false,不同对象
2. Dart 是单线程还是多线程?如何实现并发?
答案:
Dart 是单线程模型,使用 Event Loop(事件循环) + Isolate 实现并发。
- Event Loop:包含 Microtask Queue 和 Event Queue。Microtask 优先执行。
- Isolate:独立的执行单元,拥有独立的内存堆,通过 Port 通信(消息传递),不共享内存。
// 使用 async/await(主 Isolate 内异步)
Future<void> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com'));
print(response.body);
}
// 使用 Isolate 执行耗时操作
import 'dart:isolate';
void heavyTask(SendPort sendPort) {
int sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
sendPort.send(sum);
}
Future<void> main() async {
final receivePort = ReceivePort();
await Isolate.spawn(heavyTask, receivePort.sendPort);
final result = await receivePort.first;
print('Result: $result');
}
// 更简洁的方式:Isolate.run()(Dart 2.19+)
final result = await Isolate.run(() {
int sum = 0;
for (int i = 0; i < 1000000000; i++) sum += i;
return sum;
});
3. Dart 中 ??、??=、?. 和 ... 运算符的用法?
答案:
// ?? 空值合并运算符:左侧为 null 时取右侧
String? name;
print(name ?? 'Guest'); // 输出 Guest
// ??= 空值赋值运算符:变量为 null 时才赋值
int? count;
count ??= 10; // count = 10
count ??= 20; // count 仍为 10
// ?. 安全调用运算符:对象非 null 时调用
List<int>? list;
print(list?.length); // null,不会抛异常
// ... 展开运算符(在集合中使用)
final list1 = [1, 2, 3];
final list2 = [0, ...list1, 4]; // [0, 1, 2, 3, 4]
// ...? 空安全展开
List<int>? nullableList;
final result = [0, ...?nullableList]; // [0]
4. Dart 中的 Mixin 是什么?如何使用?
答案:
Mixin 是一种复用类代码的方式,允许将一组方法注入到多个类中,解决了多重继承的问题。
// 定义 Mixin
mixin LoggerMixin {
void log(String message) {
print('[${DateTime.now()}] $message');
}
}
mixin ValidatorMixin {
bool isValidEmail(String email) {
return email.contains('@');
}
}
// 使用 Mixin(with 关键字)
class UserService with LoggerMixin, ValidatorMixin {
void createUser(String email) {
if (isValidEmail(email)) {
log('Creating user: $email');
}
}
}
// Mixin 可以有约束(on 关键字)
mixin Flyable on Animal {
void fly() => print('$name is flying');
}
class Animal {
final String name;
Animal(this.name);
}
class Bird extends Animal with Flyable {
Bird(String name) : super(name);
}
关键点:
-
mixin不能有构造函数 - 通过
on关键字限制 mixin 只能用于特定类 - 多个 mixin 冲突时,最后一个 mixin 的方法生效
-
with后面的 mixin 优先级高于前面
二、Flutter 框架核心
5. Widget、Element 和 RenderObject 的关系?
答案:
Flutter 的三棵树模型:
| 类型 | 职责 | 特点 |
|---|---|---|
| Widget | 配置描述(不可变) | 轻量级,频繁重建 |
| Element | 实例化与生命周期管理 | 连接 Widget 和 RenderObject 的桥梁 |
| RenderObject | 布局和绘制 | 处理 size、position、painting |
Widget (配置)
↓ createElement()
Element (桥接)
↓ createRenderObject()
RenderObject (布局/绘制)
重建流程:
- Widget 变化时,Element 通过
canUpdate()判断是否需要重建 -
canUpdate()比较runtimeType和key - 如果相同,Element 更新引用的 Widget(
update) - 如果不同,创建新的 Element 树和 RenderObject
6. StatefulWidget 和 StatelessWidget 的区别及生命周期?
答案:
- StatelessWidget:无状态,根据输入参数渲染 UI,不可变。
-
StatefulWidget:有可变状态,通过
setState()触发重建。
StatefulWidget 生命周期:
创建阶段:
createState() → initState() → didChangeDependencies() → build()
更新阶段:
setState() → didUpdateWidget() → build()
(或 InheritedWidget 变化 → didChangeDependencies() → build())
销毁阶段:
dispose()
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
// 初始化资源、订阅、动画控制器
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// InheritedWidget 变化时调用
}
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 父 Widget 重建传入新参数时调用
}
@override
void dispose() {
// 释放资源、取消订阅、销毁动画控制器
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
注意:
-
initState中不能使用BuildContext(依赖 InheritedWidget 的操作) -
didChangeDependencies在initState之后立即调用,适合首次获取 InheritedWidget -
dispose必须释放所有资源,防止内存泄漏
7. BuildContext 是什么?
答案:
BuildContext 是 Element 的抽象接口,提供了以下能力:
-
定位 Widget 树中的位置:每个 Widget 的
build方法都接收一个 context -
获取祖先 Widget:
context.findAncestorWidgetOfExactType<T>() -
访问 InheritedWidget:
context.dependOnInheritedWidgetOfExactType<T>() -
导航:
Navigator.of(context) -
主题/本地化:
Theme.of(context)、Localizations.of(context)
常见陷阱:
// ❌ 错误:在 initState 中使用 context
@override
void initState() {
super.initState();
Navigator.of(context).push(...); // 太早了!
}
// ✅ 正确:在 didChangeDependencies 或 addPostFrameCallback 中使用
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(...);
});
}
8. Key 的作用是什么?有哪些类型?
答案:
Key 用于在 Widget 树重建时,帮助 Framework 识别和保留 Element 的状态。
主要用途:
- 列表中的元素顺序变化或增删时保持状态
- 在不同位置保留同一个 State
Key 的类型:
// 1. ValueKey:基于值相等
ListView(
children: items.map((item) => MyTile(
key: ValueKey(item.id), // 推荐
item: item,
)).toList(),
);
// 2. ObjectKey:基于对象同一性(identical)
key: ObjectKey(myObject)
// 3. UniqueKey:每次都是不同的 key(强制重建)
key: UniqueKey()
// 4. GlobalKey:全局唯一,跨 Widget 访问 State 或 RenderObject
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: ...
)
// 在其他地方
_formKey.currentState?.validate();
// 5. PageStorageKey:保持 ScrollView 的滚动位置
ListView(
key: PageStorageKey('my-list'),
...
)
三、状态管理
9. Flutter 中有哪些常见的状态管理方案?
答案:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| setState | 简单页面,局部状态 | 最简单,随复杂度膨胀 |
| InheritedWidget / InheritedModel | 小到中型应用 | Flutter 原生,无需依赖 |
| Provider | 中小型应用(官方推荐) | 简单易用,基于 InheritedWidget |
| Riverpod | 中型到大型应用 | 编译安全,无 BuildContext 依赖 |
| Bloc / Cubit | 中大型复杂应用 | 事件驱动,清晰分离,模板代码多 |
| GetX | 快速开发 | 一站式(路由/依赖注入/状态),争议大 |
| Redux | 大型复杂应用 | 单一数据源,模板代码多 |
| MobX | 中大型应用 | 响应式,自动追踪依赖 |
选择建议:
- 简单项目 →
setState+ValueNotifier - 一般项目 → Provider 或 Riverpod
- 复杂业务 → Bloc(事件流清晰可追溯)
10. 请用 Provider 实现一个购物车状态管理?
答案:
// 1. 定义 Model
import 'package:flutter/foundation.dart';
class CartItem {
final String id;
final String name;
final double price;
int quantity;
CartItem({required this.id, required this.name, required this.price, this.quantity = 1});
}
class CartModel extends ChangeNotifier {
final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
int get totalCount => _items.fold(0, (sum, item) => sum + item.quantity);
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price * item.quantity);
void addItem(CartItem item) {
final index = _items.indexWhere((e) => e.id == item.id);
if (index >= 0) {
_items[index].quantity++;
} else {
_items.add(item);
}
notifyListeners();
}
void removeItem(String id) {
_items.removeWhere((item) => item.id == id);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
// 2. 注册 Provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
// 3. 使用
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 三种读取方式
// 方式1:watch - 监听变化,自动重建
final cart = context.watch<CartModel>();
return Scaffold(
appBar: AppBar(
title: Text('购物车 (${cart.totalCount})'),
),
body: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (_, i) => ListTile(
title: Text(cart.items[i].name),
subtitle: Text('¥${cart.items[i].price} x ${cart.items[i].quantity}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 方式2:read - 不监听,仅调用方法
context.read<CartModel>().addItem(
CartItem(id: '1', name: '商品A', price: 99.0),
);
},
),
);
}
}
// 方式3:Consumer - 精确控制重建范围
Consumer<CartModel>(
builder: (_, cart, __) => Text('¥${cart.totalPrice}'),
);
四、路由与导航
11. Flutter 路由栈管理方式有哪些?
答案:
// 1. 基础导航(匿名路由)
Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage()));
Navigator.pop(context);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => HomePage()));
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => LoginPage()),
(route) => false, // 清空所有路由
);
// 2. 命名路由
MaterialApp(
routes: {
'/': (_) => HomePage(),
'/detail': (_) => DetailPage(),
},
onGenerateRoute: (settings) { // 动态路由
if (settings.name == '/user') {
final id = settings.arguments as String;
return MaterialPageRoute(builder: (_) => UserPage(id: id));
}
return null;
},
onUnknownRoute: (settings) { // 404
return MaterialPageRoute(builder: (_) => NotFoundPage());
},
);
// 导航到命名路由
Navigator.pushNamed(context, '/detail', arguments: {'id': '123'});
// 3. GoRouter(官方推荐的路由方案)
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (_, state) => HomePage(),
),
GoRoute(
path: '/detail/:id',
builder: (_, state) => DetailPage(id: state.pathParameters['id']!),
),
ShellRoute( // 嵌套路由 + 底部导航
builder: (_, state, child) => ScaffoldWithNavBar(child: child),
routes: [...],
),
GoRoute(
path: '/login',
builder: (_, state) => LoginPage(),
redirect: (_, state) {
final isLoggedIn = AuthService.isLoggedIn;
if (isLoggedIn) return '/';
return null;
},
),
],
);
12. 如何实现页面间数据传递和结果返回?
答案:
// 传递数据到新页面
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EditPage(data: myData),
),
);
// 新页面返回结果
Navigator.pop(context, '更新成功');
Navigator.pop(context, {'status': 'ok', 'id': 123});
// 接收结果
if (result != null) {
// 处理返回结果
}
// 命名路由传参
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
五、网络与数据持久化
13. Flutter 中如何发起网络请求?如何处理错误?
答案:
import 'package:http/http.dart' as http;
import 'dart:convert';
class ApiService {
static const baseUrl = 'https://api.example.com';
// GET 请求
Future<List<User>> fetchUsers() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/users'))
.timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => User.fromJson(json)).toList();
} else if (response.statusCode == 401) {
throw UnauthorizedException('Token 过期');
} else {
throw ServerException('服务器错误: ${response.statusCode}');
}
} on SocketException {
throw NetworkException('网络连接失败,请检查网络');
} on TimeoutException {
throw NetworkException('请求超时');
} on FormatException {
throw DataException('数据格式错误');
}
}
// POST 请求
Future<User> createUser(Map<String, dynamic> data) async {
final response = await http.post(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode(data),
);
// ... 处理响应
}
}
// 使用 Dio(更强大)
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
// 添加拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// 刷新 Token 或跳转登录
}
handler.next(error);
},
));
14. Flutter 中的数据持久化方案有哪些?
答案:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| SharedPreferences | 简单键值对(设置/Token) | 轻量,同步/异步读取 |
| Hive | 轻量结构化数据 | 纯 Dart,速度快,无需原生依赖 |
| Sqflite | 复杂关系型数据 | 完整的 SQLite 支持 |
| Drift(原 Moor) | 复杂数据库操作 | 类型安全的 SQLite ORM |
| File | 文件存储 | 原始文件读写 |
| Secure Storage | 敏感数据(Token/密码) | 加密存储(Keychain/Keystore) |
// Hive 示例
@HiveType(typeId: 0)
class Person extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
}
// 打开 Box
final box = await Hive.openBox<Person>('persons');
box.add(Person()..name = 'Alice'..age = 30);
final persons = box.values.toList();
// Drift 示例
@DataClassName('User')
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get age => integer()();
}
// 查询
final users = await database.select(database.users).get();
final youngUsers = await (database.select(database.users)
..where((u) => u.age.isSmallerThanValue(18)))
.get();
六、性能优化
15. 如何优化 Flutter 列表滚动性能?
答案:
// 1. 使用 ListView.builder 代替 ListView(懒加载)
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
);
// 2. 使用 const 构造函数
const Text('Hello'); // 避免不必要的重建
// 3. 提取子 Widget 为独立组件(或使用 RepaintBoundary)
class _ItemTile extends StatelessWidget {
const _ItemTile({required this.item});
final Item item;
@override
Widget build(BuildContext context) {
return ListTile(title: Text(item.name));
}
}
// 4. 使用 itemExtent 固定高度(避免布局计算)
ListView.builder(
itemExtent: 56.0, // 每个 item 高度固定
itemCount: items.length,
itemBuilder: (context, index) => ...,
);
// 5. 使用 addAutomaticKeepAlives / addRepaintBoundaries
ListView.builder(
addAutomaticKeepAlives: false, // 不需要保持状态的列表
addRepaintBoundaries: true, // 默认开启,避免整列表重绘
...
);
// 6. 复杂列表使用 ScrollController + 分页加载
final _scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
loadMore();
}
});
// 7. 图片优化
Image.network(
url,
width: 100, // 指定尺寸,按需解码
cacheWidth: 200, // 限制缓存图片宽度
fit: BoxFit.cover,
);
// 8. 使用 cached_network_image 缓存图片
CachedNetworkImage(
imageUrl: url,
placeholder: (_, __) => Shimmer(...),
errorWidget: (_, __, ___) => Icon(Icons.error),
);
16. 如何分析和定位 Flutter 性能问题?
答案:
-
Flutter DevTools:最全面的性能分析工具
- Timeline(时间线):查看帧渲染时间,UI 线程 vs Raster 线程
- Widget Inspector:查看 Widget 树重建情况
- Memory:内存使用和泄漏检测
- CPU Profiler:函数耗时分析
Debug 模式下检测重建:
// 打印 rebuild 信息
debugPrintRebuildDirtyWidgets = true;
// 或给 Widget 添加调试信息
print('HomePage build: ${DateTime.now()}');
- Performance Overlay:
MaterialApp(
showPerformanceOverlay: true, // 显示 GPU/UI 帧率图表
);
- Repaint Rainbow:
debugRepaintRainbowEnabled = true; // 重绘区域显示彩色边框
-
常见优化点:
- 减少不必要的
setState - 使用
const构造函数 - 图片缓存和尺寸控制
- 避免在
build中执行耗时操作(如复杂计算) - 使用
RepaintBoundary隔离重绘区域
- 减少不必要的
七、平台交互
17. 什么是 Platform Channel?MethodChannel 和 EventChannel 的区别?
答案:
Platform Channel 是 Flutter 与原生平台(Android/iOS)通信的机制。
// MethodChannel:Flutter 调用原生,请求-响应模式
// Flutter 端
static const channel = MethodChannel('com.example.app/battery');
Future<int> getBatteryLevel() async {
try {
final result = await channel.invokeMethod('getBatteryLevel');
return result as int;
} on PlatformException catch (e) {
print('Failed: ${e.message}');
return -1;
}
}
// Android 端(Kotlin)
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val level = getBatteryLevel()
result.success(level)
} else {
result.notImplemented()
}
}
}
}
// EventChannel:原生向 Flutter 持续推送数据流,流式模式
// Flutter 端
static const eventChannel = EventChannel('com.example.app/events');
eventChannel.receiveBroadcastStream().listen((event) {
print('收到原生事件: $event');
});
| 特性 | MethodChannel | EventChannel |
|---|---|---|
| 通信模式 | 请求-响应 | 持续流式推送 |
| 数据方向 | 双向(主要 Flutter→原生) | 原生→Flutter |
| 适用场景 | 获取数据、调用方法 | 传感器数据、事件监听 |
| 数据次数 | 单次响应 | 持续多次 |
八、架构与设计模式
18. 请描述你常用的 Flutter 项目架构?
答案:
推荐 Clean Architecture + MVVM/Bloc 分层架构:
lib/
├── core/ # 核心层
│ ├── network/ # 网络配置、拦截器
│ ├── theme/ # 主题
│ └── utils/ # 工具类
├── data/ # 数据层
│ ├── models/ # 数据模型(DTO)
│ ├── datasources/ # 数据源(远程/本地)
│ └── repositories/ # 仓库实现
├── domain/ # 领域层
│ ├── entities/ # 业务实体
│ ├── repositories/ # 仓库接口(抽象)
│ └── usecases/ # 用例
├── presentation/ # 展示层
│ ├── pages/ # 页面
│ ├── widgets/ # 可复用组件
│ └── blocs/ # 状态管理
└── main.dart
依赖方向:Presentation → Domain ← Data
MVVM 示例(使用 ChangeNotifier):
// Model (Domain)
class User {
final int id;
final String name;
User({required this.id, required this.name});
}
// Repository (Data)
abstract class UserRepository {
Future<List<User>> getUsers();
}
class UserRepositoryImpl implements UserRepository {
final ApiService apiService;
UserRepositoryImpl(this.apiService);
@override
Future<List<User>> getUsers() async {
final dtos = await apiService.fetchUsers();
return dtos.map((dto) => User(id: dto.id, name: dto.name)).toList();
}
}
// ViewModel (Presentation)
class UserViewModel extends ChangeNotifier {
final UserRepository _repository;
List<User> _users = [];
bool _isLoading = false;
String? _error;
List<User> get users => _users;
bool get isLoading => _isLoading;
String? get error => _error;
UserViewModel(this._repository);
Future<void> loadUsers() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_users = await _repository.getUsers();
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
}
九、测试
19. Flutter 中有哪些测试类型?如何编写?
答案:
// 1. 单元测试(Unit Test):测试单个函数或类
void main() {
group('CartModel', () {
late CartModel cart;
setUp(() => cart = CartModel());
test('初始购物车为空', () {
expect(cart.items.length, 0);
expect(cart.totalPrice, 0);
});
test('添加商品后数量正确', () {
cart.addItem(CartItem(id: '1', name: '商品A', price: 10.0));
expect(cart.totalCount, 1);
expect(cart.totalPrice, 10.0);
});
test('重复添加同一商品增加数量', () {
cart.addItem(CartItem(id: '1', name: '商品A', price: 10.0));
cart.addItem(CartItem(id: '1', name: '商品A', price: 10.0));
expect(cart.totalCount, 2);
});
});
}
// 2. Widget 测试(Widget Test):测试 Widget 渲染和交互
void main() {
testWidgets('点击按钮添加计数', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // 触发重建
expect(find.text('1'), findsOneWidget);
});
testWidgets('列表项长按显示删除', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await tester.longPress(find.text('Item 1'));
await tester.pumpAndSettle(); // 等待动画完成
expect(find.text('删除'), findsOneWidget);
});
}
// 3. 集成测试(Integration Test):端到端测试
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('完整登录流程', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), '123456');
await tester.tap(find.text('登录'));
await tester.pumpAndSettle();
expect(find.text('首页'), findsOneWidget);
});
}
Mock 依赖(Mockito):
@GenerateMocks([UserRepository])
void main() {
late MockUserRepository mockRepository;
late UserViewModel viewModel;
setUp(() {
mockRepository = MockUserRepository();
viewModel = UserViewModel(mockRepository);
});
test('加载用户成功', () async {
when(mockRepository.getUsers())
.thenAnswer((_) async => [User(id: 1, name: 'Test')]);
await viewModel.loadUsers();
expect(viewModel.users.length, 1);
expect(viewModel.isLoading, false);
});
}
十、进阶与实践
20. Flutter 中如何实现自定义绘制?
答案:
// 使用 CustomPainter 实现自定义绘制
class CirclePainter extends CustomPainter {
final Color color;
final double progress;
CirclePainter({this.color = Colors.blue, this.progress = 1.0});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
// 背景圆
final bgPaint = Paint()
..color = color.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 8;
canvas.drawCircle(center, radius, bgPaint);
// 进度弧
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2,
2 * pi * progress,
false,
progressPaint,
);
// 中心文字
final textPainter = TextPainter(
text: TextSpan(
text: '${(progress * 100).toInt()}%',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(
canvas,
center - Offset(textPainter.width / 2, textPainter.height / 2),
);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
color != oldDelegate.color || progress != oldDelegate.progress;
}
// 使用
SizedBox(
width: 100,
height: 100,
child: CustomPaint(
painter: CirclePainter(progress: 0.75),
),
)
// 带动画
class AnimatedProgressRing extends StatefulWidget {
@override
State<AnimatedProgressRing> createState() => _AnimatedProgressRingState();
}
class _AnimatedProgressRingState extends State<AnimatedProgressRing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (_, __) => CustomPaint(
painter: CirclePainter(progress: _controller.value),
size: Size(100, 100),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
21. 如何实现 Flutter 中的国际化(i18n)?
答案:
// 1. pubspec.yaml 添加依赖
// dependencies:
// flutter_localizations:
// sdk: flutter
// intl: any
// 2. MaterialApp 配置
import 'package:flutter_localizations/flutter_localizations.dart';
MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
AppLocalizations.delegate, // 自定义
],
supportedLocales: [
Locale('en', 'US'),
Locale('zh', 'CN'),
],
locale: Locale('zh', 'CN'), // 默认语言
);
// 3. 自定义 AppLocalizations
class AppLocalizations {
final Locale locale;
AppLocalizations(this.locale);
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static final Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'My App',
'login': 'Login',
'welcome': 'Welcome, {name}!',
},
'zh': {
'title': '我的应用',
'login': '登录',
'welcome': '欢迎, {name}!',
},
};
String get title => _localizedValues[locale.languageCode]!['title']!;
String get login => _localizedValues[locale.languageCode]!['login']!;
String welcome(String name) {
return _localizedValues[locale.languageCode]!['welcome']!.replaceAll('{name}', name);
}
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
@override
bool isSupported(Locale locale) =>
['en', 'zh'].contains(locale.languageCode);
@override
Future<AppLocalizations> load(Locale locale) async {
return AppLocalizations(locale);
}
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
// 4. 使用
Text(AppLocalizations.of(context).title)
Text(AppLocalizations.of(context).welcome('小明'))
// 5. 使用 intl + ARB 文件(推荐生产方案)
// intl/messages_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": { "description": "应用标题" },
"loginButton": "Login",
"greeting": "Hello {name}!",
"@greeting": {
"description": "问候语",
"placeholders": { "name": {} }
}
}
22. 如何实现 Flutter 中的深色模式?
答案:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 方式1:手动设置
themeMode: ThemeMode.system, // light / dark / system
theme: ThemeData.light().copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
darkTheme: ThemeData.dark().copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
),
// 方式2:使用 ThemeExtension 自定义主题
theme: ThemeData.light().copyWith(
extensions: [AppColors.light],
),
darkTheme: ThemeData.dark().copyWith(
extensions: [AppColors.dark],
),
);
}
}
// 自定义 ThemeExtension
class AppColors extends ThemeExtension<AppColors> {
final Color cardBackground;
final Color textPrimary;
const AppColors({required this.cardBackground, required this.textPrimary});
static const light = AppColors(
cardBackground: Colors.white,
textPrimary: Colors.black87,
);
static const dark = AppColors(
cardBackground: Color(0xFF1E1E1E),
textPrimary: Colors.white,
);
@override
AppColors copyWith({Color? cardBackground, Color? textPrimary}) {
return AppColors(
cardBackground: cardBackground ?? this.cardBackground,
textPrimary: textPrimary ?? this.textPrimary,
);
}
@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
return AppColors(
cardBackground: Color.lerp(cardBackground, other.cardBackground, t)!,
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
);
}
}
// 使用
final colors = Theme.of(context).extension<AppColors>()!;
Container(color: colors.cardBackground);
十一、编码实践题
23. 实现一个带搜索和下拉刷新的列表页
答案:
class SearchableListView extends StatefulWidget {
@override
State<SearchableListView> createState() => _SearchableListViewState();
}
class _SearchableListViewState extends State<SearchableListView> {
final _scrollController = ScrollController();
final _searchController = TextEditingController();
List<String> _allItems = [];
List<String> _filteredItems = [];
bool _isLoading = false;
int _page = 1;
String _query = '';
@override
void initState() {
super.initState();
_loadData();
_scrollController.addListener(_onScroll);
_searchController.addListener(_onSearchChanged);
}
void _onSearchChanged() {
setState(() {
_query = _searchController.text;
_filterItems();
});
}
void _filterItems() {
if (_query.isEmpty) {
_filteredItems = List.from(_allItems);
} else {
_filteredItems = _allItems
.where((item) => item.toLowerCase().contains(_query.toLowerCase()))
.toList();
}
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
_loadMore();
}
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
// 模拟网络请求
await Future.delayed(Duration(seconds: 1));
final newItems = List.generate(20, (i) => 'Item ${_page * 20 + i + 1}');
_allItems.addAll(newItems);
_filterItems();
_page++;
setState(() => _isLoading = false);
}
Future<void> _loadMore() async {
if (_isLoading) return;
await _loadData();
}
Future<void> _onRefresh() async {
_page = 1;
_allItems.clear();
await _loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('搜索列表')),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(8),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
controller: _scrollController,
itemCount: _filteredItems.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _filteredItems.length) {
return Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
}
return ListTile(title: Text(_filteredItems[index]));
},
),
),
),
],
),
);
}
@override
void dispose() {
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
}
24. 编写一个防抖(Debounce)工具类
答案:
class Debouncer {
final Duration delay;
Timer? _timer;
Debouncer({this.delay = const Duration(milliseconds: 300)});
void call(VoidCallback action) {
_timer?.cancel();
_timer = Timer(delay, action);
}
void dispose() {
_timer?.cancel();
}
}
// 使用场景:搜索输入防抖
class SearchPage extends StatefulWidget {
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final _debouncer = Debouncer(delay: Duration(milliseconds: 500));
void _onSearchChanged(String query) {
_debouncer.call(() {
// 500ms 无新输入后执行搜索
performSearch(query);
});
}
@override
void dispose() {
_debouncer.dispose();
super.dispose();
}
}
25. 实现一个可复用的权限请求工具
答案:
import 'package:permission_handler/permission_handler.dart';
class PermissionHelper {
/// 请求单个权限
static Future<bool> request(Permission permission) async {
final status = await permission.status;
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
// 引导用户去设置页面
await openAppSettings();
return false;
}
final result = await permission.request();
return result.isGranted;
}
/// 批量请求权限
static Future<Map<Permission, bool>> requestMultiple(
List<Permission> permissions,
) async {
final results = <Permission, bool>{};
for (final permission in permissions) {
results[permission] = await request(permission);
}
return results;
}
/// 检查并请求,带对话框提示
static Future<bool> requestWithDialog(
BuildContext context, {
required Permission permission,
required String title,
required String message,
}) async {
final status = await permission.status;
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
final openSettings = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
content: Text('$message\n\n请在设置中手动开启权限'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('去设置'),
),
],
),
);
if (openSettings == true) {
await openAppSettings();
}
return false;
}
final requested = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('拒绝'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('允许'),
),
],
),
);
if (requested != true) return false;
return await permission.request().then((s) => s.isGranted);
}
}
// 使用
final granted = await PermissionHelper.requestWithDialog(
context,
permission: Permission.camera,
title: '需要相机权限',
message: '扫描二维码需要使用相机',
);
if (granted) {
// 打开相机
}
面试评分参考
| 级别 | 要求 |
|---|---|
| 初级(1-2年) | 掌握 Dart 基础、Widget 使用、setState 状态管理、基础路由、网络请求 |
| 中级(2-4年) | 掌握状态管理方案(Provider/Riverpod/Bloc)、自定义组件、性能优化、Platform Channel |
| 高级(4年+) | 复杂架构设计、渲染原理深入理解、性能调优、插件开发、团队规范和工程化 |
以上面试题涵盖 Flutter 开发的核心知识点,可根据候选人级别灵活选择题目。建议面试时结合候选人的实际项目经验进行深入的场景提问。