Flutter面试题带答案

Flutter是一个相对新的跨平台框架,但是它的流行度正在迅速提高。雇主也意识到单一代码库的好处,依托Flutter可以使他们将两个或者三个团队合并成一个,Flutter开发者的工作数量也在增加。

在这篇文章中,你将看到一系列的关于Flutter和Dart面试题的问题与答案。

如果你是正在找工作的开发者,通过下面的问题(在查看答案前,请先尝试自行解答)可以帮助你在相关技能点查缺补漏。
如果你是潜在的雇主,浏览下面问题,以获得向你的候选人提问的想法。
或者可以用来测试你的Flutter和Dart的知识掌握程度。

下面的问题分为几个三个等级:

  • 初级:适合初级Flutter开发者,已经熟悉了基本知识,并且只做了一些示例应用程序。
  • 中级:适合对Flutter和Dart工作方式有浓厚兴趣的中级开发者,您已经阅读了很多,并且尝试了更多。
  • 高级:适合高级开发人员,乐于探索Flutter框架和Dart语言,并且知道如何管理项目的人。

在每一个级别,又分为两种类型:

  • 笔试型:适合邮件或者在线编程测试,因为它们涉及到编写代码。
  • 问答型:适合视频或者面对面的交流。

初级笔试题

问题1

给定如下类

class Recipe {
  int cows;
  int trampolines;

  Recipe(this.cows, this.trampolines);
  
  int makeMilkshake() {
    return cows + trampolines;
  }
}

使用胖箭头语法将makeMilkshake()转换成命名为milkshake的getter语法。

答:

如果一个方法只有一行代码,则可以通过使用=>语法返回结果来减少代码行数。

methodName(parameters) => statement;

注意当使用=>的时候,不需要再使用关键词return.
makeMilkshake()转换后的代码如下

int get milkshake => cows + trampolines;

问题2

给定如下Widget

class MyWidget extends StatelessWidget {
  final personNextToMe = 'That reminds me about the time when I was ten and our neighbor, her name was Mrs. Mable, and she said...';

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      Icon(Icons.airline_seat_legroom_reduced),
      Text(personNextToMe),
      Icon(Icons.airline_seat_legroom_reduced),
    ]);
  }
}

在一些窄屏设备上,文本溢出了,你会如何修复呐?


1text_overflow.png

答:

Expanded(
  child: Text(
    personNextToMe,
  ),
),

使用Expandedwidget来包裹Textwidget,以告知Row忽略Textwidget的固有宽度,并且根据行中剩余的空间来为其分配宽度。

RowColumn或者Flexwidget中使用超过一个Expandendwidget时,会均匀的分配剩余空间。当有多个Expandwidget时,可以使用flex属性对优先级进行排序。

假如你还使用了Textwidget的overflow属性,那就太棒了。

更多的介绍,可以阅读Flutter文档中的布局约束部分

问题3

重构下面代码,以便Row显示宽度太窄无法容纳它们时,子节点自动换行到下一行展示。

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(children: [
      Chip(label: Text('I')),
      Chip(label: Text('really')),
      Chip(label: Text('really')),
      Chip(label: Text('really')),
      Chip(label: Text('really')),
      Chip(label: Text('really')),
      Chip(label: Text('really')),
      Chip(label: Text('need')),
      Chip(label: Text('a')),
      Chip(label: Text('job')),
    ]);
  }
}

答:

只需要将Row替换为Wrap就可以了。
阅读Medium文章Flutter Wrap Widget以了解更多关于Wrapwidget。

问题4

如下代码,使用var声明list1final声明list2const声明list3,这些关键字的不同之处是什么,最后两行代码能够编译吗?

var list1 = ['I', '', 'Flutter'];

final list2 = list1;
list2[2] = 'Dart';   // Will this line compile?
  
const list3 = list1; // Will this line compile?

答:

当使用var关键词时,数据的类型是推断出来的,并且值可以改变。除了显示的声明了数据类型之外,下面的代码和上面第一行代码是等效的:

List list1 = ['I', '', 'Flutter'];

