Gần đây, tôi phải triển khai hướng dẫn sử dụng cho người mới sử dụng ClickUp! Đây là một công việc rất quan trọng vì nhiều người dùng mới sắp khám phá nền tảng này nhờ quảng cáo cực kỳ hài hước mà chúng tôi đã ra mắt tại Super Bowl! ✨
qua ClickUp
Hướng dẫn này cho phép nhiều người dùng mới, những người có thể chưa biết về ClickUp, nhanh chóng hiểu cách sử dụng một số chức năng của ứng dụng. Đây là một nỗ lực liên tục, giống như tài nguyên ClickUp University mới mà chúng tôi đang theo đuổi! 🚀
May mắn thay, kiến trúc phần mềm đằng sau ứng dụng di động ClickUp Flutter cho phép tôi triển khai chức năng này khá nhanh chóng, thậm chí bằng cách tái sử dụng các tiện ích thực từ ứng dụng! Điều này có nghĩa là hướng dẫn này rất động, phản hồi nhanh và hoàn toàn khớp với các màn hình ứng dụng thực tế của ứng dụng — và sẽ tiếp tục như vậy, ngay cả khi các tiện ích phát triển.
Tôi cũng có thể triển khai chức năng này nhờ sự phân tách các vấn đề một cách hợp lý.
Hãy xem tôi muốn nói gì ở đây. 🤔
Tách biệt các thành phần
Thiết kế kiến trúc phần mềm là một trong những chủ đề phức tạp nhất đối với các nhóm kỹ sư. Trong tất cả các trách nhiệm, việc dự đoán sự phát triển của phần mềm trong tương lai luôn là điều khó khăn. Đó là lý do tại sao việc tạo ra một kiến trúc phân lớp và tách biệt rõ ràng có thể giúp bạn và đồng nghiệp của mình rất nhiều!
Lợi ích chính của việc tạo các hệ thống nhỏ tách biệt là khả năng kiểm tra! Và đây là điều đã giúp tôi tạo ra một bản demo thay thế cho các màn hình hiện có trong ứng dụng!
Hướng dẫn từng bước
Bây giờ, làm thế nào chúng ta có thể áp dụng những nguyên tắc đó vào một ứng dụng Flutter?
Chúng tôi sẽ chia sẻ một số kỹ thuật mà chúng tôi sử dụng để xây dựng ClickUp với một ví dụ hướng dẫn đơn giản.
Ví dụ này rất đơn giản nên có thể không thể hiện hết tất cả các ưu điểm của nó, nhưng tin tôi đi, nó sẽ giúp bạn tạo ra nhiều ứng dụng Flutter dễ bảo trì hơn với cơ sở mã phức tạp. 💡
Ứng dụng
Ví dụ, chúng tôi sẽ tạo một ứng dụng hiển thị dân số của Hoa Kỳ cho mỗi năm.
qua ClickUp
Chúng tôi có hai màn hình ở đây:
- HomeScreen: chỉ đơn giản là danh sách tất cả các năm từ 2000 đến nay. Khi người dùng nhấn vào ô năm, họ sẽ được chuyển đến DetailScreen với đối số điều hướng được đặt thành năm đã chọn.
- DetailScreen : lấy năm từ đối số điều hướng, gọi API datausa.io cho năm này và phân tích dữ liệu JSON để trích xuất giá trị dân số liên quan. Nếu có dữ liệu, nhãn sẽ được hiển thị cùng với dân số.
Chúng tôi sẽ tập trung vào việc triển khai DetailScreen vì đây là phần thú vị nhất với cuộc gọi không đồng bộ.
Bước 1. Cách tiếp cận đơn giản

Việc triển khai rõ ràng nhất cho ứng dụng của chúng tôi là sử dụng một StatefulWidget duy nhất cho toàn bộ logic.
Truy cập đối số điều hướng năm
Để truy cập năm được yêu cầu, chúng tôi đọc RouteSettings từ tiện ích ModalRoute được kế thừa.
Gọi HTTP
Ví dụ này gọi hàm get từ gói http để lấy dữ liệu từ API datausa.io, phân tích JSON kết quả bằng phương thức jsonDecode từ thư viện dart:convert và giữ Future như một phần của trạng thái với thuộc tính có tên _future.
Render
Để tạo cây tiện ích, chúng tôi sử dụng FutureBuilder, công cụ tự tái tạo lại dựa trên trạng thái hiện tại của cuộc gọi không đồng bộ _future của chúng tôi.
Xem lại
Được rồi, việc triển khai ngắn gọn và chỉ sử dụng các tiện ích tích hợp sẵn, nhưng bây giờ hãy nghĩ về ý định ban đầu của chúng tôi: xây dựng các bản demo thay thế (hoặc thử nghiệm) cho màn hình này. Rất khó để kiểm soát kết quả của lệnh HTTP để buộc ứng dụng hiển thị ở một trạng thái nhất định.
Đó chính là nơi khái niệm inversion of control (đảo ngược điều khiển) sẽ giúp chúng ta. 🧐
Bước 2. Đảo ngược kiểm soát

