Flutter 开发工程师面试题(含答案)

一、Dart 语言基础

1. Dart 中 finalconst 的区别是什么?

答案:

特性 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 (布局/绘制)

重建流程:

  1. Widget 变化时,Element 通过 canUpdate() 判断是否需要重建
  2. canUpdate() 比较 runtimeTypekey
  3. 如果相同,Element 更新引用的 Widget(update
  4. 如果不同,创建新的 Element 树和 RenderObject

6. StatefulWidgetStatelessWidget 的区别及生命周期?

答案:

  • 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 的操作)
  • didChangeDependenciesinitState 之后立即调用,适合首次获取 InheritedWidget
  • dispose 必须释放所有资源,防止内存泄漏

7. BuildContext 是什么?

答案:

BuildContext 是 Element 的抽象接口,提供了以下能力:

  1. 定位 Widget 树中的位置:每个 Widget 的 build 方法都接收一个 context
  2. 获取祖先 Widgetcontext.findAncestorWidgetOfExactType<T>()
  3. 访问 InheritedWidgetcontext.dependOnInheritedWidgetOfExactType<T>()
  4. 导航Navigator.of(context)
  5. 主题/本地化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
  • 一般项目 → ProviderRiverpod
  • 复杂业务 → 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 性能问题?

答案:

  1. Flutter DevTools:最全面的性能分析工具

    • Timeline(时间线):查看帧渲染时间,UI 线程 vs Raster 线程
    • Widget Inspector:查看 Widget 树重建情况
    • Memory:内存使用和泄漏检测
    • CPU Profiler:函数耗时分析
  2. Debug 模式下检测重建

// 打印 rebuild 信息
debugPrintRebuildDirtyWidgets = true;

// 或给 Widget 添加调试信息
print('HomePage build: ${DateTime.now()}');
  1. Performance Overlay
MaterialApp(
  showPerformanceOverlay: true,  // 显示 GPU/UI 帧率图表
);
  1. Repaint Rainbow
debugRepaintRainbowEnabled = true;  // 重绘区域显示彩色边框
  1. 常见优化点
    • 减少不必要的 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 开发的核心知识点,可根据候选人级别灵活选择题目。建议面试时结合候选人的实际项目经验进行深入的场景提问。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容