Flutter面試題帶答案

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ù)吶?


1text_overflow.png

答:

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聲明list1final聲明list2const聲明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'];

使用finalconst時(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 reloadhot 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

StatelessWidgetStatefulWidget的區(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

WidgetsAppMaterialApp的區(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í)候,紅色訂單出了異常。

3buggy_buttons.gif

下面是具體代碼,你會如何修復(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è)例子:

4trees.png

算法應(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)管理的框架。

在raywenderlich.com 網(wǎng)站上也有一些介紹BLOC和介紹Provider的文章。

問題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ū)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容