Nguyên tắc này có thể khó hiểu đối với các nhà phát triển mới (và cũng khó giải thích), nhưng ý tưởng chung là tách các mối quan tâm ra khỏi các thành phần của chúng ta—để chúng không phải chịu trách nhiệm về việc lựa chọn hành vi—và thay vào đó, giao nhiệm vụ đó cho các thành phần khác.
Trong một tình huống phổ biến hơn, điều này đơn giản là tạo ra các trừu tượng và tiêm các thực thi vào các thành phần của chúng ta để có thể thay đổi thực thi của chúng sau này nếu cần thiết.
Nhưng đừng lo lắng, bạn sẽ hiểu rõ hơn sau ví dụ tiếp theo! 👀
Tạo đối tượng khách hàng API
Để kiểm soát cuộc gọi HTTP đến API của chúng tôi, chúng tôi đã tách triển khai của mình thành một lớp DataUsaApiClient chuyên dụng. Chúng tôi cũng đã tạo một lớp Measure để giúp dữ liệu dễ dàng thao tác và bảo trì hơn.
Cung cấp khách hàng API
Trong ví dụ của chúng tôi, chúng tôi sử dụng gói nhà cung cấp nổi tiếng để chèn đối tượng/kỳ/phiên bản DataUsaApiClient vào gốc cây của chúng tôi.
Sử dụng API khách hàng
Nhà cung cấp cho phép bất kỳ tiện ích con nào (như DetailScreen của chúng tôi) đọc DataUsaApiClient gần nhất trong cây. Sau đó, chúng tôi có thể sử dụng phương thức getMeasure của nó để bắt đầu Future, thay cho việc triển khai HTTP thực tế.
Khách hàng API demo
Bây giờ chúng ta có thể tận dụng điều này!
Trong trường hợp bạn chưa biết: bất kỳ lớp nào trong dart cũng định nghĩa ngầm một giao diện liên quan. Điều này cho phép chúng tôi cung cấp một triển khai thay thế của DataUsaApiClient, luôn trả về cùng một đối tượng/kỳ/phiên bản từ các lệnh gọi phương thức getMeasure.
Phương pháp này
Phương pháp này
Hiển thị trang demo
Giờ đây, chúng tôi đã có tất cả các khóa để hiển thị một đối tượng/kỳ/phiên bản demo của DetailPage!
Chúng tôi chỉ cần ghi đè đối tượng/kỳ/phiên bản DataUsaApiClient hiện được cung cấp bằng cách bao bọc DetailScreen của chúng tôi trong một nhà cung cấp tạo ra một đối tượng/kỳ/phiên bản DemoDataUsaApiClient thay thế!
Và thế là xong — DetailScreen của chúng tôi sẽ đọc đối tượng/kỳ/phiên bản demo này và sử dụng dữ liệu demoMeasure của chúng tôi thay vì gọi HTTP.
Xem lại
Đây là một ví dụ tuyệt vời về Đảo ngược kiểm soát. Tiện ích DetailScreen của chúng tôi không còn chịu trách nhiệm về logic lấy dữ liệu nữa, mà thay vào đó, nó ủy quyền cho một đối tượng khách hàng chuyên dụng. Và giờ đây, chúng tôi có thể tạo các đối tượng/kỳ/phiên bản demo của màn hình hoặc triển khai các thử nghiệm tiện ích cho màn hình của chúng tôi! Thật tuyệt vời! 👏
Nhưng chúng tôi có thể làm tốt hơn nữa!
Ví dụ, vì chúng tôi không thể mô phỏng trạng thái tải, nên chúng tôi không thể kiểm soát hoàn toàn bất kỳ thay đổi trạng thái nào ở cấp tiện ích.
Bước 3. Quản lý trạng thái

