如何使用Flutter web项目,并使其成为响应式?
如何开始使用Flutter web项目,并使其成为响应式的。
Flutter网页架构
在开始项目之前,我们先来看看Flutter web的架构。
如果你对移动应用中使用的Flutter的架构不熟悉,这里简单介绍一下。

Flutter在移动应用中的架构主要包括以下三层。
- 框架:这一层是纯用Dart编写的,由Flutter的核心构件组成。
- 引擎:下面这一层主要是用C/C++编写的,使用Google的Skia图形库提供低级渲染支持。
- Embedder:这一层基本上由所有平台特定的依赖关系组成。
现在,我们来看看Flutter web的架构,以及它与此的不同之处。

Flutter web的架构只用两层来描述。
- 框架:由纯Dart代码组成。
- 浏览器:由C++和JavaScript代码组成。
正如你所看到的,最顶层(Framework)包含的组件类型与常规的Flutter架构几乎相同。
主要的区别是在Browser层。实际上,在移动架构中存在的最底层的两个独立层只被一层所取代。它没有使用Skia图形引擎(因为浏览器上不支持),而是使用了一个JavaScript引擎。Flutter web涉及到将Dart编译成JavaScript,而不是用于移动应用的ARM机器代码。它使用DOM、Canvas和CSS的组合,在浏览器中渲染Flutter组件。
入门
正如我在开头提到的,Flutter web目前处于测试阶段。因此,为了创建一个支持web的Flutter应用,你需要进入Flutter的测试频道。
要运行和调试一个Flutter web应用程序,你需要Chrome。
按照以下步骤创建一个新的Flutter web项目:
- 转到测试版频道:
flutter channel beta
复制代码
- 升级Flutter:
flutter upgrade
复制代码
- 启用Web支持:
flutter config --enable-web
复制代码
- 创建一个新的Flutter项目:
flutter create explore
复制代码
在这里,explorer是我要创建的Flutter Web应用的名字。
- 用你喜欢的IDE打开项目。
要使用VS Code打开它,可以使用这个命令。
code explore 复制代码
现在,如果你看一下目录结构,你会注意到有一个叫web的文件夹。

这意味着您的项目已正确配置为在浏览器上运行。另外,你会看到Chrome是可以作为运行Flutter应用的设备之一的。
您将获得与起始项目相同的Counter应用程序。要从VS Code中运行它,您可以使用F5
,或者您可以从终端使用此命令:
flutter run -d chrome
复制代码
目前的状态是这样的。

有了启动项目,让我们开始构建我们的网络应用。
网页界面设计
网页界面设计 我们要创建的网页界面是受Tubik在Dribbble上的例子启发。
转到lib > main.dart,然后用下面的代码替换整个代码。
// main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Explore',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
return Scaffold();
}
}
复制代码
现在,我们必须定义HomePage
小部件,它将包含应用程序的用户界面。你马上就会明白为什么我把它定义为一个有状态的小部件。
你可能已经注意到,我已经使用MediaQuery
来获取屏幕的大小。我这样做是因为我将根据屏幕的大小来调整widget的大小。这将使小组件具有响应性,并将防止在调整浏览器窗口大小时出现大部分的溢出问题。
让我们添加一个顶部栏,看起来像这样。
要做到这一点,你将需要以下代码。
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
return Scaffold(
appBar: PreferredSize(
preferredSize: Size(screenSize.width, 1000),
child: Container(
color: Colors.blue,
child: Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
Text('EXPLORE'),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {},
child: Text(
'Discover',
style: TextStyle(color: Colors.black),
),
),
SizedBox(width: screenSize.width / 20),
InkWell(
onTap: () {},
child: Text(
'Contact Us',
style: TextStyle(color: Colors.black),
),
),
],
),
),
InkWell(
onTap: () {},
child: Text(
'Sign Up',
style: TextStyle(color: Colors.black),
),
),
SizedBox(
width: screenSize.width / 50,
),
InkWell(
onTap: () {},
child: Text(
'Login',
style: TextStyle(color: Colors.black),
),
),
],
),
),
),
),
body: Container(),
);
}
}
复制代码
你会马上发现不对劲,感觉不像是一个web UI。悬停效果在哪里?
是的,Flutter组件默认是没有悬浮效果的。但我会告诉你最简单的实现方法。只是让你知道,添加了悬停效果后,会是这样的。

