给 iOS 开发者的 Flutter 指南

这篇文章是为那些想将已有的 iOS 开发经验运用到 Flutter 开发中的 iOS 开发者所作。如果你理解 iOS framework 的基本原理,那么你可以将这篇文章作为学习 Flutter 开发的起点。

在开始本文档之前,建议先浏览一下这个 15 分钟的视频,了解一下 Cupertino package 是什么吧:

在使用 Flutter 开发时,你 iOS 方面的知识和技能都很是非常有用的,因为 Flutter 中很多的功能和配置都依赖于移动端操作系统。Flutter 是移动端绘制 UI 的一种新方式,而对于非 UI 层面的任务,它通过插件机制来和 iOS(Android)系统进行通信。如果你很精通 iOS 开发,使用 Flutter 并不需要完全从头开始。

Flutter 框架针对 iOS 平台做了一些适配,在 平台行为差异和适配 里可以了解更多。

你同样可以将这篇文章当作一份手册查看,以便查找并解决你所遇到的问题。

视图

UIView 相当于 Flutter 中的什么?

这里有一份关于响应式编程,或者说_声明式编程_和传统的命令式编程有什么不同之处的文章,你可以浏览 声明式 UI 介绍

在 iOS 中,你在 UI 中创建的大部分视图都是 UIView 的实例。而在构造布局时,这些视图也可以作为其他视图的容器。

在 Flutter 中,同 UIView 能够进行类比的就是 Widget 了。但 Widget 和 iOS 里的视图并不能同等对待,不过当你想要了解 Flutter 的工作原理时,你可以把它理解为“声明和构造 UI 的方法”。

然而,Widget 和 UIView 还是有着相当一部分区别的。首先,widget 拥有着不同的生命周期:整个生命周期内它是不可变的,且只能够存活到被修改的时候。一旦 widget 实例或者它的状态发生了改变, Flutter 框架就会创建一个新的由 Widget 实例构造而成的树状结构。而在 iOS 里,修改一个视图并不会导致它重新创建实例,它作为一个可变对象,只会绘制一次,只有在发生 setNeedsDisplay() 调用之后才会发生重绘。

还有,和 UIView 不同,Flutter 的 widget 是很轻量的,一部分原因就是源于它的不可变特性。因为它并不是视图,也不直接绘制任何内容,而是作为对 UI 及其特性的一种描述,而被“注入”到视图中去。

Flutter 包含了 Material Components 库。内容都是一些遵循了 Material Design 设计规范 的组件。Material Design 是一种灵活的 支持全平台 的设计体系,其中也包括了 iOS。

但是 Flutter 的灵活性和表现力使其能够适配任何的设计语言。在 iOS 中,你可以通过 Cupertino widgets 来构造类似于 Apple iOS 设计语言 的接口。

我该如何更新 widget?

在 iOS 可以直接对视图进行修改。但是在 Flutter 中,widget 都是不可变的,所以也不能够直接对其修改。所以,你必须通过修改 widget 的 state 来达到更新视图的目的。

于是,就引入了 Stateful widget 和 Stateless widget 的概念。和字面意思相同,StatelessWidget 就是一个没有绑定状态的 widget。

当某个 widget 不需要依赖任何别的初始配置来对这个 widget 进行描述时,StatelessWidgets 会是很有用的。

举个例子,在 iOS 中,你需要把 logo 当作 image 并将它放置在 UIImageView 中,如果在运行时这个 logo 不会发生变化,那么对应 Flutter 中你应该使用 StatelessWidget

但是如果你想要根据 HTTP 请求的返回结果动态的修改 UI,那么你应该使用 StatefulWidget。在 HTTP 请求结束后,通知 Flutter 更新这个 widget 的 State,然后 UI 就会得到更新。

StatefulWidget 和 StatelessWidget 最重要的区别就是,StatefulWidget 中有一个 State对象,它用来存储一些状态的信息,并在整个生命周期内保持不变。

如果你对此还存有疑虑,记住一点:如果一个 widget 在 build 方法之外(比如运行时下发生用户点击事件)被修改,那么就应该是有状态的。如果一个 widget 一旦生成就不再发生改变,那么它就是无状态的。然而,即使一个 widget 是有状态的,如果不是自身直接响应修改(或别的输入),那么他的父容器也可以是无状态的。