Đây là chủ đề hot trong Flutter!
Chắc hẳn bạn đã đọc những chủ đề dài của những người cố gắng chọn giải pháp quản lý trạng thái tốt nhất cho Flutter. Và để rõ ràng, đó không phải là việc chúng tôi sẽ làm trong bài viết này. Theo chúng tôi, miễn là bạn tách logic kinh doanh khỏi logic trực quan, bạn sẽ không gặp vấn đề gì! Việc tạo các lớp này thực sự quan trọng cho khả năng bảo trì. Ví dụ của chúng tôi rất đơn giản, nhưng trong các ứng dụng thực tế, logic có thể nhanh chóng trở nên phức tạp và việc tách biệt như vậy giúp bạn dễ dàng tìm thấy các thuật toán logic thuần túy hơn. Chủ đề này thường được tóm tắt là quản lý trạng thái.
Trong ví dụ này, chúng tôi sử dụng ValueNotifier cơ bản cùng với Provider. Nhưng chúng tôi cũng có thể sử dụng flutter_bloc hoặc riverpod (hoặc một giải pháp khác), và nó cũng sẽ hoạt động rất tốt. Các nguyên tắc vẫn giữ nguyên, và miễn là bạn tách biệt trạng thái và logic của mình, bạn thậm chí có thể chuyển cơ sở mã của mình từ một trong các giải pháp khác.
Sự tách biệt này cũng giúp chúng tôi kiểm soát mọi trạng thái của các tiện ích để có thể mô phỏng nó theo mọi cách có thể!
Tạo trạng thái chuyên dụng
Thay vì dựa vào AsyncSnapshot từ framework, giờ đây chúng tôi biểu diễn trạng thái màn hình dưới dạng đối tượng DetailState.
Điều quan trọng là phải triển khai các phương thức hashCode và toán tử == để đối tượng của chúng ta có thể so sánh được theo giá trị. Điều này cho phép chúng ta phát hiện xem hai đối tượng/kỳ/phiên bản có nên được coi là khác nhau hay không.
💡 Các gói equatable hoặc frozen là lựa chọn tuyệt vời để giúp bạn triển khai các phương thức hashCode và toán tử ==!
💡 Các gói equatable hoặc frozen là lựa chọn tuyệt vời để giúp bạn triển khai các phương thức hashCode và toán tử ==!
Trạng thái của chúng tôi luôn liên quan đến một năm, nhưng chúng tôi cũng có bốn trạng thái khác nhau về những gì chúng tôi muốn hiển thị cho người dùng:
- NotLoadedDetailState: Việc cập nhật dữ liệu chưa bắt đầu
- LoadingDetailState: Dữ liệu hiện đang được tải
- LoadedDetailState: dữ liệu đã được tải thành công với một thước đo liên quan
- NoDataDetailState: Dữ liệu đã được tải, nhưng không có dữ liệu khả dụng
- UnknownErrorDetailState: thao tác không thành công do lỗi không xác định
Các trạng thái này rõ ràng hơn AsyncSnapshot, vì nó thực sự thể hiện các trường hợp sử dụng của chúng tôi. Và một lần nữa, điều này giúp mã của chúng tôi dễ bảo trì hơn!
💡 Chúng tôi khuyên bạn nên sử dụng các loại Union từ gói freezed để biểu diễn các trạng thái logic của bạn! Nó bổ sung nhiều tiện ích như các phương thức copyWith hoặc bản đồ.
💡 Chúng tôi khuyên bạn nên sử dụng các loại Union từ gói freezed để biểu diễn các trạng thái logic của bạn! Nó bổ sung nhiều tiện ích như các phương thức copyWith hoặc bản đồ.
Đặt logic vào Notifier
Bây giờ chúng ta đã có biểu diễn trạng thái, chúng ta cần lưu trữ một đối tượng/kỳ/phiên bản của nó ở đâu đó — đó là mục đích của DetailNotifier. Nó sẽ giữ đối tượng DetailState hiện tại trong thuộc tính giá trị của nó và cung cấp các phương thức để cập nhật trạng thái.
Chúng tôi cung cấp trạng thái ban đầu NotLoadedDetailState và phương pháp làm mới để tải dữ liệu từ API và cập nhật giá trị hiện tại.
Cung cấp trạng thái cho chế độ xem
Để khởi tạo và quan sát trạng thái màn hình của chúng tôi, chúng tôi cũng dựa vào nhà cung cấp và ChangeNotifierProvider của họ. Loại nhà cung cấp này sẽ tự động tìm kiếm bất kỳ ChangeListener nào được tạo và sẽ kích hoạt quá trình xây dựng lại từ Consumer mỗi khi nhận được thông báo về thay đổi (khi giá trị thông báo của chúng tôi khác với giá trị trước đó).
Xem lại
Tuyệt vời! Kiến trúc ứng dụng của chúng ta đang dần hoàn thiện. Tất cả các thành phần đã được tách biệt thành các lớp rõ ràng, mỗi lớp chịu trách nhiệm cho các chức năng cụ thể! 🤗
Tuy nhiên, vẫn còn một điều thiếu sót để đảm bảo khả năng kiểm thử, đó là chúng ta cần định nghĩa trạng thái hiện tại của DetailState để kiểm soát trạng thái của màn hình DetailScreen liên quan.
Bước 4. Tiện ích bố cục chuyên dụng trực quan