InkWell()
widget有一个叫做onHover
的属性,你可以用它来跟踪鼠标指针进入或离开组件边界的时间。
按照下面的步骤来获得这个效果。
- 添加一个用于跟踪悬停的booleans列表(booleans的数量是你想应用悬停效果的组件数量)。
List _isHovering = [false, false, false, false];
复制代码
- 更新组件对应的布尔值,并设置文本颜色(或根据该布尔值进行任何其他你想在悬停时显示的变化)。
InkWell(
onHover: (value) {
setState(() {
_isHovering[0] = value;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Discover',
style: TextStyle(
color: _isHovering[0]
? Colors.blue[100]
: Colors.white,
),
),
SizedBox(height: 5),
// For showing an underline on hover
Visibility(
maintainAnimation: true,
maintainState: true,
maintainSize: true,
visible: _isHovering[0],
child: Container(
height: 2,
width: 20,
color: Colors.white,
),
)
],
),
),
复制代码
如果我们在Scaffoldbody
内添加图像,它将从顶栏下方开始。但根据设计,图像应该在顶栏下方流动。

要实现这个设计其实很简单,你只需要将图片从顶栏的下方开始。
实现这样的设计其实很简单,你只需要定义Scaffold的一个名为extendBodyBehindAppBar
的属性,并将其设置为true。
Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
// ...
),
// ...
);
复制代码
您可能也注意到了,我在图片顶部的底部中心位置添加了一个快速访问栏。
要得到这样的UI,可以在Scaffold的body
中使用Stack widget。
Scaffold(
// ...
body: Stack(
children: [
Container( //图片在顶栏下方
child: SizedBox(
height: screenSize.height * 0.45,
width: screenSize.width,
child: Image.asset(
'assets/images/cover.jpg',
fit: BoxFit.cover,
),
),
),
Center(
heightFactor: 1,
child: Padding(
padding: EdgeInsets.only(
top: screenSize.height * 0.40,
left: screenSize.width / 5,
right: screenSize.width / 5,
),
child: Card( //浮动的快速访问栏
// ...
),
),
)
],
),
);
复制代码
我只是包括解释UI结构的代码。这一部分的完整UI代码可以在这里找到。本文结尾处有整个项目的链接。
现在我们将在网页中再添加一些UI元素。首先,将整个脚手架的body
包裹在一个SingleChildScrollView
小部件中,使其可以滚动。
我们将保持这个网站的简单。所以,我们只增加三个部分。
- 精选
- 目的地
- 底部信息
推荐模块
这一部分将包含一个标题和一排三张图片及其标签。它看起来像这样。