下面是如何使用 StatelessWidget 的示例。Text 是一个常用的 StatelessWidget。如果你看了 Text 的源代码,就会发现它继承于 StatelessWidget

content_copy

Text(

‘I like Flutter!’,

style: TextStyle(fontWeight: FontWeight.bold),

);

看了上面的代码,你会注意到 Text 没有携带任何状态。它只会渲染初始化时传进来的内容。

然而,如果你想要动态地修改文本为 “I Like Flutter”,比如说在点击一个 FloatingActionButton 时该怎么做呢?

想要实现这个需求,只需要把 Text 放到 StatefulWidget 中,并在用户点击按钮时更新它即可。

下面是示例代码:

content_copy

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

// Default placeholder text

String textToShow = “I Like Flutter”;

void _updateText() {

setState(() {

// update the text

textToShow = “Flutter is Awesome!”;

});

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: Center(child: Text(textToShow)),

floatingActionButton: FloatingActionButton(

onPressed: _updateText,

tooltip: ‘Update Text’,

child: Icon(Icons.update),

),

);

}

}

如何对 widget 做布局?Storyboard 哪去了?

在 iOS 开发中,你可能会经常使用 Storyboard 来组织你的视图,并直接通过 Storyboard 或者在 ViewController 中通过代码来设置约束。而在 Flutter 中,你要通过代码来对 widget 进行组织来形成一个 widget 树状结构。

下面的例子展示了如何展示一个带有 padding 的 widget:

content_copy

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: Center(

child: CupertinoButton(

onPressed: () {

setState(() { _pressedCount += 1; });

},

child: Text(‘Hello’),

padding: EdgeInsets.only(left: 10.0, right: 10.0),

),

),

);

}

你可以为任何 widget 添加 padding,来达到类似在 iOS 中视图约束的作用。

你可以在 widget 目录 中查看 Flutter 提供的所有 widget 布局方法。

如何增加或者移除一个组件?

在 iOS 中,你可以通过调用父视图的 addSubview() 方法或者 removeFromSuperview() 方法来动态的添加或移除视图。在 Flutter 中,因为 widget 是不可变的,所以没有提供直接同 addSubview() 作用相同的方法。但是你可以通过向父视图传递一个返回值是 widget 的方法,并通过一个 boolean flag 来控制子视图的存在。

下面的例子中像你展示了如何让用户通过点击 FloatingActionButton 按钮来达到在两个 widget 中切换的目的。

content_copy

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

// Default value for toggle

bool toggle = true;

void _toggle() {

setState(() {

toggle = !toggle;

});

}

_getToggleChild() {

if (toggle) {

return Text(‘Toggle One’);

} else {

return CupertinoButton(

onPressed: () {},

child: Text(‘Toggle Two’),

);

}

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: Center(

child: _getToggleChild(),

),

floatingActionButton: FloatingActionButton(

onPressed: _toggle,

tooltip: ‘Update Text’,

child: Icon(Icons.update),

),

);

}

}

如何添加动画?

在 iOS 里,你可以使用调用视图的 animate(withDuration:animations:) 方法来创建动画。在 Flutter 里,通过使用动画库将 widget 封装到 animated widget 中来实现带动画效果。

在 Flutter 里,使用 AnimationController,它是一个可以暂停、查找、停止和反转动画的 Animation<double> 类型。它需要一个 Ticker,在屏幕刷新时发出信号量,并在运行时对每一帧都产生一个 0~1 的线性差值。然后你可以创建一个或多个 Animation,并把它们添加到控制器中。

比如,你可以使用 CurvedAnimation 来实现一个曲线翻页动画。这种情况下,控制器就是动画进度的主要数据源,而 CurvedAnimation 计算曲线并替换控制器的默认线性运动。和 widget 一样,在 Flutter 里动画也可以复合嵌套。

当构建一个 widget 树时,可以将 Animation 赋值给 widget 用户表现动画能力的属性,比如 FadeTransition 的 opacity 属性,然后告诉控制器启动动画。

下面的示例描述了当你点击 FloatingActionButton 时,如何实现一个视图渐淡出成 logo 的 FadeTransition 效果。

content_copy

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Fade Demo’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: MyFadeTest(title: ‘Fade Demo’),

);

}

}

class MyFadeTest extends StatefulWidget {

MyFadeTest({Key key, this.title}) : super(key: key);

final String title;

@override

_MyFadeTest createState() => _MyFadeTest();

}

