举例，比如Bilibili的应用可以设置后台停止播放的功能。那么如何在Flutter中实现呢？Flutter根据应用状态判断APP是否后台运行，通过以下代码可以实现：SystemChannels.lifecycle.setMessageHandler((msg) async { print('SystemChannels====================$msg'); switch(msg) { case "AppLifecycleState.paused": print(msg); print("当app被切到后台暂停时，暂停视频播放"); break; case "AppLifecycleState.resumed": print(msg); print("当app从后台回到前台时，继续播放视频"); break; case "AppLifecycleState.inactive": print(msg); print("AppLifecycleState.inactive"); // assetsAudioPlayer.stop(); break; default: break; } });
在Flutter中获取屏幕尺寸信息是通过MediaQuery.of(context)来获取。属性说明size一个包含宽度和高度的对象，单位是dp（乘以密度就是你设备的像素）devicePixelRatio密度（像素比）textScaleFactor每个逻辑像素的字体像素数platformBrightness主机平台当前亮度模式viewInsets完全被系统UI(通常是设备的键盘)遮挡的显示部分padding我们通常取上边刘海高度和下边导航高度alwaysUse24HourFormat格式化时间时是否使用24小时格式accessibleNavigation用户是否使用TalkBack或VoiceOver等辅助功能服务与应用程序进行交互invertColors设备是否反转平台的颜色disableAnimations平台是否要求尽可能禁用或减少动画boldText平台是否请求使用粗体字体重绘制文本具体代码如下：//屏幕大小 Size mSize = MediaQuery.of(context).size; //密度 double mRatio = MediaQuery.of(context).devicePixelRatio; //设备像素 double width = mSize.width * mRatio; double height = mSize.height * mRatio; // 上下边距 （主要用于 刘海 和 内置导航键） double topPadding = MediaQuery.of(context).padding.top; double bottomPadding = MediaQuery.of(context).padding.bottom; // 每个逻辑像素的字体像素数 double textScaleFactor = MediaQuery.of(context).textScaleFactor; // 主机平台当前亮度模式 Brightness platformBrightness = MediaQuery.of(context).platformBrightness; // 遮挡的显示部分 EdgeInsets viewInsets = MediaQuery.of(context).viewInsets; // 上边刘海高度和下边导航高度 EdgeInsets padding = MediaQuery.of(context).padding; // 格式化时间时是否使用24小时格式 bool alwaysUse24HourFormat = MediaQuery.of(context).alwaysUse24HourFormat; // 用户是否使用TalkBack或VoiceOver等辅助功能服务与应用程序进行交互 bool accessibleNavigation = MediaQuery.of(context).accessibleNavigation; // 设备是否反转平台的颜色 bool invertColors = MediaQuery.of(context).invertColors; // 平台是否要求尽可能禁用或减少动画 bool disableAnimations = MediaQuery.of(context).disableAnimations; // 平台是否请求使用粗体字体重绘制文本 bool boldText = MediaQuery.of(context).boldText;
如果使用FLutter开发TV电视端的APP，如果正确的根据按钮指令去电视上操作呢？下面就是关于监听遥控器的有些相关代码： import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class HomeIndexPage extends StatefulWidget { const HomeIndexPage({super.key}); @override State<HomeIndexPage> createState() => _HomeIndexPageState(); } class _HomeIndexPageState extends State<HomeIndexPage> { final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return RawKeyboardListener( focusNode: FocusNode(), onKey: (value) { //debugPrint("遥控器：${value.data}"); if (value.data is RawKeyEventDataAndroid) { var key = value.data as RawKeyEventDataAndroid; if (key.flags == 520) { //上19 下20 左21 右22 // 8是遥控器按下，520是遥控器 弹起 if (key.keyCode == 19) { debugPrint("遥控器：${value.data}"); // controller.previous(); } else if (key.keyCode == 20) { debugPrint("遥控器：${value.data}"); // controller.next(); } else if (key.keyCode == 21) { debugPrint("遥控器：${value.data}"); // controller.showMenu(); } else if (key.keyCode == 22) { debugPrint("遥控器：${value.data}"); // controller.showMenu(show: false); } else if (key.keyCode == 23) { debugPrint("遥控器：${value.data}"); // controller.ok(); _scaffoldKey.currentState?.openDrawer(); } } } }, child: Scaffold( key: _scaffoldKey, drawer:Drawer( child: Text('This is Drawer'), ), body: Center( child: Column( children: [ Text("YouthTV"), TextButton( onPressed: () { _scaffoldKey.currentState?.openDrawer(); }, child: Text("打开侧栏") ), ], ), ), ) ); } } 运行代码后，可以通过点击TV遥控器的确认键、上、下、左、右 等键来操作电视APP。
int --> Stringint n = 3; String str = n.toString(); // "3"String --> intString str = "10"; int n = int.parse(str); // 10String --> doubleString str = "10.6"; double d = doulbe.parse(str); // 10double --> Stringdouble d = 14.96515; String str = d.toStringAdFixed(3); // 保留小数点后三位 14.965int --> doubleint n = 14; double d = n.toDouble(); // 14.0double --> intdouble d = 21.63; int n = d.round(); // 四舍五入 22 String --> MapString str = '{"name":"张三","age":25}'; Map<String, dynamic> res = jsonDecode(str); print(res["name"]); // 张三String --> ListString str = '[20,30,60,90,100]'; List<int> list = <int>[]; for(var data in JsonDecoder().convert(str)) { list.add(data); } list.forEach((d){ print(d); }); // 20 // 30 // 60 // 90 // 100参考资料：https://blog.csdn.net/cxy18137478587/article/details/114382701
Flutter命令行工具是开发人员（或代表开发人员的IDE）与Flutter交互的方式。对于与Dart相关的命令，可以使用Dart命令行工具。以下是如何使用Flutter工具创建、分析、测试和运行应用程序： flutter create my_app cd my_app flutter analyze flutter test flutter run lib/main.dart要使用Flutter运行pub命令，请执行以下操作：flutter pub get flutter pub outdated flutter pub upgrade要查看Flutter支持的所有命令：flutter --help --verbose要获得Flutter SDK的当前版本，包括其框架、引擎和工具：flutter --versionFlutter 指令:下表展示了可以与Flutter一起使用的命令：命令示例说明analyzeflutter analyze -d <DEVICE_ID>分析项目的Dart源代码，用于代替Dart分析assembleflutter assemble -o 组装和构建Flutter资源attachflutter attach -d <DEVICE_ID>连接到正在运行的应用程序bash-completionflutter bash-completion输出命令行shell完成设置脚本buildflutter build Flutter构建命令channelflutter channel <CHANNEL_NAME>列出或切换Flutter的Channelscleanflutter clean删除build/和.dart_tool/目录configflutter config --build-dir=配置Flutter设置。若要删除设置，请将其配置为空字符串createflutter create 创建一个新的项目custom-devicesflutter custom-devices list添加、删除、列出和重置自定义设备devicesflutter devices -d <DEVICE_ID>列出所有连接的设备doctorflutter doctor显示有关已安装工具的信息downgradeflutter downgrade将Flutter降级到当前通道的最后一个活动版本driveflutter drive为当前项目运行Flutter设备程序测试emulatorsflutter emulators列出、启动和创建模拟器formatflutter format <DIRECTORY|DART_FILE>格式化Flutter源代码。已弃用，请改用dart格式gen-l10nflutter gen-l10n 为Flutter项目生成本地化installflutter install -d <DEVICE_ID>在连接的设备上安装Flutter应用程序logsflutter logs显示运行Flutter应用程序的日志输出precacheflutter precache 填充Flutter工具的二进制工件缓存pubflutter pub <PUB_COMMAND>适用于程序包。使用而不是dart-pubrunflutter run <DART_FILE>运行Flutter程序screenshotflutter screenshot从连接的设备上截取Flutter应用程序的屏幕截图。symbolizeflutter symbolize --input=<STACK_TRACK_FILE>表示来自AOT编译的flutter应用程序的堆栈跟踪testflutter test [<DIRECTORY|DART_FILE>]在此包中运行测试。使用而不是dart测试upgradeflutter upgrade升级你的Flutter版本有关任何命令的其他帮助，请输入flutter help＜command＞进行操作。您还可以获得关于pub命令的详细信息，例如，flutter help pub outdated参考文档：Flutter中文社区
Decoration是一个装饰类。这个类提供了所有装饰的抽象接口。具体示例请参见BoxDecoration。decoration : 装饰器接口，Decoration是个抽象接口，BoxDecoration是它其一个具体实现类，Decoration装饰器的绘 制最终会由BoxPainter来完成，实现一个Decoration装饰器可以通过createBoxPainter方法来获取一个BoxPainter，最终通过BoxPainter来进行绘制。position: 是个枚举类型，用来控制装饰器在哪里绘制，取值有background跟foreground，前者代表是在背景绘制，后者是在前景绘制BoxDecoration BoxDecoration({ Color? color, DecorationImage? image, BoxBorder? border, BorderRadiusGeometry? borderRadius, List<BoxShadow>? boxShadow, Gradient? gradient, BlendMode? backgroundBlendMode, BoxShape shape = BoxShape.rectangle, })BoxDecoration是Decoration的一个实现类，BoxDecoration提供了背景颜色、边框、阴影、圆角、颜色渐变、形状等等装饰能力，BoxDecoration的定义如下：BoxDecoration BoxDecoration({ Color? color, DecorationImage? image, BoxBorder? border, BorderRadiusGeometry? borderRadius, List<BoxShadow>? boxShadow, Gradient? gradient, BlendMode? backgroundBlendMode, BoxShape shape = BoxShape.rectangle, })color ： 背景颜色image ：背景图片，会绘制在背景颜色上面border ： 边框，会绘制在背景颜色背景图片上面borderRadius ： 边框圆角boxShadow ： 背景阴影gradient ： 填充渐变颜色backgroundBlendMode ： 背景混合模式shape ： 形状下面是Decoration的具体使用实例：import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: '演示Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const TestPage1(), ); } } class TestPage1 extends StatefulWidget { const TestPage1({Key? key}) : super(key: key); @override State<TestPage1> createState() => _TestPage1State(); } class _TestPage1State extends State<TestPage1> { @override Widget build(BuildContext context) { // TODO: implement build return Scaffold( appBar: AppBar( title: const Text("功能演示页面"), ), body: Container( width: 120, height: 120, // color: Colors.black26, // 背景颜色 注意：Color和decoration二选一 /// alignment元素对齐: /// 1、AlignmentDirectional 排列方向 /// 2、AlignmentGeometry 几何校准 /// 3、Alignment 对齐 /// 其中 Alignment、AlignmentDirectional 均继承自 alignment: Alignment.center, child: DecoratedBox( decoration: BoxDecoration( image: new DecorationImage( // 背景图片，会绘制在背景颜色上面 image: new ExactAssetImage('images/bg.jpeg'), fit: BoxFit.cover, ), border: Border.all( // 边框，会绘制在背景颜色背景图片上面 color: Colors.red, width: 5.0, ), backgroundBlendMode: BlendMode.color, // 背景混合模式 gradient: LinearGradient(colors:[Colors.red,Colors.orange]), //背景渐变 color: Colors.amber, // 背景颜色 /// 当 shape 为 BoxShape.circle，则需要关闭borderRadius //borderRadius: BorderRadius.all(Radius.circular(6.0)), // 边框圆角 boxShadow: [ // 背景阴影 BoxShadow( color: Colors.black12, offset: Offset(3.0,3.0), blurRadius: 2.0 ) ], shape: BoxShape.circle // 形状:圆形、矩形 ), child: Container( padding: EdgeInsets.all(16), child: Text("QQQ"), ), ), ), ); } }具体效果如下：
要想实现首页导航需要哪些材料?ScaffoldBottomNavigationBarPageViewPageController什么是Scaffold widget？Scaffold是一个实现了基本的material design的布局结构class Scaffold extends StatefulWidget { /// Creates a visual scaffold for material design widgets. const Scaffold({ Key key, this.appBar, this.body, this.floatingActionButton, this.floatingActionButtonLocation, this.floatingActionButtonAnimator, this.persistentFooterButtons, this.drawer, this.endDrawer, this.bottomNavigationBar, this.bottomSheet, this.backgroundColor, this.resizeToAvoidBottomPadding = true, this.primary = true, }) : assert(primary != null), super(key: key); ...顶部导航import 'package:flutter/material.dart'; class TabbedAppBarSample extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: DefaultTabController( length: choices.length, child: Scaffold( appBar: AppBar( title: const Text('Tabbed AppBar'), bottom: TabBar( isScrollable: true, tabs: choices.map((Choice choice) { return Tab( text: choice.title, icon: Icon(choice.icon), ); }).toList(), ), ), body: TabBarView( children: choices.map((Choice choice) { return Padding( padding: const EdgeInsets.all(16.0), child: ChoiceCard(choice: choice), ); }).toList(), ), ), ), ); } } class Choice { const Choice({this.title, this.icon}); final String title; final IconData icon; } const List<Choice> choices = const <Choice>[ const Choice(title: 'CAR', icon: Icons.directions_car), const Choice(title: 'BICYCLE', icon: Icons.directions_bike), const Choice(title: 'BOAT', icon: Icons.directions_boat), const Choice(title: 'BUS', icon: Icons.directions_bus), const Choice(title: 'TRAIN', icon: Icons.directions_railway), const Choice(title: 'WALK', icon: Icons.directions_walk), ]; class ChoiceCard extends StatelessWidget { const ChoiceCard({Key key, this.choice}) : super(key: key); final Choice choice; @override Widget build(BuildContext context) { final TextStyle textStyle = Theme.of(context).textTheme.display1; return Card( color: Colors.white, child: Center( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Icon(choice.icon, size: 128.0, color: textStyle.color), Text(choice.title, style: textStyle), ], ), ), ); } } void main() { runApp(TabbedAppBarSample()); }底部导航菜单import 'package:flutter/material.dart'; import 'package:flutter_app/pages/home_page.dart'; import 'package:flutter_app/pages/my_page.dart'; import 'package:flutter_app/pages/search_page.dart'; import 'package:flutter_app/pages/travel_page.dart'; class TabNavigator extends StatefulWidget { @override _TabNavigatorState createState() => _TabNavigatorState(); } class _TabNavigatorState extends State<TabNavigator> with SingleTickerProviderStateMixin { final _defaultColor = Colors.grey; final _activeColor = Colors.blue; int _currentIndex = 0; var _controller = PageController( initialPage: 0, ); @override void dispose() { super.dispose(); _controller.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: PageView( controller: _controller, children: <Widget>[HomePage(), SearchPage(), TravelPage(), MyPage()], physics: NeverScrollableScrollPhysics(), ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { _controller.jumpToPage(index); setState(() { _currentIndex = index; }); }, type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: Icon( Icons.home, color: _defaultColor, ), activeIcon: Icon( Icons.home, color: _activeColor, ), title: Text( '首页', style: TextStyle( color: _currentIndex != 0 ? _defaultColor : _activeColor), ), ), BottomNavigationBarItem( icon: Icon( Icons.search, color: _defaultColor, ), activeIcon: Icon( Icons.search, color: _activeColor, ), title: Text( '搜索', style: TextStyle( color: _currentIndex != 1 ? _defaultColor : _activeColor), )), BottomNavigationBarItem( icon: Icon( Icons.camera_alt, color: _defaultColor, ), activeIcon: Icon( Icons.camera_alt, color: _activeColor, ), title: Text( '旅拍', style: TextStyle( color: _currentIndex != 2 ? _defaultColor : _activeColor), )), BottomNavigationBarItem( icon: Icon( Icons.account_circle, color: _defaultColor, ), activeIcon: Icon( Icons.account_circle, color: _activeColor, ), title: Text( '我的', style: TextStyle( color: _currentIndex != 3 ? _defaultColor : _activeColor), )), ], ), ); } }侧拉栏菜单import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final appTitle = 'Drawer Demo'; @override Widget build(BuildContext context) { return MaterialApp( title: appTitle, home: MyHomePage(title: appTitle), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({Key key, this.title}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: Center(child: Text('My Page!')), drawer: Drawer( // Add a ListView to the drawer. This ensures the user can scroll // through the options in the Drawer if there isn't enough vertical // space to fit everything. child: ListView( // Important: Remove any padding from the ListView. padding: EdgeInsets.zero, children: <Widget>[ DrawerHeader( child: Text('Drawer Header'), decoration: BoxDecoration( color: Colors.blue, ), ), ListTile( title: Text('Item 1'), onTap: () { // Update the state of the app // ... // Then close the drawer Navigator.pop(context); }, ), ListTile( title: Text('Item 2'), onTap: () { // Update the state of the app // ... // Then close the drawer Navigator.pop(context); }, ), ], ), ), ); } } 什么是PageView？PageView是一个可以完成页面之间滚动的widget。class PageView extends StatefulWidget { PageView({ Key key, this.scrollDirection = Axis.horizontal, //滚动的方向，支持水平和垂直两个方向 this.reverse = false, //是否反方向滚动 PageController controller, //PageView的控制类 this.physics, //手势滚动逻辑，支持不滚动，总是滚动，与滚动到边缘时是否有bounce this.pageSnapping = true,//设置为false以禁用页面捕捉，对自定义滚动行为很有用。 this.onPageChanged,//页面切换时调用 List<Widget> children = const <Widget>[], }) : controller = controller ?? _defaultPageController, childrenDelegate = SliverChildListDelegate(children), super(key: key); ...
利用Flutter实现APP拍照功能，效果图具体如下：点击[拍照]按钮 选择【拍照】选项，调用设备镜头采集影像 按下快门后，确定选定该影像照片 选择【从相册选择】选项，从相册选择照片 显示当前选中的照片图像 开发环境：Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git Framework • revision b06b8b2710 (3 months ago) • 2023-01-23 16:55:55 -0800 Engine • revision b24591ed32 Tools • Dart 2.19.0 • DevTools 2.20.1引用插件：image_picker: ^0.8.7以下为具体实现代码：main.dart 代码：import 'package:flutter/material.dart'; import 'package:blog/photo_app.dart'; void main() { runApp(const PApp()); } class PApp extends StatefulWidget { const PApp({Key? key}) : super(key: key); @override State<PApp> createState() => _PAppState(); } class _PAppState extends State<PApp> { @override Widget build(BuildContext context) { return const MaterialApp( title: "拍照APP", home: PhotoApp(title: "拍照APP"), ); } } photo_app.dart 代码： import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class PhotoApp extends StatefulWidget { const PhotoApp({Key? key, required this.title}) : super(key: key); final String title; @override State<PhotoApp> createState() => _PhotoAppState(); } class _PhotoAppState extends State<PhotoApp> { late List<XFile> _imageFileList; void _setImageFileListFromFile(XFile? value) { if(value != null) { _imageFileList.add(value); } } @override void deactivate() { super.deactivate(); } @override void dispose() { super.dispose(); } @override void initState() { _imageFileList = []; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title!), ), body: Center( child: Center( child: Wrap( spacing: 5, runSpacing: 5, children: _genImages(), ), ), ), floatingActionButton: FloatingActionButton( onPressed: _pickImage, tooltip: "选择图片", child: const Icon(Icons.add_a_photo), ), ); } _pickImage() { showModalBottomSheet( context: context, builder: (context) => Container( height: 160, child: Column( children: [ _item("拍照", true), _item("从相册选择", false), ], ), ) ); } _item(String title, bool isTakePhoto) { return GestureDetector( child: ListTile( leading: Icon(isTakePhoto ? Icons.camera_alt : Icons.photo_library), title: Text(title), onTap: () => getImage(isTakePhoto), ), ); } Future getImage(bool isTakePhoto) async { Navigator.pop(context); final XFile? image = await ImagePicker().pickImage(source: isTakePhoto ? ImageSource.camera : ImageSource.gallery,); if(image != null) { setState(() { _setImageFileListFromFile(image); }); } } _genImages() { return _imageFileList?.map((file) { return Stack( children: <Widget>[ ClipRRect( borderRadius: BorderRadius.circular(5), child: Image.file(File(file.path), width: 120, height: 120, fit: BoxFit.fill,), ), Positioned( right: 5, top: 5, child: GestureDetector( onTap: () { setState(() { _imageFileList.remove(file); }); }, child: ClipOval( child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration(color: Colors.black54), child: Icon( Icons.close, size: 18, color: Colors.yellow, ), ), ), ), ), ], ); }).toList(); } }
初始动画Animation，我们应该了解的知识：在Flutter中有哪些类型的动画？如何使用动画库中的基础类给widget添加动画？如何为动画添加监听器？该什么时候使用AnimatedWidget与AnimatedBuilder?如何使用Hero动画？精心设计的动画会让应用界面感觉更直观、流畅，能改善用户体验。 Flutter的动画支持可以轻松实现各种动画类型。各种widget，特别是Material Design widgets， 都带有在其设计规范中定义的标准动画效果，但也可以自定义这些效果。在Flutter中有哪些类型的动画？在Flutter中动画分为两类：基于tween或基于物理的。补间(Tween)动画：在补间动画中，定义了开始点和结束点、时间线以及定义转换时间和速度的曲线。然后由框架计算如何从开始点过渡到结束点。基于物理的动画：在基于物理的动画中，运动被模拟为与真实世界的行为相似。例如，当你掷球时，它在何处落地，取决于抛球速度有多快、球有多重、距离地面有多远。 类似地，将连接在弹簧上的球落下（并弹起）与连接到绳子上的球放下的方式也是不同。如何使用动画库中的基础类给widget添加动画？在为widget添加动画之前，先让我们认识下动画的几个朋友：Animation：是Flutter动画库中的一个核心类，它生成指导动画的值；CurvedAnimation：Animation的一个子类，将过程抽象为一个非线性曲线；AnimationController：Animation的一个子类，用来管理Animation；Tween：在正在执行动画的对象所使用的数据范围之间生成值。例如，Tween可生成从红到蓝之间的色值，或者从0到255；Animation在Flutter中，Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类，它拥有其当前值和状态（完成或停止）。其中一个比较常用的Animation类是Animation<double>。Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据Animation对象的控制方式，动画可以反向运行，甚至可以在中间切换方向。Animation还可以生成除double之外的其他类型值，如：Animation<Color> 或 Animation<Size>；Animation对象有状态。可以通过访问其value属性获取动画的当前值；Animation对象本身和UI渲染没有任何关系；CurvedAnimationCurvedAnimation将动画过程定义为一个非线性曲线。final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);注: Curves 类定义了许多常用的曲线，也可以创建自己的，例如：class ShakeCurve extends Curve { @override double transform(double t) { return math.sin(t * math.PI * 2); } }AnimationControllerAnimationController是一个特殊的Animation对象，在屏幕刷新的每一帧，就会生成一个新的值。默认情况下，AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。 例如，下面代码创建一个Animation对象：final AnimationController controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this);AnimationController派生自Animation<double>，因此可以在需要Animation对象的任何地方使用。 但是，AnimationController具有控制动画的其他方法：forward()：启动动画；reverse({double from})：倒放动画；reset()：重置动画，将其设置到动画的开始位置；stop({ bool canceled = true })：停止动画；当创建一个AnimationController时，需要传递一个vsync参数，存在vsync时会防止屏幕外动画消耗不必要的资源，可以将stateful对象作为vsync的值。注意： 在某些情况下，值(position，值动画的当前值)可能会超出AnimationController的0.0-1.0的范围。例如，fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)。位置(position)可以是任何东西，因此可以在0.0到1.0范围之外。 CurvedAnimation生成的值也可以超出0.0到1.0的范围。根据选择的曲线，CurvedAnimation的输出可以具有比输入更大的范围。例如，Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。Tween默认情况下，AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型，则可以使用Tween来配置动画以生成不同的范围或数据类型的值。例如，以下示例，Tween生成从-200.0到0.0的值：final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);Tween是一个无状态(stateless)对象，需要begin和end值。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0，但这不是必须的。Tween继承自Animatable<T>，而不是继承自Animation<T>。Animatable与Animation相似，不是必须输出double值。例如，ColorTween指定两种颜色之间的过渡。final Tween colorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);Tween对象不存储任何状态。相反，它提供了evaluate(Animation<double> animation)方法将映射函数应用于动画当前值。 Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理，例如分别确保在动画值为0.0和1.0时返回开始和结束状态。Tween.animate要使用Tween对象，可调用它的animate()方法，传入一个控制器对象。例如，以下代码在500毫秒内生成从0到255的整数值。final AnimationController controller = new AnimationController( duration: const Duration(milliseconds: 500), vsync: this); Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);注意animate()返回的是一个Animation，而不是一个Animatable。以下示例构建了一个控制器、一条曲线和一个Tween：final AnimationController controller = new AnimationController( duration: const Duration(milliseconds: 500), vsync: this); final Animation curve = new CurvedAnimation(parent: controller, curve: Curves.easeOut); Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);为widget添加动画在下面的实例中我们为一个logo添加了一个从小放大的动画：import 'package:flutter/material.dart'; void main() => runApp(LogoApp()); class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; AnimationStatus animationState; double animationValue; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); // #docregion addListener animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() { // #enddocregion addListener setState(() { animationValue = animation.value; }); // #docregion addListener }) ..addStatusListener((AnimationStatus state) { setState(() { animationState = state; }); }); // #enddocregion addListener } @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(top: 50), child: Column( children: <Widget>[ GestureDetector( onTap: () { controller.reset(); controller.forward(); }, child: Text('Start', textDirection: TextDirection.ltr), ), Text('State:' + animationState.toString(), textDirection: TextDirection.ltr), Text('Value:' + animationValue.toString(), textDirection: TextDirection.ltr), Container( height: animation.value, width: animation.value, child: FlutterLogo(), ), ], ), ); } @override void dispose() { controller.dispose(); super.dispose(); } } 注意，在上述代码中要实现这个动画的关键一步是在addListener()的回调中添加setState的调用这样才能触发页面重新渲染，动画才能有效，另外也可以通过AnimatedWidget来实现。如何为动画添加监听器？有时我们需要知道动画执行的进度和状态，在Flutter中我们可以通过Animation的addListener与addStatusListener方法为动画添加监听器：addListener：动画的值发生变化时被调用；addStatusListener：动画状态发生变化时被调用；@override void initState() { super.initState(); controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = Tween<double>(begin: 0, end: 300).animate(controller) // #enddocregion print-state ..addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); } }) // #docregion print-state ..addStatusListener((state) => print('$state')); ..addListener(() { // #enddocregion addListener setState(() { // The state that has changed here is the animation object’s value. }); // #docregion addListener }); controller.forward(); } 用AnimatedWidget与AnimatedBuilder简化和重构我们对动画的使用什么是AnimatedWidget？我们可以将AnimatedWidget理解为Animation的助手，使用它可以简化我们对动画的使用，在上面的示例widget添加动画的学习中我们不难发现，在不使用AnimatedWidget的情况下需要手动调用动画的addListener()并在回调中添加setState才能看到动画效果，AnimatedWidget将为我们简化这一操作。在下面的重构示例中，LogoApp现在继承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在绘制时使用动画的当前值。LogoApp仍然管理着AnimationController和Tween。// Demonstrate a simple animation with AnimatedWidget import 'package:flutter/animation.dart'; import 'package:flutter/material.dart'; class AnimatedLogo extends AnimatedWidget { AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { final Animation<double> animation = listenable; return new Center( child: new Container( margin: new EdgeInsets.symmetric(vertical: 10.0), height: animation.value, width: animation.value, child: new FlutterLogo(), ), ); } } class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); } class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); animation = new Tween(begin: 0.0, end: 300.0).animate(controller); controller.forward(); } Widget build(BuildContext context) { return new AnimatedLogo(animation: animation); } dispose() { controller.dispose(); super.dispose(); } } void main() { runApp(new LogoApp()); }什么是AnimatedBuilder？AnimatedBuilder是用于构建动画的通用widget，AnimatedBuilder对于希望将动画作为更大构建函数的一部分包含在内的更复杂的widget时非常有用，其实你可以这样理解：AnimatedBuilder是拆分动画的一个工具类，借助它我们可以将动画和widget进行分离：在上面的实例中我们的代码存在的一个问题： 更改动画需要更改显示logo的widget。更好的解决方案是将职责分离：显示logo定义Animation对象渲染过渡效果接下来我们就借助AnimatedBuilder类来完成此分离。AnimatedBuilder是渲染树中的一个独立的类， 与AnimatedWidget类似，AnimatedBuilder自动监听来自Animation对象的通知，不需要手动调用addListener()。我们根据下图的 widget 树来创建我们的代码：import 'package:flutter/animation.dart'; import 'package:flutter/material.dart'; void main() => runApp(LogoApp()); // #docregion LogoWidget class LogoWidget extends StatelessWidget { // Leave out the height and width so it fills the animating parent Widget build(BuildContext context) => Container( margin: EdgeInsets.symmetric(vertical: 10), child: FlutterLogo(), ); } // #enddocregion LogoWidget // #docregion GrowTransition class GrowTransition extends StatelessWidget { GrowTransition({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder( animation: animation, builder: (context, child) => Container( height: animation.value, width: animation.value, child: child, ), child: child), ); } // #enddocregion GrowTransition class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } // #docregion print-state class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = Tween<double>(begin: 0, end: 300).animate(controller); controller.forward(); } // #enddocregion print-state @override Widget build(BuildContext context) => GrowTransition( child: LogoWidget(), animation: animation, ); @override void dispose() { controller.dispose(); super.dispose(); } // #docregion print-state } 如何使用Hero动画？什么是Hero动画？在 Flutter中可以用 Hero widget创建这个动画。当 hero 通过动画从源页面飞到目标页面时，目标页面逐渐淡入视野。通常， hero 是用户界面的一小部分，如图片，它通常在两个页面都有。从用户的角度来看， hero 在页面之间“飞翔”。接下来我们一起来学习如何创建Hero动画： 实现标准hero动画 import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; class PhotoHero extends StatelessWidget { const PhotoHero({ Key key, this.photo, this.onTap, this.width }) : super(key: key); final String photo; final VoidCallback onTap; final double width; Widget build(BuildContext context) { return SizedBox( width: width, child: Hero( tag: photo, child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Image.network( photo, fit: BoxFit.contain, ), ), ), ), ); } } class HeroAnimation extends StatelessWidget { Widget build(BuildContext context) { timeDilation = 10.0; // 1.0 means normal animation speed. return Scaffold( appBar: AppBar( title: const Text('Basic Hero Animation'), ), body: Center( child: PhotoHero( photo: 'https://raw.githubusercontent.com/flutter/website/master/examples/_animation/hero_animation/images/flippers-alpha.png', width: 300.0, onTap: () { Navigator.of(context).push(MaterialPageRoute<void>( builder: (BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flippers Page'), ), body: Container( // Set background to blue to emphasize that it's a new route. color: Colors.lightBlueAccent, padding: const EdgeInsets.all(16.0), alignment: Alignment.topLeft, child: PhotoHero( photo: 'https://raw.githubusercontent.com/flutter/website/master/examples/_animation/hero_animation/images/flippers-alpha.png', width: 100.0, onTap: () { Navigator.of(context).pop(); }, ), ), ); } )); }, ), ), ); } } void main() { runApp(MaterialApp(home: HeroAnimation())); } Hero的函数原型 const Hero({ Key key, @required this.tag, this.createRectTween, this.flightShuttleBuilder, this.placeholderBuilder, this.transitionOnUserGestures = false, @required this.child, }) : assert(tag != null), assert(transitionOnUserGestures != null), assert(child != null), super(key: key); tag：[必须]用于关联两个Hero动画的标识；createRectTween：[可选]定义目标Hero的边界，在从起始位置到目的位置的“飞行”过程中该如何变化；child：[必须]定义动画所呈现的widget； 实现径向hero动画 import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; class Photo extends StatelessWidget { Photo({ Key key, this.photo, this.color, this.onTap }) : super(key: key); final String photo; final Color color; final VoidCallback onTap; Widget build(BuildContext context) { return Material( // Slightly opaque color appears where the image has transparency. color: Theme.of(context).primaryColor.withOpacity(0.25), child: InkWell( onTap: onTap, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints size) { return Image.network( photo, fit: BoxFit.contain, ); }, ), ), ); } } class RadialExpansion extends StatelessWidget { RadialExpansion({ Key key, this.maxRadius, this.child, }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2), super(key: key); final double maxRadius; final clipRectSize; final Widget child; @override Widget build(BuildContext context) { return ClipOval( child: Center( child: SizedBox( width: clipRectSize, height: clipRectSize, child: ClipRect( child: child, ), ), ), ); } } class RadialExpansionDemo extends StatelessWidget { static const double kMinRadius = 32.0; static const double kMaxRadius = 128.0; static const opacityCurve = const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn); static RectTween _createRectTween(Rect begin, Rect end) { return MaterialRectCenterArcTween(begin: begin, end: end); } static Widget _buildPage(BuildContext context, String imageName, String description) { return Container( color: Theme.of(context).canvasColor, child: Center( child: Card( elevation: 8.0, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: kMaxRadius * 2.0, height: kMaxRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).pop(); }, ), ), ), ), Text( description, style: TextStyle(fontWeight: FontWeight.bold), textScaleFactor: 3.0, ), const SizedBox(height: 16.0), ], ), ), ), ); } Widget _buildHero(BuildContext context, String imageName, String description) { return Container( width: kMinRadius * 2.0, height: kMinRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).push( PageRouteBuilder<void>( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { return Opacity( opacity: opacityCurve.transform(animation.value), child: _buildPage(context, imageName, description), ); } ); }, ), ); }, ), ), ), ); } @override Widget build(BuildContext context) { timeDilation = 5.0; // 1.0 is normal animation speed. return Scaffold( appBar: AppBar( title: const Text('Radial Transition Demo'), ), body: Container( padding: const EdgeInsets.all(32.0), alignment: FractionalOffset.bottomLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildHero(context, 'https://raw.githubusercontent.com/flutter/website/master/examples/_animation/radial_hero_animation/images/chair-alpha.png', 'Chair'), _buildHero(context, 'https://raw.githubusercontent.com/flutter/website/master/examples/_animation/radial_hero_animation/images/binoculars-alpha.png', 'Binoculars'), _buildHero(context, 'https://raw.githubusercontent.com/flutter/website/master/examples/_animation/radial_hero_animation/images/beachball-alpha.png', 'Beach ball'), ], ), ), ); } } void main() { runApp(MaterialApp(home: RadialExpansionDemo())); }
[Flutter]解决ERROR:D8: Cannot fit requested classes in a single dex file的问题
最近在学习开发Flutter项目时遇到了一个问题,具体的异常信息如下：Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source! ERROR:D8: Cannot fit requested classes in a single dex file (# methods: 65853 > 65536) com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html at com.android.builder.dexing.D8DexArchiveMerger.getExceptionToRethrow(D8DexArchiveMerger.java:151) at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:138) at com.android.build.gradle.internal.tasks.DexMergingWorkAction.merge(DexMergingTask.kt:859) at com.android.build.gradle.internal.tasks.DexMergingWorkAction.run(DexMergingTask.kt:805) at com.android.build.gradle.internal.profile.ProfileAwareWorkAction.execute(ProfileAwareWorkAction.kt:74) at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204) at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157) at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53) at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73) at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$2(DefaultWorkerExecutor.java:205) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:187) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:120) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:162) at org.gradle.internal.Factories$1.create(Factories.java:31) at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:249) at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:109) at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:114) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:157) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:126) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete, position: null at Version.fakeStackEntry(Version_3.2.47.java:0) at com.android.tools.r8.internal.ci.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:68) at com.android.tools.r8.internal.ci.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:28) at com.android.tools.r8.internal.ci.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:27) at com.android.tools.r8.internal.ci.b(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:3) at com.android.tools.r8.D8.run(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:11) at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:136) ... 38 more Caused by: com.android.tools.r8.internal.a: Cannot fit requested classes in a single dex file (# methods: 65853 > 65536) at com.android.tools.r8.internal.tQ.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:14) at com.android.tools.r8.internal.tQ.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:22) at com.android.tools.r8.internal.fZ.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:560) at com.android.tools.r8.internal.bZ.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:7) at com.android.tools.r8.internal.o2.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:383) at com.android.tools.r8.internal.o2.c(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:54) at com.android.tools.r8.D8.d(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:763) at com.android.tools.r8.D8.b(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:1) at com.android.tools.r8.internal.ci.a(R8_3.2.47_ebadcf1df6fbed6005a238b8399b2cd411e753b60758261060e399f9498872a5:24) ... 41 more FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:mergeExtDexDebug'. > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingTaskDelegate > There was a failure while executing work items > A failure occurred while executing com.android.build.gradle.internal.tasks.DexMergingWorkAction > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 33s在异常信息中让我们去这个地方 https://developer.android.com/tools/building/multidex.html 解决问题。（以上地址已经失效了，新的地址是：https://developer.android.com/build/multidex）查阅资料后知道了，Android 5.0 及更高版本的 MultiDex 支持，而低版本需要手动操作配置一些东西。Android 5.0 之前版本的 MultiDex 支持 Android 5.0（API 级别 21）之前的平台版本使用 Dalvik 运行时执行应用代码。默认情况下，Dalvik 将应用限制为每个 APK 只能使用一个 classes.dex 字节码文件。为了绕过这一限制，您可以向模块级 build.gradle 文件中添加 MultiDex 库：... dependencies { def multidex_version = "2.0.1" implementation "androidx.multidex:multidex:$multidex_version" } ...如需查看此库的当前版本，请参阅版本页面中有关 MultiDex 的信息。如果您使用的不是 AndroidX，请改为添加以下已弃用的支持库依赖项：... dependencies { implementation 'com.android.support:multidex:1.0.3' } ...Android 5.0 及更高版本的 MultiDex 支持 Android 5.0（API 级别 21）及更高版本使用名为 ART 的运行时，它本身支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译，这会扫描查找 classesN.dex 文件，并将它们编译成单个 .oat 文件，以供 Android 设备执行。因此，如果您的 minSdkVersion 为 21 或更高版本，系统会默认启用 MultiDex，并且您不需要 MultiDex 库。经过网上的一番资料查询，得知是项目中android/app/build.gradle文件中defaultConfig的minSdkVersion太低。... android { ... defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.baikeyang.blog" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } ... } ... 当找到项目中对应的位置，发现defaultConfig的minSdkVersion配置的是flutter.minSdkVersion的值，并没有直接写明minSdkVersion的值那么问题来了，flutter.minSdkVersion这个对应的版本到底是多少呢？继续查询一番，找到了flutter.minSdkVersion的版本信息。前往自己的FlutterSDK目录中找到flutter.gradle文件：[盘符]:\[flutterSDK位置]\packages\flutter_tools\gradle\flutter.gradle,在文件的36行就可以找到Flutter默认的minSdkVersion版本是多少，同时还可以知道compileSdkVersion、targetSdkVersion 分别是多少。... /** For apps only. Provides the flutter extension used in app/build.gradle. */ class FlutterExtension { /** Sets the compileSdkVersion used by default in Flutter app projects. */ static int compileSdkVersion = 33 /** Sets the minSdkVersion used by default in Flutter app projects. */ static int minSdkVersion = 16 /** Sets the targetSdkVersion used by default in Flutter app projects. */ static int targetSdkVersion = 33 /** * Sets the ndkVersion used by default in Flutter app projects. * Chosen as default version of the AGP version below as found in * https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp */ static String ndkVersion = "23.1.7779620" /** * Specifies the relative directory to the Flutter project directory. * In an app project, this is ../.. since the app's build.gradle is under android/app. */ String source /** Allows to override the target file. Otherwise, the target is lib/main.dart. */ String target } ...知道了minSdkVersion值是16。为了不影响FlutterSDK，我这里并没有直接在这里修改。而是返回项目中修改了项目中android/app/build.gradle文件中defaultConfig的minSdkVersiondefaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.baikeyang.blog" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName }重新编译运行项目，编译启动正常。