前语

这是笔者作为一个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,整个过程是不会变的。
  1. 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 和序列化数据