class _MyFadeTest extends State with TickerProviderStateMixin {

AnimationController controller;

CurvedAnimation curve;

@override

void initState() {

controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);

curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(widget.title),

),

body: Center(

child: Container(

child: FadeTransition(

opacity: curve,

child: FlutterLogo(

size: 100.0,

)

)

)

),

floatingActionButton: FloatingActionButton(

tooltip: ‘Fade’,

child: Icon(Icons.brush),

onPressed: () {

controller.forward();

},

),

);

}

@override

dispose() {

controller.dispose();

super.dispose();

}

}

关于更多的内容,可以查看 Animation 和 Motion widgets, Animations 教程,以及 Animations 概览

如何渲染到屏幕上?

在 iOS 里,可以使用 CoreGraphics 绘制线条和图形到屏幕上。 Flutter 里有一套基于 Cavans实现的 API,有两个类可以帮助你进行绘制: CustomPaint 和 CustomPainter,后者实现了绘制图形到 canvas 的算法。

想要学习在 Flutter 里如何实现一个画笔,可以查看 Collin 在 [StackOverflow][] 里的回答。

content_copy

class SignaturePainter extends CustomPainter {

SignaturePainter(this.points);

final List points;

void paint(Canvas canvas, Size size) {

var paint = Paint()

…color = Colors.black

…strokeCap = StrokeCap.round

…strokeWidth = 5.0;

for (int i = 0; i < points.length - 1; i++) {

if (points[i] != null && points[i + 1] != null)

canvas.drawLine(points[i], points[i + 1], paint);

}

}

bool shouldRepaint(SignaturePainter other) => other.points != points;

}

class Signature extends StatefulWidget {

SignatureState createState() => SignatureState();

}

class SignatureState extends State {

List _points = [];

Widget build(BuildContext context) {

return GestureDetector(

onPanUpdate: (DragUpdateDetails details) {

setState(() {

RenderBox referenceBox = context.findRenderObject();

Offset localPosition =

referenceBox.globalToLocal(details.globalPosition);

_points = List.from(_points)…add(localPosition);

});

},

onPanEnd: (DragEndDetails details) => _points.add(null),

child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),

);

}

}

如何设置视图 widget 的透明度?

在 iOS 里,视图都有一个 opacity 或者 alpha 属性。而在 Flutter 里,大部分时候你都需要封装 widget 到一个 Opacity widget 中来实现这一功能。

如何构建自定义 widget?

在 iOS 里,你可以直接继承 UIView 或者使用已经存在的视图,然后重写并实现对应的方法来达到想要的效果。在 Flutter 里,构建自定义 widget 需要通过 组合 一些小的 widget(而不是对它们进行扩展)来实现。

例如,应该如何构建一个初始方法中就包含文本标签的 CustomButton?需要创建一个合成一个 RaisedButton 和一个文本标签的 CustomButton,而不是继承 RaisedButton

content_copy

class CustomButton extends StatelessWidget {

final String label;

CustomButton(this.label);

@override

Widget build(BuildContext context) {

return RaisedButton(onPressed: () {}, child: Text(label));

}

}

与其他 Flutter widget 一样的用法,下面我们使用 CustomButton

content_copy

@override

Widget build(BuildContext context) {

return Center(

child: CustomButton(“Hello”),

);

}

导航

如何在两个页面之间切换?

在 iOS 里,想要在多个 viewcontroller 中切换,可以使用 UINavigationController 管理 viewcontroller 构成的栈进行显示。

Flutter 中也有类似的实现,使用 Navigator 和 Routes。一个 Route 是应用中屏幕或者页面的抽象概念,而一个 Navigator 是管多个 Route 的 widget。也可以理解把 Route 理解为 UIViewController。而 Navigator 的工作方式和 iOS 里的 UINavigationController 类似,当你想要进入或退出一个新页面的时候,它也可以进行 push() 和 pop() 操作。

To navigate between pages, you have a couple options: 想要在不同页面间跳转,你有两个选择:

  • 构建由 route 名称组成的 Map

  • 直接跳转到一个 route。

下面的示例构建了一个 Map

content_copy

void main() {

runApp(CupertinoApp(

home: MyAppHome(), // becomes the route named ‘/’

routes: <String, WidgetBuilder> {

‘/a’: (BuildContext context) => MyPage(title: ‘page A’),

‘/b’: (BuildContext context) => MyPage(title: ‘page B’),

‘/c’: (BuildContext context) => MyPage(title: ‘page C’),

},

));

}