标题和描述的UI代码如下:
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'Featured',
style: GoogleFonts.montserrat(
fontSize: 40,
fontWeight: FontWeight.w500,
),
),
Expanded(
child: Text(
'Unique wildlife tours & destinations',
textAlign: TextAlign.end,
),
),
],
),
复制代码
每张图片及其标签的代码如下。
Column(
children: [
SizedBox(
height: screenSize.width / 6,
width: screenSize.width / 3.8,
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Image.asset(
'assets/images/trekking.jpg',
fit: BoxFit.cover,
),
),
),
Padding(
padding: EdgeInsets.only(
top: screenSize.height / 70,
),
child: Text(
'Trekking',
style: GoogleFonts.montserrat(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
复制代码
如果你现在尝试调整浏览器窗口的大小,你会发现它的反应已经相当灵敏。

目的地模块
在本节中,我们将添加一个图片轮播,其中有一个浮动的潜在目的地选择器,称为 "目的地多样性"。它看起来像这样。

对于轮播图,你可以使用名为carousel_slider的Flutter包。
按照下面的步骤来构建轮播:
- 定义一个图像列表和它们的标签。
final List<String> images = [
'assets/images/asia.jpg',
'assets/images/africa.jpg',
'assets/images/europe.jpg',
'assets/images/south_america.jpg',
'assets/images/australia.jpg',
'assets/images/antarctica.jpg',
];
final List<String> places = [
'ASIA',
'AFRICA',
'EUROPE',
'SOUTH AMERICA',
'AUSTRALIA',
'ANTARCTICA',
];
复制代码
- 生成一个Widget列表,以显示在旋转木马中。
List<Widget> generateImageTiles(screenSize) {
return images
.map(
(element) => ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.asset(
element,
fit: BoxFit.cover,
),
),
)
.toList();
}
复制代码
- 在
build
方法中存储widget的列表,并在旋转木马中显示它们各自的标签。
@override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
var imageSliders = generateImageTiles(screenSize);
return Stack(
children: [
CarouselSlider(
items: imageSliders,
options: CarouselOptions(
enlargeCenterPage: true,
aspectRatio: 18 / 8,
autoPlay: true,
onPageChanged: (index, reason) {
setState(() {
_current = index;
});
}),
carouselController: _controller,
),
AspectRatio(
aspectRatio: 18 / 8,
child: Center(
child: Text(
places[_current],
style: GoogleFonts.electrolize(
letterSpacing: 8,
fontSize: screenSize.width / 25,
color: Colors.white,
),
),
),
),
],
);
}
复制代码

如果你运行该应用程序,旋转木马将看起来像这样。

要添加浮动选择器,请按照以下步骤进行:
- 增加两个布尔值列表。
List _isHovering = [false, false, false, false, false, false, false];
List _isSelected = [true, false, false, false, false, false, false];
复制代码
- 修改
CarouselOptions
widget的onPageChanged
属性。
CarouselOptions(
// ...
onPageChanged: (index, reason) {
setState(() {
_current = index;
// add the following
for (int i = 0; i < imageSliders.length; i++) {
if (i == index) {
_isSelected[i] = true;
} else {
_isSelected[i] = false;
}
}
});
},
)
复制代码
- 在包含rext的
Card
内显示一行小部件,并显示一个下划线来突出显示被选中的选项。荧光笔可以这样创建。
Visibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
visible: _isSelected[i],
child: Container(
height: 5,
decoration: BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
width: screenSize.width / 10,
),
)
复制代码
浮动选择器会是这样的:

底部信息部分
这只是一个简单的信息部分,会是这样的。

这一部分的UI代码在这里。
提高响应速度
虽然网页应用的响应速度相当快,但你还是会发现一些溢出。另外,UI设计也不方便移动设备的小屏幕。为了解决这个问题,我们将使用响应式布局,根据设备屏幕大小来构建和调整widget的大小。
我们将为smallScreen
、mediumScreen
和largeScreen
设置一些断点,当达到该断点时,用新的布局重建widget。你可以使用下面的代码来实现这一点。
import 'package:flutter/material.dart';
class ResponsiveWidget extends StatelessWidget {
final Widget largeScreen;
final Widget mediumScreen;
final Widget smallScreen;
const ResponsiveWidget(
{Key key,
@required this.largeScreen,
this.mediumScreen,
this.smallScreen})
: super(key: key);
static bool isSmallScreen(BuildContext context) {
return MediaQuery.of(context).size.width < 800;
}
static bool isLargeScreen(BuildContext context) {
return MediaQuery.of(context).size.width > 1200;
}
static bool isMediumScreen(BuildContext context) {
return MediaQuery.of(context).size.width >= 800 &&
MediaQuery.of(context).size.width <= 1200;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 1200) {
return largeScreen;
} else if (constraints.maxWidth <= 1200 &&
constraints.maxWidth >= 800) {
return mediumScreen ?? largeScreen;
} else {
return smallScreen ?? largeScreen;
}
},
);
}
}
复制代码
现在您可以为不同的屏幕尺寸定义不同的小组件布局。对于较小的屏幕,最好是显示一个带有顶部栏选项的drawer
。为此,我们将使用不同的AppBar
widget。如果我们在Scaffold的drawer
属性中定义了汉堡包图标,那么汉堡包图标将默认在应用程序栏中可见。
Scaffold(
appBar: ResponsiveWidget.isSmallScreen(context)
? AppBar( // for smaller screen sizes
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
'EXPLORE',
style: TextStyle(
color: Colors.blueGrey[100],
fontSize: 20,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w400,
letterSpacing: 3,
),
),
)
: PreferredSize( // for larger & medium screen sizes
preferredSize: Size(screenSize.width, 1000),
child: TopBarContents(_opacity),
),
drawer: ExploreDrawer(), // The drawer widget
// ...
);
复制代码
抽屉的UI将是这样的。

抽屉的UI代码在这里
通过这种方式,你可以为不同的屏幕尺寸定义不同的布局。
最终的版本在桌面和移动浏览器上会是这个样子。

结束语
希望这篇文章能帮助你开始使用Flutter web。在Flutter web系列的下一部分,我们将尝试通过添加一些动画和主题来改进这个UI。
有用的链接和参考资料
- 官方Flutter Web文档
- 该项目在GitHub上提供。
- 在线尝试Web应用。
Souvik Biswas是一个充满激情的移动应用开发者(Android和Flutter)。他在整个旅程中参与了大量的移动应用。喜欢开源贡献到GitHub。他目前正在印度信息技术学院Kalyani攻读计算机科学与工程专业的技术学士学位。他还在Flutter Community上写Flutter文章。
作者:Sunbreak
链接:https://juejin.im/post/6855532468121190408
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。