使用finalconst时,你不能在初始值分配后重新分配新值。final修饰的变量在运行时分配一次,const修饰的变量,在运行程序之前的编译期就需要知道、设置或者硬编码变量的值。

第三行代码可以编译成功,因为并没有对list2重新赋值,只是改变了第三个位置的元素的值(请记住,下标从0开始),默认情况下,在Dart中List是可变的。

假如你试图按照下面的样子做,将会编译不通过,因为你在对final修饰的变量重新赋值。

list2 = ['I', '', 'Dart'];

第四行不会编译,因为list1得值并没有确定,直到runtime时。阅读Dartlang的文章Const, Static, Final, Oh my!以了解更多。

问题5

给定下面类

class Pizza {
  String cheese = 'cheddar';
}

你如何将cheese变成私有变量,怎样将它变成全局变量,什么时候你使用全局变量?

答:

在变量的前面添加下划线_,可以使它在库中私有化。

class Pizza {
  String _cheese = 'cheddar';
}

Dart没有类私有变量的概念。一个库通常是一个文件,一个文件可以包含多个类。
假如你想要一个全局变量,只需要将变量移到类的外面就可以了。

String cheese = 'cheddar';

将其放在类的外面会使其变成顶级变量,导入其所在文件后,就可以在任何地方使用它了。

全局变量通常不建议使用,因为很难知道是哪里修改了它们,难以追踪修改路径,这会使调试和测试变得困难,但是有时它们也会很有用:

  • 快速搭建你并不打算长期维护的demo示例时。
  • 创建单例以提供类似于数据库或者网络身份验证的服务。
  • 制作const变量以共享颜色、尺寸、样式、主题等内容。这些类型的全局变量通常存储在单独的文件中,例如Constants.dart,然后引入待库中。

阅读Dart语言的库和可见性文章以了解更多。

初级问答题

问题1

hot reloadhot restart的区别是什么?

答:

hot reload在立刻更新UI的同时保持程序的状态,相比之下hot restart花费更长一点的时间,因为它会在更新UI之前将程序的状态置为初始状态。两者都比完全重新启动(full restart)要快,这需要重新编译应用程序。

当有重大的更改时,你需要停止并重新运行该程序,在极少数的情况下,你可能还需要在模拟器或者真机上删除应用程序,然后重新安装。

问题2

StatelessWidgetStatefulWidget的区别是什么?

答:

StatelessWidget是一个不可变的类,充当UI布局中某些部分的蓝图,当某个组件在显示期间不需要改变,或者说没有状态(State),你可以使用它。
StatefulWidget也是不可变的,但是它和一个State对象关联在一起,该对象允许你每次通过调用setState()时,使用新值重建这个widget,当UI可以动态改变时使用StatefulWidget

假如State变得越来越复杂,或者一些状态存在于两个不同的widget中,则应该考虑更复杂的状态管理方案

阅读stateless and stateful widgets以了解更多。

问题3

WidgetsAppMaterialApp的区别什么?

答:

WidgetsApp提供了基础的导航能力,和widgets库一起,它包含了很多Flutter使用的基础widget。

MaterialApp和与之相应的的material库,是在WidgetsApp和与之相应的widgets库之上构建的一层,它遵循了Material设计风格,可以再任何平台或者设备上为应用程序提供统一的外观,material库提供了更多的Widget。

在你的项目中,你并不一定要使用MaterialApp,也可以使用CupertinoApp来构建iOS风格的应用程序,这可以使iOS用户感觉更亲切,甚至你也可以自己定义一些widget。

问题4

可以嵌套使用Scaffold吗,为什么或者为什么不?

答:

当然可以,你绝对可以嵌套使用Scaffold,这体现Flutter的美,你可以控制整个UI。

Scaffold也是个widget,因此你可以把它放在任何widget可以放置的地方。通过嵌套Scaffold,你可以对抽屉(drawers)、卡片(snack bars)、底页(bottom sheets)进行分层。

2nested_scaffolds的副本.png

问题5

什么时候适合使用packagesplugins或者三方库?

答:

packages和plugins可以极大的节约你的时间,当别人已经解决了一个复杂问题时,你没必要再解决一遍,尤其是该解决方案已经获得了很好的评价时。