通过把 route 名称传递给 Naivgator 来实现 push 效果。

content_copy

Navigator.of(context).pushNamed(‘/b’);

Navigator 类对 Flutter 中的路由事件做处理,还可以用来获取入栈之后的路由的结果。这需要通过 push() 返回的 Future 中的 await 来实现。

例如,要打开一个“定位”页面来让用户选择他们的位置,你需要做如下事情:

content_copy

Map coordinates = await Navigator.of(context).pushNamed(‘/location’);

然后,在”定位“页面中,一旦用户选择了自己的定位,就 pop() 出栈并返回结果。

content_copy

Navigator.of(context).pop({“lat”:43.821757,“long”:-79.226392});

如何跳转到其他应用?

在 iOS 里,想要跳转到其他应用,可以使用特定的 URL scheme。对于系统级别的应用,scheme 都是取决于应用的。在 Flutter 里想要实现这个功能,需要创建原生平台的整合层,或者使用已经存在的 插件,例如 url_launcher

如何退回到 iOS 原生的 viewcontroller?

在 Dart 代码中调用 SystemNavigator.pop() 将会调用下面的 iOS 代码:

content_copy

UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;

if ([viewController isKindOfClass:[UINavigationController class]]) {

[((UINavigationController*)viewController) popViewControllerAnimated:NO];

}

如果这不是你需要的功能,你可以创建你自己的 平台通道 来调用对应的 iOS 代码。

线程和异步


如何编写异步代码?

Dart 是单线程执行模型,支持 Isolate(一种在其他线程运行 Dart 代码的方法)、事件循环和异步编程。除非生成了 Isolate,否则所有 Dart 代码将永远在主 UI 线程运行,并由事件循环驱动。Flutter 中的事件循环类似于 iOS 中的 main loop—,也就是主线程上的 Looper

Dart 的单线程模型并不意味着你需要以阻塞 UI 的形式来执行代码,相反,你更应该使用 Dart 语言提供的异步功能,比如使用 async/awati 来实现异步操作。

例如,你可以使用 async/await 来执行网络代码以避免 UI 挂起,让 Dart 来完成这个繁重的任务:

content_copy

loadData() async {

String dataURL = “https://jsonplaceholder.typicode.com/posts”;

http.Response response = await http.get(dataURL);

setState(() {

widgets = json.decode(response.body);

});

}

一旦 await 等待的网络操作结束,通过调用 setState() 来更新 UI,这将会触发 widget 子树的重新构建并更新数据。

下面的示例展示了如何异步加载数据,并在 ListView 中展示出来:

content_copy

import ‘dart:convert’;

import ‘package:flutter/material.dart’;

import ‘package:http/http.dart’ as http;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

List widgets = [];

@override

void initState() {

super.initState();

loadData();

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: ListView.builder(

itemCount: widgets.length,

itemBuilder: (BuildContext context, int position) {

return getRow(position);

}));

}

Widget getRow(int i) {

return Padding(

padding: EdgeInsets.all(10.0),

child: Text(“Row ${widgets[i][“title”]}”)

);

}

loadData() async {

String dataURL = “https://jsonplaceholder.typicode.com/posts”;

http.Response response = await http.get(dataURL);

setState(() {

widgets = json.decode(response.body);

});

}

}

更多关于在后台执行任务的信息,以及 Flutter 和 iOS 的区别,可以参考下一章节。

如何让你的任务在后台线程执行?

由于 Flutter 是单线程模型,而且执行着一个 event loop(就像 Node.js),你不需要为线程管理或是开启后台线程操心。如果你在处理 I/O 操作,例如磁盘访问或网络请求,那么你安全地使用 async/await 就可以了。但是,如果你需要大量的计算来让 CPU 保持忙碌状态,你需要使用 Isolate 来防治阻塞 event loop。

对于 I/O 操作,把方法声明为 async 方法,然后通过 await 来等待异步方法的执行完成:

content_copy

loadData() async {

String dataURL = “https://jsonplaceholder.typicode.com/posts”;

http.Response response = await http.get(dataURL);

setState(() {

widgets = json.decode(response.body);

});

}

这就是处理网络或数据库请求等 I/O 操作的经典做法。

