เมื่อเร็ว ๆ นี้ ฉันต้องจัดทำคำแนะนำการเริ่มต้นใช้งานสำหรับผู้ใช้ใหม่ของ ClickUp! นี่เป็นงานที่สำคัญมากเพราะผู้ใช้ใหม่จำนวนมากกำลังจะค้นพบแพลตฟอร์มนี้ผ่านโฆษณาที่ตลกมาก ๆ ที่เราเปิดตัวครั้งแรกในงาน Super Bowl! ✨
ผ่านทาง ClickUp
คู่มือการใช้งานนี้ช่วยให้ผู้ใช้ใหม่จำนวนมากของเรา ซึ่งอาจยังไม่คุ้นเคยกับ ClickUp ได้เข้าใจวิธีการใช้งานฟังก์ชันต่าง ๆ ของแอปพลิเคชันได้อย่างรวดเร็ว นี่เป็นความพยายามอย่างต่อเนื่อง เช่นเดียวกับแหล่งข้อมูลClickUp Universityใหม่ที่เราตั้งใจพัฒนาต่อไป! 🚀
โชคดีที่สถาปัตยกรรมซอฟต์แวร์ที่อยู่เบื้องหลังแอปพลิเคชันมือถือ ClickUp Flutter ช่วยให้ฉันสามารถนำฟังก์ชันนี้ไปใช้ได้อย่างรวดเร็ว แม้กระทั่งการนำวิดเจ็ตจริงจากแอปพลิเคชันมาใช้ซ้ำ! ซึ่งหมายความว่า การสาธิตนี้มีความไดนามิก ตอบสนองได้ดี และตรงกับหน้าจอแอปพลิเคชันจริงของแอปอย่างแม่นยำ—และจะยังคงเป็นเช่นนั้นต่อไป แม้เมื่อวิดเจ็ตมีการพัฒนาเปลี่ยนแปลงก็ตาม
ฉันสามารถนำฟังก์ชันการทำงานนี้ไปใช้ได้เพราะการแยกความกังวลที่เหมาะสม
มาดูกันว่าฉันหมายถึงอะไรที่นี่ 🤔
การแยกแยะปัญหาออกจากกัน
การออกแบบสถาปัตยกรรมซอฟต์แวร์เป็นหนึ่งในหัวข้อที่ซับซ้อนที่สุดสำหรับทีมวิศวกรรม ในบรรดาความรับผิดชอบทั้งหมด การคาดการณ์การพัฒนาซอฟต์แวร์ในอนาคตนั้นเป็นเรื่องที่ยากเสมอ นั่นคือเหตุผลที่การสร้างสถาปัตยกรรมที่มีการแบ่งชั้นและแยกส่วนอย่างเหมาะสมสามารถช่วยคุณและเพื่อนร่วมทีมของคุณได้ในหลายๆ ด้าน!
ประโยชน์หลักของการสร้างระบบย่อยที่แยกการทำงานออกจากกันขนาดเล็กอย่างชัดเจน คือ ความสามารถในการทดสอบ อย่างไม่ต้องสงสัย! และนี่เองคือสิ่งที่ช่วยให้ผมสามารถสร้างตัวอย่างเดโมของหน้าจอที่มีอยู่ในแอปขึ้นมาได้!
คู่มือแบบทีละขั้นตอน
ตอนนี้ เราจะสามารถนำหลักการเหล่านั้นไปประยุกต์ใช้กับแอปพลิเคชัน Flutter ได้อย่างไร?
เราจะแบ่งปันเทคนิคบางประการที่เราใช้ในการสร้าง ClickUp พร้อมตัวอย่างการสาธิตอย่างง่าย
ตัวอย่างนี้ง่ายมากจนอาจไม่สามารถเผยให้เห็นข้อดีทั้งหมดที่อยู่เบื้องหลังได้ แต่เชื่อเถอะว่ามันจะช่วยคุณสร้างแอปพลิเคชัน Flutter ที่มีความยั่งยืนมากขึ้นและมีโค้ดเบสที่ซับซ้อนได้ 💡
แอปพลิเคชัน
ตัวอย่างเช่น เราจะสร้างแอปพลิเคชันที่แสดงจำนวนประชากรของสหรัฐอเมริกาในแต่ละปี
ผ่านทาง ClickUp
เรามีสองหน้าจอที่นี่:
- หน้าหลัก: แสดงรายการปีทั้งหมดตั้งแต่ปี 2000 จนถึงปัจจุบัน เมื่อผู้ใช้แตะที่แผ่นป้ายปีใดปีหนึ่ง พวกเขาจะไปยังหน้าข้อมูลโดยมีการตั้งค่าอาร์กิวเมนต์การนำทางเป็นปีนั้น
- DetailScreen: รับปีจากอาร์กิวเมนต์การนำทาง เรียกใช้API ของ datausa.ioสำหรับปีนี้ และแยกวิเคราะห์ข้อมูล JSON เพื่อดึงค่าจำนวนประชากรที่เกี่ยวข้อง หากมีข้อมูลพร้อมใช้งาน จะแสดงป้ายกำกับพร้อมจำนวนประชากร
เราจะมุ่งเน้นไปที่การนำไปใช้ใน DetailScreen เนื่องจากมันน่าสนใจที่สุดด้วยการเรียกแบบไม่พร้อมกัน
ขั้นตอนที่ 1. วิธีการแบบไม่รู้อะไรมาก่อน

