使用 Flutter 进行响应式 Web 和桌面开发

已发表: 2022-03-10
快速总结↬ Flutter 已经在移动开发领域引起了轰动。 现在它也正在使用更大的设备。 这是您准备好使用这个出色的跨平台框架开发 Web 和桌面应用程序所需的知识。

本教程不是对 Flutter 本身的介绍。 网上有大量文章、视频和几本书,其中包含简单的介绍,可帮助您学习 Flutter 的基础知识。 相反,我们将涵盖以下两个目标:

  1. Flutter 非移动开发的当前状态,以及如何在浏览器、台式机或笔记本电脑上运行 Flutter 代码;
  2. 如何使用 Flutter 创建响应式应用程序,以便您可以看到它的强大功能——尤其是作为 Web 框架——全面展示,最后附上关于基于 URL 的路由的说明。

让我们开始吧!

什么是 Flutter,为什么它很重要,它已经演变成什么,它的去向

Flutter 是谷歌最新的应用开发框架。 谷歌设想它是包罗万象的:它将使相同的代码能够在所有品牌的智能手机、平板电脑、台式机和笔记本电脑上作为本地应用程序或网页执行。

这是一个非常雄心勃勃的项目,但到目前为止,谷歌已经取得了令人难以置信的成功,特别是在两个方面:为 Android 和 iOS 原生应用程序创建了一个真正独立于平台的框架,该框架运行良好并且完全可以投入生产使用,并创建了一个令人印象深刻的前端-end web 框架,可以与兼容的 Flutter 应用程序共享 100% 的代码。

在下一节中,我们将了解是什么让应用程序兼容,以及目前非移动端 Flutter 开发的状态如何。

跳跃后更多! 继续往下看↓

使用 Flutter 进行非移动开发

使用 Flutter 进行非移动开发首先在 Google I/O 2019 上以重要的方式进行了宣传。本节将介绍如何使其工作以及何时工作。

如何启用 Web 和桌面开发

要启用 Web 开发,您必须首先使用 Flutter 的 beta 通道。 有两种方法可以达到这一点:

  • 通过从 SDK 存档下载适当的最新 beta 版本,直接在 beta 通道上安装 Flutter。
  • 如果您已经安装了 Flutter,请使用$ flutter channel beta切换到 beta 通道,然后通过使用$ flutter upgrade更新您的 Flutter 版本(实际上是 Flutter 安装文件夹上的git pull )来执行切换。

之后,您可以运行以下命令:

 $ flutter config --enable-web

桌面支持更具实验性,特别是由于缺乏适用于 Linux 和 Windows 的工具,使得插件开发尤其成为一个主要的痛苦,并且由于用于它的 API 旨在用于概念验证而不是用于生产。 这与 Web 开发不同,后者使用久经考验的 dart2js 编译器进行发布构建,Windows 和 Linux 本机桌面应用程序甚至不支持这些编译器。

注意对 macOS 的支持略好于对 Windows 和 Linux 的支持,但仍不如对 Web 的支持,也不如对移动平台的完全支持。

要启用对桌面开发的支持,您需要按照前面为beta通道列出的相同步骤切换到master发布通道。 然后,通过将<OS_NAME>替换为linuxwindowsmacos来运行以下命令:

 $ flutter config --enable-<OS_NAME>-desktop

此时,如果您对我将描述的以下任何步骤有问题,因为 Flutter 工具没有按照我所说的那样做,一些常见的故障排除步骤如下:

  • 运行flutter doctor检查问题。 这个 Flutter 命令的一个副作用是它应该下载它不需要的任何工具。
  • 运行flutter upgrade
  • 将其关闭并重新打开。 重新启动计算机的旧 1 层技术支持答案可能正是您能够享受 Flutter 的全部财富所需要的。

运行和构建 Flutter Web 应用

Flutter Web 支持一点也不差,这反映在 Web 开发的简易性上。

运行这个…

 $ flutter devices

