Flutter Clean Architecture (PART 1) – Giải thích & Cấu trúc project

Trần Đình Quý

Updated on:

Flutter Clean Architecture

Trong quá trình phát triển Flutter chúng ta rất muốn làm ra 1 app thật nhanh nhưng mặt khác, những app lớn thường sẽ bắt đầu khó khăn và dễ thất bại khi ta kết hợp nghiệp vụ logic ở tất cả mọi nơi. Ngay cả các pattern quản lý state như BLoC (Business Logic Component) cũng không đủ để cho phép codebase có thể quản lý và mở rộng dễ dàng. Một trong các phương pháp có thể giải quyết vấn đề về quản lý và mở rộng là clean architecture.

Bí mật về việc 1 app có thể bảo trì được

Đây là lúc chúng ta có thể áp dụng clean architecture vào dự án của chúng ta. Như ông bạn thân thiện của chúng ta, Uncle Bob, chúng ta nên chia code thành từng layer riêng biệt dựa theo abstractions thay vì implement cứng luôn.

Làm sao có thể đạt được sự độc lập giữa các layer như vậy. Trước tiên chúng ta hãy xem hình ảnh phía dưới với 1 củ hành đích thực. Vì sao có thể nói vậy? Các layer sẽ nằm từ ngoài vào trong và khi muốn lấy dữ liệu ở cuối thì phải đi qua từng layer. Như việc bóc tách 1 củ hành vậy mũi tên ngang —> biểu thị luồng phụ thuộc. Ví dụ: Các Entity không phụ thuộc vào bất cứ thứ gì, Use case chỉ phụ thuộc vào các Entity, v.v.

Clean architecture

Uncle Bob’s Clean Architecture Proposal

Bản chất bản chất của clean architecture vẫn giống nhau trong mọi framework. Mặc dù các nguyên tắc như SOLID và YAGNI nghe có vẻ hay, thậm chí ta có thể hiểu ý nghĩa của chúng nhưng sẽ chẳng ích gì nếu như ta không biết được làm thế nào để viết clean code. Nên bây giờ chúng ta hãy thử bắt đầu sử dụng clean architecture và 1 dự án Flutter thử xem.

Dự án mà chúng ta xây dựng

Lý thuyết mà chẳng có tí thực hành thì cũng bỏ đi. Chúng ta hãy tạo 1 app lấy những sự thật thú vị từ những con số – a Number Trivia App.

Các thành phần cốt lõi như việc lấy dữ liệu từ API, local cache, xử lý lỗi, validation và những thứ khác. Về phần quản lý state chúng ta sẽ dùng BloC. Bạn không cần phải sử dụng BLoC, có thể sử dụng bất kì phương thức quản lý trạng thái nào. Nhưng trong bài hướng dẫn này tôi sẽ dùng nó để làm ví dụ.

Clean architecture không phải về 1 kỹ thuật quản lý trạng thái cụ thể. Bạn có thể thấy trong các phần tiếp theo, việc xây dựng kiến trúc app của bạn đúng cách sẽ khiến cho việc lựa chọn quản lý trạng thái gần như không còn quan trọng. Tôi sẽ không chọn cách tiếp cận Stateful Widget bởi vì =))) chắc nó chỉ cho các app nho nhỏ thôi.

Clean Architecture & Flutter

Để làm mọi thứ rõ ràng và cụ thể cho Flutter, tôi sẽ giới thiệu cho bạn đề xuất về clean architecture mà tôi hay áp dụng.

Giải thích và tổ chức dự án

Mỗi tính năng của app như việc lấy sự thật thú vị về 1 con số, sẽ chia ra làm 3 layers – presentation, domain and data. App sẽ chỉ có 1 tính năng cơ bản.

Hình phía trên chính là cấu trúc thư mục của dự án chúng ta sẽ thực hiện. Tôi sẽ giải thích sơ qua về các thành phần trên.

Presentation

Cái này là thứ bạn đã quen từ khi dùng Flutter. Rõ ràng là ta cần các widget để hiển thị các thứ trên màn hình. Các widget này sau đó sẽ gửi các event đến BloC và lắng nghe các state (hoặc tương đương nếu bạn không sử dụng BloC trong việc quản lý trạng thái).

Lưu ý rằng “Presentation Logic Holder” (ví dụ: BLoC) tự nó không làm được gì nhiều. Nó ủy thác tất cả công việc của mình cho các trường hợp sử dụng. Nhiều nhất thì lớp presentation sẽ xử lý việc chuyển đổi và xác thực đầu vào cơ bản. 