การใช้งานที่ชัดเจนที่สุดสำหรับแอปของเรา คือการใช้ StatefulWidget เพียงตัวเดียวสำหรับตรรกะทั้งหมด
การเข้าถึงอาร์กิวเมนต์การนำทางปี
เพื่อเข้าถึงปีที่ต้องการ เราอ่านค่า RouteSettings จากวิดเจ็ตที่สืบทอดมาจาก ModalRoute
การเรียก HTTP
ตัวอย่างนี้เรียกใช้ฟังก์ชัน get จากแพ็กเกจ http เพื่อดึงข้อมูลจากAPI ของdatausa.io จากนั้นทำการแยกวิเคราะห์ JSON ที่ได้มาด้วยเมธอด jsonDecode จากไลบรารี dart:convert และเก็บ Future ไว้เป็นส่วนหนึ่งของสถานะด้วยคุณสมบัติชื่อ _future
การเรนเดอร์
ในการสร้างต้นไม้ของวิดเจ็ต เราใช้ FutureBuilder ซึ่งจะสร้างตัวเองใหม่ตามสถานะปัจจุบันของการเรียกแบบอะซิงโครนัส _future ของเรา
รีวิว
โอเค การนำไปใช้งานนั้นสั้นและใช้เพียงวิดเจ็ตที่มีอยู่ในตัวเท่านั้น แต่ตอนนี้ลองนึกถึงจุดประสงค์เริ่มต้นของเรา: การสร้างทางเลือกสำหรับเดโม (หรือการทดสอบ) สำหรับหน้าจอนี้ มันยากมากที่จะควบคุมผลลัพธ์ของการเรียก HTTP เพื่อบังคับให้แอปพลิเคชันแสดงผลในสถานะที่ต้องการ
นั่นคือจุดที่แนวคิดเรื่อง การกลับด้านของการควบคุม จะเข้ามาช่วยเราได้ 🧐
ขั้นตอนที่ 2 การกลับด้านของการควบคุม