另一方面,过度依赖三方库也可能有一些风险,他们可能编译不过、有bug或者被丢弃,当你需要切换到新的package或者plugin,可能会对代码做巨大的更改。

这就是为什么需要将业务逻辑和三方库隔离开的原因,你可以通过创建一个Dart的抽象类,来充当package或者plugin的接口。一旦你设置完这种结构后,再遇到需要切换package或者plugin情况,你所要做的就只是重写接口层的具体实现了。

中级笔试题

问题1

你正在编写一个称之为RubberBaby的购物程序,它可以用来卖玩偶,不幸的是在订单页,遇到了一个问题。假如顾客下了一个蓝色的订单和一个红色的订单,但是当顾客试图删除蓝色订单的时候,红色订单出了异常。

3buggy_buttons.gif

下面是具体代码,你会如何修复RubberBaby的有问题的按钮呐?

class OrderPage extends StatefulWidget {
  @override
  _OrderPageState createState() => _OrderPageState();
}

class _OrderPageState extends State {
  bool isShowing = true;
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      RaisedButton(
        child: (Text('Delete blue')),
        onPressed: () {
          setState(() {
            isShowing = false;
          });
        },
      ),
      if (isShowing) CounterButton(color: Colors.blue),
      CounterButton(color: Colors.red),
    ]);
  }
}

答:

当你使用stateful widget时,当widget树发生了改变时,框架会比较widget的类型,看看是否能够重用。

因为两个CounterButton是相同的类型,Flutter并不知道哪个widget和state进行了绑定。这样的结果就是红色按钮进行更新时,使用了蓝色按钮内部的state。

为了解决这个问题,可以为每个widget使用key属性,此属性为每个widget添加了一个ID:

CounterButton(
  key: ValueKey('red'),
  color: Colors.red,
),

通过添加key,你已经唯一的标记了红色计数按钮,Flutter将能够保留其状态。你可以在Medium文章Keys! What are they good for?.中了解更多关于如何使用key。

问题2

GitHub Jobs有一个开放的接口用于查询软件工程相关的职位,下面是接口地址,将会返回一个远程工作的岗位的列表:

https://jobs.github.com/positions.json?location=remote

下面给了一个简单的数据模型,你只需要关心公司名字和岗位名称,请编写一个返回值类型是Future>的方法,在这个问题中你可以忽略先错误检查。

class Job {
  Job(this.company, this.title);

  final String company;
  final String title;
}

答:

因为Api返回一个JSON map类型的数组,添加一个fromJson析构方法到Job类中将会是解析变得容易一些。

class Job {
  Job(this.company, this.title);

  Job.fromJson(Map json)
      : company = json['company'],
        title = json['title'];

  final String company;
  final String title;
}

有很多packages可以用来进行网络请求,Dart官方团队维护了基本的http库,为了使用它,可以添加下面依赖到你的pubspec.yaml中:

dependencies:
  http: ^0.12.1

然后使用这个库创建一个方法,在后台从GitHub拉去网络数据:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future> fetchJobs() async {
  final host = 'jobs.github.com';
  final path = 'positions.json';
  final queryParameters = {'location': 'remote'};
  final headers = {'Accept': 'application/json'};
  final uri = Uri.https(host, path, queryParameters);
  final results = await http.get(uri, headers: headers);
  final jsonList = json.decode(results.body) as List;
  return jsonList.map((job) => Job.fromJson(job)).toList();
}

在定义Uri变量后,创建了http.get请求,它将会返回一个JSON字符串。

下一步。使用json.decode,将JSON数据解析成一个map数据,然后转换成job对象的list。

之前的文章Parsing JSON in Flutter,将会向你介绍如何使用web API,来进行更高效的创建模型和解析JSON数据。

问题3

给定一个Dart stream 产出无限的字符串,这些字符串可能是salmon或者trout

final fishStream = FishHatchery().stream; 
// salmon, trout, trout, salmon, ...

将这个stream进行转换,要求仅当当前五次产出salmon字符串时,返回sushi字符串。

答:

stream转换如下:

final fishStream = FishHatchery().stream;
final sushiStream = fishStream
    .where((fish) => fish == 'salmon')
    .map((fish) => 'sushi')
    .take(5);

假如你想了解更多,下面是FishHatchery类的代码

class FishHatchery {
  FishHatchery() {
    Timer.periodic(Duration(seconds: 1), (t) {
      final isSalmon = Random().nextBool();
      final fish = (isSalmon) ? 'salmon' : 'trout';
      _controller.sink.add(fish);
    });
  }

  final _controller = StreamController();
  Stream get stream => _controller.stream;
}

你可以在Flutter团队的视频Dart Streams — Flutter in Focus,和Dart的文档Creating Streams中学到更多关于streams的内容。

问题4

为什么下面的代码会阻塞你的Flutter应用程序呐?

String playHideAndSeekTheLongVersion() {
  var counting = 0;
  for (var i = 1; i <= 1000000000; i++) {
    counting = i;
  }
  return '$counting! Ready or not, here I come!';
}

把它改成async异步方法会有帮助吗?

答:

这将会阻塞你的应用程序,因为计算到10亿,即使对计算机来说也是一个代价昂贵的任务。

Dart代码在自己的被称之为isolate的内存区域运行,也被称为内存线程。每个isolate有自己的堆空间,这确保没有isolate可以访问到其它isolate的状态。

将这个方法改造成async方法也无济于事,因为它仍旧在同一个isolate上运行,如下:

Future playHideAndSeekTheLongVersion() async {
  var counting = 0;
  await Future(() {
    for (var i = 1; i <= 10000000000; i++) {
      counting = i;
    }
  });
  return '$counting! Ready or not, here I come!';
}

解决方案是将它运行在不同的isolate上:

Future makeSomeoneElseCountForMe() async {
  return await compute(playHideAndSeekTheLongVersion, 10000000000);
}

String playHideAndSeekTheLongVersion(int countTo) {
  var counting = 0;
  for (var i = 1; i <= countTo; i++) {
    counting = i;
  }
  return '$counting! Ready or not, here I come!';
}

这样就不会阻塞你的UI线程了。

想要了解更多关于异步任务和isolate的内容,可以看Flutter团队的视频Isolates and Event Loops — Flutter in Focus和的文章Futures — Isolates — Event Loop

在下个问题中,你将会见到另一种类型的isolate。

中级问答题

问题1

什么是event loop,它和isolate的关系是什么?

答:

Dart是早期遵循社交距离的采用者,Dart代码运行在一个独立的被称之为isolate的线程上,相互隔离的的isolate不会一起出去玩,最多也就是互相发信息。用计算机术语来说就是,isolate之间不共享内存,它们之间的通信仅通过端口(port)进行。

每一个isolate都有一个event loop,用于管理异步任务的运行,这些任务可能来自于两个队列之中:microtask queue,或者event queue

Microtasks任务总是优先运行,它们主要是内核任务,开发者不必关心。调用Future时将会把任务放置到event queue中。

很多新手Dart开发者,认为async方法运行在一个单独的线中,尽管对于系统处理的I/O操作这样说可能是正确的,但是它并不适用于你所写的代码,这就是为什么假如你有一个计算量很大的任务,你需要将它运行在一个单独的isolate之中。

如果你想了解更多关于isolate、event loop、和并发相关的内容,可以参考Medium上的文章Dart asynchronous programming: Isolates and event loops,Futures — Isolates — Event Loops

问题2

怎么减少Widget的重新构建?

答:

当state发生改变时,你将重新构建widget,这种正常且理想的状态,因为它允许用户查看反映在UI中的状态更改。但是重新构建那些不需要改变的UI是性能浪费的。

你可以采取以下措施来减少不必要的Widget重建。

  • 首先要做的就是将大的Widget树重构成较小的单个的Widget,每一个Widget都有它自己的build方法。
  • 尽可能的使用const构造函数,这将告知Flutter不需要重建这个widget。
  • 使stateful widget的子树尽可能的小,如果stateful widget有一个widget子树,那么为这个stateful widget创建一个自定义widget,并为其提供一个child参数。

你可以在Flutter文档中,阅读更多关于性能优化的注意事项