... 应该立即显示这样的条目:

 Web Server • web-server • web-javascript • Flutter Tools

此外,运行 Chrome 浏览器应该也会导致 Flutter 显示一个条目。 在兼容的 Flutter 项目上flutter run Flutter(稍后会详细介绍),当唯一显示的“连接设备”是 Web 服务器时,Flutter 会在localhost:<RANDOM_PORT>上启动一个 Web 服务器,这将允许您访问您的 Flutter来自任何浏览器的网络应用程序。

如果您已经安装了 Chrome 但它没有显示,您需要将CHROME_EXECUTABLE环境变量设置为 Chrome 可执行文件的路径。

运行和构建 Flutter 桌面应用

启用 Flutter 桌面支持后,您可以使用flutter run -d <OS_NAME>在开发工作站上本地运行 Flutter 应用程序,将<OS_NAME>替换为启用桌面支持时使用的相同值。 您还可以使用flutter build <OS_NAME>build目录中构建二进制文件。

不过,在您执行上述任何操作之前,您需要有一个目录,其中包含 Flutter 需要为您的平台构建的内容。 这将在您创建新项目时自动创建,但您需要使用flutter create . . 此外,Linux 和 Windows API 不稳定,因此如果应用程序在 Flutter 更新后停止运行,您可能必须为这些平台重新生成它们。

应用程序何时兼容?

当我提到 Flutter 应用程序必须是“兼容项目”才能在桌面或 Web 上运行时,我一直是什么意思? 简而言之,我的意思是它不能使用任何没有针对您要构建的平台的特定于平台的实现的插件。

为了让每个人都清楚这一点并避免误解,请注意Flutter 插件是一个特定的Flutter 包,其中包含提供其功能所必需的特定于平台的代码。

例如,您可以随心所欲地使用 Google 开发的url_launcher包(而且您可能会想要,因为网络是建立在超链接之上的)。

一个谷歌开发的包的例子是path_provider ,它用于获取保存文件的本地存储路径。 这是一个包的示例,顺便提一下,它对 Web 应用程序没有任何用处,所以不能使用它并不是一个真正的麻烦,除了你需要更改代码以便如果您正在使用它,它可以在网络上工作。

例如,您可以使用 shared_preferences 包,它依赖于 web 上的 HTML localStorage

类似的警告也适用于桌面平台:很少有插件与桌面平台兼容,而且,由于这是一个反复出现的主题,因此需要在桌面端完成比 Flutter for web 真正需要的更多工作。

在 Flutter 中创建响应式布局

由于我上面描述的内容,为了简单起见,我将在本文的其余部分假设您的目标平台是 Web,但基本概念也适用于桌面开发。

支持网络既有好处也有责任。 几乎被迫支持不同的屏幕尺寸可能听起来像是一个缺点,但考虑到在 Web 浏览器中运行应用程序可以让您非常轻松地看到您的应用程序在不同尺寸和纵横比的屏幕上的外观,而无需单独运行移动设备模拟器。

现在,让我们谈谈代码。 如何让你的应用程序响应?

本次分析主要从两个角度进行:

  1. “我正在使用或可以使用哪些可以或应该适应不同尺寸屏幕的小部件?”
  2. “如何获取有关屏幕大小的信息,以及在编写 UI 代码时如何使用它?”

我们稍后会回答第一个问题。 让我们先谈谈后者,因为它很容易处理,并且是问题的核心。 有两种方法可以做到这一点:

  1. 一种方法是从MediaQueryDataInheritedWidgetMediaQuery获取信息,该信息必须存在于小部件树中才能使 Flutter 应用程序正常工作(它是MaterialApp/WidgetsApp/CupertinoApp的一部分),您可以获得这些信息,就像任何其他带有MediaQuery.of(context)InheritedWidget ,它具有Size类型的size属性,因此具有double类型的两个widthheight属性。
  2. 另一种方法是使用LayoutBuilder ,它是一个构建器小部件(就像StreamBuilderFutureBuilder ),它传递给builder函数(连同context )一个具有minHeightmaxHeightminWidthmaxWidth属性的BoxConstraints对象。