หลักการนี้อาจเข้าใจยากสำหรับนักพัฒนาใหม่ (และยากที่จะอธิบายด้วย) แต่แนวคิดโดยรวมคือการ แยกความกังวลออกนอกส่วนประกอบของเรา—เพื่อให้พวกมันไม่ต้องรับผิดชอบในการเลือกพฤติกรรม—และมอบหมายให้ส่วนอื่นแทน
ในสถานการณ์ที่พบได้บ่อยกว่า มันประกอบด้วยการสร้างนามธรรมและการฉีดการนำไปใช้เข้าไปในองค์ประกอบของเรา เพื่อให้การนำไปใช้ขององค์ประกอบเหล่านั้นสามารถเปลี่ยนแปลงได้ในภายหลังหากจำเป็น
แต่อย่ากังวลไป มันจะเข้าใจมากขึ้นหลังจากตัวอย่างถัดไปของเรา! 👀
การสร้างอ็อบเจ็กต์ไคลเอนต์ API
เพื่อควบคุมการเรียก HTTP ไปยัง API ของเรา เราได้แยกการดำเนินการของเราไว้ในคลาส DataUsaApiClient ที่เฉพาะเจาะจง เราได้สร้างคลาส Measure ขึ้นเพื่อทำให้ข้อมูลง่ายต่อการจัดการและบำรุงรักษา
ให้ลูกค้าใช้ API
สำหรับตัวอย่างของเรา เราใช้แพ็กเกจผู้ให้บริการที่เป็นที่รู้จักกันดีเพื่อฉีดอินสแตนซ์ของ DataUsaApiClient ที่รากของต้นไม้ของเรา
การใช้ไคลเอนต์ API
ผู้ให้บริการอนุญาตให้วิดเจ็ตลูกหลานใดๆ (เช่น DetailScreen ของเรา) อ่าน DataUsaApiClient ที่อยู่ใกล้ที่สุดในต้นไม้ได้ จากนั้นเราสามารถใช้เมธอด getMeasure ของมันเพื่อเริ่มต้น Future ของเรา แทนที่จะใช้การดำเนินการ HTTP จริง
ลูกค้าตัวอย่าง API
ตอนนี้เราสามารถใช้ประโยชน์จากสิ่งนี้ได้แล้ว!
ในกรณีที่คุณยังไม่ทราบ: คลาสใดๆใน dart จะกำหนดอินเทอร์เฟซที่เกี่ยวข้องโดยปริยายด้วย ซึ่งช่วยให้เราสามารถให้การใช้งาน DataUsaApiClient แบบอื่นได้ ซึ่งจะคืนค่าอินสแตนซ์เดียวกันเสมอจากการเรียกใช้เมธอด getMeasure
วิธีนี้
วิธีนี้
แสดงหน้าตัวอย่าง
ตอนนี้เรามีกุญแจทั้งหมดแล้วเพื่อแสดงตัวอย่างการสาธิตของ DetailPage!
เราเพียงแค่แทนที่อินสแตนซ์ DataUsaApiClient ที่ถูกจัดเตรียมไว้ในปัจจุบันด้วยการห่อหุ้ม DetailScreen ของเราไว้ในผู้ให้บริการที่สร้างอินสแตนซ์ DemoDataUsaApiClient แทน!
และนั่นแหละ—หน้าจอรายละเอียดของเราจะอ่านตัวอย่างการสาธิตนี้แทน และใช้ข้อมูลการวัดของเราแทนการเรียก HTTP
รีวิว
นี่คือตัวอย่างที่ยอดเยี่ยมของ การกลับด้านของการควบคุม วิดเจ็ต DetailScreen ของเราไม่มีความรับผิดชอบในการจัดการตรรกะของการดึงข้อมูลอีกต่อไป แต่จะมอบหมายให้วัตถุลูกค้าเฉพาะทางแทน และตอนนี้เราสามารถสร้างตัวอย่างการสาธิตของหน้าจอ หรือทำการทดสอบวิดเจ็ตสำหรับหน้าจอของเราได้แล้ว! ยอดเยี่ยมมาก! 👏
แต่เราสามารถทำได้ดีกว่านี้อีก!
เนื่องจากเราไม่สามารถจำลองสถานะการโหลดได้ เช่น เราจึงไม่สามารถควบคุมการเปลี่ยนแปลงสถานะใดๆ ได้ทั้งหมดในระดับวิดเจ็ตของเรา
ขั้นตอนที่ 3 การจัดการสถานะ