问题3

什么是BuildContext,它有什么用?

答:

BuildContext实际上是在Element树中的Widget的元素,因此每个Widget都有其自己的BuildContext

你通常使用BuildContext来获取主题(theme)或者另一个Widget的引用,例如:假如你想要展示一个material dialog,那么你需要获取scaffold的引用,可以通过Scaffold.of(context)来得到它,其中context就是上下文信息,通过of()来往上搜索树,直到找到最近的Scaffold。

阅读didierboelens.com网站的文章Widget — State — Context — Inherited Widget 不仅可以了解到BuildContext,也可以了解到stateful widget的生命周期和inherited widget。

此外,我们的文章Flutter Text Rendering将会带你窥探Flutter底层源码,通过这篇文章,你会了解到build context、elements甚至render对象。

问题4

在Flutter应用程序中,你怎么和native进行交互?

答:

通常你不需要和原生进行交互,因为Flutter或三方插件会处理这些问题,但是,如果你发现确实有特殊需要访问一些底层平台,你可以使用平台channel。

其中一种类型是method channel,数据在Dart侧进行序列化,然后会将数据发送到原生侧,你可以在原生侧编写代码响应交互,然后回传序列化后的数据。在Android侧可以选用Kotlin或者Java,在iOS侧可以使用Objective-C或者Swift进行编写。

但是,在开发web的时候,你不需要使用channel,这时非必要的步骤。

第二种channel类型是event channel,你可以用来从native发送stream数据到flutter侧,这对监控传感器数据的场景很有用。

可以在Flutter的文档platform channels中看到更详细的介绍

问题5

你可以做哪种类型的测试?

答:

Flutter中有三种类型的测试:unit testswidget testsintegration tests,单元测试是关于检查业务逻辑的有效性,widget测试确保UI Widget能够正确的响应你的期望,集成测试用于检测你的APP能否整体正常运行。

还有一种测试不为大家所知,称作golden test,在golden test中,你有Widget或者屏幕的图像,以查看实际展示的Widget是否和它匹配。

可以通过Flutter教程了解更多关于测试的内容,通过Medium的文章Flutter: Golden tests — compare Widgets with Snapshots了解更多golden test内容。

raywenderlich.com网站上也有一篇文章介绍Flutter unit testing

高级笔试题

问题1

遵从以下要求,演示Dart isolate通过port的交互过程:
1、将downloadAndCompressTheInternet()函数发送到新的isolate中。
2、上面方法的返回值是42

答:

import 'dart:isolate';

void main() async {
  // 1
  final receivePort = ReceivePort();
  // 2
  final isolate = await Isolate.spawn(
    downloadAndCompressTheInternet,
    receivePort.sendPort,
  );
  // 3
  receivePort.listen((message) {
    print(message);
    receivePort.close();
    isolate.kill();
  });
}

// 4
void downloadAndCompressTheInternet(SendPort sendPort) {
  sendPort.send(42);
}

在上述代码中,你:
1、创建了一个端口用于从接收新的isolate中接收数据。
2、创建一个新的isolate,给他一些工作去做,并且提供一个方式回传数据。
3、监听新isolate中发送的任何数据,然后关闭这个isolate。
4、使用main isolate正在监听的端口,将数据发送回来。

下载和解压算法仍在开发中...

阅读Joe的文章Dart Fundamentals — Isolates,以了解更多关于isolate通信的知识。

问题2

有两个树数据结构的数据,其中树的节点都是随机整数,这些数字不一定是唯一的,也不一定是有序存储的,两棵树的深度也是任意的。编写一个算法以识别出在第一颗树中但是不在第二棵树中的数字。

下面是一个例子:

4trees.png

算法应该识别出,数字1在第一棵树中,并且不在第二棵树中。

答:

首先定义出输的节点:

class Node {
  int data;
  List children;

  Node(this.data, {this.children});
}

编写逻辑,递归查找树,对整数进行去重。

 class UniqueTreeItems {
  final Set _uniqueIntegers = HashSet();

  Set search(Node tree) {
    _addInOrder(tree);
    return _uniqueIntegers;
  }

  void _addInOrder(Node node) {
    _uniqueIntegers.add(node.data);
    if (node.children == null) return;
    for (final child in node.children) {
      _addInOrder(child);
    }
  }
}

