การพัฒนาเว็บและเดสก์ท็อปที่ตอบสนองด้วย Flutter

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ Flutter ได้สร้างความกระฉับกระเฉงให้กับฉากการพัฒนามือถือแล้ว ตอนนี้ก็ใช้อุปกรณ์ขนาดใหญ่ขึ้นเช่นกัน นี่คือสิ่งที่คุณจำเป็นต้องรู้เพื่อให้พร้อมที่จะทำงานในการพัฒนาเว็บและแอปเดสก์ท็อปโดยใช้เฟรมเวิร์กข้ามแพลตฟอร์มที่ยอดเยี่ยมนี้

บทช่วยสอนนี้ไม่ใช่การแนะนำ Flutter เอง มีบทความ วิดีโอ และหนังสือหลายเล่มทางออนไลน์พร้อมคำแนะนำง่ายๆ ที่จะช่วยให้คุณเรียนรู้พื้นฐานของ Flutter เราจะครอบคลุมวัตถุประสงค์สองข้อต่อไปนี้แทน:

  1. สถานะปัจจุบันของการพัฒนา Flutter ที่ไม่ใช่มือถือและวิธีเรียกใช้โค้ด Flutter ในเบราว์เซอร์ บนคอมพิวเตอร์เดสก์ท็อปหรือแล็ปท็อป
  2. วิธีสร้างแอปที่ตอบสนองด้วย 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

เนื่องจากสิ่งที่ฉันได้อธิบายไว้ข้างต้นและเพื่อความเรียบง่าย ฉันจะถือว่าสำหรับส่วนที่เหลือของโพสต์นี้ว่าแพลตฟอร์มเป้าหมายของคุณคือเว็บ แต่แนวคิดพื้นฐานก็นำไปใช้กับการพัฒนาเดสก์ท็อปได้เช่นกัน

การสนับสนุนเว็บมีประโยชน์และความรับผิดชอบ การถูกบังคับค่อนข้างมากเพื่อรองรับขนาดหน้าจอที่แตกต่างกันอาจดูเหมือนเป็นข้อเสียเปรียบ แต่ให้พิจารณาว่าการเรียกใช้แอปในเว็บเบราว์เซอร์ช่วยให้คุณเห็นได้ง่ายว่าแอปของคุณจะมีลักษณะอย่างไรบนหน้าจอที่มีขนาดและอัตราส่วนต่างๆ ต่างกัน โดยไม่ต้องเรียกใช้แยกต่างหาก เครื่องจำลองอุปกรณ์มือถือ

ทีนี้มาคุยกันเรื่องรหัสกัน คุณจะทำให้แอปตอบสนองได้อย่างไร

มีสองมุมมองจากการวิเคราะห์นี้:

  1. “ฉันใช้วิดเจ็ตอะไรหรือฉันสามารถใช้ที่สามารถหรือควรปรับให้เข้ากับหน้าจอที่มีขนาดต่างกันได้”
  2. “ฉันจะรับข้อมูลเกี่ยวกับขนาดของหน้าจอได้อย่างไร และฉันจะใช้งานเมื่อเขียนโค้ด UI ได้อย่างไร”

เราจะตอบคำถามแรกในภายหลัง ก่อนอื่นเรามาพูดถึงเรื่องหลังกันก่อนเพราะสามารถจัดการได้ง่ายมากและเป็นหัวใจสำคัญของปัญหา มีสองวิธีในการทำเช่นนี้:

  1. วิธีหนึ่งคือนำข้อมูลจาก MediaQueryData ของรู MediaQuery InheritedWidget ซึ่งต้องมีอยู่ในแผนผังวิดเจ็ตเพื่อให้แอป Flutter ทำงานได้ (เป็นส่วนหนึ่งของ MaterialApp/WidgetsApp/CupertinoApp ) ซึ่งคุณจะได้รับเช่นเดียวกับ InheritedWidget อื่น ๆ ที่มี MediaQuery.of(context) ซึ่งมีคุณสมบัติ size ซึ่งเป็นประเภท Size และมีคุณสมบัติ width และ height สองประการของประเภท double
  2. อีกวิธีหนึ่งคือการใช้ 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 ซึ่งมีวัตถุประสงค์สองประการ:

  1. ในเว็บแอป พวกเขามีคุณสมบัติตรงตามที่ฉันพูดถึงในประโยคที่แล้ว
  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]) ) ) ) ); }

ก้าวไปข้างหน้ากับการผจญภัยกระพือของคุณ

ข้อมูลดังกล่าวน่าจะให้แนวคิดแก่คุณเกี่ยวกับสิ่งที่คุณสามารถทำได้ด้วย Flutter บนหน้าจอที่ใหญ่ขึ้น โดยเฉพาะบนเว็บ เป็นเฟรมเวิร์กที่น่ารัก ใช้งานง่ายมาก และการสนับสนุนข้ามแพลตฟอร์มอย่างสุดขีดทำให้การเรียนรู้และเริ่มต้นใช้งานจำเป็นมากขึ้นเท่านั้น ดังนั้น ไปข้างหน้าและเริ่มไว้วางใจ Flutter สำหรับเว็บแอปด้วย!

แหล่งข้อมูลเพิ่มเติม

  • “เดสก์ท็อปเชลล์”, GitHub
    สถานะ Flutter บนเดสก์ท็อปที่เป็นปัจจุบันและเป็นปัจจุบันเสมอ
  • “รองรับเดสก์ท็อปสำหรับ Flutter”, Flutter
    ข้อมูลเกี่ยวกับแพลตฟอร์มเดสก์ท็อปที่รองรับอย่างสมบูรณ์
  • “รองรับเว็บสำหรับ Flutter”, Flutter
    ข้อมูลเกี่ยวกับ Flutter สำหรับเว็บ
  • “ตัวอย่างทั้งหมด”, Flutter
    รายการตัวอย่างและแอพ Flutter ที่รวบรวมไว้