Trong bước cuối cùng, chúng tôi đã giao quá nhiều trách nhiệm cho tiện ích DetailScreen: nó chịu trách nhiệm khởi tạo DetailNotifier. Và như những gì chúng ta đã thấy trước đó, chúng tôi cố gắng tránh bất kỳ trách nhiệm logic nào ở lớp chế độ xem!
Chúng ta có thể dễ dàng giải quyết vấn đề này bằng cách tạo một lớp khác cho tiện ích màn hình: chúng ta sẽ chia tiện ích DetailScreen thành hai phần:
- DetailScreen chịu trách nhiệm cài đặt các phụ thuộc khác nhau của màn hình từ trạng thái ứng dụng hiện tại (điều hướng, thông báo, trạng thái, dịch vụ, …),
- DetailLayout chỉ đơn giản là chuyển đổi DetailState thành một cây tiện ích chuyên dụng.
Bằng cách kết hợp cả hai, chúng ta sẽ có thể dễ dàng tạo các đối tượng/kỳ/phiên bản demo/thử nghiệm DetailLayout, nhưng vẫn có DetailScreen cho trường hợp sử dụng thực tế trong ứng dụng của chúng ta.
Bố cục chuyên dụng
Để tách biệt các vấn đề rõ ràng hơn, chúng tôi đã chuyển mọi thứ trong tiện ích Consumer sang tiện ích DetailLayout chuyên dụng. Tiện ích mới này chỉ sử dụng dữ liệu và không chịu trách nhiệm cho bất kỳ quá trình khởi tạo nào. Nó chỉ chuyển đổi trạng thái đọc thành một cây tiện ích cụ thể.
ModalRoute. của đối tượng/kỳ/phiên bản call và ChangeNotifierProvider vẫn ở trong DetailScreen, và tiện ích này chỉ trả về DetailLayout với cây phụ thuộc được cấu hình sẵn!
Cải tiến nhỏ này chỉ dành riêng cho việc sử dụng nhà cung cấp, nhưng bạn sẽ nhận thấy rằng chúng tôi cũng đã thêm ProxyProvider để bất kỳ tiện ích con nào cũng có thể sử dụng trực tiếp DetailState. Điều này sẽ giúp việc mô phỏng dữ liệu trở nên dễ dàng hơn.
Trích xuất tiện ích dưới dạng các lớp chuyên dụng
Đừng ngần ngại trích xuất cây tiện ích vào một lớp chuyên dụng! Điều này sẽ cải thiện hiệu suất và giúp mã dễ bảo trì hơn.
Trong ví dụ của chúng tôi, chúng tôi đã tạo một tiện ích bố cục trực quan cho từng loại trạng thái liên quan:
Đối tượng/kỳ/phiên bản demo
Bây giờ chúng ta có toàn quyền kiểm soát những gì có thể mô phỏng và hiển thị trên màn hình!
Chúng tôi chỉ cần bao bọc DetailLayout bằng Provider
Kết luận
Việc tạo ra một kiến trúc phần mềm có thể bảo trì chắc chắn không phải là điều dễ dàng! Dự đoán các tình huống trong tương lai có thể đòi hỏi rất nhiều nỗ lực, nhưng tôi hy vọng những mẹo nhỏ mà tôi đã chia sẻ sẽ giúp ích cho bạn trong tương lai!
Các ví dụ có thể trông đơn giản — thậm chí có vẻ như chúng tôi đang thiết kế quá phức tạp — nhưng khi ứng dụng của bạn ngày càng phức tạp, những tiêu chuẩn này sẽ giúp ích rất nhiều cho bạn! 💪
Hãy tận hưởng Flutter và theo dõi blog để nhận thêm các bài viết kỹ thuật như bài này! Hãy theo dõi chúng tôi!