然而,有时候你需要处理大量的数据,从而导致 UI 挂起。在 Flutter 里,当处理长期运行或者运算密集的任务时,可以使用 Isolate 来发挥出多核 CPU 的优势。

Isolates 是相互隔离的执行线程,并不和主线程共享内存。这意味着你不能够访问主线程的变量,也不能使用 setState() 来更新 UI。Isolates 正如起字面意思是不能共享内存(例如静态变量表)的。

下面的例子展示了在一个简单的 isolate 中,如何把数据推到主线程上用来更新 UI。

content_copy

loadData() async {

ReceivePort receivePort = ReceivePort();

await Isolate.spawn(dataLoader, receivePort.sendPort);

// The ‘echo’ isolate sends its SendPort as the first message

SendPort sendPort = await receivePort.first;

List msg = await sendReceive(sendPort, “https://jsonplaceholder.typicode.com/posts”);

setState(() {

widgets = msg;

});

}

// The entry point for the isolate

static dataLoader(SendPort sendPort) async {

// Open the ReceivePort for incoming messages.

ReceivePort port = ReceivePort();

// Notify any other isolates what port this isolate listens to.

sendPort.send(port.sendPort);

await for (var msg in port) {

String data = msg[0];

SendPort replyTo = msg[1];

String dataURL = data;

http.Response response = await http.get(dataURL);

// Lots of JSON to parse

replyTo.send(json.decode(response.body));

}

}

Future sendReceive(SendPort port, msg) {

ReceivePort response = ReceivePort();

port.send([msg, response.sendPort]);

return response.first;

}

在这里,dataLoader 就是运行在独立线程上的 Isolate。在 Isolate 中,你可以处理 CPU 密集型任务(如解析一个庞大的 JSON 文件),或者处理复杂的数学运算,比如加密操作或者信号处理等。

下面是一个完整示例:

content_copy

import ‘dart:convert’;

import ‘package:flutter/material.dart’;

import ‘package:http/http.dart’ as http;

import ‘dart:async’;

import ‘dart:isolate’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

List widgets = [];

@override

void initState() {

super.initState();

loadData();

}

showLoadingDialog() {

if (widgets.length == 0) {

return true;

}

return false;

}

getBody() {

if (showLoadingDialog()) {

return getProgressDialog();

} else {

return getListView();

}

}

getProgressDialog() {

return Center(child: CircularProgressIndicator());

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: getBody());

}

ListView getListView() => ListView.builder(

itemCount: widgets.length,

itemBuilder: (BuildContext context, int position) {

return getRow(position);

});

Widget getRow(int i) {

return Padding(padding: EdgeInsets.all(10.0), child: Text(“Row ${widgets[i][“title”]}”));

}

loadData() async {

ReceivePort receivePort = ReceivePort();

await Isolate.spawn(dataLoader, receivePort.sendPort);

// The ‘echo’ isolate sends its SendPort as the first message

SendPort sendPort = await receivePort.first;

List msg = await sendReceive(sendPort, “https://jsonplaceholder.typicode.com/posts”);

setState(() {

widgets = msg;

});

}

// the entry point for the isolate

static dataLoader(SendPort sendPort) async {

// Open the ReceivePort for incoming messages.

ReceivePort port = ReceivePort();

// Notify any other isolates what port this isolate listens to.

sendPort.send(port.sendPort);

await for (var msg in port) {

String data = msg[0];

SendPort replyTo = msg[1];

String dataURL = data;

http.Response response = await http.get(dataURL);

// Lots of JSON to parse

replyTo.send(json.decode(response.body));

}

}

Future sendReceive(SendPort port, msg) {

ReceivePort response = ReceivePort();

port.send([msg, response.sendPort]);

return response.first;

}

}

如何发起网络请求?

在 Flutter 里,想要构造网络请求十分简单,直接使用 http 库 即可。它把你可能要实现的网络操作进行了抽象封装,让处理网络请求变得十分简单。

要使用 http 库,需要在 pubspec.yaml 中把它添加为依赖:

content_copy

dependencies:

http: ^0.11.3+16

构造网络请求,需要在 async 方法 http.get() 中调用 await

content_copy

import ‘dart:convert’;

import ‘package:flutter/material.dart’;

import ‘package:http/http.dart’ as http;

[…]

loadData() async {

String dataURL = “https://jsonplaceholder.typicode.com/posts”;

http.Response response = await http.get(dataURL);

setState(() {

widgets = json.decode(response.body);

});

}

}