设置测试数据

final treeOne = Node(1, children: [
  Node(4, children: [
    Node(10),
    Node(12),
  ]),
  Node(3, children: [
    Node(3),
    Node(10),
    Node(1),
  ]),
]);

final treeTwo = Node(4, children: [
  Node(10),
  Node(3),
  Node(12),
]);

剔除在Tree1中并且也在Tree2中的数据:

void main() async {
  final uniqueOne = UniqueTreeItems().search(treeOne);
  final uniqueTwo = UniqueTreeItems().search(treeTwo);
  final answer = uniqueOne.where((element) => !uniqueTwo.contains(element));
  answer.forEach(print); // 1
}

得到的结果是1

高级问答题

问题1

不同状态管理框架的优缺点是什么?

答:

有多种多样的框架,其中一些比较知名状态管理框架,包括BLOC、伴随ChangeNotifier的Provider、Redux、MobX以及RxDart。这些都适用于中大型的应用程序。如果你只是快速开发一个小demo,那么stateful widget通常就足够了。

与其列出不同状态管理框架的优缺点,不如查看这些框架更适用哪种场景。例如,对于某些人与其淹没在不胜枚举的选择中,不如选择一种比较容易掌握的方案,Provider和MobX都是不错的选择,它们可以直接在state类上调用方法以响应事件,使得这种场景更加直观。

假如你重度依赖流,例如使用Firebase的ApI,那么自然会选择给予数据流的解决方案,比如BLOC和RxDart。

假如你需要撤销/重做功能,那么你需要类似BLOC或者Redux这样,能够很好的处理不可变状态的解决方案。

最后,更多的是归结于个人喜好,你可以在Flutter官方的文章list of state management approaches中找到更多流行的的关于状态管理的框架。

在raywenderlich.com 网站上也有一些介绍BLOC和介绍Provider的文章。

问题2

如何设计一个控制电梯的应用程序?

答:

这个问题测试您的分析技能,以及组织和使用SOLID原则的能力。

下面是一个参考答案。
1、首先,确定核心的功能是什么:开门、关门,向上向下移动到不同楼层,寻求帮助,与其他电梯进行协调。这时您的业务逻辑,画一个流程图可能会更有帮助。

2、以测试驱动(TDD)的方式去实现商业逻辑。也就是说,编写一个失败的测试用例,编写足够的以使它通过的业务逻辑代码,进行重构,然后编写另一个测试用例,再次进行所有操作。

3、刚开始时,有没有物理按钮或者Flutter驱动的触摸屏都没关系,电梯的外观或者位置在哪无关紧要,紧急呼叫系统是什么也不重要,你可以在开发测试阶段,将这些外部因素抽象到模拟的接口后面。

4、一旦完成了核心逻辑,你便可以实现前面仅通过接口表示的各个组件。对于UI来说,你需要设置一套状态管理系统,该系统用来处理按钮点击、电梯到达之类的事件,然后更新状态,这可能导致按钮编号点亮或者更新屏幕。你可能还需要实现与系统交互的服务,以处理紧急呼叫或打开们的硬件。

5、安全对于乘坐电梯的人来说非常重要,因此除了单独的测试核心的业务逻辑和各个系统组件之外,你还需要进行全面的集成测试。对于电梯,将由机器人或者人进行手动测试。

进阶引申

恭喜你,你做到了最后,如果你不能完全回答上述问题也不要觉得难过,在写这篇文章的过程中,我也做了很多搜索。

把这看成一个起点,记下你觉得薄弱的地方,然后在这些方面进行更多的研究。阅读Flutter文档和Dart指南会教会你很多东西。

如果你想学习跟多关于Dart的内容,关注我们的视频课程Dart基础,我们也会在raywenderlich.com网站上持续更新所有关于Flutter的新内容。

假如你有更多面试问题的建议,更好的答案,甚至是代码挑战,请把它们写在下面的评论区。

你可能感兴趣的:(Flutter面试题带答案)