การแยกความกังวลในแอปพลิเคชัน Flutter
Engineering at ClickUp

การแยกความกังวลในแอปพลิเคชัน Flutter

เมื่อเร็ว ๆ นี้ ฉันต้องจัดทำคำแนะนำการเริ่มต้นใช้งานสำหรับผู้ใช้ใหม่ของ ClickUp! นี่เป็นงานที่สำคัญมากเพราะผู้ใช้ใหม่จำนวนมากกำลังจะค้นพบแพลตฟอร์มนี้ผ่านโฆษณาที่ตลกมาก ๆ ที่เราเปิดตัวครั้งแรกในงาน Super Bowl! ✨

ผ่านทาง ClickUp

คู่มือการใช้งานนี้ช่วยให้ผู้ใช้ใหม่จำนวนมากของเรา ซึ่งอาจยังไม่คุ้นเคยกับ ClickUp ได้เข้าใจวิธีการใช้งานฟังก์ชันต่าง ๆ ของแอปพลิเคชันได้อย่างรวดเร็ว นี่เป็นความพยายามอย่างต่อเนื่อง เช่นเดียวกับแหล่งข้อมูลClickUp Universityใหม่ที่เราตั้งใจพัฒนาต่อไป! 🚀

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

ฉันสามารถนำฟังก์ชันการทำงานนี้ไปใช้ได้เพราะการแยกความกังวลที่เหมาะสม

มาดูกันว่าฉันหมายถึงอะไรที่นี่ 🤔

การแยกแยะปัญหาออกจากกัน

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

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

คู่มือแบบทีละขั้นตอน

ตอนนี้ เราจะสามารถนำหลักการเหล่านั้นไปประยุกต์ใช้กับแอปพลิเคชัน Flutter ได้อย่างไร?

เราจะแบ่งปันเทคนิคบางประการที่เราใช้ในการสร้าง ClickUp พร้อมตัวอย่างการสาธิตอย่างง่าย

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

แอปพลิเคชัน

ตัวอย่างเช่น เราจะสร้างแอปพลิเคชันที่แสดงจำนวนประชากรของสหรัฐอเมริกาในแต่ละปี

ผ่านทาง ClickUp

เรามีสองหน้าจอที่นี่:

  • หน้าหลัก: แสดงรายการปีทั้งหมดตั้งแต่ปี 2000 จนถึงปัจจุบัน เมื่อผู้ใช้แตะที่แผ่นป้ายปีใดปีหนึ่ง พวกเขาจะไปยังหน้าข้อมูลโดยมีการตั้งค่าอาร์กิวเมนต์การนำทางเป็นปีนั้น
  • DetailScreen: รับปีจากอาร์กิวเมนต์การนำทาง เรียกใช้API ของ datausa.ioสำหรับปีนี้ และแยกวิเคราะห์ข้อมูล JSON เพื่อดึงค่าจำนวนประชากรที่เกี่ยวข้อง หากมีข้อมูลพร้อมใช้งาน จะแสดงป้ายกำกับพร้อมจำนวนประชากร

เราจะมุ่งเน้นไปที่การนำไปใช้ใน DetailScreen เนื่องจากมันน่าสนใจที่สุดด้วยการเรียกแบบไม่พร้อมกัน

ขั้นตอนที่ 1. วิธีการแบบไม่รู้อะไรมาก่อน

วิธีการแบบไร้เดียงสา วิดเจ็ตแบบมีสถานะ
ผ่านทาง ClickUp

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

การเข้าถึงอาร์กิวเมนต์การนำทางปี

เพื่อเข้าถึงปีที่ต้องการ เราอ่านค่า RouteSettings จากวิดเจ็ตที่สืบทอดมาจาก ModalRoute

การเรียก HTTP

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

การเรนเดอร์

ในการสร้างต้นไม้ของวิดเจ็ต เราใช้ FutureBuilder ซึ่งจะสร้างตัวเองใหม่ตามสถานะปัจจุบันของการเรียกแบบอะซิงโครนัส _future ของเรา

รีวิว

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

นั่นคือจุดที่แนวคิดเรื่อง การกลับด้านของการควบคุม จะเข้ามาช่วยเราได้ 🧐