展示耗时任务的进度

在 iOS 里,在后台运行耗时任务时,会使用 UIProgressView

在 Flutter 里,应该使用 ProgressIndicator。它在渲染时通过一个 boolean flag 来控制是否显示进度。在耗时任务开始前,告诉 Flutter 去更新状态,并在任务结束后隐藏。

在下面的例子中,build 函数被分为三个不同的函数。当 showLoadingDialog() 是 true 时(当 widgets.length == 0),渲染 ProgressIndicator。否则,使用网络请求返回的数据渲染 ListView

content_copy

import ‘dart:convert’;

import ‘package:flutter/material.dart’;

import ‘package:http/http.dart’ as http;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

List widgets = [];

@override

void initState() {

super.initState();

loadData();

}

showLoadingDialog() {

return widgets.length == 0;

}

getBody() {

if (showLoadingDialog()) {

return getProgressDialog();

} else {

return getListView();

}

}

getProgressDialog() {

return Center(child: CircularProgressIndicator());

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: getBody());

}

ListView getListView() => ListView.builder(

itemCount: widgets.length,

itemBuilder: (BuildContext context, int position) {

return getRow(position);

});

Widget getRow(int i) {

return Padding(padding: EdgeInsets.all(10.0), child: Text(“Row ${widgets[i][“title”]}”));

}

loadData() async {

String dataURL = “https://jsonplaceholder.typicode.com/posts”;

http.Response response = await http.get(dataURL);

setState(() {

widgets = json.decode(response.body);

});

}

}

工程结构,本地化,依赖和资源


如何在 Flutter 中引入图片资源?如何处理多分辨率?

在 iOS 里,图片和其他资源会被视为不同的资源分别处理,而在 Flutter 中只有资源这一个概念。 iOS 里被放置在 Images.xcasset 文件夹的资源在 Flutter 中都被放置到了 assets 文件夹中。和 iOS 一样,assets 中可以放置任意类型的文件,而不仅仅是图片。例如,你可以把一个 JSON 文件放置到 my-assets 文件夹中。

content_copy

my-assets/data.json

在 pubspec.yaml 中声明 assets:

content_copy

assets:

  • my-assets/data.json

然后在代码中通过 AssetBundle 访问资源:

content_copy

import ‘dart:async’ show Future;

import ‘package:flutter/services.dart’ show rootBundle;

Future loadAsset() async {

return await rootBundle.loadString(‘my-assets/data.json’);

}

对于图片,Flutter 和 iOS 一样遵循了一个简单的基于屏幕密度的格式。 Image assets 可能是 1.0x2.0x3.0x 或者其他任意的倍数。而 devicePixelRatio 则表达了物理分辨率到逻辑分辨率的对照比例。

Assets 可以放在任何属性的文件夹中—Flutter 没有任何预置的文件结构。你需要在 pubspec.yaml 中声明 assets (包括路径),然后 Flutter 将会识别它们。

例如,要添加一个名为 my_icon.png 的图片到你的 Flutter 工程中,你可以把它存储在 images 文件夹下。把基础的图片(一倍图)放到 images 文件夹下,然后把其他倍数的图片放置到对应的比例下的子文件夹中。

content_copy

images/my_icon.png // Base: 1.0x image

images/2.0x/my_icon.png // 2.0x image

images/3.0x/my_icon.png // 3.0x image

接着,在 pubspec.yaml 文件中声明这些图片:

content_copy

assets:

  • images/my_icon.png

现在你可以使用 AssetImage 访问你的图片了:

content_copy

return AssetImage(“images/a_dot_burr.jpeg”);

或者直接在 Image widget 进行使用:

content_copy

@override

Widget build(BuildContext context) {

return Image.asset(“images/my_image.png”);

}

关于更多的细节,请参见文档 在 Flutter 中添加资源和图片

字符串存储在哪里?如何处理本地化?

iOS 里有 Localizable.strings 文件,而 Flutter 则不同,目前并没有关于字符串的处理系统。目前,最佳的方案就是在静态区声明你的文本,然后进行访问。例如:

content_copy

class Strings {

static String welcomeMessage = “Welcome To Flutter”;

}

你可以这样访问字符串:

content_copy

Text(Strings.welcomeMessage)

默认情况下,Flutter 只支持美式英语的本地化字符串。如果你需要添加其他语言支持,请引入 flutter_localizations 库。同时你可能还需要添加 intl 库来使用 i10n 机制,比如日期 / 时间的格式化等。

