Flutter是一個(gè)相對新的跨平臺框架,但是它的流行度正在迅速提高。雇主也意識到單一代碼庫的好處,依托Flutter可以使他們將兩個(gè)或者三個(gè)團(tuán)隊(duì)合并成一個(gè),F(xiàn)lutter開發(fā)者的工作數(shù)量也在增加。
在這篇文章中,你將看到一系列的關(guān)于Flutter和Dart面試題的問題與答案。
如果你是正在找工作的開發(fā)者,通過下面的問題(在查看答案前,請先嘗試自行解答)可以幫助你在相關(guān)技能點(diǎn)查缺補(bǔ)漏。
如果你是潛在的雇主,瀏覽下面問題,以獲得向你的候選人提問的想法。
或者可以用來測試你的Flutter和Dart的知識掌握程度。
下面的問題分為幾個(gè)三個(gè)等級:
-
初級:適合初級Flutter開發(fā)者,已經(jīng)熟悉了基本知識,并且只做了一些示例應(yīng)用程序。 -
中級:適合對Flutter和Dart工作方式有濃厚興趣的中級開發(fā)者,您已經(jīng)閱讀了很多,并且嘗試了更多。 -
高級:適合高級開發(fā)人員,樂于探索Flutter框架和Dart語言,并且知道如何管理項(xiàng)目的人。
在每一個(gè)級別,又分為兩種類型:
-
筆試型:適合郵件或者在線編程測試,因?yàn)樗鼈兩婕暗骄帉懘a。 -
問答型:適合視頻或者面對面的交流。
初級筆試題
問題1
給定如下類
class Recipe {
int cows;
int trampolines;
Recipe(this.cows, this.trampolines);
int makeMilkshake() {
return cows + trampolines;
}
}
使用胖箭頭語法將makeMilkshake()轉(zhuǎn)換成命名為milkshake的getter語法。
答:
如果一個(gè)方法只有一行代碼,則可以通過使用
=>語法返回結(jié)果來減少代碼行數(shù)。methodName(parameters) => statement;注意當(dāng)使用
=>的時(shí)候,不需要再使用關(guān)鍵詞return.
makeMilkshake()轉(zhuǎn)換后的代碼如下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),
]);
}
}
在一些窄屏設(shè)備上,文本溢出了,你會如何修復(fù)吶?