这是一个使用MediaQuery获取约束的 DartPad 示例,其代码如下:

 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: MyHomePage() ); } class MyHomePage extends StatelessWidget { @override Widget build(context) => Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( "Width: ${MediaQuery.of(context).size.width}", style: Theme.of(context).textTheme.headline4 ), Text( "Height: ${MediaQuery.of(context).size.height}", style: Theme.of(context).textTheme.headline4 ) ] ) ) ); }

这是一个使用LayoutBuilder同样事情的人:

 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: MyHomePage() ); } class MyHomePage extends StatelessWidget { @override Widget build(context) => Scaffold( body: LayoutBuilder( builder: (context, constraints) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( "Width: ${constraints.maxWidth}", style: Theme.of(context).textTheme.headline4 ), Text( "Height: ${constraints.maxHeight}", style: Theme.of(context).textTheme.headline4 ) ] ) ) ) ); }

现在,让我们考虑一下哪些小部件可以适应约束。

首先,让我们考虑一下根据屏幕大小布置多个小部件的不同方式。

最容易适应的小部件是GridView 。 事实上,使用GridView构造函数构建的GridView.extent甚至不需要您的参与即可做出响应,正如您在这个非常简单的示例中所见:

 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: MyHomePage() ); } class MyHomePage extends StatelessWidget { final List elements = [ "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit" ]; @override Widget build(context) => Scaffold( body: GridView.extent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, children: elements.map((el) => Card(child: Center(child: Padding(padding: EdgeInsets.all(8.0), child: Text(el))))).toList() ) ); } import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: MyHomePage() ); } class MyHomePage extends StatelessWidget { final List elements = [ "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit" ]; @override Widget build(context) => Scaffold( body: GridView.extent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, children: elements.map((el) => Card(child: Center(child: Padding(padding: EdgeInsets.all(8.0), child: Text(el))))).toList() ) ); }

您可以通过更改maxCrossAxisExtent来容纳不同大小的内容。

该示例主要用于显示GridView.extent GridView构造函数的存在,但更智能的方法是使用带有SliverGridDelegateWithMaxCrossAxisExtentGridView.builder ,在这种情况下,小部件将显示在网格中是从另一个数据结构动态创建的,如您在此示例中所见:

 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: MyHomePage() ); } class MyHomePage extends StatelessWidget { final List<String> elements = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit"]; @override Widget build(context) => Scaffold( body: GridView.builder( itemCount: elements.length, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, ), itemBuilder: (context, i) => Card( child: Center( child: Padding( padding: EdgeInsets.all(8.0), child: Text(elements[i]) ) ) ) ) ); }

GridView 适应不同屏幕的一个例子是我的个人登陆页面,这是一个非常简单的 Flutter web 应用程序,由一个带有一堆CardsGridView组成,就像前面的示例代码一样,只是Cards稍微复杂一些,更大一些.

可以对为手机设计的应用程序进行的一个非常简单的更改是在有空间时将Drawer替换为左侧的永久菜单。

例如,我们可以有一个ListView小部件,如下所示,用于导航:

 class Menu extends StatelessWidget { @override Widget build(context) => ListView( children: [ FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_one), title: Text("First Link"), ) ), FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_two), title: Text("Second Link"), ) ) ] ); }

在智能手机上,一个常见的使用位置是在Drawer内(也称为汉堡菜单)。

替代方案是BottomNavigationBarTabBar ,与TabBarView结合使用,但我们必须进行比抽屉所需的更多更改,因此我们将坚持使用抽屉。