content_copy

dependencies:

flutter_localizations:

sdk: flutter

intl: “^0.15.6”

To use the flutter_localizations package, specify the localizationsDelegates and supportedLocales on the app widget:

content_copy

import ‘package:flutter_localizations/flutter_localizations.dart’;

MaterialApp(

localizationsDelegates: [

// Add app-specific localization delegate[s] here

GlobalMaterialLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

supportedLocales: [

const Locale(‘en’, ‘US’), // English

const Locale(‘he’, ‘IL’), // Hebrew

// … other locales the app supports

],

// …

)

supportedLocales 指定了应用支持的语言,而这些 delegates 则包含了实际的本地化内容。上面的示例使用了一个 MaterialApp,所以它既使用了处理基础 widget 本地化的 GlobalWidgetsLocalizations,也使用了处理 Material widget 本地化的 MaterialWidgetsLocalizations。如果你在应用中使用的是 WidgetApp,就不需要后者了。注意,这两个 delegates 虽然都包含了“默认”值,但是如果你想要实现本地化,就必须在本地提供一个或多个 delegates 的实现副本。

当初始化的时候,WidgetsApp(或 MaterialApp)会根据你提供的 delegates 创建一个 Localizations widget。 Localizations widget 可以随时从当前上下文中中获取设备所用的语言,也可以使用 Window.locale

要使用本地化资源,使用 Localizations.of() 方法可以访问提供代理的特定本地化类。使用 intl_translation 库解压翻译的副本到 arb 文件,然后在应用中通过 intl 来引用它们。

关于 Flutter 中国际化和本地化的细节内容,请参看 Flutter 应用里的国际化,里面包含有使用和不使用 intl 库的示例代码。

注意在 Flutter 1.0 beta 2 之前,在 Flutter 里定义的资源是不能被原生代码访问的,反之亦然,而原生的资源也是不能在 Flutter 中使用,因为它们都被放在了独立的文件夹中。

CocoaPods 相当于 Flutter 中的什么?如何添加依赖?

在 iOS 里,可以通过 Podfile 添加依赖。而 Flutter 使用 Dart 构建系统和 Pub 包管理器来处理依赖。这些工具将原生应用的打包任务分发给相应 Android 或 iOS 构建系统。

如果你的 Flutter 项目 iOS 文件夹中存在 Podfile,那么请仅在里面添加原生平台的依赖。总而言之,在 Flutter 中使用 pubspec.yaml 来声明外部依赖。你可以通过 pub.dev 来查找一些优秀的 Flutter 第三方包。

ViewControllers


ViewControllers 相当于 Flutter 中的什么?

在 iOS 里,一个 ViewController 是用户界面的一部分,通常是作为屏幕或者其中的一部分来使用。这些组合在一起构成了复杂的用户界面,并以此对应用的 UI 做不断的扩充。在 Flutter 中,这一任务又落到了 Widget 这里。就像在导航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因为“万物皆 widget!”。使用 Naivgator 在不同的 Route 之间切换,而不同的路由则代表了不同的屏幕或页面,或是不同的状态,也可能是渲染相同的数据。

如何监听 iOS 中的生命周期?

在 iOS 里,可以重写 ViewController 的方法来捕获自身的生命周期,或者在 AppDelegate中注册生命周期的回调。Flutter 中则没有这两个概念,但是你可以通过在 WidgetsBinding的 observer 中挂钩子,也可以通过监听 didChangeAppLifecycleState() 事件,来实现相应的功能。

可监听的生命周期事件有:

  • inactive - 应用当前处于不活跃状态,不接收用户输入事件。这个事件只在 iOS 上有效,Android 中没有类似的状态。

  • paused - 应用当前处于用户不可见状态,不接收用户输入事件,但仍在后台运行。

  • resumed - 应用可见,也响应用户输入。

  • suspending - 应用被挂起,在 iOS 平台没有这一事件。

关于这些状态的更多细节,请参看 AppLifecycleStatus 文档

布局

UITableView 和 UICollectionView 相当于 Flutter 中的什么?

在 iOS 里,你可能使用 UITableView 或者 UICollectionView 来展示一个列表。而在 Flutter 里,你可以使用 ListView 来达到类似的实现。在 iOS 中,你通过 delegate 方法来确定显示的行数,相应位置的 cell,以及 cell 的尺寸。

由于 Flutter 中 widget 的不可变特性,你需要向 ListView 传递一个 widget 列表,Flutter 会确保滚动快速而流畅。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: ListView(children: _getListData()),

);

}