答:
Expanded( child: Text( personNextToMe, ), ),使用
Expandedwidget來包裹Textwidget,以告知Row忽略Textwidget的固有寬度,并且根據(jù)行中剩余的空間來為其分配寬度。在
Row、Column或者Flexwidget中使用超過一個(gè)Expandendwidget時(shí),會均勻的分配剩余空間。當(dāng)有多個(gè)Expandwidget時(shí),可以使用flex屬性對優(yōu)先級進(jìn)行排序。假如你還使用了
Textwidget的overflow屬性,那就太棒了。更多的介紹,可以閱讀Flutter文檔中的布局約束部分
問題3
重構(gòu)下面代碼,以便Row顯示寬度太窄無法容納它們時(shí),子節(jié)點(diǎn)自動(dòng)換行到下一行展示。
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以了解更多關(guān)于Wrapwidget。
問題4
如下代碼,使用var聲明list1,final聲明list2,const聲明list3,這些關(guān)鍵字的不同之處是什么,最后兩行代碼能夠編譯嗎?
var list1 = ['I', '??', 'Flutter'];
final list2 = list1;
list2[2] = 'Dart'; // Will this line compile?
const list3 = list1; // Will this line compile?
答:
當(dāng)使用
var關(guān)鍵詞時(shí),數(shù)據(jù)的類型是推斷出來的,并且值可以改變。除了顯示的聲明了數(shù)據(jù)類型之外,下面的代碼和上面第一行代碼是等效的:List<String> list1 = ['I', '??', 'Flutter'];使用
final和const時(shí),你不能在初始值分配后重新分配新值。final修飾的變量在運(yùn)行時(shí)分配一次,const修飾的變量,在運(yùn)行程序之前的編譯期就需要知道、設(shè)置或者硬編碼變量的值。第三行代碼可以編譯成功,因?yàn)椴]有對
list2重新賦值,只是改變了第三個(gè)位置的元素的值(請記住,下標(biāo)從0開始),默認(rèn)情況下,在Dart中List是可變的。假如你試圖按照下面的樣子做,將會編譯不通過,因?yàn)槟阍趯?code>final修飾的變量重新賦值。
list2 = ['I', '??', 'Dart'];第四行不會編譯,因?yàn)?code>list1得值并沒有確定,直到runtime時(shí)。閱讀Dartlang的文章Const, Static, Final, Oh my!以了解更多。
問題5
給定下面類
class Pizza {
String cheese = 'cheddar';
}
你如何將cheese變成私有變量,怎樣將它變成全局變量,什么時(shí)候你使用全局變量?
答:
在變量的前面添加下劃線
_,可以使它在庫中私有化。class Pizza { String _cheese = 'cheddar'; }Dart沒有類私有變量的概念。一個(gè)庫通常是一個(gè)文件,一個(gè)文件可以包含多個(gè)類。
假如你想要一個(gè)全局變量,只需要將變量移到類的外面就可以了。String cheese = 'cheddar';將其放在類的外面會使其變成頂級變量,導(dǎo)入其所在文件后,就可以在任何地方使用它了。
全局變量通常不建議使用,因?yàn)楹茈y知道是哪里修改了它們,難以追蹤修改路徑,這會使調(diào)試和測試變得困難,但是有時(shí)它們也會很有用:
- 快速搭建你并不打算長期維護(hù)的demo示例時(shí)。
- 創(chuàng)建單例以提供類似于數(shù)據(jù)庫或者網(wǎng)絡(luò)身份驗(yàn)證的服務(wù)。
- 制作
const變量以共享顏色、尺寸、樣式、主題等內(nèi)容。這些類型的全局變量通常存儲在單獨(dú)的文件中,例如Constants.dart,然后引入待庫中。閱讀Dart語言的庫和可見性文章以了解更多。
初級問答題
問題1
hot reload和hot restart的區(qū)別是什么?
答:
hot reload在立刻更新UI的同時(shí)保持程序的狀態(tài),相比之下hot restart花費(fèi)更長一點(diǎn)的時(shí)間,因?yàn)樗鼤诟耈I之前將程序的狀態(tài)置為初始狀態(tài)。兩者都比完全重新啟動(dòng)(full restart)要快,這需要重新編譯應(yīng)用程序。當(dāng)有重大的更改時(shí),你需要停止并重新運(yùn)行該程序,在極少數(shù)的情況下,你可能還需要在模擬器或者真機(jī)上刪除應(yīng)用程序,然后重新安裝。
問題2
StatelessWidget和StatefulWidget的區(qū)別是什么?
答:
StatelessWidget是一個(gè)不可變的類,充當(dāng)UI布局中某些部分的藍(lán)圖,當(dāng)某個(gè)組件在顯示期間不需要改變,或者說沒有狀態(tài)(State),你可以使用它。
StatefulWidget也是不可變的,但是它和一個(gè)State對象關(guān)聯(lián)在一起,該對象允許你每次通過調(diào)用setState()時(shí),使用新值重建這個(gè)widget,當(dāng)UI可以動(dòng)態(tài)改變時(shí)使用StatefulWidget。假如State變得越來越復(fù)雜,或者一些狀態(tài)存在于兩個(gè)不同的widget中,則應(yīng)該考慮更復(fù)雜的狀態(tài)管理方案
閱讀stateless and stateful widgets以了解更多。
問題3
WidgetsApp和MaterialApp的區(qū)別什么?
答:
WidgetsApp提供了基礎(chǔ)的導(dǎo)航能力,和widgets庫一起,它包含了很多Flutter使用的基礎(chǔ)widget。
MaterialApp和與之相應(yīng)的的material庫,是在WidgetsApp和與之相應(yīng)的widgets庫之上構(gòu)建的一層,它遵循了Material設(shè)計(jì)風(fēng)格,可以再任何平臺或者設(shè)備上為應(yīng)用程序提供統(tǒng)一的外觀,material庫提供了更多的Widget。在你的項(xiàng)目中,你并不一定要使用
MaterialApp,也可以使用CupertinoApp來構(gòu)建iOS風(fēng)格的應(yīng)用程序,這可以使iOS用戶感覺更親切,甚至你也可以自己定義一些widget。
問題4
可以嵌套使用Scaffold嗎,為什么或者為什么不?
答:
當(dāng)然可以,你絕對可以嵌套使用
Scaffold,這體現(xiàn)Flutter的美,你可以控制整個(gè)UI。
Scaffold也是個(gè)widget,因此你可以把它放在任何widget可以放置的地方。通過嵌套Scaffold,你可以對抽屜(drawers)、卡片(snack bars)、底頁(bottom sheets)進(jìn)行分層。2nested_scaffolds的副本.png
問題5
什么時(shí)候適合使用packages、plugins或者三方庫?
答:
packages和plugins可以極大的節(jié)約你的時(shí)間,當(dāng)別人已經(jīng)解決了一個(gè)復(fù)雜問題時(shí),你沒必要再解決一遍,尤其是該解決方案已經(jīng)獲得了很好的評價(jià)時(shí)。
另一方面,過度依賴三方庫也可能有一些風(fēng)險(xiǎn),他們可能編譯不過、有bug或者被丟棄,當(dāng)你需要切換到新的package或者plugin,可能會對代碼做巨大的更改。
這就是為什么需要將業(yè)務(wù)邏輯和三方庫隔離開的原因,你可以通過創(chuàng)建一個(gè)Dart的抽象類,來充當(dāng)package或者plugin的接口。一旦你設(shè)置完這種結(jié)構(gòu)后,再遇到需要切換package或者plugin情況,你所要做的就只是重寫接口層的具體實(shí)現(xiàn)了。
中級筆試題
問題1
你正在編寫一個(gè)稱之為RubberBaby的購物程序,它可以用來賣玩偶,不幸的是在訂單頁,遇到了一個(gè)問題。假如顧客下了一個(gè)藍(lán)色的訂單和一個(gè)紅色的訂單,但是當(dāng)顧客試圖刪除藍(lán)色訂單的時(shí)候,紅色訂單出了異常。

