การพัฒนาเว็บและเดสก์ท็อปที่ตอบสนองด้วย Flutter
เผยแพร่แล้ว: 2022-03-10บทช่วยสอนนี้ไม่ใช่การแนะนำ Flutter เอง มีบทความ วิดีโอ และหนังสือหลายเล่มทางออนไลน์พร้อมคำแนะนำง่ายๆ ที่จะช่วยให้คุณเรียนรู้พื้นฐานของ Flutter เราจะครอบคลุมวัตถุประสงค์สองข้อต่อไปนี้แทน:
- สถานะปัจจุบันของการพัฒนา Flutter ที่ไม่ใช่มือถือและวิธีเรียกใช้โค้ด Flutter ในเบราว์เซอร์ บนคอมพิวเตอร์เดสก์ท็อปหรือแล็ปท็อป
- วิธีสร้างแอปที่ตอบสนองด้วย Flutter เพื่อให้คุณเห็นพลังของมัน — โดยเฉพาะอย่างยิ่งในฐานะเฟรมเวิร์กของเว็บ — บนจอแสดงผลแบบเต็ม และลงท้ายด้วยหมายเหตุเกี่ยวกับการกำหนดเส้นทางตาม URL
เข้าไปกันเถอะ!
กระพือปีกคืออะไร เหตุใดจึงสำคัญ วิวัฒนาการไปสู่ที่ใด
Flutter คือเฟรมเวิร์กการพัฒนาแอปล่าสุดของ Google Google มองว่าเป็นข้อมูลที่ครอบคลุมทั้งหมด: จะช่วยให้โค้ดเดียวกันสามารถรันบนสมาร์ทโฟนของทุกยี่ห้อ บนแท็บเล็ต และบนคอมพิวเตอร์เดสก์ท็อปและแล็ปท็อป เป็นแอปที่มาพร้อมเครื่องหรือเป็นหน้าเว็บ
เป็นโครงการที่มีความทะเยอทะยานมาก แต่ Google ประสบความสำเร็จอย่างไม่น่าเชื่อจนถึงขณะนี้โดยเฉพาะอย่างยิ่งในสองด้าน: ในการสร้างเฟรมเวิร์กที่ไม่ขึ้นกับแพลตฟอร์มอย่างแท้จริงสำหรับแอปเนทีฟ Android และ iOS ที่ใช้งานได้ดีเยี่ยมและพร้อมสำหรับการใช้งานจริงและในการสร้างส่วนหน้าที่น่าประทับใจ - เว็บเฟรมเวิร์กที่แชร์โค้ดได้ 100% กับแอป Flutter ที่เข้ากันได้
ในส่วนถัดไป เราจะมาดูกันว่าอะไรทำให้แอปสามารถทำงานร่วมกันได้ และสถานะของการพัฒนา Flutter ที่ไม่ใช่มือถือ ณ ตอนนี้เป็นอย่างไร
การพัฒนาที่ไม่ใช่มือถือด้วย Flutter
การพัฒนาที่ไม่ใช่อุปกรณ์เคลื่อนที่ด้วย Flutter ได้รับการเผยแพร่ครั้งแรกอย่างมีนัยสำคัญที่ Google I/O 2019 ส่วนนี้เกี่ยวกับวิธีทำให้มันใช้งานได้และเมื่อทำงาน
วิธีเปิดใช้งานการพัฒนาเว็บและเดสก์ท็อป
หากต้องการเปิดใช้งานการพัฒนาเว็บ คุณต้องอยู่ในช่องเบต้าของ Flutter ก่อน มีสองวิธีในการไปยังจุดนั้น:
- ติดตั้ง Flutter โดยตรงบนแชนเนลเบต้าโดยดาวน์โหลดเวอร์ชันเบต้าล่าสุดที่เหมาะสมจากไฟล์เก็บถาวร SDK
- หากคุณได้ติดตั้ง Flutter ไว้แล้ว ให้เปลี่ยนไปใช้เวอร์ชันเบต้าด้วย
$ flutter channel beta
จากนั้นดำเนินการเปลี่ยนเองโดยอัปเดตเวอร์ชัน Flutter ของคุณ (ซึ่งจริงๆ แล้วเป็นgit pull
ในโฟลเดอร์การติดตั้ง Flutter) ด้วย$ flutter upgrade
หลังจากนั้น คุณสามารถเรียกใช้สิ่งนี้:
$ flutter config --enable-web
การสนับสนุนเดสก์ท็อปมีการทดลองมากกว่ามาก โดยเฉพาะอย่างยิ่งเนื่องจากขาดเครื่องมือสำหรับ Linux และ Windows ทำให้การพัฒนาปลั๊กอินโดยเฉพาะอย่างยิ่งเป็นปัญหาใหญ่ และเนื่องจากความจริงที่ว่า API ที่ใช้สำหรับมันมีไว้สำหรับใช้พิสูจน์แนวคิด ไม่ใช่สำหรับ การผลิต. ซึ่งไม่เหมือนกับการพัฒนาเว็บซึ่งใช้คอมไพเลอร์ dart2js ที่ทดลองและทดสอบแล้วสำหรับบิลด์ที่เผยแพร่ ซึ่งไม่รองรับแม้แต่แอปเดสก์ท็อปดั้งเดิมของ Windows และ Linux
หมายเหตุ : การรองรับ macOS ดีกว่าการรองรับ Windows และ Linux เล็กน้อย แต่ก็ยังไม่ดีเท่ากับการรองรับเว็บและไม่ดีเท่ากับการรองรับแพลตฟอร์มมือถืออย่างเต็มรูปแบบ
ในการเปิดใช้การสนับสนุนสำหรับการพัฒนาเดสก์ท็อป คุณต้องเปลี่ยนไปใช้ช่องทางการเผยแพร่ master
โดยทำตามขั้นตอนเดียวกับที่สรุปไว้ก่อนหน้าสำหรับช่อง beta
จากนั้นเรียกใช้สิ่งต่อไปนี้โดยแทนที่ <OS_NAME>
ด้วย linux
, windows
หรือ macos
:
$ flutter config --enable-<OS_NAME>-desktop
ณ จุดนี้ หากคุณมีปัญหากับขั้นตอนใดๆ ต่อไปนี้ซึ่งฉันจะอธิบายเพราะเครื่องมือ Flutter ไม่ได้ทำในสิ่งที่ฉันบอกว่าควรทำ ขั้นตอนการแก้ปัญหาทั่วไปบางประการมีดังนี้:
- เรียกใช้
flutter doctor
เพื่อตรวจสอบปัญหา ผลข้างเคียงของคำสั่ง Flutter นี้คือควรดาวน์โหลดเครื่องมือที่จำเป็นซึ่งไม่มี - เรียกใช้การ
flutter upgrade
- ปิดและเปิดใหม่อีกครั้ง คำตอบแบบเก่าของการสนับสนุนทางเทคนิคระดับ 1 ในการรีสตาร์ทคอมพิวเตอร์อาจเป็นสิ่งที่จำเป็นสำหรับคุณเพื่อให้สามารถเพลิดเพลินกับ Flutter ได้อย่างเต็มที่
การใช้งานและสร้างเว็บแอป Flutter
การสนับสนุนเว็บ Flutter นั้นไม่เลวเลย และสิ่งนี้สะท้อนให้เห็นในความสะดวกในการพัฒนาสำหรับเว็บ
กำลังดำเนินการนี้...
$ flutter devices
… ควรแสดงรายการสำหรับสิ่งนี้ทันที:
Web Server • web-server • web-javascript • Flutter Tools
นอกจากนี้ การเรียกใช้เบราว์เซอร์ Chrome จะทำให้ Flutter แสดงรายการด้วยเช่นกัน การเรียกใช้ flutter run
บนโปรเจ็กต์ Flutter ที่ เข้ากันได้ (เพิ่มเติมในภายหลัง) เมื่อ "อุปกรณ์ที่เชื่อมต่อ" ปรากฏขึ้นเท่านั้นคือเว็บเซิร์ฟเวอร์จะทำให้ Flutter เริ่มเว็บเซิร์ฟเวอร์บน localhost:<RANDOM_PORT>
ซึ่งจะช่วยให้คุณเข้าถึง Flutter ของคุณได้ เว็บแอปจากเบราว์เซอร์ใดก็ได้
หากคุณติดตั้ง Chrome แล้ว แต่ไม่ปรากฏขึ้น คุณต้องตั้งค่าตัวแปรสภาพแวดล้อม CHROME_EXECUTABLE
เป็นเส้นทางไปยังไฟล์สั่งการของ Chrome
การใช้งานและสร้าง Flutter Desktop Apps
หลังจากที่คุณเปิดใช้งานการสนับสนุนเดสก์ท็อป Flutter แล้ว คุณสามารถเรียกใช้แอป Flutter แบบเนทีฟบนเวิร์กสเตชันการพัฒนาของคุณด้วย flutter run -d <OS_NAME>
โดยแทนที่ <OS_NAME>
ด้วยค่าเดียวกับที่คุณใช้เมื่อเปิดใช้งานการรองรับเดสก์ท็อป คุณยังสามารถสร้างไบนารีในไดเร็กทอรี build
ด้วย flutter build <OS_NAME>
ก่อนที่คุณจะสามารถดำเนินการใดๆ ได้ คุณต้องมีไดเร็กทอรีที่มีสิ่งที่ Flutter ต้องการเพื่อสร้างสำหรับแพลตฟอร์มของคุณ สิ่งนี้จะถูกสร้างขึ้นโดยอัตโนมัติเมื่อคุณสร้างโปรเจ็กต์ใหม่ แต่คุณจะต้องสร้างสำหรับโปรเจ็กต์ที่มีอยู่ด้วย flutter create .
. นอกจากนี้ Linux และ Windows API นั้นไม่เสถียร ดังนั้นคุณอาจต้องสร้างใหม่อีกครั้งสำหรับแพลตฟอร์มเหล่านั้น หากแอปหยุดทำงานหลังจากอัปเดต Flutter
แอพเข้ากันได้เมื่อใด
ฉันหมายถึงอะไรมาตลอดเมื่อพูดถึงแอป Flutter ต้องเป็น "โครงการที่เข้ากันได้" เพื่อให้ทำงานบนเดสก์ท็อปหรือเว็บได้ พูดง่ายๆ คือ ต้องไม่ใช้ปลั๊กอินใดๆ ที่ไม่มีการใช้งานเฉพาะแพลตฟอร์มสำหรับแพลตฟอร์มที่คุณกำลังพยายามสร้าง
เพื่อให้ประเด็นนี้ชัดเจนสำหรับทุกคนและหลีกเลี่ยงความเข้าใจผิด โปรดทราบว่า ปลั๊กอิน Flutter เป็น แพ็คเกจ Flutter เฉพาะที่มีรหัสเฉพาะแพลตฟอร์มที่จำเป็นสำหรับการนำเสนอคุณลักษณะ
ตัวอย่างเช่น คุณสามารถใช้แพ็คเกจ url_launcher
ที่ Google พัฒนาขึ้นได้มากเท่าที่คุณต้องการ (และคุณอาจต้องการ เนื่องจากเว็บสร้างจากไฮเปอร์ลิงก์)
ตัวอย่างของแพ็คเกจที่พัฒนาโดย Google การใช้งานซึ่งจะขัดขวางการพัฒนาเว็บคือ path_provider
ซึ่งใช้เพื่อรับเส้นทางการจัดเก็บในเครื่องเพื่อบันทึกไฟล์ นี่คือตัวอย่างของแพ็คเกจที่บังเอิญ ไม่ได้มีประโยชน์อะไรกับเว็บแอป ดังนั้นการไม่สามารถใช้งานได้จึงไม่ใช่เรื่องน่าอาย ยกเว้นว่าคุณต้องเปลี่ยนรหัสเพื่อ ให้ทำงานบนเว็บได้หากคุณใช้งานอยู่
ตัวอย่างเช่น คุณสามารถใช้แพ็คเกจ shared_preferences ซึ่งอาศัย HTML localStorage
บนเว็บ
คำเตือนที่คล้ายกันใช้ได้กับแพลตฟอร์มเดสก์ท็อป: มีปลั๊กอินน้อยมากที่เข้ากันได้กับแพลตฟอร์มเดสก์ท็อป และเนื่องจากเป็นธีมที่เกิดซ้ำ การทำงานนี้ต้องทำมากขึ้นบนเดสก์ท็อปมากกว่าที่จำเป็นจริงๆ บน Flutter สำหรับเว็บ
การสร้างเลย์เอาต์ที่ตอบสนองใน Flutter
เนื่องจากสิ่งที่ฉันได้อธิบายไว้ข้างต้นและเพื่อความเรียบง่าย ฉันจะถือว่าสำหรับส่วนที่เหลือของโพสต์นี้ว่าแพลตฟอร์มเป้าหมายของคุณคือเว็บ แต่แนวคิดพื้นฐานก็นำไปใช้กับการพัฒนาเดสก์ท็อปได้เช่นกัน
การสนับสนุนเว็บมีประโยชน์และความรับผิดชอบ การถูกบังคับค่อนข้างมากเพื่อรองรับขนาดหน้าจอที่แตกต่างกันอาจดูเหมือนเป็นข้อเสียเปรียบ แต่ให้พิจารณาว่าการเรียกใช้แอปในเว็บเบราว์เซอร์ช่วยให้คุณเห็นได้ง่ายว่าแอปของคุณจะมีลักษณะอย่างไรบนหน้าจอที่มีขนาดและอัตราส่วนต่างๆ ต่างกัน โดยไม่ต้องเรียกใช้แยกต่างหาก เครื่องจำลองอุปกรณ์มือถือ
ทีนี้มาคุยกันเรื่องรหัสกัน คุณจะทำให้แอปตอบสนองได้อย่างไร
มีสองมุมมองจากการวิเคราะห์นี้:
- “ฉันใช้วิดเจ็ตอะไรหรือฉันสามารถใช้ที่สามารถหรือควรปรับให้เข้ากับหน้าจอที่มีขนาดต่างกันได้”
- “ฉันจะรับข้อมูลเกี่ยวกับขนาดของหน้าจอได้อย่างไร และฉันจะใช้งานเมื่อเขียนโค้ด UI ได้อย่างไร”
เราจะตอบคำถามแรกในภายหลัง ก่อนอื่นเรามาพูดถึงเรื่องหลังกันก่อนเพราะสามารถจัดการได้ง่ายมากและเป็นหัวใจสำคัญของปัญหา มีสองวิธีในการทำเช่นนี้:
- วิธีหนึ่งคือนำข้อมูลจาก
MediaQueryData
ของรูMediaQuery
InheritedWidget
ซึ่งต้องมีอยู่ในแผนผังวิดเจ็ตเพื่อให้แอป Flutter ทำงานได้ (เป็นส่วนหนึ่งของMaterialApp/WidgetsApp/CupertinoApp
) ซึ่งคุณจะได้รับเช่นเดียวกับInheritedWidget
อื่น ๆ ที่มีMediaQuery.of(context)
ซึ่งมีคุณสมบัติsize
ซึ่งเป็นประเภทSize
และมีคุณสมบัติwidth
และheight
สองประการของประเภทdouble
- อีกวิธีหนึ่งคือการใช้
LayoutBuilder
ซึ่งเป็นวิดเจ็ตตัวสร้าง (เช่นเดียวกับStreamBuilder
หรือFutureBuilder
) ที่ส่งผ่านไปยังฟังก์ชันตัวbuilder
(พร้อมกับcontext
) วัตถุBoxConstraints
ที่มีminHeight
,maxHeight
,minWidth
และmaxWidth
นี่คือตัวอย่าง 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() ) ); }
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
แต่วิธีที่ชาญฉลาดกว่ามากในการทำเช่นนั้นคือการใช้ GridView.builder
กับ 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 ที่ปรับให้เข้ากับหน้าจอต่างๆ คือหน้า Landing Page ส่วนตัวของฉัน ซึ่งเป็นเว็บแอป Flutter ที่เรียบง่ายซึ่งประกอบด้วย 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
(เรียกอีกอย่างว่าเมนูแฮมเบอร์เกอร์)
ทางเลือกอื่นที่จะเป็น BottomNavigationBar
หรือ TabBar
ร่วมกับ TabBarView
แต่สำหรับทั้งสองอย่าง เราต้องทำการเปลี่ยนแปลงมากกว่าที่จำเป็นใน Drawer ดังนั้นเราจะยึดติดกับ Drawer ต่อไป