นี่เป็นหัวข้อที่ร้อนแรงใน Flutter!
ผมมั่นใจว่าคุณคงได้อ่านกระทู้ยาว ๆ ที่มีคนพยายามเลือก วิธีจัดการ state ที่ดีที่สุด สำหรับ Flutter กันมาแล้ว และเพื่อความชัดเจน นี่ไม่ใช่สิ่งที่เราจะทำในบทความนี้ ในความเห็นของเรา ตราบใดที่คุณแยกตรรกะทางธุรกิจออกจากตรรกะทางภาพ คุณก็ไม่มีปัญหา! การสร้างชั้นเหล่านี้มีความสำคัญมากสำหรับการบำรุงรักษา ตัวอย่างของเราอาจเรียบง่าย แต่ในแอปพลิเคชันจริง ตรรกะสามารถซับซ้อนได้อย่างรวดเร็ว และการแยกเช่นนี้จะทำให้ง่ายขึ้นมากในการค้นหาอัลกอริทึมตรรกะบริสุทธิ์ของคุณ หัวข้อนี้มักจะถูกสรุปว่าเป็นการจัดการสถานะ
ในตัวอย่างนี้ เราใช้ ValueNotifier พื้นฐานร่วมกับ Provider แต่เราสามารถใช้flutter_blocหรือriverpod (หรือวิธีอื่น), และมันก็จะทำงานได้ดีเช่นกัน หลักการยังคงเหมือนเดิม และตราบใดที่คุณแยกสถานะและตรรกะของคุณออกจากกัน ก็สามารถย้ายโค้ดของคุณจากวิธีอื่นมาใช้ได้เช่นกัน
การแยกนี้ยังช่วยให้เราสามารถควบคุมสถานะของวิดเจ็ตของเราได้ เพื่อที่เราจะสามารถจำลองมันได้ในทุกวิถีทางที่เป็นไปได้!
การสร้างสถานะเฉพาะ
แทนที่จะพึ่งพา AsyncSnapshot จากเฟรมเวิร์ก เราได้แทนสถานะหน้าจอของเราเป็นวัตถุ DetailState
นอกจากนี้ยังสำคัญที่จะต้องนำวิธีการ hashCode และ operator == มาใช้เพื่อให้วัตถุของเราสามารถเปรียบเทียบได้ตามค่า ซึ่งช่วยให้เราสามารถตรวจจับได้ว่าสองอินสแตนซ์ควรถูกพิจารณาว่าแตกต่างกันหรือไม่
💡 แพ็กเกจequatableหรือfreezedเป็นตัวเลือกที่ยอดเยี่ยมในการช่วยให้คุณนำวิธีการ hashCode และ operator == ไปใช้!
💡 แพ็กเกจที่เท่าเทียมกันหรือ แพ็กเกจที่ถูกแช่แข็งเป็นตัวเลือกที่ยอดเยี่ยมในการช่วยให้คุณนำไปใช้กับเมธอด hashCode และ operator == ได้!
รัฐของเรามักจะถูกเชื่อมโยงกับปีเสมอ แต่เรายังมีสถานะที่แตกต่างกันสี่แบบเกี่ยวกับสิ่งที่เราต้องการแสดงให้ผู้ใช้เห็น:
- สถานะข้อมูลที่ยังไม่ได้โหลด: การอัปเดตข้อมูลยังไม่เริ่มต้น
- กำลังโหลดสถานะรายละเอียด: ข้อมูลกำลังถูกโหลดอยู่ในขณะนี้
- LoadedDetailState: ข้อมูลได้ถูกโหลดสำเร็จแล้วพร้อมกับมาตรการที่เกี่ยวข้อง
- NoDataDetailState: ข้อมูลได้ถูกโหลดแล้ว แต่ไม่มีข้อมูลที่สามารถใช้ได้
- สถานะรายละเอียดข้อผิดพลาดที่ไม่ทราบสาเหตุ: การดำเนินการล้มเหลวเนื่องจากข้อผิดพลาดที่ไม่ทราบสาเหตุ
รัฐเหล่านั้นชัดเจนกว่า AsyncSnapshot เพราะมันแทนกรณีการใช้งานของเราได้จริง ๆ และอีกครั้ง สิ่งนี้ทำให้โค้ดของเราดูแลรักษาง่ายขึ้น!
💡 เราขอแนะนำอย่างยิ่งให้เลือกใช้ประเภท Union จากแพ็กเกจแบบฟรีซเพื่อใช้แทนสถานะตรรกะของคุณ! มันเพิ่มประโยชน์มากมาย เช่น เมธอด copyWith หรือ map
💡 เราขอแนะนำอย่างยิ่งให้เลือกใช้ประเภท Union จากแพ็กเกจแบบฟรีซเพื่อใช้แทนสถานะตรรกะของคุณ! มันเพิ่มประโยชน์มากมาย เช่น เมธอด copyWith หรือ map
การใส่ตรรกะในตัวแจ้งเตือน
ตอนนี้เรามีตัวแทนของสถานะของเราแล้ว เราจำเป็นต้องเก็บอินสแตนซ์ของมันไว้ที่ไหนสักแห่ง—นั่นคือจุดประสงค์ของ DetailNotifier มันจะเก็บอินสแตนซ์ของ DetailState ปัจจุบันไว้ในคุณสมบัติ value ของมัน และจะจัดเตรียมเมธอดสำหรับการอัปเดตสถานะ
เราให้สถานะเริ่มต้น NotLoadedDetailState และเมธอดรีเฟรชเพื่อโหลดข้อมูลจาก api และอัปเดตค่าปัจจุบัน
ให้สถานะแก่หน้าจอ
ในการสร้างและสังเกตสถานะของหน้าจอของเรา เราต้องพึ่งพาผู้ให้บริการและ ChangeNotifierProvider ของมันด้วย ผู้ให้บริการประเภทนี้จะค้นหา ChangeListener ที่ถูกสร้างขึ้นโดยอัตโนมัติและจะกระตุ้นการสร้างใหม่จาก Consumer ทุกครั้งที่ได้รับแจ้งว่ามีการเปลี่ยนแปลง (เมื่อค่า notifier ของเราแตกต่างจากค่าก่อนหน้า).
รีวิว
ยอดเยี่ยม! สถาปัตยกรรมแอปพลิเคชันของเรากำลังดูดีขึ้นมาก ทุกอย่างถูกแยกออกเป็นชั้นๆ ที่ชัดเจน โดยมีข้อกังวลเฉพาะเจาะจง! 🤗
อย่างไรก็ตาม ยังมีสิ่งหนึ่งที่ยังขาดอยู่สำหรับการทดสอบ เราต้องการกำหนด DetailState ปัจจุบันเพื่อควบคุมสถานะของ DetailScreen ที่เกี่ยวข้อง
ขั้นตอนที่ 4. วิดเจ็ตเลย์เอาต์เฉพาะที่มองเห็น

