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),
]);
}
}
在一些窄屏设备上,文本溢出了,你会如何修复呐?
答:
Expanded( child: Text( personNextToMe, ), ),
使用
Expanded
widget来包裹Text
widget,以告知Row
忽略Text
widget的固有宽度,并且根据行中剩余的空间来为其分配宽度。在
Row
、Column
或者Flex
widget中使用超过一个Expandend
widget时,会均匀的分配剩余空间。当有多个Expand
widget时,可以使用flex
属性对优先级进行排序。假如你还使用了
Text
widget的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以了解更多关于Wrap
widget。
问题4
如下代码,使用var
声明list1
,final
声明list2
,const
声明list3
,这些关键字的不同之处是什么,最后两行代码能够编译吗?
var list1 = ['I', '💙', 'Flutter'];
final list2 = list1;
list2[2] = 'Dart'; // Will this line compile?
const list3 = list1; // Will this line compile?
答:
当使用
var
关键词时,数据的类型是推断出来的,并且值可以改变。除了显示的声明了数据类型之外,下面的代码和上面第一行代码是等效的:List<String> list1 = ['I', '💙', 'Flutter'];
使用
final
和const
时,你不能在初始值分配后重新分配新值。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 reload
和hot restart
的区别是什么?
答:
hot reload
在立刻更新UI的同时保持程序的状态,相比之下hot restart
花费更长一点的时间,因为它会在更新UI之前将程序的状态置为初始状态。两者都比完全重新启动(full restart)要快,这需要重新编译应用程序。当有重大的更改时,你需要停止并重新运行该程序,在极少数的情况下,你可能还需要在模拟器或者真机上删除应用程序,然后重新安装。
问题2
StatelessWidget
和StatefulWidget
的区别是什么?
答:
StatelessWidget
是一个不可变的类,充当UI布局中某些部分的蓝图,当某个组件在显示期间不需要改变,或者说没有状态(State),你可以使用它。
StatefulWidget
也是不可变的,但是它和一个State
对象关联在一起,该对象允许你每次通过调用setState()
时,使用新值重建这个widget,当UI可以动态改变时使用StatefulWidget
。假如State变得越来越复杂,或者一些状态存在于两个不同的widget中,则应该考虑更复杂的状态管理方案
阅读stateless and stateful widgets以了解更多。
问题3
WidgetsApp
和MaterialApp
的区别什么?
答:
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)进行分层。
问题5
什么时候适合使用packages
、plugins
或者三方库?
答:
packages和plugins可以极大的节约你的时间,当别人已经解决了一个复杂问题时,你没必要再解决一遍,尤其是该解决方案已经获得了很好的评价时。
另一方面,过度依赖三方库也可能有一些风险,他们可能编译不过、有bug或者被丢弃,当你需要切换到新的package或者plugin,可能会对代码做巨大的更改。
这就是为什么需要将业务逻辑和三方库隔离开的原因,你可以通过创建一个Dart的抽象类,来充当package或者plugin的接口。一旦你设置完这种结构后,再遇到需要切换package或者plugin情况,你所要做的就只是重写接口层的具体实现了。
中级笔试题
问题1
你正在编写一个称之为RubberBaby
的购物程序,它可以用来卖玩偶,不幸的是在订单页,遇到了一个问题。假如顾客下了一个蓝色的订单和一个红色的订单,但是当顾客试图删除蓝色订单的时候,红色订单出了异常。
下面是具体代码,你会如何修复RubberBaby的有问题的按钮呐?
class OrderPage extends StatefulWidget {
@override
_OrderPageState createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> {
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<List<Job>>
的方法,在这个问题中你可以忽略先错误检查。
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<String, dynamic> 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<List<Job>> 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<String>(); Stream<String> 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<String> 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<String> 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 tests
、widget tests
、integration 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
有两个树数据结构的数据,其中树的节点都是随机整数,这些数字不一定是唯一的,也不一定是有序存储的,两棵树的深度也是任意的。编写一个算法以识别出在第一颗树中但是不在第二棵树中的数字。
下面是一个例子:
算法应该识别出,数字1在第一棵树中,并且不在第二棵树中。
答:
首先定义出输的节点:
class Node { int data; List<Node> children; Node(this.data, {this.children}); }
编写逻辑,递归查找树,对整数进行去重。
class UniqueTreeItems { final Set<int> _uniqueIntegers = HashSet<int>(); Set<int> 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中找到更多流行的的关于状态管理的框架。
问题2
如何设计一个控制电梯的应用程序?
答:
这个问题测试您的分析技能,以及组织和使用SOLID原则的能力。
下面是一个参考答案。
1、首先,确定核心的功能是什么:开门、关门,向上向下移动到不同楼层,寻求帮助,与其他电梯进行协调。这时您的业务逻辑,画一个流程图可能会更有帮助。2、以测试驱动(TDD)的方式去实现商业逻辑。也就是说,编写一个失败的测试用例,编写足够的以使它通过的业务逻辑代码,进行重构,然后编写另一个测试用例,再次进行所有操作。
3、刚开始时,有没有物理按钮或者Flutter驱动的触摸屏都没关系,电梯的外观或者位置在哪无关紧要,紧急呼叫系统是什么也不重要,你可以在开发测试阶段,将这些外部因素抽象到模拟的接口后面。
4、一旦完成了核心逻辑,你便可以实现前面仅通过接口表示的各个组件。对于UI来说,你需要设置一套状态管理系统,该系统用来处理按钮点击、电梯到达之类的事件,然后更新状态,这可能导致按钮编号点亮或者更新屏幕。你可能还需要实现与系统交互的服务,以处理紧急呼叫或打开们的硬件。
5、安全对于乘坐电梯的人来说非常重要,因此除了单独的测试核心的业务逻辑和各个系统组件之外,你还需要进行全面的集成测试。对于电梯,将由机器人或者人进行手动测试。
进阶引申
恭喜你,你做到了最后,如果你不能完全回答上述问题也不要觉得难过,在写这篇文章的过程中,我也做了很多搜索。
把这看成一个起点,记下你觉得薄弱的地方,然后在这些方面进行更多的研究。阅读Flutter文档和Dart指南会教会你很多东西。
如果你想学习跟多关于Dart的内容,关注我们的视频课程Dart基础,我们也会在raywenderlich.com网站上持续更新所有关于Flutter的新内容。
假如你有更多面试问题的建议,更好的答案,甚至是代码挑战,请把它们写在下面的评论区。