为了只显示包含我们之前在较小屏幕上看到的MenuDrawer ,您将编写类似于以下代码段的代码,使用MediaQuery.of(context)检查宽度并仅将Drawer对象传递给Scaffold小于我们认为适合我们应用的某个宽度值:

 Scaffold( appBar: AppBar(/* ... \*/), drawer: MediaQuery.of(context).size.width < 500 ? Drawer( child: Menu(), ) : null, body: /* ... \*/ )

现在,让我们考虑一下Scaffoldbody 。 作为我们应用程序的示例主要内容,我们将使用我们之前构建的GridView ,我们将其保存在一个名为Content的单独小部件中以避免混淆:

 class Content extends StatelessWidget { final List elements = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit"]; @override Widget build(context) => GridView.builder( itemCount: elements.length, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, ), itemBuilder: (context, i) => Card( child: Center( child: Padding( padding: EdgeInsets.all(8.0), child: Text(elements[i]) ) ) ) ); } class Content extends StatelessWidget { final List elements = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit"]; @override Widget build(context) => GridView.builder( itemCount: elements.length, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, ), itemBuilder: (context, i) => Card( child: Center( child: Padding( padding: EdgeInsets.all(8.0), child: Text(elements[i]) ) ) ) ); }

在更大的屏幕上,主体本身可能是一个显示两个小部件的RowMenu ,它被限制为固定宽度,以及Content填充屏幕的其余部分。

在较小的屏幕上,整个body将是Content

我们将把所有东西都包装在一个SafeArea和一个Center小部件中,因为有时 Flutter Web 应用小部件,尤其是在使用RowColumn时,最终会超出可见的屏幕区域,这可以通过SafeArea和/或Center来解决。

这意味着Scaffoldbody将如下所示:

 SafeArea( child:Center( child: MediaQuery.of(context).size.width < 500 ? Content() : Row( children: [ Container( width: 200.0, child: Menu() ), Container( width: MediaQuery.of(context).size.width-200.0, child: Content() ) ] ) ) )

这是所有这些放在一起:

(大预览)
 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( home: HomePage() ); } class HomePage extends StatelessWidget { @override Widget build(context) => Scaffold( appBar: AppBar(title: Text("test")), drawer: MediaQuery.of(context).size.width < 500 ? Drawer( child: Menu(), ) : null, body: SafeArea( child:Center( child: MediaQuery.of(context).size.width < 500 ? Content() : Row( children: [ Container( width: 200.0, child: Menu() ), Container( width: MediaQuery.of(context).size.width-200.0, child: Content() ) ] ) ) ) ); } class Menu extends StatelessWidget { @override Widget build(context) => ListView( children: [ FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_one), title: Text("First Link"), ) ), FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_two), title: Text("Second Link"), ) ) ] ); } class Content extends StatelessWidget { final List<String> elements = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit"]; @override Widget build(context) => GridView.builder( itemCount: elements.length, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, ), itemBuilder: (context, i) => Card( child: Center( child: Padding( padding: EdgeInsets.all(8.0), child: Text(elements[i]) ) ) ) ); }

这是 Flutter 中响应式 UI 的一般介绍所需要的大部分内容。 它的大部分应用程序将取决于您的应用程序的特定 UI,并且很难准确指出您可以做什么来使您的应用程序响应,您可以根据自己的喜好采取多种方法。 不过,现在让我们看看如何将更完整的示例制作成响应式应用程序,考虑常见的应用程序元素和 UI 流程。

把它放在上下文中:使应用程序响应

到目前为止,我们只有一个屏幕。 让我们将其扩展为具有基于 URL 的导航的两屏应用程序!

创建响应式登录页面

您的应用可能有登录页面。 我们怎样才能做出响应?

移动设备上的登录屏幕通常彼此非常相似。 可用空间不多; 它通常只是一个在其小部件周围带有一些PaddingColumn ,并且它包含用于输入用户名和密码的TextField以及用于登录的按钮。因此,这是一个非常标准的(尽管不能正常工作,因为这需要,除其他外) ,每个TextFieldTextEditingController )移动应用程序的登录页面可能如下:

 Scaffold( body: Container( padding: const EdgeInsets.symmetric( vertical: 30.0, horizontal: 25.0 ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text("Welcome to the app, please log in"), TextField( decoration: InputDecoration( labelText: "username" ) ), TextField( obscureText: true, decoration: InputDecoration( labelText: "password" ) ), RaisedButton( color: Colors.blue, child: Text("Log in", style: TextStyle(color: Colors.white)), onPressed: () {} ) ] ), ), )

