为什么要跨平台?
以前我们将开发人员简单粗暴的分成前端后端,但现在不行了,由于种种原因,越来越多的程序员被逼成全栈,啥都得会、啥都得干。
其实软件开发领域细分工种很多,就以前端为例,不仅包含通常意义上的Web端,其实还有移动端(安卓、ios、鸿蒙等)、桌面端、嵌入式前端、小程序等等。要命的是,它们各自的技术栈都不一样,一个人几乎不可能掌握所有的开发技术。想起来一个经典段子,“你是学计算机的,来帮我修修电脑吧!”
若干年以前,为了做一款手机App,我们部门配备了一个安卓开发以及ios开发,如果是小型团队,人手不足,通常是从Web端做起,然后用H5技术打包成各个平台运行的程序,比如移动端的WebView嵌套,桌面端的Electron,这种开发方式也是存在诸多问题。
然后就出现了跨平台技术,统一技术栈,真正的一套代码多端运行。Flutter是热度较高,成熟度也较好的框架,除此之外还有Qt、React Native,以及Jetpack Compose在跨平台方面的尝试等等,当然,各自都有适用场景和优缺点。今天我们先简单认识一下Flutter。
认识Flutter
Flutter是Google开源的应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用。支持移动、Web、桌面和嵌入式端,对于小程序,也可以通过插件进行转换,基本涵盖了所有涉及UI的平台。
Flutter框架自下而上分为Embedder、Engine和Framework三层。
- Embedder:操作系统适配层,实现了渲染Surface设置,线程设置,以及平台插件等平台相关特性的适配;
- Engine:该层负责图形绘制、文字排版和提供Dart运行时,Engine层具有独立虚拟机,正是由于它的存在,Flutter程序才能运行在不同的平台上,实现跨平台运行;
- Framework:使用Dart编写的一套基础视图库,包含了动画、图形绘制和手势识别等功能,是开发者主要接触的部分。
核心特性
跨平台一致性:
- 采用 Dart 语言编写,通过 Skia 图形引擎直接渲染 UI;
- 不使用系统原生控件,而是完全自己绘制 UI,因此在不同平台上能保持一致的 UI 外观和行为;
高性能:

- 采用AOT(Ahead-Of-Time)编译技术,直接编译为原生机器代码。渲染性能接近原生应用;
- 基于事件队列的非阻塞IO模型,配合async/await语法简化异步编程,使用Isolate实现多线程计算;
热重载(Hot Reload):
- 开发时保存代码更改后,1秒内即可看到效果;
- 相比原生开发需要重新编译安装,效率提升显著;
丰富的组件库:
- 提供300+预构建组件(Widgets);
- 包含Material Design(Android风格)和Cupertino(iOS风格)两套组件;
- 支持自定义主题和样式扩展;
核心概念
基础概念
- Statefulwidget:有状态的Widget,可以根据用户交互或其他因素而改变。
- StatelessWidget:无状态的Widget,一旦创建就不会改变,适用于静态内容展示,
- Dart语言:Flutter使用Dart语言进行开发,它具有强类型和面向对象的特性。
- MaterialApp:基于Material Design的Flutter应用的顶层Widget,包含整个应用的基本配置。
- Scaffold:提供应用程序的基本结构,例如AppBar、Drawer和BottomNavigationBar。
布局与结构
- Container:用于布局和装饰的基本组件,可以包含其他Widget。
- Column:垂直排列的Widget,用于构建垂直布局。
- Row:水平排列的Widget,用于构建水平布局。
- ListView:可滚动的列表视图,用于展示大量数据。
- AppBar:顶部应用栏,通常包含标题、图标和操作按钮。
- GestureDetector:用于处理用户手势的Widget例如点击、滑动等。
功能进阶
- Future和异步编程:Flutter中使用Future和async/await进行异步编程。
- Navigator:用于管理页面导航的Widget,包括页面的跳转和返回。
- PageRoute:用于定义页面切换的基本类,可自定义页面切换效果。
- Asset lmage:加载应用内部资源中的图片。
- Provider:状态管理库,简化数据共享和更新。
- Theme:定义应用程序的主题,包括颜色、字体等。
状态管理
状态管理是所有UI框架的核心设计之一,我们以第一个Flutter程序——计数器为例来看一下
import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue), home: MyHomePage(title: 'Flutter Demo Home Page'), ); }}class MyHomePage extends StatefulWidget { final String title; MyHomePage({Key? key, required this.title}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() => _counter++); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.title)), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text('$_counter', style: Theme.of(context).textTheme.headline4), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); }}这里包含了前面提到的很多核心概念。状态管理主要就是这个setState,它会更新关联的StatefulWidget,也就是重新执行build,这是最基本的状态管理,在例子中就表现为点击一次按钮,页面的数字就+1。
但使用不当的话,这种方式容易失控。setState会触发整个Widget子树重建,且状态无法跨页面共享。靠setState堆砌代码,往往导致页面卡顿、状态混乱、跨页面传参耦合等。
因此,setState是基础,但不能过度依赖。官方推荐的Provider是更适用大型项目的状态管理方案。看我们完善过后的计数器例子
import 'package:flutter/material.dart';import 'package:provider/provider.dart';import 'package:dio/dio.dart';/// 计数器状态模型class CounterModel with ChangeNotifier { int _count = 0; int get count => _count; // 增加计数 void increment() { _count++; _notify(); } // 重置状态 void reset() { _count = 0; _notify(); } // 异步接口请求 Future<void> fetchData() async { try { final response = await Dio().get("http://test.com/api/getCount/user1"); // 请求成功,数据赋值 _count = response.data.toString(); } catch (e) { // 请求失败 } finally { _notify(); } } void _notify() { if (mounted) { notifyListeners(); } } @override void dispose() { super.dispose(); }}// 程序入口void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => CounterModel()), ], child: const MyApp(), ), );}// 页面class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("计数器")), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Consumer<CounterModel>( // 仅监听count变化 builder: (context, count, child) => Text( "计数:$count", style: const TextStyle(fontSize: 20), ), ), const SizedBox(height: 20), ElevatedButton( onPressed: () => context.read<CounterModel>().reset(), // 避坑:read不监听,仅获取模型 child: const Text("重置状态"), ), ], ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: const Icon(Icons.add), ), const SizedBox(height: 10), FloatingActionButton( onPressed: () => context.read<CounterModel>().fetchData(), child: const Icon(Icons.download), ), ], ), ); }}示例代码为了演示方便还有些粗糙,我们忽略一些细节,Provider其实就是经典的观察者模式,只会影响Consumer组件的状态,并且我们将数据和展示层分开了,如果更进一步,model中只定义数据模型,将model和view之间的通信及业务逻辑处理提取出来(Presenter),就得到了Model-View-Presenter (MVP)的开发范式。看到这里,一个能用在企业生产中的正式项目框架已初步成型。
项目结构
直接看图
pubspec.yaml就是项目的依赖管理,lib是主要写代码的目录,箭头所指的是各平台目录,这些目录是新建项目自动生成的,只需要在里面配置平台的一些编译配置即可,比如安卓的sdk等,开发基本都在lib目录下。
我们再看下lib目录里面的结构
main.dart是程序入口文件。前面例子中的main函数,基础页面框架、导航、主题等,还有一些初始化的东西,如权限申请等可以放在里面。然后可以按照模块来划分目录,比如图中的登录模块,里面包含model、page、router,还有一些公共组件widget等,以及前面提到的presenter等,可以据此来划分子目录。当然,这些没有明确的规范,这是我个人的习惯思路。
说到这里,其实Flutter的一些语法结构,包括设计思想,跟安卓原生开发的Jetpack Compose有点像,就连Dart语言跟Kotlin也有一些共通之处,不过都是Google的产品,倒也正常。
以上我们初步认识了Flutter,感兴趣或者有跨平台开发需求的可以更深入学习哈~