给Android工程师的Flutter入门手册(一)

前言

这是笔者作为一个Android工程师入门Flutter的学习笔记,笔者不想通过一种循规蹈矩的方式来学习:先学Dart语言,然后学习Flutter的基本使用,再到实践应用这样的步骤。这样的方式有点无趣且效率较低。

笔者觉得对于已经有Android基础的来说,通过类比Android的方式来学习Flutter,掌握核心基础概念后,直接开发实践应用,在这个过程中去学习其中的知识比如Dart语法、深入的知识点。这是笔者的一次学习尝试,并将其记录下来:

本篇是该系列的第一篇,主要内容是:

(1)视图在 Flutter 中对应什么概念?如何布局Widget?

(2)Android 中的IntentFlutter中的对应什么?

(3) Flutter中如何在页面间导航?与Activity层的数据如何传递?

(4) Flutter中如何实现网络请求和数据处理?

视图

Android 中的 View 是显示在屏幕上的一切的基础。常见的控件比如按钮、工具栏、输入框都是 View

Flutter 中有个概念叫 Widget,它是Flutter中声明和构建 UI 的方式,可以粗略对比成 Android 中的 View,但 Widget 并非完全对应于 Android中的 View,它们是有差异的:

widget有着不一样的生命周期:它们是不可变的,一旦需要变化则生命周期终止。任何时候widget 或它们的状态变化时,Flutter框架都会创建一个新的 widget 树的实例

Android中的View一般情况下只会绘制一次,除非调用 invalidate 才会重绘。

Flutterwidget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象语义的描述。

Widget状态