下面是具體代碼,你會如何修復(fù)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),
]);
}
}
答:
當(dāng)你使用stateful widget時(shí),當(dāng)widget樹發(fā)生了改變時(shí),框架會比較widget的類型,看看是否能夠重用。
因?yàn)閮蓚€(gè)
CounterButton是相同的類型,F(xiàn)lutter并不知道哪個(gè)widget和state進(jìn)行了綁定。這樣的結(jié)果就是紅色按鈕進(jìn)行更新時(shí),使用了藍(lán)色按鈕內(nèi)部的state。為了解決這個(gè)問題,可以為每個(gè)widget使用
key屬性,此屬性為每個(gè)widget添加了一個(gè)ID:CounterButton( key: ValueKey('red'), color: Colors.red, ),通過添加
key,你已經(jīng)唯一的標(biāo)記了紅色計(jì)數(shù)按鈕,F(xiàn)lutter將能夠保留其狀態(tài)。你可以在Medium文章Keys! What are they good for?.中了解更多關(guān)于如何使用key。
問題2
GitHub Jobs有一個(gè)開放的接口用于查詢軟件工程相關(guān)的職位,下面是接口地址,將會返回一個(gè)遠(yuǎn)程工作的崗位的列表:
https://jobs.github.com/positions.json?location=remote
下面給了一個(gè)簡單的數(shù)據(jù)模型,你只需要關(guān)心公司名字和崗位名稱,請編寫一個(gè)返回值類型是Future<List<Job>>的方法,在這個(gè)問題中你可以忽略先錯(cuò)誤檢查。
class Job {
Job(this.company, this.title);
final String company;
final String title;
}
答:
因?yàn)锳pi返回一個(gè)JSON map類型的數(shù)組,添加一個(gè)
fromJson析構(gòu)方法到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可以用來進(jìn)行網(wǎng)絡(luò)請求,Dart官方團(tuán)隊(duì)維護(hù)了基本的http庫,為了使用它,可以添加下面依賴到你的
pubspec.yaml中:dependencies: http: ^0.12.1然后使用這個(gè)庫創(chuàng)建一個(gè)方法,在后臺從GitHub拉去網(wǎng)絡(luò)數(shù)據(jù):
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變量后,創(chuàng)建了http.get請求,它將會返回一個(gè)JSON字符串。下一步。使用
json.decode,將JSON數(shù)據(jù)解析成一個(gè)map數(shù)據(jù),然后轉(zhuǎn)換成job對象的list。之前的文章Parsing JSON in Flutter,將會向你介紹如何使用web API,來進(jìn)行更高效的創(chuàng)建模型和解析JSON數(shù)據(jù)。
問題3
給定一個(gè)Dart stream 產(chǎn)出無限的字符串,這些字符串可能是salmon或者trout
final fishStream = FishHatchery().stream;
// salmon, trout, trout, salmon, ...
將這個(gè)stream進(jìn)行轉(zhuǎn)換,要求僅當(dāng)當(dāng)前五次產(chǎn)出salmon字符串時(shí),返回sushi字符串。
答:
stream轉(zhuǎn)換如下:
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團(tuán)隊(duì)的視頻Dart Streams — Flutter in Focus,和Dart的文檔Creating Streams中學(xué)到更多關(guān)于streams的內(nèi)容。
問題4
為什么下面的代碼會阻塞你的Flutter應(yīng)用程序吶?
String playHideAndSeekTheLongVersion() {
var counting = 0;
for (var i = 1; i <= 1000000000; i++) {
counting = i;
}
return '$counting! Ready or not, here I come!';
}
把它改成async異步方法會有幫助嗎?
答:
這將會阻塞你的應(yīng)用程序,因?yàn)橛?jì)算到10億,即使對計(jì)算機(jī)來說也是一個(gè)代價(jià)昂貴的任務(wù)。
Dart代碼在自己的被稱之為
isolate的內(nèi)存區(qū)域運(yùn)行,也被稱為內(nèi)存線程。每個(gè)isolate有自己的堆空間,這確保沒有isolate可以訪問到其它isolate的狀態(tài)。將這個(gè)方法改造成
async方法也無濟(jì)于事,因?yàn)樗耘f在同一個(gè)isolate上運(yùn)行,如下: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!'; }解決方案是將它運(yùn)行在不同的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線程了。
想要了解更多關(guān)于異步任務(wù)和isolate的內(nèi)容,可以看Flutter團(tuán)隊(duì)的視頻Isolates and Event Loops — Flutter in Focus和的文章Futures — Isolates — Event Loop
在下個(gè)問題中,你將會見到另一種類型的isolate。
中級問答題
問題1
什么是event loop,它和isolate的關(guān)系是什么?
答:
Dart是早期遵循社交距離的采用者,Dart代碼運(yùn)行在一個(gè)獨(dú)立的被稱之為isolate的線程上,相互隔離的的isolate不會一起出去玩,最多也就是互相發(fā)信息。用計(jì)算機(jī)術(shù)語來說就是,isolate之間不共享內(nèi)存,它們之間的通信僅通過端口(port)進(jìn)行。
每一個(gè)isolate都有一個(gè)event loop,用于管理異步任務(wù)的運(yùn)行,這些任務(wù)可能來自于兩個(gè)隊(duì)列之中:
microtask queue,或者event queue。Microtasks任務(wù)總是優(yōu)先運(yùn)行,它們主要是內(nèi)核任務(wù),開發(fā)者不必關(guān)心。調(diào)用Future時(shí)將會把任務(wù)放置到event queue中。
很多新手Dart開發(fā)者,認(rèn)為
async方法運(yùn)行在一個(gè)單獨(dú)的線中,盡管對于系統(tǒng)處理的I/O操作這樣說可能是正確的,但是它并不適用于你所寫的代碼,這就是為什么假如你有一個(gè)計(jì)算量很大的任務(wù),你需要將它運(yùn)行在一個(gè)單獨(dú)的isolate之中。如果你想了解更多關(guān)于isolate、event loop、和并發(fā)相關(guān)的內(nèi)容,可以參考Medium上的文章Dart asynchronous programming: Isolates and event loops,Futures — Isolates — Event Loops
問題2
怎么減少Widget的重新構(gòu)建?
答:
當(dāng)state發(fā)生改變時(shí),你將重新構(gòu)建widget,這種正常且理想的狀態(tài),因?yàn)樗试S用戶查看反映在UI中的狀態(tài)更改。但是重新構(gòu)建那些不需要改變的UI是性能浪費(fèi)的。
你可以采取以下措施來減少不必要的Widget重建。
- 首先要做的就是將大的Widget樹重構(gòu)成較小的單個(gè)的Widget,每一個(gè)Widget都有它自己的
build方法。- 盡可能的使用
const構(gòu)造函數(shù),這將告知Flutter不需要重建這個(gè)widget。- 使stateful widget的子樹盡可能的小,如果stateful widget有一個(gè)widget子樹,那么為這個(gè)stateful widget創(chuàng)建一個(gè)自定義widget,并為其提供一個(gè)
child參數(shù)。你可以在Flutter文檔中,閱讀更多關(guān)于性能優(yōu)化的注意事項(xiàng)
問題3
什么是BuildContext,它有什么用?
答:
BuildContext實(shí)際上是在Element樹中的Widget的元素,因此每個(gè)Widget都有其自己的BuildContext。你通常使用
BuildContext來獲取主題(theme)或者另一個(gè)Widget的引用,例如:假如你想要展示一個(gè)material dialog,那么你需要獲取scaffold的引用,可以通過Scaffold.of(context)來得到它,其中context就是上下文信息,通過of()來往上搜索樹,直到找到最近的Scaffold。閱讀didierboelens.com網(wǎng)站的文章Widget — State — Context — Inherited Widget 不僅可以了解到BuildContext,也可以了解到stateful widget的生命周期和inherited widget。
此外,我們的文章Flutter Text Rendering將會帶你窺探Flutter底層源碼,通過這篇文章,你會了解到build context、elements甚至render對象。
問題4
在Flutter應(yīng)用程序中,你怎么和native進(jìn)行交互?
答:
通常你不需要和原生進(jìn)行交互,因?yàn)镕lutter或三方插件會處理這些問題,但是,如果你發(fā)現(xiàn)確實(shí)有特殊需要訪問一些底層平臺,你可以使用平臺channel。
其中一種類型是
method channel,數(shù)據(jù)在Dart側(cè)進(jìn)行序列化,然后會將數(shù)據(jù)發(fā)送到原生側(cè),你可以在原生側(cè)編寫代碼響應(yīng)交互,然后回傳序列化后的數(shù)據(jù)。在Android側(cè)可以選用Kotlin或者Java,在iOS側(cè)可以使用Objective-C或者Swift進(jìn)行編寫。但是,在開發(fā)web的時(shí)候,你不需要使用channel,這時(shí)非必要的步驟。
第二種channel類型是
event channel,你可以用來從native發(fā)送stream數(shù)據(jù)到flutter側(cè),這對監(jiān)控傳感器數(shù)據(jù)的場景很有用。可以在Flutter的文檔platform channels中看到更詳細(xì)的介紹
問題5
你可以做哪種類型的測試?
答:
Flutter中有三種類型的測試:
unit tests、widget tests、integration tests,單元測試是關(guān)于檢查業(yè)務(wù)邏輯的有效性,widget測試確保UI Widget能夠正確的響應(yīng)你的期望,集成測試用于檢測你的APP能否整體正常運(yùn)行。還有一種測試不為大家所知,稱作
golden test,在golden test中,你有Widget或者屏幕的圖像,以查看實(shí)際展示的Widget是否和它匹配。可以通過Flutter教程了解更多關(guān)于測試的內(nèi)容,通過Medium的文章Flutter: Golden tests — compare Widgets with Snapshots了解更多golden test內(nèi)容。
raywenderlich.com網(wǎng)站上也有一篇文章介紹Flutter unit testing
高級筆試題
問題1
遵從以下要求,演示Dart isolate通過port的交互過程:
1、將downloadAndCompressTheInternet()函數(shù)發(fā)送到新的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、創(chuàng)建了一個(gè)端口用于從接收新的isolate中接收數(shù)據(jù)。
2、創(chuàng)建一個(gè)新的isolate,給他一些工作去做,并且提供一個(gè)方式回傳數(shù)據(jù)。
3、監(jiān)聽新isolate中發(fā)送的任何數(shù)據(jù),然后關(guān)閉這個(gè)isolate。
4、使用main isolate正在監(jiān)聽的端口,將數(shù)據(jù)發(fā)送回來。下載和解壓算法仍在開發(fā)中...
閱讀Joe的文章Dart Fundamentals — Isolates,以了解更多關(guān)于isolate通信的知識。
問題2
有兩個(gè)樹數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù),其中樹的節(jié)點(diǎn)都是隨機(jī)整數(shù),這些數(shù)字不一定是唯一的,也不一定是有序存儲的,兩棵樹的深度也是任意的。編寫一個(gè)算法以識別出在第一顆樹中但是不在第二棵樹中的數(shù)字。
下面是一個(gè)例子:

算法應(yīng)該識別出,數(shù)字1在第一棵樹中,并且不在第二棵樹中。
答:
首先定義出輸?shù)墓?jié)點(diǎn):
class Node { int data; List<Node> children; Node(this.data, {this.children}); }編寫邏輯,遞歸查找樹,對整數(shù)進(jìn)行去重。
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); } } }設(shè)置測試數(shù)據(jù)
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中的數(shù)據(jù):
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 }得到的結(jié)果是
1
高級問答題
問題1
不同狀態(tài)管理框架的優(yōu)缺點(diǎn)是什么?
答:
有多種多樣的框架,其中一些比較知名狀態(tài)管理框架,包括BLOC、伴隨ChangeNotifier的Provider、Redux、MobX以及RxDart。這些都適用于中大型的應(yīng)用程序。如果你只是快速開發(fā)一個(gè)小demo,那么stateful widget通常就足夠了。
與其列出不同狀態(tài)管理框架的優(yōu)缺點(diǎn),不如查看這些框架更適用哪種場景。例如,對于某些人與其淹沒在不勝枚舉的選擇中,不如選擇一種比較容易掌握的方案,Provider和MobX都是不錯(cuò)的選擇,它們可以直接在state類上調(diào)用方法以響應(yīng)事件,使得這種場景更加直觀。
假如你重度依賴流,例如使用Firebase的ApI,那么自然會選擇給予數(shù)據(jù)流的解決方案,比如BLOC和RxDart。
假如你需要撤銷/重做功能,那么你需要類似BLOC或者Redux這樣,能夠很好的處理不可變狀態(tài)的解決方案。
最后,更多的是歸結(jié)于個(gè)人喜好,你可以在Flutter官方的文章list of state management approaches中找到更多流行的的關(guān)于狀態(tài)管理的框架。
問題2
如何設(shè)計(jì)一個(gè)控制電梯的應(yīng)用程序?
答:
這個(gè)問題測試您的分析技能,以及組織和使用SOLID原則的能力。
下面是一個(gè)參考答案。
1、首先,確定核心的功能是什么:開門、關(guān)門,向上向下移動(dòng)到不同樓層,尋求幫助,與其他電梯進(jìn)行協(xié)調(diào)。這時(shí)您的業(yè)務(wù)邏輯,畫一個(gè)流程圖可能會更有幫助。2、以測試驅(qū)動(dòng)(TDD)的方式去實(shí)現(xiàn)商業(yè)邏輯。也就是說,編寫一個(gè)失敗的測試用例,編寫足夠的以使它通過的業(yè)務(wù)邏輯代碼,進(jìn)行重構(gòu),然后編寫另一個(gè)測試用例,再次進(jìn)行所有操作。
3、剛開始時(shí),有沒有物理按鈕或者Flutter驅(qū)動(dòng)的觸摸屏都沒關(guān)系,電梯的外觀或者位置在哪無關(guān)緊要,緊急呼叫系統(tǒng)是什么也不重要,你可以在開發(fā)測試階段,將這些外部因素抽象到模擬的接口后面。
4、一旦完成了核心邏輯,你便可以實(shí)現(xiàn)前面僅通過接口表示的各個(gè)組件。對于UI來說,你需要設(shè)置一套狀態(tài)管理系統(tǒng),該系統(tǒng)用來處理按鈕點(diǎn)擊、電梯到達(dá)之類的事件,然后更新狀態(tài),這可能導(dǎo)致按鈕編號點(diǎn)亮或者更新屏幕。你可能還需要實(shí)現(xiàn)與系統(tǒng)交互的服務(wù),以處理緊急呼叫或打開們的硬件。
5、安全對于乘坐電梯的人來說非常重要,因此除了單獨(dú)的測試核心的業(yè)務(wù)邏輯和各個(gè)系統(tǒng)組件之外,你還需要進(jìn)行全面的集成測試。對于電梯,將由機(jī)器人或者人進(jìn)行手動(dòng)測試。
進(jìn)階引申
恭喜你,你做到了最后,如果你不能完全回答上述問題也不要覺得難過,在寫這篇文章的過程中,我也做了很多搜索。
把這看成一個(gè)起點(diǎn),記下你覺得薄弱的地方,然后在這些方面進(jìn)行更多的研究。閱讀Flutter文檔和Dart指南會教會你很多東西。
如果你想學(xué)習(xí)跟多關(guān)于Dart的內(nèi)容,關(guān)注我們的視頻課程Dart基礎(chǔ),我們也會在raywenderlich.com網(wǎng)站上持續(xù)更新所有關(guān)于Flutter的新內(nèi)容。
假如你有更多面試問題的建議,更好的答案,甚至是代碼挑戰(zhàn),請把它們寫在下面的評論區(qū)。