它在移动设备上看起来不错,但是那些非常宽的TextField在平板电脑上开始看起来很刺耳,更不用说更大的屏幕了。 但是,我们不能只决定固定的宽度,因为手机有不同的屏幕尺寸,我们应该保持一定程度的灵活性。

例如,通过实验,我们可能会发现最大宽度应该是 500。好吧,我们将Containerconstraints设置为 500(我在前面的示例中使用了Container而不是Padding ,因为我知道我要使用这个) 我们很高兴,对吧? 不是真的,因为这会导致登录小部件粘在屏幕的左侧,这可能比拉伸所有内容更糟糕。 因此,我们包装在一个Center小部件中,如下所示:

 Center( child: Container( constraints: BoxConstraints(maxWidth: 500), padding: const EdgeInsets.symmetric( vertical: 30.0, horizontal: 25.0 ), child: Column(/* ... \*/) ) )

这看起来已经很好了,我们甚至不必使用LayoutBuilderMediaQuery.of(context).size 。 不过,让我们更进一步,让它看起来非常好。 在我看来,如果前景部分以某种方式与背景分离,它看起来会更好。 我们可以通过为具有输入小部件的Container后面的内容赋予背景颜色并保持前景Container为白色来实现这一点。 为了让它看起来更好一点,让我们保持Container在大型设备上不拉伸到屏幕的顶部和底部,给它圆角,并在两种布局之间给它一个漂亮的动画过渡。

所有这些现在都需要一个LayoutBuilder和一个外部Container来设置背景颜色并在Container周围添加填充,而不仅仅是在大屏幕上的侧面。 此外,为了使填充量的变化具有动画效果,我们只需要将外部Container转换为AnimatedContainer ,这需要动画的duration ,我们将其设置为半秒,即Duration(milliseconds: 500) in代码。

这是响应式登录页面的示例:

(大预览)
 class LoginPage extends StatelessWidget { @override Widget build(context) => Scaffold( body: LayoutBuilder( builder: (context, constraints) { return AnimatedContainer( duration: Duration(milliseconds: 500), color: Colors.lightGreen[200], padding: constraints.maxWidth < 500 ? EdgeInsets.zero : EdgeInsets.all(30.0), child: Center( child: Container( padding: EdgeInsets.symmetric( vertical: 30.0, horizontal: 25.0 ), constraints: BoxConstraints( maxWidth: 500, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5.0), ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text("Welcome to the app, please log in"), TextField( decoration: InputDecoration( labelText: "username" ) ), TextField( obscureText: true, decoration: InputDecoration( labelText: "password" ) ), RaisedButton( color: Colors.blue, child: Text("Log in", style: TextStyle(color: Colors.white)), onPressed: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => HomePage() ) ); } ) ] ), ), ) ); } ) ); }

如您所见,我还将RaisedButtononPressed为一个回调,该回调将我们导航到名为HomePage的屏幕(例如,这可能是我们之前使用GridView和菜单或抽屉构建的视图)。 但是,现在,导航部分是我们要关注的。

命名路由:让您的应用程序的导航更像是一个适当的 Web 应用程序

Web 应用程序的一个常见功能是能够根据 URL 更改屏幕。 例如,去https://appurl/login应该会给你一些不同于https://appurl/somethingelse的东西。 Flutter 实际上支持命名路由,有两个目的:

  1. 在 Web 应用程序中,它们具有我在上一句中提到的功能。
  2. 在任何应用程序中,它们都允许您为应用程序预定义路线并为其命名,然后只需指定它们的名称即可导航到它们。