ในขั้นตอนสุดท้าย เราได้มอบความรับผิดชอบมากเกินไปให้กับวิดเจ็ต DetailScreen ของเรา: มันรับผิดชอบในการสร้างอินสแตนซ์ของ DetailNotifier และเช่นเดียวกับที่เราได้เห็นก่อนหน้านี้ เราพยายามหลีกเลี่ยงความรับผิดชอบด้านตรรกะใดๆ ที่ชั้นวิว!
เราสามารถแก้ไขปัญหานี้ได้อย่างง่ายดายโดยการสร้างเลเยอร์ใหม่สำหรับวิดเจ็ตหน้าจอของเรา: เราจะแยกวิดเจ็ต DetailScreen ของเราออกเป็นสองส่วน:
- DetailScreen มีหน้าที่รับผิดชอบในการตั้งค่าการพึ่งพาต่างๆ ของหน้าจอเราจากสถานะปัจจุบันของแอปพลิเคชัน (การนำทาง, ตัวแจ้งเตือน, สถานะ, บริการ, ...)
- DetailLayout แปลง DetailState เป็นต้นไม้ของวิดเจ็ตเฉพาะอย่างง่ายๆ
โดยการรวมสองสิ่งนี้เข้าด้วยกัน เราจะสามารถสร้างตัวอย่างการสาธิต/ทดสอบ DetailLayout ได้อย่างง่ายดาย แต่ยังคงมี DetailScreen สำหรับกรณีการใช้งานจริงในแอปพลิเคชันของเรา
รูปแบบที่ออกแบบเฉพาะ
เพื่อให้การแยกความรับผิดชอบดีขึ้น เราได้ย้ายทุกอย่างที่อยู่ภายใต้ widget ผู้บริโภคไปยัง widget DetailLayout ที่เฉพาะเจาะจง widget ใหม่นี้จะรับข้อมูลเท่านั้น และไม่รับผิดชอบต่อการสร้างอินสแตนซ์ใด ๆ มันเพียงแค่แปลงสถานะการอ่านให้เป็นต้นไม้ของ widget ที่เฉพาะเจาะจง
โมเดลเส้นทางของการเรียกและอินสแตนซ์ ChangeNotifierProvider ยังคงอยู่ใน DetailScreen และวิดเจ็ตนี้จะส่งคืน DetailLayout พร้อมต้นไม้การพึ่งพาที่ตั้งค่าไว้ล่วงหน้า!
การปรับปรุงเล็กน้อยนี้เฉพาะสำหรับการใช้งานของผู้ให้บริการ แต่คุณจะสังเกตเห็นว่าเราได้เพิ่ม ProxyProvider ด้วย เพื่อให้วิดเจ็ตลูกหลานใดๆ สามารถใช้ DetailState ได้โดยตรง ซึ่งจะช่วยให้การจำลองข้อมูลง่ายขึ้น
การแยกวิดเจ็ตเป็นคลาสเฉพาะ
อย่าลังเลที่จะแยกต้นไม้ของวิดเจ็ตออกมาเป็นคลาสเฉพาะ! มันจะช่วยปรับปรุงประสิทธิภาพและทำให้โค้ดมีความสามารถในการบำรุงรักษามากขึ้น
ในตัวอย่างของเรา เราได้สร้างวิดเจ็ตเลย์เอาต์แบบภาพหนึ่งรายการสำหรับแต่ละประเภทสถานะที่เกี่ยวข้อง:
อินสแตนซ์ตัวอย่าง
ตอนนี้เรามีการควบคุมอย่างเต็มที่แล้วว่าเราจะสามารถจำลองและแสดงอะไรบนหน้าจอของเราได้บ้าง!
เราเพียงแค่ต้องห่อ DetailLayout ด้วย Provider
สรุป
การสร้างสถาปัตยกรรมซอฟต์แวร์ที่สามารถดูแลรักษาได้อย่างยั่งยืนนั้นไม่ใช่เรื่องง่ายอย่างแน่นอน! การคาดการณ์สถานการณ์ในอนาคตอาจต้องใช้ความพยายามอย่างมาก แต่ผมหวังว่าเคล็ดลับเล็กๆ น้อยๆ ที่ผมได้แบ่งปันไปจะเป็นประโยชน์กับคุณในอนาคต!
ตัวอย่างอาจดูเรียบง่าย—อาจดูเหมือนว่าเราออกแบบเกินความจำเป็น—แต่เมื่อแอปของคุณมีความซับซ้อนมากขึ้น การมีมาตรฐานเหล่านี้จะช่วยคุณได้มาก! 💪
สนุกกับ Flutter และติดตามบล็อกเพื่อรับบทความเชิงเทคนิคเพิ่มเติมเช่นนี้! ติดตามต่อไป!

