Flutter - UIKit开发相关指南 - 概览
环境
Flutter 3.29
macOS Sequoia 15.4.1
Xcode 16.3
概览
UIView与Widgets的比较
在UIKit使用UIView类的对象进行页面开发,布局也是UIView类的对象,在Flutter中使用的是Widget,在概念上Widget可以理解成UIView。
差异:
- 有效期: Widgets是不可变的,它的生存期只到被改变前。当Widgets或它们的状态改变了。Flutter’s 框架会创建一个widget的实例,而UIKit中的UIView是不会重新创建,它是可变的,绘制一次并且在使用setNeedDisplay()使其失效之前不会重新绘制
- 轻量: Widgets相对更轻量,一个原因是它们是不可变,而且它们不负责显示和绘制,更多的是一种语义的描述。
Flutter 包含 Matterial 组件库,其中的Widgets都符合了Material设计指引。Material设计是个适配多平台的设计系统,也支持iOS
但如果想用iOS的UI风格,可以使用Cupertino widgets libray
更新Widgets
使用UIKit开发时可以直接改变对应的视图。就像上面提到的,因为Flutter的Widgets是不可变的,可以通过更新Widget的状态来更新Widget。
这就是有状态 widget 与无状态 widget 的概念。StatelessWidget是没有附加状态的 widget。
当需要实现HTTP请求获取数据后根据数据动态改变的UI,可以使用StatefulWidget。
无状态和有状态 widget 之间的重要区别在于,StatefulWidgets 有一个 State 对象,用于存储状态数据并在树重建中传递数据,因此它不会丢失。
Text Widget是常见的无状态的Widget
class Text extends StatelessWidget {const Text(String this.data, {...
Text('I like Flutter!',style: TextStyle(fontWeight: FontWeight.bold),
);
初始化时并没有传递状态,要实现动态的修改Text Widget的内容,可以通过包装到一个StatefulWidget中
import 'package:flutter/material.dart';void main() {// 1. 创建MainApp WidgetrunApp(MainApp());
}class MainApp extends StatelessWidget {const MainApp({super.key}); Widget build(BuildContext context) {// 2.调用return MaterialApp(title: 'Sample App', home: CustomStatefulPage());}
}class _CustomStatefulePageState extends State<CustomStatefulPage> {String text = 'I Like Flutter';// 5.点击按钮触发_updateTextvoid _updateText() {// 6.通知Flutter更新setState(() {text = "Flutter is Awesome!";});}// 7.调用build方法,刷新页面// 4.调用build方法,按钮点击方法绑定_updateText方法Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Sample App')),body: Center(child: Text(text)),floatingActionButton: FloatingActionButton(onPressed: _updateText,tooltip: 'Update Text',child: const Icon(Icons.update),),);}
}class CustomStatefulPage extends StatefulWidget {const CustomStatefulPage({super.key});// 3.创建_CustomStatefulePageState对象State<StatefulWidget> createState() => _CustomStatefulePageState();
}
Widget 布局
在UIKit中可以使用Storyboard和用代码去更新View的约束。在Flutter中,通过组合Widget树来展示布局。
build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Sample App')),body: Center(child: CupertinoButton(onPressed: () {},// 指定内边距padding: const EdgeInsets.only(left: 10, right: 10),child: const Text('Hello'),),),);
}
Widget
移除Widget
在UIKit中使用addSubview()
或removeFromSuperview()
来动态的添加或移除视图。在Flutter中Widget是不可变,没有那种类似addSubview()
的方法。可以在父Widget传一个函数,然后控制显示的效果
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';void main() {// 1. 创建MainApp WidgetrunApp(MainApp());
}class MainApp extends StatelessWidget {// This widget is the root of your application.const MainApp({super.key}); Widget build(BuildContext context) {// 2. 创建SampleAppPagereturn const MaterialApp(title: 'Sample App', home: SampleAppPage());}
}class SampleAppPage extends StatefulWidget {const SampleAppPage({super.key});// 3. 调用_SampleAppPageState方法创建State<SampleAppPage>State<SampleAppPage> createState() => _SampleAppPageState();
}class _SampleAppPageState extends State<SampleAppPage> {// Default value for toggle.bool toggle = true;// 5.点击按钮触发void _toggle() {// 6.通知Flutter更新togglesetState(() {toggle = !toggle;});}Widget _getToggleChild() {// 8.根据toggle的值显示不同页面效果if (toggle) {return const Text('Toggle One');}return CupertinoButton(onPressed: () {}, child: const Text('Toggle Two'));}// 4. 触发build方法,FloatingActionButton点击绑定_toggle// 7. 重新执行_getToggleChildWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Sample App')),body: Center(child: _getToggleChild()),floatingActionButton: FloatingActionButton(onPressed: _toggle,tooltip: 'Update Text',child: const Icon(Icons.update),),);}
}
动画
在UIKit中,通过UIView的animate(withDuration:animations:)
执行动画。在Flutter中,使用动画库将Widget包装在动画Widget内。
Flutter中使用AnimationController
来控制动画的暂停,进度,停止。在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。
import 'package:flutter/material.dart';void main() {// 1. 创建MainApp WidgetrunApp(MainApp());
}class MainApp extends StatelessWidget {const MainApp({super.key}); Widget build(BuildContext context) {return const MaterialApp(title: 'Fade Demo',// 2. 创建MyFadeTesthome: MyFadeTest(title: 'Fade Demo'),);}
}class MyFadeTest extends StatefulWidget {const MyFadeTest({super.key, required this.title});final String title;// The framework can call this method multiple times over the lifetime of a [StatefulWidget].// For example, if the widget is inserted into the tree in multiple locations,// the framework will create a separate [State] object for each location.// 添加到tree中的每个MyFadeTest Widget都会创建对应的State<MyFadeTest>对象// 3.创建_MyFadeTestState<MyFadeTest> createState() => _MyFadeTest();
}class _MyFadeTest extends State<MyFadeTest>// 单个 AnimationController 的时候使用 SingleTickerProviderStateMixinwith SingleTickerProviderStateMixin {late AnimationController controller;late CurvedAnimation curve;// Called when this object is inserted into the tree.// 添加到tree中时调用// 4. 调用initState,指定AnimationControllervoid initState() {super.initState();controller = AnimationController(duration: const Duration(milliseconds: 2000),// TickerProvider(抽象类),用于接收动画变化过程中的通知,类似于接口回调vsync: this,);// 指定生成0.0到1.0的规则curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);}// Called when this object is removed from the tree permanently.void dispose() {// 8.当对象对tree中移除时,回收AnimationController的资源controller.dispose();super.dispose();} Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title)),body: Center(child: FadeTransition(// 5. 应用透明效果,opacity的变化规则指定为上面创建的curveopacity: curve,// 6. 动画包装的Widgetchild: const FlutterLogo(size: 100),),),floatingActionButton: FloatingActionButton(onPressed: () {// Starts running this animation forwards (towards the end).// 7. 正向播放动画controller.forward();},tooltip: 'Fade',child: const Icon(Icons.brush),),);}
}
绘图
在UIKit中使用CoreGraphics
在手机屏幕上绘制线条和凸显。Flutter使用Canvas
,CustomPaint
,CustomPainter
来实现绘制操作。
import 'package:flutter/material.dart';void main() => runApp(const MaterialApp(home: DemoApp()));class DemoApp extends StatelessWidget {const DemoApp({super.key}); Widget build(BuildContext context) => const Scaffold(body: Signature());
}class Signature extends StatefulWidget {const Signature({super.key}); State<Signature> createState() => SignatureState();
}class SignatureState extends State<Signature> {List<Offset?> _points = <Offset?>[]; Widget build(BuildContext context) {return GestureDetector(// 绘制时移动的回调onPanUpdate: (details) {setState(() {// 当前Widget关联的RenderObjectRenderBox? referenceBox = context.findRenderObject() as RenderBox;// 坐标转换: 全局坐标转换为局部坐标// globalPosition表示当前手势触点在全局坐标系位置与对应组件顶点坐标的偏移量// localPosition则就表示当前手势触点在对应组件坐标系位置与对应组件顶点坐标的偏移量Offset localPosition = referenceBox.globalToLocal(details.globalPosition,);_points = List.from(_points)..add(localPosition);});},// 绘制结束时调用onPanEnd: (details) => _points.add(null),child: CustomPaint(// 具体的绘制操作调用Canvaspainter: SignaturePainter(_points),// 不限制长度size: Size.infinite,),);}
}class SignaturePainter extends CustomPainter {SignaturePainter(this.points);final List<Offset?> points;void paint(Canvas canvas, Size size) {final Paint paint =//「..」意思是 「级联 操作符」,为了方便配置而使用。//「..」和「.」不同的是 调用「..」后返回的相当于是 thisPaint()..color = const Color.fromARGB(255, 29, 25, 25)..strokeCap = StrokeCap.round..strokeWidth = 5;for (int i = 0; i < points.length - 1; i++) {// 起点与终点都不为null,说明是一个线段,然后执行绘制if (points[i] != null && points[i + 1] != null) {canvas.drawLine(points[i]!, points[i + 1]!, paint);}}} bool shouldRepaint(SignaturePainter oldDelegate) =>oldDelegate.points != points;
}
透明度
UIKit中使用.opactiy
或者.alpha
实现。Flutter中使用Opacity
组件来实现
import 'package:flutter/material.dart';void main() {runApp(MainApp());
}class MainApp extends StatelessWidget {const MainApp({super.key}); Widget build(BuildContext context) {return MaterialApp(title: 'Flutter layout demo',home: CustomStatefulPage(),);}
}class CustomStatefulPage extends StatefulWidget {const CustomStatefulPage({super.key}); State<StatefulWidget> createState() => _CustomStatefulePageState();
}class _CustomStatefulePageState extends State<CustomStatefulPage> {double opacity = 0.3;String text = "透明度"; Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(text)),body: Center(child: Opacity(opacity: opacity, child: Text("透明度:$opacity")),),floatingActionButton: FloatingActionButton(onPressed: () {setState(() {opacity = 1.0;});},tooltip: 'Update Text',child: const Icon(Icons.update),),);}
}
自定义 Widgets
在UIKit中通过继承UIView来自定义组件,而在Flutter中定义自定义组件通常使用组合的方式
import "package:flutter/material.dart";void main() {runApp(MaterialApp(home: CustomWidget()));
}class CustomWidget extends StatelessWidget {const CustomWidget({super.key}); Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("Custom Widget")),body: Center(child: CustomButton("自定义按钮")),);}
}/// 自定义按钮
class CustomButton extends StatelessWidget {const CustomButton(this.label, {super.key});final String label; Widget build(BuildContext context) {return ElevatedButton(onPressed: () {}, child: Text(label));}
}
管理依赖
iOS中使用CocoaPods管理时,可以通过配置Podfile文件。Flutter 是用pubspec.yaml 来管理依赖的库。iOS端需要的库写在Podfile里
/// 包名 此属性表示包名(package name),此属性是非常重要的,引入其他文件时需要使用此包名:
/// import 'package:uikit/home_page.dart';
name: uikit
/// description 属性是一个可选配置属性,是对当前项目的介绍
description: "A new Flutter project."
publish_to: 'none'
// 此属性应用程序的版本和内部版本号,格式为 x.x.x+x,例如:1.0.0+1,这个版本号称为 语义版本号(semantic versioning )
// 版本号 + 前面到部分,叫做 version number,由 2 个小点隔开,后面的部分叫做 build number。
// 在 Android 中 version number 对应 versionName,build number 对应 versionCode,在 android/build.gradle 下有相关配置,
version: 0.1.0/// Environment 属性下添加 Flutter 和 Dart 版本控制。
environment:sdk: ^3.7.2/// dependencies 和 dev_dependencies 下包含应用程序所依赖的包,dependencies 和 dev_dependencies 就像其名字一样,dependencies 下的所有依赖会编译到项目中,而 dev_dependencies 仅仅是运行期间的包,比如自动生成代码的库/// 可以通过四种方式依赖包
dependencies:/// 1. 依赖pub.devflutter:sdk: flutter/// 2. 依赖本地库flutter_local_pacakge:path: /path/to/flutter_local_package/// 3. 依赖git repository/// url:github 地址/// ref:表示git引用,可以是 commit hash, tag 或者 branch/// path:如果 git 仓库中有多个软件包,则可以使用此属性指定软件包bloc:git:url:https://wwww.xxx.gitref: bloc_fixes_issue_100path: package/bloc/// 4. 依赖私有仓dependencies:bloc: hosted:name: blocurl: http://your-package-server.comversion: ^6.0.0dev_dependencies:flutter_test:sdk: flutterflutter_lints: ^5.0.0/// Flutter 下面的配置都是 Flutter 的相关配置。
flutter:/// 应用程序中包含Material Icons字体uses-material-design: true/// 是对当前资源的配置,比如图片,字体等本地图片assets:- images/a_dot_burr.jpeg- images/a_dot_ham.jpegflutter:/// 插件plugin:platforms:android:package: com.flutter.app_marketpluginClass: AppMarketPluginios:pluginClass: AppMarketPlugin
参考
- 给 UIKit 开发者的 Flutter 指南
- 【Flutter 实战】pubspec.yaml 配置文件详解
- No Directionality widget found错误背后的原理
- Flutter - Dart中(.)、(…)、(…)语法使用