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

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

时间:2020-7-12 作者:gykj

本教程不是Flutter本身的简介。在线提供大量文章,视频和几本书,并提供简单的介绍,这些内容将帮助您学习Flutter的基础知识。相反,我们将涵盖以下两个目标:

  1. Flutter非移动开发的当前状态以及如何在台式机或笔记本电脑上的浏览器中运行Flutter代码;
  2. 如何使用Flutter创建响应式应用程序,以便在全屏显示其功能(尤其是作为Web框架),并以基于URL路由的注释结尾。

让我们开始吧!

什么是颤动,为什么重要,它演变成什么,去向何方

Flutter是Google最新的应用开发框架。Google设想它是无所不包的:它将使相同的代码能够像本机应用程序或网页一样在所有品牌的智能手机,平板电脑以及台式机和笔记本电脑上执行。

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

在下一节中,我们将了解什么使该应用程序兼容以及到目前为止非移动Flutter开发的状态。

Flutter的非移动开发

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

如何启用WEB和桌面开发

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

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

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

$ flutter config --enable-web

桌面支持更具实验性,尤其是由于缺乏用于Linux和Windows的工具,这使得插件开发尤其困难,并且由于其所用的API旨在用于概念验证而非事实证明。生产。这与Web开发不同,Web开发使用经过反复测试的dart2js编译器进行发行版本构建,Windows和Linux本机桌面应用程序甚至不支持该版本。

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

为了支持桌面开发,您需要master按照前面概述的相同步骤切换到发行版beta。然后,通过用运行以下<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也显示相应的条目。当显示的唯一“已连接设备”是Web服务器时,如果flutter run兼容的 Flutter项目上运行(稍后会详细介绍),将导致Flutter在上启动Web服务器localhost:<RANDOM_PORT>,这将使您可以从任何浏览器访问Flutter Web应用程序。

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

运行和构建FLUTTER桌面应用程序

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

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

应用何时兼容?

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

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

例如,您可以根据需要使用Google开发的url_launcher程序包(鉴于网络是建立在超链接上的,因此您可能想要使用)。

由Google开发的软件包的示例是path_provider,它的使用会阻止Web开发,该示例用于获取将文件保存到的本地存储路径。顺便说一句,这是一个程序包的示例,对于Web应用程序没有任何用处,因此,不能使用它并不是一个真正的遗憾,除非您需要更改代码以实现以下目的:如果您正在使用它,则可以在网络上运行。

例如,您可以使用shared_preferences包,该包依赖于localStorageWeb 上的HTML 。

对于桌面平台,类似的警告也是有效的:很少有插件与桌面平台兼容,并且由于这是一个反复出现的主题,因此需要在桌面端完成比在Flutter上实际进行更多的工作。

在Flutter中创建响应式布局

由于上面已经进行了描述,并且为了简单起见,在本文的其余部分中,我将假定您的目标平台是Web,但是基本概念也适用于桌面开发。

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

现在,让我们谈谈代码。如何使您的应用程序具有响应能力?

从两个角度进行此分析:

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

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

  1. 一种方法是从以信息MediaQueryData的的MediaQueryInheritedWidget,它在小部件树存在为了一扑应用工作(它的一部分MaterialApp/WidgetsApp/CupertinoApp),你可以得到的,就像任何其他的InheritedWidget,有MediaQuery.of(context),具有size属性,该属性是类型的Size,因此具有类型的两个widthheight属性double
  2. 另一种方法是使用一个LayoutBuilder,这是构建器插件(就像一个StreamBuilder或一个FutureBuilder),其传递给builder功能(与沿context一个)BoxConstraints,其具有对象minHeightmaxHeightminWidthmaxWidth特性。

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

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()
      )
   );
}

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

该示例主要用于显示GridView.extent GridView构造函数的存在,但是更聪明的方法是将a GridView.builder与a配合使用SliverGridDelegateWithMaxCrossAxisExtent,在这种情况下,要在网格中显示的小部件是从另一个数据结构动态创建的,如本例所示

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应用程序,由,GridViewCards前面的示例代码组成,其中包含一堆,除了前者的示例代码Cards更复杂和更大之外。

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

