通过上一篇《开篇:介绍一下flutter》, 我了解到 Flutter 大部分内容都是UI相关的库,打算先从UI 组件使用下手,这个看上去简单,如果直接学习dart语法会让我失去学习的兴趣。
一、笔记2:
正常学习一门语言会从 hello word 开始,这次我先不着急搭建环境,准备花点时间把Flutter UI组件大致熟悉一下
接下来,首先搜索一个中文版教程,我搜索“flutter 中文教程” 找到一个 “中文开发者网站” 的地址 (当然参考很多,比如 docs.flutter.dev/get-started… 看个人)
docs.flutter.cn/cookbook/ (2024.12可访问)
我进入 实用教程-动画-第一个示例页面,底部可以在线编译示例代码:
整体看上去基本满足我初学要求了,接下来我将快速学习 UI 组件的搭建示例了。
整体看下 docs.flutter.cn/cookbook/an… 示例。
二、实现“页面跳转”功能:
要求:一个主页路由,包含了 "Go!" 按钮,还有第二个路由,包含了一个显示 "Page 2 的标题
下面是flutter实现的代码示例:
1、由大模型添加注释
2、由于掘金对flutter的代码高亮效果几乎没有(2024/12),代码我使用js高亮
// 导入Flutter的Material Design组件库,这是构建用户界面所必需的。
import 'package:flutter/material.dart';
// main函数是Flutter应用的入口点。
void main() {
// 使用runApp函数启动应用,参数是应用的根组件。
runApp(
// 使用const关键字创建MaterialApp的实例,这表示这个实例是不可变的。
const MaterialApp(
// home属性指定了应用启动时显示的第一个页面。
home: Page1(),
),
);
}
// 定义一个名为Page1的类,它继承自StatelessWidget,表示这是一个无状态组件。
class Page1 extends StatelessWidget {
// 使用const构造函数,允许在声明时直接创建实例,且实例不可变。
const Page1({super.key});
// build方法是每个Widget必须实现的,它描述了组件的UI结构。
@override
Widget build(BuildContext context) {
// Scaffold是一个布局结构,通常用于包含AppBar和页面主体内容。
return Scaffold(
// appBar属性定义了应用栏。
appBar: AppBar(),
// body属性定义了Scaffold的主体内容。
body: Center(
// Center组件用于将其子组件居中显示。
child: ElevatedButton(
// onPressed属性定义了按钮被点击时的行为。
onPressed: () {
// Navigator用于管理路由(页面跳转)。push方法用于将新页面添加到路由栈中。
Navigator.of(context).push(_createRoute());
},
// child属性定义了按钮的内容。
child: const Text('Go!'),
),
),
);
}
}
// _createRoute是一个私有方法,用于创建并返回一个Route对象。
Route _createRoute() {
// PageRouteBuilder是一个灵活的页面路由构建器,允许自定义页面转换动画和页面构建逻辑。
return PageRouteBuilder(
// pageBuilder属性定义了如何构建新页面。
pageBuilder: (context, animation, secondaryAnimation) => const Page2(),
// transitionsBuilder属性定义了页面转换动画。这里我们直接返回child,表示没有特殊动画。
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child;
},
);
}
// 定义了一个名为Page2的类,同样继承自StatelessWidget。
class Page2 extends StatelessWidget {
// 使用const构造函数。
const Page2({super.key});
// build方法定义了Page2的UI结构。
@override
Widget build(BuildContext context) {
// 返回Scaffold组件,其中包含一个AppBar和一个居中的文本“Page 2”。
return Scaffold(
appBar: AppBar(),
body: const Center(
// 使用const因为Text组件的内容在构建后不会改变。
child: Text('Page 2'),
),
);
}
}
整体看下看作为UI 组件的代码逻辑,各个平台都大同小异,因为本质上都是UI功能的封装,
贴一个SwiftUI 的类似功能的示例代码:
在 SwiftUI 中实现页面切换并加入动画效果,你可以使用
NavigationView、NavigationLink或NavigationStack(iOS 16+)来管理导航,并使用withAnimation或transition来添加动画效果。下面是一个简单的例子,展示了一个包含 "Go!" 按钮的主页路由,以及一个显示 "Page 2 的标题" 的第二个路由,并在页面切换时添加动画效果。
import SwiftUI
struct ContentView: View {
@State private var isPresentingSecondPage = false
var body: some View {
NavigationStack {
HomePage(isPresentingSecondPage: $isPresentingSecondPage)
.navigationDestination(for: IsPresentingSecondPage.self) { isPresentingin
if isPresenting {
SecondPage()
.transition(.move(along: .horizontal))
.animation(.spring())
}
}
}
}
}
enum IsPresentingSecondPage: Hashable {
case presenting
case notPresenting
}
struct HomePage: View {
@Binding var isPresentingSecondPage: Bool
var body: some View {
VStack {
Text("Home Page")
.font(.largeTitle)
.padding()
Button("Go!") {
withAnimation(.spring()) {
isPresentingSecondPage = true
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
struct SecondPage: View {
var body: some View {
VStack {
Text("Page 2 的标题")
.font(.largeTitle)
.padding()
// 添加一个返回按钮(如果需要)
Button("Go Back") {
NavigationStack.pop()
}
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
同样,我再使用鸿蒙实现上述push页面的功能:
在鸿蒙(HarmonyOS)开发中,实现页面切换并加入动画效果,你可以使用ArkUI框架中的页面导航组件和动画API。以下是一个简单的示例,展示了一个包含"Go!"按钮的主页路由,以及一个显示"Page 2 的标题"的第二个路由,并在页面切换时添加动画效果。
主页路由(HomePage.ets)
@Entry
@Component
struct HomePage {
@State goToPage2: boolean = false
build() {
Column() {
Text('Home Page')
.fontSize(24)
.textAlign(TextAlign.Center)
.padding(20)
Button('Go!')
.onClick(() => {
this.goToPage2 = true
router.push({
uri: 'pages/Page2/Page2',
params: {
// 可以传递参数,如果需要的话
},
options: {
animation: {
type: AnimationType.Slide,
direction: AnimationDirection.RightToLeft,
duration: 300 // 动画持续时间,单位为毫秒
}
}
})
})
.padding(20)
.margin({ top: '20vp' })
}
}
}
第二个路由(Page2.ets)
@Entry
@Component
struct Page2 {
build() {
Column() {
Text('Page 2 的标题')
.fontSize(24)
.textAlign(TextAlign.Center)
.padding(20)
// 你可以在这里添加返回按钮或其他UI元素
// 注意:鸿蒙系统通常会在页面顶部自动添加一个返回按钮(如果适用)
}
}
}
// 如果需要处理从主页传递过来的参数,可以在这里添加逻辑
// 例如,通过this.$route.params访问参数
以上SwiftUI 和 鸿蒙ArkUI 示例仅供我对比,不进行解释,通过以上UI 对比,我对Flutter UI学习心里更有底了。
继续回到开头Flutter 代码示例:
三、了解 Flutter Widget
Flutter Widget是Flutter框架中用于构建用户界面的基本构建模块。以下是对Flutter Widget搭建UI的详细介绍:
1、Flutter Widget的基本概念
-
定义:Widget是Flutter中定义和构建用户界面的基本单元,相当于其他UI框架中的“控件”或“组件”。无论是简单的文本、按钮、图标,还是复杂的布局、列表、滑动容器,甚至包括动画效果、手势处理、主题样式等,一切都是以Widget的形式来实现和组织的。
-
特性:
- 不可变性:Widget是不可变的,即它们的属性在创建后不能更改。如果需要更新Widget,则需要创建一个新的Widget实例。
- 轻量级:Widget是轻量级的,它们只是描述了UI的结构和外观,而不包含任何状态或行为。
- 组合性:Widget可以通过嵌套和组合来创建更复杂的UI组件。
2、Flutter Widget的类型
Flutter中的Widget主要分为两种类型:StatelessWidget和StatefulWidget。
- StatelessWidget:无状态Widget,适用于不需要维护内部状态或不随时间变化的UI元素。一旦创建,其属性和外观就不会改变。
- StatefulWidget:有状态Widget,用于需要维护内部状态并在状态变化时触发界面更新的场景。例如,一个计数器组件,其计数值随着用户的点击操作而增加。
3、Flutter Widget的层次结构
Widget之间可以通过嵌套形成树状结构,反映UI的层级关系。父Widget可以包含多个子Widget,而每个子Widget又可以有自己的子Widget。这种结构使得布局、样式传递以及事件响应能够沿着树形结构进行管理和传递。
4、Flutter Widget的构建过程
- 定义Widget:通过定义一个或多个Widget类来描述UI的结构和外观。
- 构建Widget树:在应用的根Widget(通常由
runApp函数指定)中,通过嵌套和组合其他Widget来构建完整的Widget树。 - 渲染Widget树:Flutter框架会遍历Widget树,并根据每个Widget的描述来渲染UI。当Widget的状态发生变化时,Flutter框架会自动计算出差异并更新界面,无需手动管理界面刷新过程。
5、Flutter Widget的常用组件
Flutter提供了许多内置的Widget,用于构建各种不同的界面元素。以下是一些常用的Widget:
-
页面元素
-
runApp 函数是Flutter应用的入口点。它接受一个
Widget作为参数,并将这个Widget设置为整个Widget树的根。例如:
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized().attachRootWidget(app).scheduleWarmUpFrame(); } -
Widget 是Flutter中用于描述用户界面的基本构建块。每个
Widget都描述了界面的一部分,并可以包含其他Widget,是不可变的,当它们的属性发生变化时,Flutter会创建新的Widget来替换旧的 -
build 方法是每个
Widget类必须实现的方法。它返回一个Widget,描述了该Widget的当前状态。在Flutter中,
Widget树是通过递归调用每个Widget的build方法来构建的 -
StatelessWidget 是一个基础类,用于构建不可变的用户界面。
一旦创建,
StatelessWidget的状态就不会再改变。因此,它们通常用于展示静态内容,如文本、图像或图标。例如:
Text、Icon、ImageIcon、Dialog等都是常见的StatelessWidget。 -
StatefulWidget 与
StatelessWidget不同,StatefulWidget是可变的。它们的状态可以在运行时改变,并触发界面的重新构建。StatefulWidget通过实现State<T>类来管理其状态。当状态发生变化时,需要调用setState方法来通知Flutter框架,以便重新构建界面。例如:
Checkbox、Radio、Slider、InkWell、Form、TextField等都是常见的StatefulWidget。 -
body 和child
body和child是Flutter中一些布局Widget的属性,用于指定其子Widget。例如,在
Scaffold中,body属性用于指定页面的主要内容。而在一些容器类Widget中,如Container、Column、Row等,child或children属性用于指定其子Widget。 -
onPressed
onPressed是一个回调函数属性,通常用于处理按钮点击事件。当用户点击按钮时,
onPressed会被调用,并可以执行一些操作,如打开对话框、导航到另一个页面等 -
Navigator
Navigator是Flutter中用于实现页面导航的组件。它管理着一个路由栈,允许用户在应用的不同页面之间切换。Navigator提供了多种方法来实现页面跳转,如push、pushReplacement、pop等。通过命名路由或路由表,可以更方便地在应用的不同部分之间导航。
-
-
基础Widget:
- Text:用于显示文本。
- Image:用于显示图片。
- Icon:用于显示图标,通常与
IconData类一起使用来指定要显示的图标。
-
容器组件(Container Widgets)
- Container:一个强大的容器组件,可以包含其他组件,并提供了一系列属性来控制其外观和布局,如背景色、边框、阴影、内外边距等。
- Padding和Margin:这两个组件分别用于在组件内部和外部添加空白区域,以控制其子组件或相邻组件之间的间距。
- Decoration:用于设置组件的装饰效果,如边框、圆角、阴影等。
-
布局Widget:
- Row:用于在水平方向上排列子元素。
- Column:用于在垂直方向上排列子元素。
- Stack:允许子Widget堆叠,允许子组件在Z轴上重叠。
- GridView和ListView:用于展示列表或网格布局,其中
ListView可以垂直或水平滚动,而GridView则创建一个二维网格。 - Flexible和Expanded:
Flexible用于容纳多行文本或自适应高度的子组件,而Expanded则使组件尽可能地占据剩余的空间。
-
交互Widget:
- Button:用于创建按钮,可以响应用户的点击操作。
- CheckBox 和 RadioButton:用于创建复选框和单选按钮。
- Slider:用于创建滑动条,可以响应用户的拖动操作。
-
导航Widget:
- Navigator:用于管理页面导航和路由。
- TabBar 和 TabBarView:用于创建底部导航栏和与之关联的视图。
6、Flutter Widget的状态管理
状态管理是Flutter应用中的一个重要概念,它帮助开发者有效地管理应用程序的数据和UI状态,以便实现复杂的交互和数据流。在Flutter中,有多种状态管理的方法可供选择,每种方法都有其适用的场景和优势。
- 局部状态管理:通过StatefulWidget和setState方法来实现。适用于小规模的Widget和临时性状态管理。
- 全局状态管理:通过Provider、Redux等状态管理库来实现。适用于大规模应用和需要跨组件共享状态的情况。
小结:通过不断修改示例代码,观察页面变化来熟悉以上概念
四、实现“列表” 功能
4.1、展示一个基础列表
从页面只有一个按钮“go” 改为 显示一个基础列表
使用flutter标准的 ListView 构造方法非常适合只有少量数据的列表。我们还将使用内置的 ListTile widget 来给我们的条目提供可视化结构。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = '基础列表';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: ListView(
children: const <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('地图'),
),
ListTile(
leading: Icon(Icons.photo_album),
title: Text('相册'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('电话'),
),
],
),
),
);
}
}
运行效果
在线(docs.flutter.cn)运行,效果如下:
4.2、展示一个“横向”列表
在实用教程-创建一个水平滑动的列表 示例:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = '水平滑动的列表';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
height: 100,
child: ListView(
// This next line does the trick.
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 100,
color: Colors.red,
),
Container(
width: 60,
color: Colors.orange,
),
Container(
width: 90,
color: Colors.yellow,
),
Container(
width: 100,
color: Colors.green,
),
Container(
width: 160,
color: Colors.orange,
),
],
),
),
),
);
}
}
运行效果
在线(docs.flutter.cn)运行,效果如下:
4.3、展示一个“网格”列表
参考 创建一个网格列表 我展示了一个九宫格:
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = '九宫格';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 5.0, // 横向间距
mainAxisSpacing: 5.0, // 纵向间距
),
itemCount: 9,
itemBuilder: (BuildContext context, int index) {
Color randomColor = Color(
(math.Random().nextInt(0xFFFFFFFF) << 8) + 0xFF000088
).withOpacity(1.0); // 生成随机颜色
return Container(
color: randomColor,
child: Center(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headlineSmall,
),
),
margin: const EdgeInsets.all(5.0), // 外边距
);
},
),
),
);
}
}
运行效果
在线(docs.flutter.cn)运行,效果如下:
还有更多,这里不列举了。
部分概念由文心参考网络生成并整理
五、实现“Tab”功能
在Flutter中,AppBar、tabs(通常通过TabBar实现)、Scaffold、TabController和DefaultTabController是用于构建具有多页面导航功能的应用界面的关键组件。以下是对这些组件的详细介绍:
5.1. tab页一些概念
1. Scaffold
-
作用:Scaffold是Flutter中用于构建页面骨架的组件,它提供了标准的Material Design布局结构,包括标题栏(AppBar)、主体内容区域、底部导航栏等。
-
常用属性:
appBar:用于设置页面的顶部导航栏(AppBar)。body:页面的主体内容区域。bottomNavigationBar:底部的导航栏。drawer:侧拉导航菜单。
2. AppBar
-
作用:AppBar是Scaffold组件中用于设置顶部导航栏的组件。
-
常用属性:
title:导航栏的标题,通常显示为Text组件,但也可以是其他Widget。backgroundColor:导航栏的背景颜色。leading:导航栏最左侧的组件,通常是返回按钮或应用的logo。actions:导航栏右侧的组件组,通常用于放置IconButton。bottom:可以设置为TabBar,以实现顶部标签页的切换。isScrollable:顶部TabBar是否可以滚动。
3. TabBar
-
作用:TabBar是用于显示一行标签(Tab)的组件,用户可以通过点击标签来切换不同的页面或视图。
-
常用属性:
tabs:一个包含多个Tab对象的列表,用于定义标签页的内容。controller:一个TabController对象,用于控制标签页的切换。isScrollable:是否允许标签页滚动。indicator及相关属性:用于自定义标签页选中时的指示器样式。
4. TabBarView
-
作用:TabBarView是与TabBar配合使用的组件,用于显示当前选中的标签页对应的内容。
-
常用属性:
children:一个Widget列表,每个Widget对应一个标签页的内容。controller:与TabBar共用的TabController对象,用于同步标签页的切换。
5. TabController
-
作用:TabController是一个控制器对象,用于在TabBar和TabBarView之间协调标签页的选择。
-
使用场景:
- 当需要动态添加或删除标签页时。
- 当需要监听标签页的变化时。
- 在更复杂的功能场景中,通常需要手动创建TabController。
6. DefaultTabController
-
作用:DefaultTabController是一个简化的TabController,它通常用于快速构建具有固定数量标签页的页面。
-
特点:
- 它不需要手动创建和释放TabController。
- 它将TabBar和TabBarView封装在一起,简化了标签页的实现。
- 它适用于功能不复杂的场景。
5.2 综合示例
以下是一个综合使用这些组件的示例代码:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3, // 标签页的数量
child: Scaffold(
appBar: AppBar(
title: Text('AppBar Tabs Demo'),
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.search), text: 'Search'),
Tab(icon: Icon(Icons.settings), text: 'Settings'),
],
),
),
body: TabBarView(
children: [
Center(child: Text('Home Page')),
Center(child: Text('Search Page')),
Center(child: Text('Settings Page')),
],
),
),
),
);
}
}
在这个示例中,我们使用了DefaultTabController来快速构建了一个具有三个标签页的页面。每个标签页都对应一个TabBar中的标签和一个TabBarView中的内容。用户可以通过点击TabBar中的标签来切换不同的页面或视图。
六、实现“导航/路由”功能
前面实现了基础列表,现在完善点击跳转到详情页的功能,这涉及到“导航/路由”功能
在Flutter中,导航和路由功能是实现页面之间跳转和管理的关键机制。以下是对Flutter导航和路由功能的详细介绍,以及一个具体的例子。
6.1 Flutter导航和路由功能介绍
1. 路由(Route)
-
定义:在Flutter中,路由(Route)通常指页面(Page),它是页面的抽象表示。路由管理和导航时,会根据特定的路由名或路由对象来决定跳转到哪个页面。
-
类型:
- 命名路由:有名字的路由,可以通过路由名字直接打开新的路由,为路由管理带来了一种直观、简单的方式。使用命名路由时,需要在应用程序的路由表中注册路由名称和对应的页面组件。
- 普通路由:直接指定要跳转的页面组件的方式,而不使用路由名称。
2. 导航(Navigation)
-
定义:导航是从一个页面跳转到另一个页面的过程。
-
核心组件:Navigator是Flutter提供的内置路由管理器,它使用栈(stack)的方式来管理页面的进出。栈模型页面导航类似于入栈(push)和出栈(pop)的操作,最新的页面会被推入栈顶,点击返回按钮时,页面从栈顶弹出,回到之前的页面。
-
常用方法:
- Navigator.push:将一个新的页面压入栈中,显示该页面。
- Navigator.pop:将当前页面从栈中弹出,返回到上一个页面。
- Navigator.pushReplacement:将当前页面替换为一个新的页面,并清除当前页面。
- Navigator.pushNamed和Navigator.popNamed:使用命名路由进行页面导航。
- Navigator.pushAndRemoveUntil:跳转到新页面,并移除直到满足条件的所有页面。
- Navigator.maybePop:尝试弹出当前页面,如果成功则返回true,否则返回false。
6.2 简单示例:导航和路由实现
以下是一个简单的Flutter应用示例,展示了如何使用Navigator进行页面跳转和管理路由。
1.代码结构
-
main.dart:应用的主入口文件,通常包含MaterialApp和初始路由设置。 -
screens/:存放应用的各个页面(Screen),每个页面通常作为一个单独的Dart文件进行管理。first_page.dart:第一个页面。second_page.dart:第二个页面。
2.代码实现
main.dart
import 'package:flutter/material.dart'; // 导入Flutter的Material Design组件库
import 'screens/first_page.dart'; // 导入FirstPage页面
import 'screens/second_page.dart'; // 导入SecondPage页面
void main() => runApp(MyApp());
// MyApp类,它继承自StatelessWidget,表示这个组件没有需要管理的状态
class MyApp extends StatelessWidget {
// build方法是StatelessWidget类必须实现的方法,它返回一个Widget,即这个组件的UI描述
@override
Widget build(BuildContext context) {
// 返回MaterialApp组件,它是Flutter应用的基础组件之一,提供了Material Design风格的UI
return MaterialApp(
// initialRoute属性设置应用的初始路由,即应用启动时首先显示的页面
initialRoute: '/',
// routes属性是一个Map,它定义了应用中所有可能的路由及其对应的组件
// 当Navigator接收到一个路由请求时,它会在这个Map中查找对应的组件并显示
routes: {
// '/'路由对应的组件是FirstPage
'/': (context) => FirstPage(),
// '/second'路由对应的组件是SecondPage
'/second': (context) => SecondPage(),
},
// 注意:这里省略了其他可能的MaterialApp属性,如theme、home等
// 因为在这个例子中,我们使用了routes属性来定义所有路由,所以不需要home属性
);
}
}
first_page.dart
import 'package:flutter/material.dart';
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: Text('Go to Second Page'),
),
),
);
}
}
second_page.dart
import 'package:flutter/material.dart';
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back to First Page'),
),
),
);
}
}
3.功能说明
-
main.dart:
- 定义了应用的入口
MyApp,它是一个StatelessWidget。 - 在
MaterialApp中设置了初始路由'/',并定义了路由表,将'/'映射到FirstPage,将'/second'映射到SecondPage。
- 定义了应用的入口
-
first_page.dart:
- 定义了第一个页面
FirstPage,它是一个StatelessWidget。 - 在
Scaffold的body中放置了一个按钮,当按钮被点击时,使用Navigator.pushNamed方法跳转到'/second'路由,即SecondPage。
- 定义了第一个页面
-
second_page.dart:
- 定义了第二个页面
SecondPage,它是一个StatelessWidget。 - 在
Scaffold的body中放置了一个按钮,当按钮被点击时,使用Navigator.pop方法返回上一个页面,即FirstPage。
- 定义了第二个页面
通过上述代码,我们实现了一个简单的Flutter应用,其中包含了两个页面,并能够通过按钮点击在它们之间进行导航和路由跳转。
至此我大致了解了实现一个简单tab页,以及页面间跳转
6.3 路由设计
在Flutter中,实现最优的路由方案需要考虑多个因素,包括性能、灵活性、可维护性以及用户体验。以下是一些建议,可以帮助你设计出高效的Flutter路由方案:
1. 使用Flutter内置的路由系统
Flutter提供了内置的路由系统,通过Navigator和Route这两个核心概念来管理页面导航。Navigator是一个堆栈管理器,用于管理应用中的路由栈,而Route则表示导航堆栈中的一个页面。
- 基本路由:你可以使用
Navigator.push和Navigator.pop等方法在路由栈上进行页面的添加和移除。 - 命名路由:通过给路由起一个名字,你可以使用
Navigator.pushNamed和Navigator.popNamed等方法进行页面跳转,这种方式使得页面跳转更为直观和易于管理。
2. 路由表的设计
为了管理多个路由,你可以定义一个路由表(routing table),它是一个Map,将路由名称映射到对应的组件构建函数。这样,你就可以通过路由名称来查找和构建对应的页面组件。
3. 参数传递与状态管理
- 参数传递:在Flutter路由中,你可以通过路由参数来传递数据。例如,在命名路由中,你可以使用
arguments参数来传递数据到目标页面。 - 状态管理:对于复杂的应用,你可能需要使用状态管理库(如Provider、Riverpod、MobX等)来管理全局状态。这些库可以帮助你在不同的页面和组件之间共享和更新状态。
4. 路由守卫与权限管理
在某些情况下,你可能需要在页面跳转之前进行权限检查或数据验证。为此,你可以实现路由守卫(route guards),在路由跳转之前执行一些逻辑来判断是否允许跳转。
5. 深度链接与全局导航
深度链接(deep linking)允许用户从应用的外部链接直接跳转到应用内的特定页面。为了实现深度链接,你需要在路由表中定义与URL路径对应的路由,并在应用启动时解析URL以进行页面跳转。
全局导航则涉及到在整个应用中维护一致的导航体验。你可以通过定义统一的导航条、页面切换动画等方式来提升用户体验。
6. 多引擎路由方案(针对复杂应用)
对于特别复杂的应用,特别是那些需要在Flutter页面和原生页面之间频繁切换的应用,你可以考虑使用多引擎路由方案。这种方案允许你为每个页面或模块创建独立的Flutter引擎实例,从而实现更灵活的路由管理和状态隔离。然而,需要注意的是,多引擎方案可能会增加应用的资源消耗和复杂性。
7. 路由解析与动态路由
在处理外部传入的URI时,你需要进行路由解析以获取路由名称和参数。Flutter提供了强大的路由解析工具,你可以使用这些工具来解析URI并执行相应的页面跳转。此外,你还可以实现动态路由,根据用户的操作或数据变化来动态地添加或删除路由。
8. 路由表的注册
//注册路由表
routes:{
"new_page":(context) => EchoRoute(),
},
在路由⻚通过 RouteSetting 对象获取路由参数:
class EchoRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
//获取路由参数
var args=ModalRoute.of(context).settings.arguments; //...省略无关代码
}
}
在打开路由时传递参数
Navigator.of(context).pushNamed("new_page", arguments: "hi");
适配 假设我们也想将上面路由传参示例中的 TipRoute 路由⻚注册到路由表中,以便也可以通过路由名来打开它。 但是,由于TipRoute接受一个text 参数,在不改变TipRoute源码的前提下适配这种情况:
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context).settings.arguments);
},
},
9. 路由生成钩子
背景
在开发电商APP时,我们希望实现以下功能:
- 用户未登录时,可以查看店铺、商品等公开信息。
- 用户需要登录后才能查看交易记录、购物车、个人信息等隐私页面。
问题
每次打开路由前判断用户登录状态会非常繁琐。
解决方案
使用 MaterialApp 的 onGenerateRoute 属性,在打开命名路由时进行统一的权限控制。
原理
onGenerateRoute回调在尝试打开命名路由时可能会被调用。- 如果指定的路由名在路由表中已注册,则调用路由表中的
builder函数生成路由组件。 - 如果路由表中没有注册,才会调用
onGenerateRoute来生成路由。
回调签名
Route<dynamic> Function(RouteSettings settings)
实现步骤
- 放弃使用路由表,转而使用
onGenerateRoute回调。 - 在
onGenerateRoute回调中进行统一的权限控制。
示例代码
MaterialApp(
... // 省略无关代码
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (context) {
String routeName = settings.name;
// 判断当前路由是否需要登录权限
if (/* 路由需要登录且当前未登录 */) {
// 返回登录页面路由,引导用户登录
return LoginPage();
} else {
// 根据路由名返回相应的页面
if (routeName == 'home') {
return HomePage();
} else if (routeName == 'product') {
return ProductPage();
} else if (routeName == 'user_info') {
return UserInfoPage();
}
// 可以继续添加其他路由判断
// 默认返回未找到的页面(可选)
return NotFoundPage();
}
}
);
}
);
注意事项
onGenerateRoute只会对命名路由生效。- 在实际项目中,需要完善权限判断逻辑,例如通过用户状态管理(如
Provider、Riverpod等)来判断用户是否登录。
通过这种方式,我们能够在不增加每个路由判断逻辑的情况下,实现统一的页面权限控制,提高代码的可维护性和可读性
10. 第三方路由方案参考
在Flutter应用开发中,除了使用Flutter官方提供的路由解决方案外,还可以选择多种优秀的第三方路由方案来满足不同项目的需求。以下是一些值得推荐的Flutter第三方路由方案:
1. go_router
-
特点:
- 提供了灵活的路由定义和管理方式,支持URL格式的自定义和URL跳转。
- 可以处理深度链接,适用于Web和App应用。
- 支持模板语法解析路由路径和查询参数。
- 支持单个目标路由展示多个页面(子路由)。
- 提供重定向功能,可以基于应用状态跳转到不同的URL。
- 支持嵌套的Tab导航(使用StatefulShellRoute)。
- 兼容Navigator API,易于集成和迁移。
-
使用:
- 在
pubspec.yaml文件中添加go_router依赖。 - 在应用中配置
GoRouter,包括定义初始位置、路由等。 - 使用
MaterialApp.router或CupertinoApp.router构造函数,将GoRouter配置对象传递给routerConfig参数。 - 通过
GoRoute对象配置路由参数,包括路径参数和查询参数。 - 使用
context.go()或context.goNamed()方法进行页面跳转。
- 在
2. Fluro
-
特点:
- 层次分明、条理化,方便扩展和整体管理路由。
- 适合中大型项目,提供了清晰的路由配置和跳转方式。
- 使用
RouteTree存储已定义的路由,通过define方法注册路由。 - 提供了
navigateTo方法进行页面跳转,支持自定义过渡效果。
-
使用:
- 在
pubspec.yaml文件中添加Fluro依赖。 - 初始化
Router对象。 - 使用
define方法注册路由,包括路由路径和处理器(Handler)。 - 在需要跳转的地方使用
Application.router.navigateTo()方法进行跳转。
- 在
3. GetX(包含路由管理功能)
-
特点:
- 是一个快速、轻量级的状态管理和路由管理库。
- 提供了依赖注入、路由管理、国际化、主题切换等便利功能。
- 语法简洁,性能优秀,适合构建中小型应用。
- 路由管理部分与状态管理部分紧密集成,提供了便捷的状态和路由管理体验。
-
使用:
- 在
pubspec.yaml文件中添加get依赖。 - 使用
Get.to()或Get.toNamed()方法进行页面跳转。 - 可以通过
Get.find<>()方法获取状态管理对象,实现状态共享和管理。
- 在
4. BeeRouter
-
特点:
- 提供了简洁明了的路由配置方式。
- 支持路径参数和查询参数的传递。
- 支持嵌套路由和子路由。
- 提供了全局和局部的路由守卫功能,可以在路由跳转前后执行特定逻辑。
-
使用:
- 需要在
pubspec.yaml文件中添加BeeRouter的依赖。 - 在应用中配置BeeRouter,包括定义路由、设置初始页面等。
- 使用BeeRouter提供的API进行页面跳转和参数传递。
- 需要在
5. VRouter
-
特点:
- 提供了强大的路由管理功能,包括嵌套路由、路径参数、查询参数等。
- 支持动态路由和静态路由的混合使用。
- 提供了全局和局部的导航守卫,可以在导航前后执行自定义逻辑。
- 支持多语言路由,可以根据语言动态调整路由路径。
-
使用:
- 在
pubspec.yaml文件中添加VRouter的依赖。 - 配置VRouter,包括定义路由、设置导航守卫等。
- 使用VRouter提供的API进行页面跳转和参数传递。
- 在
6. Beamer
-
特点:
- 提供了基于URL的声明式路由管理。
- 支持路径参数、查询参数和嵌套路由。
- 提供了路由状态管理功能,可以方便地跟踪当前路由和导航历史。
- 提供了强大的错误处理和恢复机制,可以在路由错误时执行自定义逻辑。
-
使用:
- 在
pubspec.yaml文件中添加Beamer的依赖。 - 配置Beamer,包括定义路由、设置状态管理等。
- 使用Beamer提供的API进行页面跳转和参数传递。
- 在
七、页面之间传值
在Flutter中,页面之间传值有多种方法,每种方法都有其适用的场景和优缺点。以下是一些常用的传值方法及其推荐理由:
7.1. 构造函数参数传递
-
实现方式:在目标页面的构造函数中定义参数,并在导航到该页面时将值传递给构造函数。
-
优点:
- 直观易懂,符合面向对象编程的常规做法。
- 适用于传递简单的数据类型,如字符串、数字等。
-
缺点:
- 当需要传递的数据较多或类型复杂时,构造函数可能会变得臃肿。
-
代码示例:
假设我们有两个页面,FirstPage和SecondPage。我们希望在FirstPage中点击一个按钮后,跳转到SecondPage并传递一个字符串值。
// FirstPage.dart
import 'package:flutter/material.dart';
import 'SecondPage.dart';
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(value: 'Hello from FirstPage!'),
),
);
},
child: Text('Go to Second Page'),
),
),
);
}
}
// SecondPage.dart
import 'package:flutter/material.dart';
class SecondPage extends StatelessWidget {
final String value;
SecondPage({required this.value});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text(value),
),
);
}
}
7.2. 路由参数传递
-
实现方式:使用
Navigator.push或Navigator.pushNamed方法时,在RouteSettings的arguments参数中传递数据。在目标页面中,使用ModalRoute.of(context).settings.arguments来获取传递的参数。 -
优点:
- 可以在不修改目标页面构造函数的情况下传递数据。
- 适用于传递任意类型的对象。
-
缺点:
- 需要手动从
ModalRoute中提取参数,可能会增加一些样板代码。
- 需要手动从
-
代码示例:
在这个例子中,我们同样使用FirstPage和SecondPage,但这次我们使用命名路由和路由参数来传递值。
// main.dart
import 'package:flutter/material.dart';
import 'FirstPage.dart';
import 'SecondPage.dart';
void main() {
runApp(MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FirstPage(),
'/second': (context) => SecondPage(),
},
));
}
// FirstPage.dart
import 'package:flutter/material.dart';
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/second',
arguments: 'Hello from FirstPage with Named Route!',
);
},
child: Text('Go to Second Page with Named Route'),
),
),
);
}
}
// SecondPage.dart
import 'package:flutter/material.dart';
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final String value = ModalRoute.of(context)!.settings.arguments as String;
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text(value),
),
);
}
}
7.3. 状态管理工具
-
实现方式:使用Flutter的状态管理工具(如Provider、GetX、Riverpod等)来共享和传递值。这些工具可以在应用程序的不同页面之间共享状态,并在需要时更新值。
-
优点:
- 实现了全局状态管理,方便在不同页面之间共享数据。
- 支持数据的实时更新和同步。
-
缺点:
- 需要学习并掌握状态管理工具的用法。
- 可能会增加项目的复杂性和代码量。
-
代码示例:
在这个例子中,我们将使用Provider库来管理全局状态,并在页面之间共享这个状态。
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'FirstPage.dart';
import 'SecondPage.dart';
import 'my_provider.dart'; // 假设我们在这里定义了MyProvider
void main() {
runApp(
MultiProvider(
providers: [
Provider<MyProvider>(create: (_) => MyProvider()),
],
child: MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FirstPage(),
'/second': (context) => SecondPage(),
},
),
),
);
}
// my_provider.dart
import 'package:flutter/material.dart';
class MyData {
String value = 'Initial Value';
}
class MyProvider with ChangeNotifier {
MyData _data = MyData();
MyData get data => _data;
void setValue(String newValue) {
_data.value = newValue;
notifyListeners();
}
}
// FirstPage.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myProvider = Provider.of<MyProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('First Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Current Value: ${myProvider.data.value}'),
ElevatedButton(
onPressed: () {
myProvider.setValue('New Value from FirstPage');
Navigator.pushNamed(context, '/second');
},
child: Text('Update Value and Go to Second Page'),
),
],
),
),
);
}
}
// SecondPage.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myProvider = Provider.of<MyProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text('Current Value: ${myProvider.data.value}'),
),
);
}
}
- 代码示例: 使用 Riverpod 状态管理
Riverpod 是 Provider 的一个更现代、更灵活的替代品,它提供了更好的依赖注入和响应式状态管理。
import 'package:flutter_riverpod/flutter_riverpod.dart';
final myProvider = StateProvider<String>((ref) {
return "Initial Value";
});
// 在 MaterialApp 外部创建 ProviderContainer
void main() {
runApp(
ProviderScope(
overrides: [myProvider.overrideWithValue("Overridden Initial Value")],
child: MyApp(),
),
);
}
// 在小部件中使用
class SomeWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(myProvider);
return Text(value);
}
}
- 代码示例:使用 GetX 状态管理
GetX 是一个全面的 Flutter 框架,它提供了状态管理、依赖注入和路由功能。
import 'package:get/get.dart';
class MyController extends GetxController {
var someValue = "Initial Value".obs; // 创建一个可观察变量
}
// 在 MaterialApp 外部初始化控制器(可选,但推荐)
void main() {
Get.put(MyController());
runApp(MyApp());
}
// 在小部件中使用
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find<MyController>();
return Obx(() => Text(controller.someValue.value)); // 使用 Obx 来监听变化
}
}
7.4. 全局变量
-
实现方式:在Flutter应用程序的顶层定义全局变量,并在不同的屏幕中访问和修改这些变量。
-
优点:
- 适用于需要在整个应用程序中共享的数据。
- 简单易用,不需要额外的状态管理工具。
-
缺点:
- 全局变量可能导致代码难以维护和理解。
- 容易出现数据竞争和状态不一致的问题。
-
代码示例: 使用单例模式
单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。你可以创建一个包含全局变量的类,并将其实现为单例。
class GlobalVariables {
static let instance = GlobalVariables()
private init() {} // 私有构造函数,防止外部实例化
var someValue: String?
}
// 使用
GlobalVariables.instance.someValue = "Hello, World!"
let value = GlobalVariables.instance.someValue
注意:在 Dart 和 Flutter 中,通常使用静态变量和函数来模拟单例模式,因为 Dart 没有真正的单例构造函数概念。上面的代码是 Swift 的示例,用于说明单例的概念。在 Dart 中,你可以这样做:
class GlobalVariables {
static String? someValue;
}
// 使用
GlobalVariables.someValue = "Hello, World!";
let value = GlobalVariables.someValue;
7.5. 数据库
-
实现方式:使用Flutter的数据库插件(如sqflite、moor等)在屏幕之间传递和存储数据。
-
优点:
- 适用于需要持久化存储和查询数据的场景。
- 可以在不同页面之间共享数据,而无需担心数据丢失。
-
缺点:
- 需要学习并掌握数据库的使用和查询语法。
- 可能会增加项目的复杂性和性能开销。
7.6 小结
对于大多数Flutter应用程序来说,推荐使用路由参数传递或状态管理工具进行页面之间的传值。这两种方法都具有较高的灵活性和可扩展性,可以满足大多数场景的需求。
- 如果需要传递的数据类型简单且数量较少,可以选择路由参数传递。
- 如果需要跨页面共享和管理复杂的状态,或者需要实现数据的实时更新和同步,建议选择状态管理工具。
八、Deep Links
Flutter Deep Links(深度链接)是一种特殊类型的链接,它允许用户直接打开应用程序中的特定页面或执行特定操作,而无需通过应用程序的主屏幕或导航菜单进行手动搜索。以下是关于Flutter Deep Links的详细介绍:
8.1、作用与优势
- 直接访问特定页面:用户点击深度链接后,可以直接跳转到应用程序内的指定页面,提高了用户体验和转化率。
- 跨平台支持:Flutter Deep Links支持在iOS、Android和Web浏览器上使用,实现跨平台的无缝链接。
- 参数传递:深度链接中可以包含参数,用于传递用户信息、页面状态等,便于应用程序进行个性化展示和处理。
8.2、组成部分
Flutter Deep Links通常由以下几个部分组成:
- Schema:指定要打开的应用程序或资源的类型和协议,如
https、myapp等。 - Host:指定资源所在的位置或域名,在Flutter应用程序中通常用于标识应用程序本身。
- Path:指定应用程序内具体页面的路径或位置。
- Query Parameter(可选):用于传递额外的查询参数或排序条件。
- Port(可选):一般用于访问后端服务时指定端口号,但在深度链接中较少使用。
- Fragment(可选):也称为锚点,用于直接跳转到网页或应用程序内页面的某个部分。
8.3、自定义模式与安全性
- 自定义模式:开发者可以自定义Schema和Host等部分,创建符合自己应用程序需求的深度链接格式。但需要注意安全性问题,防止恶意用户利用自定义模式进行非法访问。
- HTTPS模式:使用HTTPS协议创建的深度链接相对更安全,因为需要后端验证和应用程序的配合才能识别和处理。
84、配置与实现
-
Android配置:
- 在
AndroidManifest.xml文件中为需要处理深度链接的Activity添加<intent-filter>标签。 - 指定Schema、Host、Path等必要信息。
- 可以使用
path、pathPattern和pathPrefix等属性来匹配不同的路径模式。
- 在
-
iOS配置:
- 在
Info.plist文件中添加CFBundleURLTypes数组,指定自定义的URL Scheme。 - 对于Universal Links,需要在
apple-app-site-association文件中声明应用程序支持的域名和路径。 - 在iOS项目中配置相关的entitlements和provisioning profiles。
- 在
-
Flutter实现:
- 使用Flutter提供的路由系统(如
Navigator)来处理深度链接的跳转。 - 可以使用第三方库(如
uni_links)来更方便地获取和处理深度链接。 - 在应用程序启动时检查是否有深度链接传入,并根据需要进行处理。
- 使用Flutter提供的路由系统(如
8.5、应用场景
- H5唤醒APP:通过短信、邮件或社交媒体等渠道分享深度链接,用户点击后可以直接打开应用程序并跳转到指定页面。
- 其他APP跳转:其他应用程序可以通过深度链接直接跳转到本应用程序的特定页面,实现跨应用程序的导航和交互。
- 个性化推荐:根据用户的兴趣和行为数据,生成个性化的深度链接并推送给用户,提高用户粘性和转化率。
综上所述,Flutter Deep Links是一种强大的功能,可以帮助开发者实现跨平台的无缝链接和个性化推荐。但在使用过程中需要注意安全性和配置的正确性,以确保用户能够顺利访问到指定的页面或功能。
九、pubspec 管理
1、包、资源管理示例
以下是一个简单的pubspec.yaml文件示例,展示了如何在Flutter项目中使用Pub进行包管理:
name: my_flutter_app
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# 依赖pub仓库版本
cupertino_icons: ^1.0.0
http: ^0.13.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/fonts/
fonts:
- family: Montserrat
fonts:
- asset: assets/fonts/Montserrat-Regular.ttf
- asset: assets/fonts/Montserrat-Bold.ttf
weight: 700
在这个示例中,我们定义了一个名为my_flutter_app的Flutter项目,并添加了flutter、cupertino_icons和http三个普通依赖项,以及一个flutter_test开发依赖项。同时,我们还在flutter字段中指定了项目的资源文件(图像和字体)的配置。
2、Flutter包管理
Flutter使用一种高效的包管理系统来管理和依赖项目的外部库和插件。这个系统基于pubspec.yaml文件,该文件位于项目的根目录下,并包含了项目的各种依赖项和配置信息。
- 依赖项声明:在
pubspec.yaml文件中,开发者可以声明项目所需的依赖项,包括普通依赖(在运行时需要的库)和开发依赖(仅在开发过程中需要的库,如测试框架)。 - 版本管理:每个依赖项都可以指定一个版本号,Flutter包管理系统会确保下载并安装与指定版本兼容的包。同时,它支持版本约束和范围,允许开发者指定接受哪些版本的更新。
- 包获取与更新:通过运行
flutter pub get命令,Flutter会下载并安装项目中声明的所有依赖项。当依赖项有新版本发布时,开发者可以使用flutter pub upgrade命令来更新依赖项。
Pub
Pub是Dart编程语言的包管理器,也是Flutter项目的核心依赖管理工具。它提供了一个集中的仓库,开发者可以在其中查找、下载和发布Dart包。
- 包仓库:Pub维护了一个包含大量Dart包的仓库,这些包涵盖了从基础库到特定功能的插件。
- 包发布与查找:开发者可以将自己开发的Dart包发布到Pub仓库中,供其他开发者使用。同时,他们也可以在Pub仓库中查找并使用其他开发者发布的包。
- 版本控制:Pub支持对包进行版本控制,允许开发者发布新版本的包,并管理旧版本的兼容性。
关键词解析
- Flutter:一种开源的UI软件开发工具包,用于在iOS、Android、Web、Windows、Mac以及Linux等平台上构建美观的原生用户界面。
- 包管理:指对项目中使用的外部库和插件进行管理和依赖管理的过程,包括声明依赖项、下载和安装依赖项、更新依赖项等。
- Pub:Dart语言的包管理器,为Flutter项目提供核心依赖管理工具,支持包的查找、下载、发布和版本控制等功能。
- pubspec.yaml:Flutter项目的配置文件,用于声明项目的依赖项、资源、配置信息等。
依赖方式
dependencies:
flutter:
sdk: flutter
# 1、依赖pub仓库版本
cupertino_icons: ^1.0.0
http: ^0.13.3
# 2、依赖本地包
pkg1:
path: ../../code/pkg1
# 3、依赖Git:你也可以依赖存储在Git仓库中的包
pkg2:
git:
url: git://github.com/xxx/pkg1.git
# 4、上面假定包位于Git存储库的根目录中。如果不是这种情况,
# 可以使用path参数指定相对位置:
pkg3:
git:
url: git://github.com/flutter/packages.git
path: packages/package1
3、资源管理
Flutter APP安装包包含代码和assets(资源)两部分。Assets在构建时被打包进程序安装包中,供运行时访问,包括静态数据(如JSON文件)、配置文件、图标、图片(JPEG、WebP、GIF等)等。
指定Assets:
Flutter使用pubspec.yaml文件管理应用程序资源。通过在flutter部分的assets字段下列出文件路径,可以指定要包含在应用程序中的资源。资源路径是相对于pubspec.yaml文件的。
assets:
- assets/my_icon.png
- assets/background.png
Asset变体:
构建过程支持“asset变体”概念,允许在不同上下文中显示不同版本的资源。在指定asset路径时,构建过程会在相邻子目录中查找同名文件,并将它们一起包含在资源包中。主资源和变体资源都会被打包。
.../graphics/background.png
assets:
- graphics/background.png
加载Assets:
- 加载文本资源:通过
rootBundle或DefaultAssetBundle加载字符串资源。 - 加载图片资源:使用
AssetImage类加载图片,或通过Image.asset()方法直接获取显示图片的widget。Flutter支持根据设备像素比选择最接近分辨率的图片。
# 多倍图资源
.../my_icon.png
.../2.0x/my_icon.png
.../3.0x/my_icon.png
AssetImage加载图片
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('graphics/background.png'),
),
), );
}
可以使用 Image.asset() 直接得到一个显示图片的widget:
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}
依赖包中的资源:
要加载依赖包中的资源,必须在AssetImage或Image.asset()方法中提供package参数,指定资源所属的包名。
# 使用AssetImage
new AssetImage('icons/heart.png', package: 'my_icons')
# Image.asset
new Image.asset('icons/heart.png', package: 'my_icons')
包也可以选择在其 lib/ 文件夹中包含未在其 pubspec.yaml 文件中声明的资源。 在这种情况下,对于 要打包的图片,应用程序必须在 pubspec.yaml 中指定包含哪些图像。
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
特定平台Assets:
Flutter应用中的资源仅在Flutter框架运行后可用。对于APP图标和启动图等特定平台资源,需要按照Android或iOS的原生方式设置。
- Android:在
android/app/src/main/res目录中替换占位符图像,并更新AndroidManifest.xml中的android:icon属性。 - iOS:在
ios/Runner目录中的Assets.xcassets/AppIcon.appiconset替换占位符图片,保留原始文件名。
更新启动页:
Flutter使用本地平台机制绘制启动页,直到Flutter渲染应用程序的第一帧。可以通过自定义drawable(Android)或storyboard(iOS)来添加自定义启动界面。
十、Flutter异常捕获
Flutter异常捕获必须先了解一下Dart单线程模型
1. Dart单线程模型
-
Java和Objective-C的对比:
- 在Java和Objective-C中,如果程序发生异常且未被捕获,程序将会终止。
- 这是因为Java和Objective-C是多线程模型,任意一个线程触发未捕获的异常会导致整个进程退出。
-
Dart和JavaScript的对比:
- Dart和JavaScript是单线程模型,程序不会因为一个未捕获的异常而终止。
- Dart通过消息循环机制运行,包含“微任务队列”(microtask queue)和“事件队列”(event queue)。
-
Dart线程运行过程:
- 入口函数
main()执行完后,消息循环机制启动。 - 微任务队列的优先级高于事件队列,按照先进先出的顺序执行。
- 在事件任务执行过程中可以插入新的微任务和事件任务,形成循环,不会退出。
- 入口函数
-
任务类型:
- 外部事件任务(如IO、计时器、点击、绘制事件)在事件队列中。
- 微任务通常来源于Dart内部,通过
Future.microtask(...)方法插入。
-
异常处理:
- 当某个任务发生异常且未被捕获时,程序不会退出,但当前任务的后续代码不会执行。
- 一个任务中的异常不会影响其他任务执行。
2. Flutter异常捕获
-
Dart中的异常捕获:
- 使用
try/catch/finally来捕获代码块异常,与其他编程语言类似。
- 使用
-
Flutter框架异常捕获:
- Flutter框架在很多关键方法上进行了异常捕获。
- 例如,当布局发生越界或不合规范时,Flutter会自动弹出一个错误界面。
- 这是因为Flutter在执行
build方法时添加了异常捕获。
@override
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e,
stack));
}
...
}
异常捕获和上报代码大致如下:
void collectLog(String line){ ... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
... //上报错误和日志逻辑
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
...// 构建错误信息
}
void main() {
// 在发生异常时,Flutter默认的处理方式是弹一个ErrorWidget
// 进入 _debugReportException() 方法发现,错误是通过 FlutterError.reportError 方法中上报的
// 自定义错误处理回调
FlutterError.onError = (FlutterErrorDetails details) {
reportErrorAndLog(details);
};
// Dart中有一个runZoned(...) 方法,可以给执行对象指定一个Zone。
// Zone表示一个代码执行的环境 范围,为了方便理解,
// 可以理解为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以 捕获、拦截或修改一些代码行为,
// 如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时 Zone也可以捕获所有未处理的异常。
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line); // 收集日志
},
),
onError: (Object obj, StackTrace stack) {
var details = makeDetails(obj, stack);
reportErrorAndLog(details);
},
);
}