使用 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 )一個BoxConstraints對象,該對象具有minHeightmaxHeightminWidthmaxWidth屬性。

這是一個使用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 應用程序,由一個GridView和一堆Cards組成,就像前面的示例代碼一樣,只是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 示例和應用程序的精選列表