_getListData() {

List widgets = [];

for (int i = 0; i < 100; i++) {

widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text(“Row $i”)));

}

return widgets;

}

}

如何确定列表中被点击的元素?

在 iOS 里,可以通过 tableView:didSelectRowAtIndexPath: 代理方法来实现。而在 Flutter 里,需要通过 widget 传递进来的 touch 响应处理来实现。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: ListView(children: _getListData()),

);

}

_getListData() {

List widgets = [];

for (int i = 0; i < 100; i++) {

widgets.add(GestureDetector(

child: Padding(

padding: EdgeInsets.all(10.0),

child: Text(“Row $i”),

),

onTap: () {

print(‘row tapped’);

},

));

}

return widgets;

}

}

如何动态更新 ListView

在 iOS 里,可以更新列表的数据,然后通过调用 reloadData 方法来通知 tableView 或者 collectionView。

在 Flutter 里,如果你在 setState() 中更新了 widget 列表,你会发现展示的数据并不会立刻更新。这是因为当 setState() 被调用时,Flutter 的渲染引擎回去检索 widget 树是否有改变。当它获取到 ListView,会进行 == 判断,然后发现两个 ListView 是相等的。没发现有改变,所以也就不会进行更新。

一个更新 ListView 的简单方法就是,在 setState() 创建一个新的 List,然后拷贝旧列表中的所有数据到新列表。这样虽然简单,但是像下面示例一样数据量很大时,并不推荐这样做。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

List widgets = [];

@override

void initState() {

super.initState();

for (int i = 0; i < 100; i++) {

widgets.add(getRow(i));

}

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: ListView(children: widgets),

);

}

Widget getRow(int i) {

return GestureDetector(

child: Padding(

padding: EdgeInsets.all(10.0),

child: Text(“Row $i”),

),

onTap: () {

setState(() {

widgets = List.from(widgets);

widgets.add(getRow(widgets.length + 1));

print(‘row $i’);

});

},

);

}

}

一个推荐的、高效且有效的方法就是使用 ListView.Builder 来构建列表。当你的数据量很大,且需要构建动态列表时,这个方法会非常好用。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

image

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
s.all(10.0),

child: Text(“Row $i”),

),

onTap: () {

print(‘row tapped’);

},

));

}

return widgets;

}

}

如何动态更新 ListView

在 iOS 里,可以更新列表的数据,然后通过调用 reloadData 方法来通知 tableView 或者 collectionView。

在 Flutter 里,如果你在 setState() 中更新了 widget 列表,你会发现展示的数据并不会立刻更新。这是因为当 setState() 被调用时,Flutter 的渲染引擎回去检索 widget 树是否有改变。当它获取到 ListView,会进行 == 判断,然后发现两个 ListView 是相等的。没发现有改变,所以也就不会进行更新。

一个更新 ListView 的简单方法就是,在 setState() 创建一个新的 List,然后拷贝旧列表中的所有数据到新列表。这样虽然简单,但是像下面示例一样数据量很大时,并不推荐这样做。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: SampleAppPage(),

);

}

}

class SampleAppPage extends StatefulWidget {

SampleAppPage({Key key}) : super(key: key);

@override

_SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State {

List widgets = [];

@override

void initState() {

super.initState();

for (int i = 0; i < 100; i++) {

widgets.add(getRow(i));

}

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(“Sample App”),

),

body: ListView(children: widgets),

);

}

Widget getRow(int i) {

return GestureDetector(

child: Padding(

padding: EdgeInsets.all(10.0),

child: Text(“Row $i”),

),

onTap: () {

setState(() {

widgets = List.from(widgets);

widgets.add(getRow(widgets.length + 1));

print(‘row $i’);

});

},

);

}

}

一个推荐的、高效且有效的方法就是使用 ListView.Builder 来构建列表。当你的数据量很大,且需要构建动态列表时,这个方法会非常好用。

content_copy

import ‘package:flutter/material.dart’;

void main() {

runApp(SampleApp());

}

class SampleApp extends StatelessWidget {

// This widget is the root of your application.

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Sample App’,

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

[外链图片转存中…(img-ywjV4lER-1715356047896)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值