ขั้นตอนที่ 2 การกลับด้านของการควบคุม

การกลับด้านของการควบคุมไปยังผู้ให้บริการและไคลเอนต์ API
ผ่านทาง ClickUp

หลักการนี้อาจเข้าใจยากสำหรับนักพัฒนาใหม่ (และยากที่จะอธิบายด้วย) แต่แนวคิดโดยรวมคือการ แยกความกังวลออกนอกส่วนประกอบของเรา—เพื่อให้พวกมันไม่ต้องรับผิดชอบในการเลือกพฤติกรรม—และมอบหมายให้ส่วนอื่นแทน

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

แต่อย่ากังวลไป มันจะเข้าใจมากขึ้นหลังจากตัวอย่างถัดไปของเรา! 👀

การสร้างอ็อบเจ็กต์ไคลเอนต์ 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
ผ่านทาง ClickUp

นี่เป็นหัวข้อที่ร้อนแรงใน 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. วิดเจ็ตเลย์เอาต์เฉพาะที่มองเห็น

วิดเจ็ตเลย์เอาต์เฉพาะทางแบบแสดงผลในแอปพลิเคชัน Flutter
ผ่านทาง ClickUp

ในขั้นตอนสุดท้าย เราได้มอบความรับผิดชอบมากเกินไปให้กับวิดเจ็ต DetailScreen ของเรา: มันรับผิดชอบในการสร้างอินสแตนซ์ของ DetailNotifier และเช่นเดียวกับที่เราได้เห็นก่อนหน้านี้ เราพยายามหลีกเลี่ยงความรับผิดชอบด้านตรรกะใดๆ ที่ชั้นวิว!

เราสามารถแก้ไขปัญหานี้ได้อย่างง่ายดายโดยการสร้างเลเยอร์ใหม่สำหรับวิดเจ็ตหน้าจอของเรา: เราจะแยกวิดเจ็ต DetailScreen ของเราออกเป็นสองส่วน:

  • DetailScreen มีหน้าที่รับผิดชอบในการตั้งค่าการพึ่งพาต่างๆ ของหน้าจอเราจากสถานะปัจจุบันของแอปพลิเคชัน (การนำทาง, ตัวแจ้งเตือน, สถานะ, บริการ, ...)
  • DetailLayout แปลง DetailState เป็นต้นไม้ของวิดเจ็ตเฉพาะอย่างง่ายๆ

โดยการรวมสองสิ่งนี้เข้าด้วยกัน เราจะสามารถสร้างตัวอย่างการสาธิต/ทดสอบ DetailLayout ได้อย่างง่ายดาย แต่ยังคงมี DetailScreen สำหรับกรณีการใช้งานจริงในแอปพลิเคชันของเรา

รูปแบบที่ออกแบบเฉพาะ

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

โมเดลเส้นทางของการเรียกและอินสแตนซ์ ChangeNotifierProvider ยังคงอยู่ใน DetailScreen และวิดเจ็ตนี้จะส่งคืน DetailLayout พร้อมต้นไม้การพึ่งพาที่ตั้งค่าไว้ล่วงหน้า!

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

การแยกวิดเจ็ตเป็นคลาสเฉพาะ

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

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

อินสแตนซ์ตัวอย่าง

ตอนนี้เรามีการควบคุมอย่างเต็มที่แล้วว่าเราจะสามารถจำลองและแสดงอะไรบนหน้าจอของเราได้บ้าง!

เราเพียงแค่ต้องห่อ DetailLayout ด้วย Provider เพื่อจำลองสถานะของเลย์เอาต์

สรุป

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

ตัวอย่างอาจดูเรียบง่าย—อาจดูเหมือนว่าเราออกแบบเกินความจำเป็น—แต่เมื่อแอปของคุณมีความซับซ้อนมากขึ้น การมีมาตรฐานเหล่านี้จะช่วยคุณได้มาก! 💪

สนุกกับ Flutter และติดตามบล็อกเพื่อรับบทความเชิงเทคนิคเพิ่มเติมเช่นนี้! ติดตามต่อไป!