例如,我们可以有一个如下所示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(也称为汉堡菜单)。

或的替代方案是BottomNavigationBar或与TabBar结合使用TabBarView,但同时使用这两种方法,我们必须做出比抽屉所需的更改更多的更改,因此我们将坚持使用抽屉。

为了只显示我们先前在较小的屏幕上看到的Drawer包含内容Menu,您需要编写类似于以下代码段的代码,使用来检查宽度,MediaQuery.of(context)并在Drawer对象Scaffold的宽度小于我们认为的某个宽度值时将其传递给对象适用于我们的应用程序:

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

现在,让我们考虑bodyScaffold。作为应用程序的示例主要内容,我们将使用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])
        )
      )
    )
  );
}

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

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

我们将所有内容都包装在SafeAreaCenter窗口小部件中,因为有时Flutter Web应用程序窗口小部件(尤其是在使用Rows和Columns时)最终会出现在可见的屏幕区域之外,并且用SafeArea和/或固定Center

这意味着bodyScaffold将是如下:

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()
        )
      ]
    )
  )
)

这是所有这些的总和

使用Flutter进行响应式Web和桌面开发
大型预览
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的导航将其扩展为两屏应用程序!

创建响应式登录页面

您的应用可能有一个登录页面。我们如何使这种响应能力?

通常,移动设备上的登录屏幕彼此非常相似。可用空间不多;它通常只是一个Column有一些Padding围绕它的部件,它包含TextFieldS代表在用户名输入和密码,一键登录。因此,一个非常标准的(虽然不能正常工作,因为这需要,除其他外,一TextEditingControllerTextField移动应用的每个)登录页面都可以是以下内容:

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。那么,我们将Containers 设置constraints为500(由于我知道我要去哪里,所以我在上一个示例中使用Container而不是Padding),并且好走吧?并非如此,因为那样会使登录窗口小部件粘贴在屏幕的左侧,这可能比拉伸所有内容还要糟糕。因此,我们包装了一个Center小部件,如下所示:

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

看起来已经很好了,我们甚至不必使用a LayoutBuilder或the MediaQuery.of(context).size。不过,让我们进一步走一步,使它看起来非常好。在我看来,如果前景部分与背景有所不同,看起来会更好。我们可以通过为Container输入小部件的后面加上背景颜色,并使前景保持Container白色来实现。为了使它看起来更好,让我们Container避免在大型设备上将其拉伸到屏幕的顶部和底部,为它提供圆角,并在两种布局之间进行漂亮的动画过渡。

现在,所有这些都需要一个LayoutBuilder和一个外部控件Container,以设置背景色并Container在整个屏幕周围添加填充,而不仅仅是在大屏幕上仅在侧面添加填充。另外,要使填充量的变化具有动画性,我们只需要将外部Container变成AnimatedContainer,就需要duration动画来设置,我们将其设置为半秒,这Duration(milliseconds: 500)在代码中。

是响应式登录页面的示例

使用Flutter进行响应式Web和桌面开发
大型预览
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()
                          )
                        );
                      }  
                    )
                  ]
                ),
              ),
            )
          );
        }
      )
   );
}

如您所见,我还将RaisedButtons 更改onPressed为一个回调,该回调将我们导航到名为的屏幕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或在您自己的机器上尝试使用,或者查看实际使用的示例

使用Flutter进行响应式Web和桌面开发
大型预览
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])
        )
      )
    )
  );
}

随心所欲的冒险

这应该使您了解在更大的屏幕上(特别是在网络上)如何使用Flutter。这是一个可爱的框架,非常易于使用,并且其极端的跨平台支持仅使学习和开始使用它变得更加重要。因此,继续吧,也开始信任Flutter的Web应用程序!

版权所有:https://www.eraycloud.com 转载请注明出处