为此,我们需要将MaterialApp构造函数更改为如下所示:

 MaterialApp( initialRoute: "/login", routes: { "/login": (context) => LoginPage(), "/home": (context) => HomePage() } );

然后我们可以使用Navigator.pushNamed(context, routeName)Navigator.pushReplacementNamed(context, routeName)切换到不同的路由,而不是Navigator.push(context, route)Navigator.pushReplacement(context, route)

这适用于我们在本文其余部分构建的假设应用程序。 在 DartPad 中你无法真正看到命名路由的运行情况,因此你应该在自己的机器上使用flutter run尝试一下,或者查看运行中的示例:

(大预览)
 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) => MaterialApp( initialRoute: "/login", routes: { "/login": (context) => LoginPage(), "/home": (context) => HomePage() } ); } class LoginPage extends StatelessWidget { @override Widget build(context) => Scaffold( body: LayoutBuilder( builder: (context, constraints) { return AnimatedContainer( duration: Duration(milliseconds: 500), color: Colors.lightGreen[200], padding: constraints.maxWidth < 500 ? EdgeInsets.zero : const EdgeInsets.all(30.0), child: Center( child: Container( padding: const EdgeInsets.symmetric( vertical: 30.0, horizontal: 25.0 ), constraints: BoxConstraints( maxWidth: 500, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5.0), ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text("Welcome to the app, please log in"), TextField( decoration: InputDecoration( labelText: "username" ) ), TextField( obscureText: true, decoration: InputDecoration( labelText: "password" ) ), RaisedButton( color: Colors.blue, child: Text("Log in", style: TextStyle(color: Colors.white)), onPressed: () { Navigator.pushReplacementNamed( context, "/home" ); } ) ] ), ), ) ); } ) ); } class HomePage extends StatelessWidget { @override Widget build(context) => Scaffold( appBar: AppBar(title: Text("test")), drawer: MediaQuery.of(context).size.width < 500 ? Drawer( child: Menu(), ) : null, body: SafeArea( child:Center( child: MediaQuery.of(context).size.width < 500 ? Content() : Row( children: [ Container( width: 200.0, child: Menu() ), Container( width: MediaQuery.of(context).size.width-200.0, child: Content() ) ] ) ) ) ); } class Menu extends StatelessWidget { @override Widget build(context) => ListView( children: [ FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_one), title: Text("First Link"), ) ), FlatButton( onPressed: () {}, child: ListTile( leading: Icon(Icons.looks_two), title: Text("Second Link"), ) ), FlatButton( onPressed: () {Navigator.pushReplacementNamed( context, "/login");}, child: ListTile( leading: Icon(Icons.exit_to_app), title: Text("Log Out"), ) ) ] ); } class Content extends StatelessWidget { final List<String> elements = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "A Million Billion Trillion", "A much, much longer text that will still fit"]; @override Widget build(context) => GridView.builder( itemCount: elements.length, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 130.0, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, ), itemBuilder: (context, i) => Card( child: Center( child: Padding( padding: EdgeInsets.all(8.0), child: Text(elements[i]) ) ) ) ); }

继续你的颤振冒险

这应该让您了解在更大的屏幕上,尤其是在 Web 上,您可以使用 Flutter 做什么。 它是一个可爱的框架,非常易于使用,其极端的跨平台支持只会让它变得更加重要,学习和开始使用。 所以,继续并开始信任 Flutter 的 Web 应用程序吧!

更多资源

  • “桌面外壳”,GitHub
    Flutter 在桌面上的最新状态
  • “桌面对 Flutter 的支持”,Flutter
    有关完全支持的桌面平台的信息
  • “Web 对 Flutter 的支持”,Flutter
    网页版 Flutter 的相关信息
  • “所有样本”,颤振
    Flutter 示例和应用程序的精选列表