Applied to the Number Trivia App

Chúng ta sẽ chỉ có một trang duy nhất với các widget được gọi là NumberTriviaPage với một NumberTriviaBloc duy nhất.

Domain

Domain là layer bên trong không dễ bị ảnh hưởng bởi những thay đổi bất chợt trong việc thay đổi data source hoặc chuyển ứng dụng của chúng ta sang Angular Dart. Nó sẽ chỉ chứa business logic cốt lõi (Use Case) và các đối tượng nghiệp vụ (Entity). Nó nên được hoàn toàn độc lập với mọi layer khác.

Use Case là các lớp gói gọn tất cả business logic của một trường hợp sử dụng cụ thể của ứng dụng (ví dụ: GetConcreteNumberTrivia hoặc GetRandomNumberTrivia).

Nhưng… Làm thế nào mà domain layer hoàn toàn độc lập khi nó lấy dữ liệu từ repository, tức là từ data layer? Bạn có thấy dải màu chuyển màu lạ mắt đó của Kho lưu trữ không? Điều đó biểu thị rằng nó thuộc về cả hai lớp cùng một lúc. Chúng ta có thể thực hiện điều này bằng cách đảo ngược sự phụ thuộc. Như hình thì thì repository có màu gradient từ hồng sang xanh, điều này có nghĩa là nó thuộc cả 2 layer cùng 1 lúc. Chúng ta có thể thực hiện điều này bằng dependency inversion.

Tóm lại là chúng ta sẽ tạo 1 lớp abstract class cho repository định nghĩa contract mà repository phải làm, cái này sẽ nằm ở domain layer. Sau đó chúng ta sẽ dựa vào repository contract được định nghĩa ở domain này. Việc kế thừa và implement nó sẽ được thực hiện ở trong data layer.

Nguyên tắc dependency inversion là nguyên tắc cuối cùng trong số các nguyên tắc SOLID. Về cơ bản, nó nói rằng ranh giới giữa các lớp phải được xử lý bằng các interface (các abstract trong Dart).

Applied to the Number Trivia App

Sẽ không có nhiều business logic kinh doanh để thực thi trong ứng dụng vì chúng tôi chỉ hiển thị các sự kiện Trivia Number. Đối với các đối tượng Business, sẽ có một Entity duy nhất, khá gọn gàng được gọi là NumberTrivia – chỉ là một con số và văn bản của trivia.

Data

Data layer bao gồm 1 implement của repository (contract đến từ domain layer) và các data source – một nguồn thường dùng để lấy dữ liệu (API) remote và nguồn kia để lưu trữ dữ liệu đó vào bộ nhớ đệm. Repository là nơi bạn quyết định xem bạn trả lại dữ liệu mới hay dữ liệu được lưu trong bộ nhớ đệm, khi nào nên lưu vào bộ nhớ đệm, v.v.

Bạn có thể nhận thấy rằng data source không trả về Entity mà trả về Model. Lý do đằng sau điều này là việc chuyển đổi dữ liệu thô (ví dụ JSON) thành đối tượng Dart yêu cầu code chuyển đổi JSON. Chúng ta không muốn code cho JSON này bên trong các Domain layer – điều gì sẽ xảy ra nếu chúng ta quyết định chuyển sang XML?

Do đó, chúng ta sẽ tạo các Model class extend Entity và thêm một số chức năng cụ thể (toJson, fromJson) hoặc các trường bổ sung, chẳng hạn như ID cơ sở dữ liệu.

Applied to the Number Trivia App

RemoteDataSource sẽ thực hiện các yêu cầu HTTP GET trên Numbers API. LocalDataSource sẽ chỉ lưu trữ dữ liệu bằng cách sử dụng package shared_preferences.

Hai nguồn dữ liệu này sẽ được “kết hợp” trong NumberTriviaRepository, đây sẽ là nguồn duy nhất cho những dữ liệu interesting number trivia.

Chính sách bộ nhớ đệm sẽ rất đơn giản. Nếu có kết nối mạng, luôn lấy dữ liệu từ API và lưu vào bộ đệm. Sau đó, nếu không có mạng, hãy trả lại dữ liệu được lưu trong bộ nhớ đệm mới nhất.

Phần tiếp theo: https://dev.tora-tech.com/flutter-ca-entity-use-case/

Viết một bình luận