หากต้องการแสดงเฉพาะ Drawer
ที่มี Menu
ที่เราเห็นในหน้าจอขนาดเล็กก่อนหน้านี้ คุณจะต้องเขียนโค้ดที่ดูเหมือนตัวอย่างต่อไปนี้ ตรวจสอบความกว้างโดยใช้ MediaQuery.of(context)
และส่งอ็อบเจ็กต์ Drawer
ไปยัง Scaffold
เฉพาะในกรณีที่เป็น น้อยกว่าค่าความกว้างที่เราเชื่อว่าเหมาะสมกับแอปของเรา:
Scaffold( appBar: AppBar(/* ... \*/), drawer: MediaQuery.of(context).size.width < 500 ? Drawer( child: Menu(), ) : null, body: /* ... \*/ )
ทีนี้ มาคิดถึง body
ของ Scaffold
กัน ตามตัวอย่างเนื้อหาหลักของแอป เราจะใช้ 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]) ) ) ) ); }
บนหน้าจอที่ใหญ่ขึ้น เนื้อหาอาจเป็น Row
ที่แสดงวิดเจ็ตสองตัว: Menu
ซึ่งจำกัดความกว้างคงที่ และ Content
จะเติมส่วนที่เหลือของหน้าจอ
บนหน้าจอขนาดเล็ก body
ทั้งหมดจะเป็น Content
เราจะรวมทุกอย่างไว้ในวิดเจ็ต SafeArea
และ Center
เพราะบางครั้ง Flutter วิดเจ็ตเว็บแอป โดยเฉพาะอย่างยิ่งเมื่อใช้ Row
s และ Column
s จะอยู่นอกพื้นที่หน้าจอที่มองเห็นได้ และได้รับการแก้ไขด้วย SafeArea
และ/หรือ Center
ซึ่งหมายความว่า body
ของ Scaffold
จะเป็นดังนี้:
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]) ) ) ) ); }
นี่คือสิ่งส่วนใหญ่ที่คุณต้องการเพื่อเป็นข้อมูลเบื้องต้นเกี่ยวกับ UI ที่ตอบสนองใน Flutter แอปพลิเคชันส่วนใหญ่จะขึ้นอยู่กับ UI เฉพาะของแอป และเป็นการยากที่จะระบุว่าคุณสามารถทำอะไรได้บ้างเพื่อให้แอปตอบสนองได้ และคุณสามารถใช้วิธีการต่างๆ ได้มากมายขึ้นอยู่กับความชอบของคุณ ในตอนนี้ เรามาดูกันว่าเราจะสร้างตัวอย่างที่สมบูรณ์ยิ่งขึ้นในแอปที่ตอบสนองได้อย่างไร โดยคำนึงถึงองค์ประกอบแอปทั่วไปและโฟลว์ UI
ใส่ไว้ในบริบท: ทำให้แอปตอบสนอง
จนถึงตอนนี้เรามีเพียงหน้าจอ มาขยายมันให้เป็นแอพสองหน้าจอพร้อมการนำทางตาม URL ที่ใช้งานได้!
การสร้างหน้าเข้าสู่ระบบที่ตอบสนอง
โอกาสที่แอปของคุณมีหน้าเข้าสู่ระบบ เราจะทำให้ตอบสนองได้อย่างไร?
หน้าจอเข้าสู่ระบบบนอุปกรณ์มือถือค่อนข้างจะคล้ายกันโดยปกติ พื้นที่ว่างมีไม่มากนัก มันมักจะเป็นเพียง Column
ที่มีการเติมบาง Padding
รอบวิดเจ็ตและมี TextField
สำหรับพิมพ์ชื่อผู้ใช้และรหัสผ่านและปุ่มเพื่อเข้าสู่ระบบ ดังนั้นมาตรฐานที่ค่อนข้างดี (แม้ว่าจะไม่ทำงานตามที่ต้องการ เหนือสิ่งอื่นใด TextEditingController
สำหรับแต่ละหน้าการเข้าสู่ระบบ TextField
) สำหรับแอปมือถืออาจเป็นดังต่อไปนี้:
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 ทีนี้ เราจะตั้งค่า constraints
ของ Container
เป็น 500 (ฉันใช้ Container
แทน Padding
ในตัวอย่างก่อนหน้านี้ เพราะฉันรู้ว่าฉันกำลังจะทำอะไรกับสิ่งนี้ ) แล้วเราก็ไปกันเลยดีไหม? ไม่ได้จริงๆ เพราะนั่นจะทำให้วิดเจ็ตการเข้าสู่ระบบติดอยู่ทางด้านซ้ายของหน้าจอ ซึ่งอาจแย่ยิ่งกว่าการยืดทุกอย่าง ดังนั้นเราจึงรวมวิดเจ็ต Center
ไว้ดังนี้:
Center( child: Container( constraints: BoxConstraints(maxWidth: 500), padding: const EdgeInsets.symmetric( vertical: 30.0, horizontal: 25.0 ), child: Column(/* ... \*/) ) )
ดูดีอยู่แล้ว และเราไม่จำเป็นต้องใช้ LayoutBuilder
หรือ MediaQuery.of(context).size
ไปอีกขั้นหนึ่งเพื่อทำให้มันดูดีมาก ในความคิดของฉัน คงจะดูดีกว่านี้ ถ้าส่วนโฟร์กราวด์แยกออกจากแบ็คกราวด์ในทางใดทางหนึ่ง เราสามารถทำได้โดยกำหนดสีพื้นหลังให้กับสิ่งที่อยู่เบื้องหลัง Container
ด้วยวิดเจ็ตอินพุต และทำให้ Container
เบื้องหน้าเป็นสีขาว เพื่อให้ดูดีขึ้นเล็กน้อย อย่าให้ Container
ยืดออกไปจนถึงด้านบนและด้านล่างของหน้าจอบนอุปกรณ์ขนาดใหญ่ ให้มุมที่โค้งมน และให้ภาพเคลื่อนไหวที่สวยงามระหว่างสองเลย์เอาต์
ทั้งหมดนี้ต้องใช้ LayoutBuilder
และ Container
ภายนอก เพื่อตั้งค่าสีพื้นหลังและเพิ่มช่องว่างภายใน Container
ไม่ใช่แค่ด้านข้างบนหน้าจอขนาดใหญ่เท่านั้น นอกจากนี้ หากต้องการเปลี่ยนแปลงจำนวนช่องว่างภายในให้เคลื่อนไหว เราเพียงแค่เปลี่ยน Container
ภายนอกนั้นให้เป็น AnimatedContainer
ซึ่งต้องใช้ duration
สำหรับแอนิเมชัน ซึ่งเราจะตั้งค่าเป็นครึ่งวินาที ซึ่งก็คือ Duration(milliseconds: 500)
ใน รหัส.
นี่คือตัวอย่างหน้าเข้าสู่ระบบแบบตอบสนอง:

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() ) ); } ) ] ), ), ) ); } ) ); }
อย่างที่คุณเห็น ฉันได้เปลี่ยน RaisedButton
ของ onPressed
เป็นการโทรกลับที่นำทางเราไปยังหน้าจอชื่อ HomePage
(ซึ่งอาจเป็น ตัวอย่างเช่น มุมมองที่เราสร้างไว้ก่อนหน้านี้ด้วย GridView
และเมนูหรือลิ้นชัก) แม้ว่าตอนนี้ ส่วนการนำทางนั้นคือสิ่งที่เราจะเน้น
เส้นทางที่มีชื่อ: ทำให้การนำทางแอปของคุณเหมือนเว็บแอปที่เหมาะสม
สิ่งที่พบได้ทั่วไปสำหรับเว็บแอปคือความสามารถในการเปลี่ยนหน้าจอตาม URL ตัวอย่างเช่น การไปที่ https://appurl/login
ควรให้สิ่งที่แตกต่างจาก https://appurl/somethingelse
แก่คุณ อันที่จริง Flutter รองรับการ ตั้งชื่อ routes ซึ่งมีวัตถุประสงค์สองประการ:
- ในเว็บแอป พวกเขามีคุณสมบัติตรงตามที่ฉันพูดถึงในประโยคที่แล้ว
- ในแอปใดๆ แอปเหล่านี้อนุญาตให้คุณกำหนดเส้นทางล่วงหน้าสำหรับแอปของคุณและตั้งชื่อ จากนั้นจึงนำทางไปยังแอปเหล่านั้นได้โดยการระบุชื่อ
ในการทำเช่นนั้น เราจำเป็นต้องเปลี่ยนตัวสร้าง 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]) ) ) ) ); }
ก้าวไปข้างหน้ากับการผจญภัยกระพือของคุณ
ข้อมูลดังกล่าวน่าจะให้แนวคิดแก่คุณเกี่ยวกับสิ่งที่คุณสามารถทำได้ด้วย Flutter บนหน้าจอที่ใหญ่ขึ้น โดยเฉพาะบนเว็บ เป็นเฟรมเวิร์กที่น่ารัก ใช้งานง่ายมาก และการสนับสนุนข้ามแพลตฟอร์มอย่างสุดขีดทำให้การเรียนรู้และเริ่มต้นใช้งานจำเป็นมากขึ้นเท่านั้น ดังนั้น ไปข้างหน้าและเริ่มไว้วางใจ Flutter สำหรับเว็บแอปด้วย!
แหล่งข้อมูลเพิ่มเติม
- “เดสก์ท็อปเชลล์”, GitHub
สถานะ Flutter บนเดสก์ท็อปที่เป็นปัจจุบันและเป็นปัจจุบันเสมอ - “รองรับเดสก์ท็อปสำหรับ Flutter”, Flutter
ข้อมูลเกี่ยวกับแพลตฟอร์มเดสก์ท็อปที่รองรับอย่างสมบูรณ์ - “รองรับเว็บสำหรับ Flutter”, Flutter
ข้อมูลเกี่ยวกับ Flutter สำหรับเว็บ - “ตัวอย่างทั้งหมด”, Flutter
รายการตัวอย่างและแอพ Flutter ที่รวบรวมไว้