Android 中,你可以直接操作更新View。然而在Flutter 中,Widget 是不可变的,无法被直接更新,你需要操作 Widget的状态。有两种状态的Widget

  1. StatelessWidget(无状态): 没有状态信息的 Widget,用于描述用户界面的一部分,不依赖于除了对象中的配置信息以外的任何东西的场景,类似 Android中 一个展示图标的 ImageView,整个过程是不会变的。

  2. StatefulWidget(有状态):比如根据HTTP 请求返回的数据或者用户的交互来动态地更新界面,那么你就必须使用 StatefulWidget,并告诉 Flutter 框架Widget 的``状态(State) 更新了,以便 Flutter可以更新这个Widget`。

无状态Widget和有状态Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget 有一个跨帧存储和恢复状态数据的 State 对象。

比如 Text Widget 就是一个普通的 StatelessWidget, 它没有相关联的状态信息,只是渲染传入构造器的信息,所以内部的数据是没办法更新的。

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如果想动态更新内部的文本就需要借助StatefulWidget,将 Text Widget 嵌入一个 StatefulWidget 中,例如下面的ScaffoldStatefulWidget

class _SampleAppPageState extends State<SampleAppPage> {

  String textToShow = 'I Try Learn Flutter';

  void updateText(){
    setState(() {
      textToShow = "I Like Flutter!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(  // Scaffold 是 StatefulWidget
      appBar: AppBar(title: const Text('Sample App')),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: updateText,
        tooltip: 'Update Text',
        child: const Icon(Icons.update),
      ),
    );
  }
}

Widget布局

Android 中,通过XML 文件定义布局,但是在Flutter 中,要通过一个 widget 树来定义布局的

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Sample App'),
    ),
    body: Center(
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.only(left: 20.0, right: 30.0),
        ),
        onPressed: () {},
        child: const Text('Hello'),
      ),
    ),
  );
}

如何添加/删除一个Widget?

Android 中,你通过调用父 ViewaddChild()removeChild() 方法动态地添加或者删除子 View

Flutter 中,由于Widget 是不可变的,所以没有类似 addChild() 这样的方法。

不过,我们可以给返回一个 Widget 的父Widget 传入一个方法,并通过布尔标记值控制子Widget的创建。

举个例子:点击一个 FloatingActionButton 时在两个 widget 之间切换

class _SampleAppPageState extends State<SampleAppPage> {

  bool toggle = true;

  int a = 1;

  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return const Text("Toggle One");
    } else {
      a++;
      return ElevatedButton(onPressed: () {}, child: Text('Toggle $a'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: Center(child: _getToggleChild()),
      floatingActionButton: FloatingActionButton(
          onPressed: _toggle,
          tooltip: 'Update Widget',
          child: const Icon(Icons.update)),
    );
  }
}

Widget动画

Android既可以通过XML 文件定义动画,也可以调用View 对象的 animate() 方法。

Flutter 里,则使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。

AnimationController 是个特殊的 Animation 对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController 在给定期间内会线性生成从 0.0 到 1.0 的数字。使用 .forward() 方法启动动画。

创建 AnimationController 的同时,也赋予了一个 vsync 参数。 vsync 的存在防止后台动画消耗不必要的资源。您可以通过添加 SingleTickerProviderStateMixin 或者TickerProviderStateMixin 到类定义,将有状态的对象用作 vsync

SingleTickerProviderStateMixin只适用于单个AnimationController的情况,如需使用多个AnimationController,请使用TickerProviderStateMixin

举个例子:实现一个淡出的动画

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {

  late AnimationController controller;

  late CurvedAnimation curvedAnimation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 2000));
    curvedAnimation =
        CurvedAnimation(parent: (controller), curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: FadeTransition(
          opacity: curvedAnimation,
          child: const FlutterLogo(size: 100.0),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade Animation',
        onPressed: () {
          controller.forward();
        },
        child: const Icon(Icons.brush),
      ),
    );
  }
}

上述代码解释:

1.with作用:Dart 支持 Mixin ,而 Mixin 能够更好的解决 多继承 中容易出现的问题, 如: 方法优先顺序混乱、参数冲突、类结构变得复杂化等等。结论上简单来说,就是相同方法被覆盖了,并且 with 后面的会覆盖前面的。

2.TickerProviderStateMixin 作用:使用Animation controller时,需要在控制器初始化时传递一个vsync参数,此时需要用到TickerProvider SingleTickerProviderStateMixin只适用于单个AnimationController的情况,如需使用多个AnimationController,请使用TickerProviderStateMixin

3.CurvedAnimation: 为非线性曲线

4.override initState:覆盖此方法以执行初始化,这取决于此对象插入树中的位置(即 [context])或用于配置此对象的小部件(即 [widget])。

Canvas进行绘制

Android 中,你可以使用 CanvasDrawable 将图片和形状绘制到屏幕上。

Flutter也有一个类似于 Canvas 的 API,因为它基于相同的底层渲染引擎Skia

Flutter有两个帮助你用画布 (canvas) 进行绘制的类: CustomPaintCustomPainter,后者可以实现自定义的绘制算法。

举个例子:实现一个手写笔迹功能

/// ..的作用:级联运算符 (.. or ?..) 可以让你在同一个对象上连续调用多个对象的变量或方法。
class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset>[];

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onPanUpdate: (details) {
          setState(() {
            RenderBox? renderBox = context.findRenderObject() as RenderBox;
            Offset localPosition =
                renderBox.globalToLocal(details.globalPosition);
            _points = List.from(_points)..add(localPosition);
          });
        },
        onPanEnd: (details) => _points.add(null),
        child: CustomPaint(
            painter: SignaturePainter(_points), size: Size.infinite));
  }
}

/// 自定义绘制算法,实现手写笔迹
class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset?> points;

  @override
  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);
      }
    }
  }

  /// 如果新实例表示与旧实例不同的信息,则该方法应返回 true,否则应返回 false
  @override
  bool shouldRepaint(SignaturePainter oldDelegate) =>
      oldDelegate.points != points;
}

自定义 Widget

Android 中,一般通过继承 View 类,或者使用已有的视图类,再重载或实现以达到特定效果的方法。

Flutter中,通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。

举个例子:自定义一个带标签的按钮

通过组合 ElevatedButton 和一个标签来创建自定义按钮,而不是继承 ElevatedButton

class CustomButton extends StatelessWidget {
  final String label;

  const CustomButton(this.label, {super.key});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: Text(label),
    );
  }
}

之后就可以和其他Widget一样使用了:

@override
Widget build(BuildContext context) {
  return const Center(
    child: CustomButton('自定义Widget'),
  );
}

意图(Intent)

Android 中,Intent 主要有两个使用场景:在Activity 之间进行导航,以及组件间通信。

Flutter实际上并没有ActivityFragment 的对应概念。在Flutter 中你需要使用 NavigatorRoute 在同一个 Activity 内的不同界面间进行跳转。

Navigator和Route

Route 是应用内屏幕和页面的抽象,Navigator 是管理路径route 的工具。一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。 Navigator 可以通过对route 进行压栈和弹栈操作实现页面的跳转。

Navigator的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push方法),想要返回的时候将route 出栈 (pop方法)

Flutter 中,你有多种不同的方式在页面间导航:

  • 定义一个route 名字的 Map(MaterialApp)
  • 直接导航到一个 route(WidgetApp)

Flutter接收原生Activity的数据

Android 原生层面(在我们的 Activity 中)处理分享的文本数据,然后Flutter 再通过使用 MethodChannel 获取这个数据。

AndroidActivityconfigureFlutterEngine 里通过 call 在方法名getSharedText里处理分享的数据,再通过 result 回掉给最终结果

 class MainActivity : FlutterActivity() { 

 companion object {
     private const val CHANNEL = "app.channel.shared.data"
 }

   ... 

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine);

      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
              .setMethodCallHandler(
                      (call, result) -> {
                          if (call.method.contentEquals("getSharedText")) {
                              result.success(sharedText);  //
                              sharedText = null;
                          }
                      }
              );
  }
 }

Flutter使用一个平台通道请求数据,数据便会从原生端发送过来:


class _SampleAppPageState extends State<SampleAppPage> {
  static const platform = MethodChannel('app.channel.shared.data');
  String dataShared = 'No data';

  @override
  void initState() {
    super.initState();
    getSharedText();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }

  // 调用Activity的共享数据方法
  Future<void> getSharedText() async {
    var sharedData = await platform.invokeMethod('getSharedText');
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

遇到的问题:Unresolved reference: FlutterActivity

异步UI

Dart 有一个单线程执行的模型,Dart的单线程模型并不意味着你需要以会导致 UI 冻结的阻塞操作的方式来运行所有代码。

可以使用 Dart 语言提供的异步工具,例如 async/await 来执行异步任务,用 await 修饰的网络操作完成,再调用 setState() 更新 UI,就会触发 widget 子树的重建并更新数据。

Future<void> loadData() async {
  var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

Isolate

有时候你可能需要处理大量的数据并挂起你的 UI。在Flutter 中,可以通过使用 Isolate 来利用多核处理器的优势执行耗时或计算密集的任务。

Dart 同时也支持 Isolate (在另一个线程运行 Dart 代码的方法),它是一个事件循环和异步编程方式。除非你创建一个 Isolate,否则你的Dart 代码会运行在主 UI 线程,并被一个事件循环所驱动。Flutter 的事件循环对应于Android 里的主 Looper—即绑定到主线程上的 Looper

Isolate 之间通讯的方式:port 端口,可以很方便的实现Isolate 之间的双向通讯,原理是向对方的队列里写入任务

Future<void> loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);
  SendPort sendPort = await receivePort.first;
  List msg = await sendReceive(
    sendPort,
    'https://jsonplaceholder.typicode.com/posts',
  );

  setState(() {
    widgets = msg;
  });
}

static Future<void> dataLoader(SendPort sendPort) async {
  // 打开ReceivePort接收消息
  ReceivePort port = ReceivePort();

  // 通知任何其他这个 isolate 监听的端口。
  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(Uri.parse(dataURL));
    replyTo.send(jsonDecode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

网络数据处理

虽然http包没有 OkHttp 中的所有功能,但是它抽象了很多通常你会自己实现的网络功能,这使其本身在执行网络请求时简单易用。

使用 asyncawait 的代码是异步的,但是看起来有点像同步代码。必须在带有 async 关键字的 异步函数 中使用 await

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

尽管 async 函数可能会执行一些耗时操作,但是它并不会等待这些耗时操作完成,相反,异步函数执行时会在其遇到第一个 await 表达式时返回一个Future 对象,然后等待 await 表达式执行完毕后继续执行。 Future 对象代表一个“承诺”, await表达式会阻塞直到需要的对象返回。

数据序列化

Flutter 中基础的序列化JSON 十分容易的。Flutter 有一个内置的 dart:convert 的库,这个库包含了一个简单的 JSON 编码器和解码器。将JSON 字符串作为方法的参数,调用 jsonDecode() 方法来解码 JSON。

不过这种方式虽然简单,但不好的是,jsonDecode() 返回一个 Map<String, dynamic>,这意味着你在运行时以前都不知道值的类型。使用这个方法,你失去了大部分的静态类型语言特性:类型安全、自动补全以及最重要的编译时异常。你的代码会立即变得更加容易出错。

Flutter中也有可利用第三方库json_serializable来实现自动序列化JSON数据,可以参考JSON 和序列化数据

class _SampleAppPage extends State<SampleAppPage> {

  // TODO:可以优化,widgets弄成实体对象,通过自动序列化
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Sample App'),
        ),
        body: getBody());
  }

  Future<void> loadData() async {
    // 创建 receivePort 接受端口
    ReceivePort receivePort = ReceivePort();
    // 创建 Isolate,因为这是个异步操作,所以加上 await
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    // 创建 SendPort 发送端口
    SendPort sendPort = await receivePort.first;
    // 发送
    List msg = await sendReceive(
      sendPort,
      'https://jsonplaceholder.typicode.com/posts',
    );

    setState(() {
      widgets = msg;
    });
  }

  Future sendReceive(SendPort sendPort, address) {
    ReceivePort response = ReceivePort();
    sendPort.send([address, response.sendPort]);
    return response.first;
  }

  static Future<void> dataLoader(SendPort sendPort) async {
    ReceivePort port = ReceivePort();
    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(Uri.parse(dataURL));
      replyTo.send(jsonDecode(response.body));
      developer.log(response.body);
    }
  }

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return const Center(child: CircularProgressIndicator());
    } else {
      return ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (context, position) {
          return getRow(position);
        },
      );
    }
  }

  Widget getRow(int i) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text("Row$i: ${widgets[i]["title"]}"),
    );
  }
}

参考

源代码地址:github.com/Kingwentao/…

给 Android 开发者的 Flutter 指南

Widget 动画

Dart之Mixins的with用法

JSON 和序列化数据

作者:树獭非懒
链接:https://juejin.cn/post/7199840152217600057

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容