[FLUTTER] Sự khác biệt giữa Future và Stream trong Flutter
Lập trình không đồng bộ trong Flutter được đặc trưng bởi hai lớp Future và Stream
1. Future
Khi một hàm bất đồng bộ được thực hiện xong nó sẽ trả về một Future.
Một hàm Future có thể trả về một giá trị.
Một hàm Future cũng có thể trả về một lỗi nếu có bất kì ngoại lệ nào xảy ra.
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info from another service or database.
return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
2. Stream
Định nghĩa: Stream là một chuỗi các sự kiện không đồng bộ. Nó giống như một Lặp lại không đồng bộ – trong đó, thay vì nhận được sự kiện tiếp theo khi bạn yêu cầu, luồng cho bạn biết rằng có một sự kiện khi nó sẵn sàng.
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
sum += value;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // 55
}
3. Khác biệt giữa Future và Stream
Như chúng ta có thể thấy ở trên thì điểm khác biệt rõ rệt nhất của Future và Stream là:
Trong quá trình xử lý Future sẽ chờ đợi đến khi hoàn thành và chỉ trả lại kết quả tại thời điểm đó.
Stream thì sẽ trả về dữ liệu liên tục nếu nó vẫn tiếp tục được chuyển về, tạo thành một luồng.
Từ những sự khác biệt này chúng ta có thể suy luận ra các trường hợp nào nên sử dụng Future, trường hợp nào nên sử dụng Stream.
Future:
Chụp một bức ảnh từ camera, hoặc lấy từ trong bộ nhớ.
Lẩy các thông tin về file.
Tạo các https request.
…
Stream:
Lắng nghe sự thay đổi của vị trí.
Chơi nhạc.
Đồng hồ bấm giờ.
Làm việc với web-socket.
…
Hãy chọn cho mình những cách phù hợp nhất với các vấn đề chúng ta cần giải quyết.
Stack memory allocation: Việc cấp phát này xảy ra trên các khối bộ nhớ liền kề. Chúng ta gọi nó là Stack memory allocation vì việc cấp phát xảy ra trong ngăn xếp lệnh gọi hàm. Kích thước của bộ nhớ được cấp phát đã được trình biên dịch biết và bất cứ khi nào một hàm được gọi, các biến của nó sẽ nhận được bộ nhớ được cấp phát trên ngăn xếp. Và bất cứ khi nào gọi hàm kết thúc, bộ nhớ cho các biến sẽ được hủy cấp phát. Tất cả điều này xảy ra bằng cách sử dụng một số quy trình được xác định trước trong trình biên dịch. Lập trình viên không phải lo lắng về việc cấp phát bộ nhớ và hủy cấp phát các biến ngăn xếp. Loại cấp phát bộ nhớ này còn được gọi là Cấp phát bộ nhớ tạm thời bởi vì ngay sau khi phương thức kết thúc việc thực thi, tất cả dữ liệu thuộc về phương thức đó sẽ tự động thoát ra khỏi ngăn xếp. Có nghĩa là, bất kỳ giá trị nào được lưu trữ trong lược đồ bộ nhớ ngăn xếp đều có thể truy cập được miễn là phương thức chưa hoàn thành việc thực thi và hiện ở trạng thái đang chạy.
Những điểm chính của bộ nhớ stack:
Nó cấp phát bộ nhớ tạm thời trong đó các thành viên dữ liệu chỉ có thể truy cập được nếu phương thức chứa chúng hiện đang chạy.
Nó tự động cấp phát hoặc hủy cấp phát bộ nhớ ngay sau khi phương thức tương ứng hoàn thành việc thực thi.
Chúng ta nhận được nếu bộ nhớ ngăn xếp được lấp đầy hoàn toàn.
Cấp phát bộ nhớ stack được coi là an toàn hơn so với cấp phát bộ nhớ heap vì dữ liệu được lưu trữ chỉ có thể được truy cập bởi luồng chủ của nó.
Cấp phát và thu hồi cấp phát bộ nhớ nhanh hơn so với cấp phát bộ nhớ Heap.
Bộ nhớ stack được cấp phát bộ nhớ lưu trữ nhỏ hơn so với bộ nhớ Heap.
Tất cả các biện trên sẽ được cấp pháp bộ nhớ trong bộ nhớ stack.
2. Heap memory allocation
Bộ nhớ được cấp phát trong quá trình thực thi các lệnh do người lập trình viết. Lưu ý rằng tên heap không liên quan gì đến cấu trúc dữ liệu heap. Nó được gọi là heap vì nó là một không gian bộ nhớ có sẵn cho các lập trình viên để cấp phát và thu hồi cấp phát. Mỗi khi chúng ta tạo một đối tượng, nó luôn tạo ra trong Heap-space và thông tin tham chiếu đến các đối tượng này luôn được lưu trữ trong bộ nhớ stack. Phân bổ bộ nhớ Heap không an toàn như phân bổ bộ nhớ Stack vì dữ liệu được lưu trữ trong vùng này có thể truy cập hoặc hiển thị cho tất cả các chuỗi. Nếu một lập trình viên không xử lý tốt bộ nhớ này, thì chương trình có thể bị thiếu bộ nhớ.
Việc cấp phát bộ nhớ Heap được chia thànhba loại. Ba loại này giúp chúng ta sắp xếp thứ tự ưu tiên dữ liệu (đối tượng) sẽ được lưu trữ trong bộ nhớ Heap hoặc trong quá trình thu gom rác (quá trình xác định và loại bỏ các Object không được sử dụng (unreferenced) khỏi bộ nhớ Heap) bao gồm:
Young Generation: Phần bộ nhớ nơi tất cả dữ liệu (đối tượng) mới được tạo ra để phân bổ vùng và bất cứ khi nào bộ nhớ này được lấp đầy hoàn toàn thì phần còn lại của dữ liệu sẽ được lưu trữ trong bộ sưu tập Rác.
Old or Tenured Generation: Phần của bộ nhớ Heap chứa các đối tượng dữ liệu cũ hơn không được sử dụng thường xuyên hoặc không được sử dụng nữa sẽ được đặt.
Permanent Generation: Đây là phần của bộ nhớ Heap chứa siêu dữ liệu của JVM cho các lớp thời gian chạy và các phương thức ứng dụng.
Những điểm chính của hepa memory allocation:
Chúng ta nhận được thông báo lỗi tương ứng nếu Heap-space đã đầy hoàn toàn.
Việc cấp phát bộ nhớ này khác với bộ nhớ stack, ở đây không cung cấp tính năng tự động thu hồi bộ nhớ. Chúng ta cần sử dụng quá trình xác định và loại bỏ rác để loại bỏ các đối tượng cũ không sử dụng để sử dụng bộ nhớ một cách hiệu quả.
Thời gian xử lý (Thời gian truy cập) của bộ nhớ này khá chậm so với bộ nhớ stack.
Bộ nhớ Heap cũng không phải là một luồng an toàn như bộ nhớ stack vì dữ liệu được lưu trữ trong bộ nhớ Heap được hiển thị cho tất cả các luồng.
Bộ nhớ đống có thể truy cập hoặc tồn tại miễn là toàn bộ ứng dụng chạy.
bộ nhớ Heap được cấp phát bộ nhớ lưu trữ lớn hơn so với bộ nhớ stack.
Bộ nhớ này cho 10 kiểu dữ liệu interger được cấp phát trong Heap.
Ví dụ:
Trong ví dụ trên:
Khi chúng ta bắt đầu thực thi chương trình có, tất cả các lớp thời gian chạy được lưu trữ trong không gian bộ nhớ Heap.
Sau đó, chúng ta tìm thấy phương thức main() trong dòng tiếp theo được lưu trữ trong bộ nhớ stack cùng với tất cả các phương thức nguyên thủy (hoặc cục bộ) và biến tham chiếu Emp kiểu Emp_detail cũng sẽ được lưu trữ trong bộ nhớ stack và sẽ trỏ đến đối tượng tương ứng được lưu trữ trong bộ nhớ Heap.
Sau đó, dòng tiếp theo sẽ gọi đến phương thức khởi tạo tham số Emp(int, String) từ main() và nó cũng sẽ cấp phát cho phần trên cùng của cùng một khối bộ nhớ Stack. Điều này sẽ lưu trữ:
Đối tượng tham chiếu của đối tượng được gọi của bộ nhớ Stack.
Giá trị nguyên thủy (kiểu dữ liệu nguyên thủy) int id trong bộ nhớ Stack.
Biến tham chiếu của đối số String emp_name sẽ trỏ đến chuỗi thực từ nhóm chuỗi vào bộ nhớ heap.
Sau đó, phương thức main sẽ lại gọi đến phương thức static Emp_detail(), phương thức này sẽ được thực hiện trong khối bộ nhớ Stack trên đầu khối bộ nhớ trước đó.
Vì vậy, đối với đối tượng mới tạo Emp kiểu Emp_detail và tất cả các biến thể hiện sẽ được lưu trữ trong bộ nhớ heap.
Flutter không vẽ lại hay tạo lại toàn bộ giao diện người dùng mỗi khi ta phương thức build(){...} được gọi.
Flutter cố gắng đáp ứng để ứng dụng chạy ở 60 FPS. Vì vậy, nó cập nhật màn hình 60 lần mỗi giây. Có nghĩa là màn hình được Flutter repaint lại 60 lần mỗi giây. Điều này dễ hiểu vì tất cả các ứng dụng và trò chơi chạy ở tốc độ 60 FPS trở lên theo mặc định.
Điều này sẽ chỉ trở nên kém hiệu quả nếu Flutter cứ phải tính toán lại toàn bộ bố cục 60 lần mỗi giây.
Nếu Flutter vẽ thứ gì đó lên màn hình lần đầu tiên, nó cần tính toán ra vị trí, màu sắc, văn bản, v.v. của mọi phần tử trên màn hình.
Đối với các lần sửa/vẽ tiếp theo, để làm mới giao diện người dùng nếu không có gì thay đổi thì Flutter sẽ lấy thông tin cũ mà nó đã có từ trước đó và vẽ thông tin đó lên màn hình rất nhanh và rất hiệu quả. Từ đó, tốc độ vẽ lại không phải là vấn đề, sẽ chỉ là vấn đề nếu Flutter phải tính toán lại mọi thứ trên màn hình với mỗi lần làm mới mà thôi.
Đây là những gì chúng ta sẽ thảo luận chi tiết ở bài viết này: liệu Flutter có tính toán lại mọi thứ mỗi khi phương thức build(){...} được gọi hay không?
Widget Tree
Widget Tree chỉ đơn giản là tất cả các Widget mà ta đang dùng để xây dựng ứng dụng, tức là tất cả code mà ta viết sẽ tạo nên widget tree.
Nó hoàn toàn do ta kiểm soát. Bạn khai báo các widget lồng ghép chúng lại với nhau để tạo nên giao diện mong muốn.
Widget tree được xây dựng bởi Flutter khi call phương thức build(){...} từ code của chúng ta, chúng chỉ là một loạt các cài đặt cấu hình mà Flutter sẽ xử lý.
Nó không chỉ đơn giản xuất hiện ra trên màn hình rồi thôi. Thay vào đó, nó sẽ cho Flutter biết những gì sẽ vẽ lên màn hình ở lần tiếp theo. Widget tree được rebuild rất thường xuyên.
Element Tree
Element Tree liên kết vào Widget Tree, là thông tin được thiết lập với các đối tượng/phần tử thực sự được hiển thị. Nó rất hiếm khi rebuild.
Element Tree được quản lý theo một cách khác và sẽ không rebuild khi phương thức build(){...} được gọi.
Ở mỗi Widget trong Widget Tree, Flutter sẽ tự động tạo một element cho nó. Nó được thực hiện ngay khi Flutter xử lý Widget ở lần đầu tiên.
Ở đây chúng ta có thể nói rằng một element là một đối tượng được quản lý trong bộ nhớ bởi Flutter, nó có liên quan đến Widget trong Widget Tree.
Element chỉ giữ một tham chiếu tới Widget (trong Widget Tree) đang giữ các thông số giao diện đầu cuối.
Tóm lại
Khi Flutter nhìn thấy stateful widget, nó sẽ tạo element và sau đó cũng gọi phương thức createState() để tạo một state object mới dựa trên state class. Do đó, state là một đối tượng độc lập trong một Stateful Widget được kết nối với cả hai là element trong Element Tree và Widget trong Widget Tree.
Render Tree
Render Tree đại diện của các element/đối tượng thực sự được hiển thị trên màn hình.
Render Tree cũng không rebuild thường xuyên.
Element Tree cũng được liên kết với Render Tree. Element trong Element Tree trỏ đến render object mà chúng ta thực sự thấy trên màn hình.
Bất cứ khi nào Flutter thấy một element chưa được render trước đó thì nó sẽ tham chiếu đến Widget trong Widget Tree để thiết lập, sau đó tạo một element trong element tree.
Flutter cũng có một layout phase, giai đoạn mà nó tính toán và lấy không gian diện tích có sẵn trên màn hình, chiều, kích thước, hướng, ….
Nó cũng có một phase khác để thiết lập các listeners với các Widget để chúng ta có thể thao tác các sự kiện, ….
Tóm lại
Một cách đơn giản, chúng ta có thể thấy rằng phần tử chưa được render thì sẽ được render ra màn hình. Element (trong element tree) sau đó có có một con trỏ đến Render Object (trong Render Tree) trên màn hình. Nó cũng có một con trỏ tới Widget (trong Widget Tree) mang theo các thông tin cấu hình cho phần tử này.
Cách Flutter thực thi phương thức build(){...}
Phương thức build(){...} được Flutter gọi bất cứ khi nào state thay đổi. Về cơ bản, có hai kích hoạt quan trọng có thể dẫn đến việc rebuild.
Một là khi phương thức setState(){...} được gọi trong một Stateful Widget. Việc call setState(){...} khi được gọi tự động sẽ dẫn đến phương thức build(){...} được gọi ngay sau đó.
Thứ hai, bất cứ khi nào MediaQuery hoặc lệnh Theme.of(...) được gọi, bàn phím ảo xuất hiện hoặc biến mất, v.v. Bất cứ khi nào dữ liệu của những thứ này thay đổi, nó sẽ tự động kích hoạt phương thức build(){...}.
Chính xác là việc gọi setState(){...} sẽ đánh dấu phần tử tương ứng là dirty. Đối với lần render tiếp theo, diễn ra 60 lần mỗi giây, Flutter sau đó sẽ xem xét đến các thông tin thiết lập mới được tạo bởi phương thức build(){…} và sau đó cập nhật màn hình.
Tất cả các Widget lồng vào nhau bên trong Widget được đánh dấu là dirty sẽ được tạo các đối tượng mới widget/dart cho chúng. Do đó, một Widget Tree mới sẽ được tạo ra tương ứng các phiên bản mới của tất cả các Widget này.
Chú thích
Một số Widget sẽ không bao giờ thay đổi ngay cả khi rebuild Widget Tree nên bạn có thể tối ưu hóa quá trình build hơn. Bạn có thể sử dụng từ khóa const phía trước chúng để cho Flutter biết rằng Widget này sẽ không bao giờ thay đổi, do đó làm cho Flutter bỏ qua việc rebuild hoàn toàn widget đó.
Ví dụ trên ta chỉ định const bỏ qua việc rebuild Padding Widget.
Làm thế nào để chuyển màn hình mà không cần context trong Flutter?
Chúng ta sẽ học cách loại bỏ context khi navigate trong Flutter nhé.
Navigate là một phần không thể thiếu trong bất kì ứng dụng nào. Flutter sẽ hỗ trợ bạn navigate đến bất cứ màn hình nào một cách dễ dàng hơn chỉ với việc sử dụng các chức năng navigate đơn giản như Push và Pop.
Để Push:
Để Pop:
Điều này hoạt động khá tốt cho đến khi ứng dụng của bạn mở rộng quy mô và bạn tách logic nghiệp vụ của mình với logic UI. Và bây giờ bạn phải chuyển BuildContext từ một function này sang một function khác. Đôi khi việc này sẽ trở nên rắc rối khiến bạn muốn tránh việc chuyển context.
Lỗi (Error) Lỗi là vấn đề khá là nghiêm trọng và khó có thể “deal with” với nó, và không thể phục hồi. ví dụ: out of memory (đầy bộ nhớ).
Ngoại lệ (Exceptions)] nhằm truyền đạt thông tin cho người dùng về lỗi, để lỗi có thể được giải quyết theo chương trình. Nó được dự định là đã bắt được và nó phải chứa các trường dữ liệu hữu ích.
Khi đang chạy chương trình, đột nhiên ngừng lại và xuất hiện thông báo lỗi – đó chính là ngoại lệ ( Exceptions).
Trong quá trình xây dựng phần mềm sẽ có thể sảy ra nhiều lỗi và những ngoại lệ , điều này là không tránh khỏi. Vậy cách nào để kiểm soát và phát hiện chúng ? Ở bài viết này mình sẽ sử dụng try catch để xử lý lỗi giúp cho app không bị chết đột ngột .
Cú pháp
Try{
// Khối lệnh có nguy cơ xảy ra exception
} catch(e){
}finally{
}
Sau đây chúng ta hãy đi vào ví dụ cụ thể :
Ở ví dụ trên ta có thể thấy đã xảy ra lỗi nhưng bản thân mỗi lập trình viên không thể vì lỗi mà làm chết app ảnh hưởng đến trải nhiệm người dùng . Trong thực tế app có thể chết do mạng internet hay filenotfound hay nhiều vô vàn lý do khác . Các lập trình viên phải handle các lỗi nhiều nhất có thể để ứng dụng luôn luôn “sống” .
finally : try-catch-finally hay try – finally , khối lệnh trong finally sẽ được thực hiện bất chấp có sảy ra lỗi trong khối try hay không.
Try-on-catch : để bắt được loại ngoại lệ cụ thể. Ví dụ:
Khi bắt được chính xác loại ngoại lệ ở trước thì khối lệnh trong catch sẽ không được thực thi.
Tóm lại bài viết này nhằm mục đích gới thiệu về try catch finaly một cách cơ bản , hi vọng giúp ích được cho bạn
Cách viết các ứng dụng mạnh mẽ mọi lúc, bằng cách sử dụng “Clean Architecture”
Là nhà phát triển, chúng ta không thể tiếp tục sử dụng các thư viện và framework bên ngoài trong hệ thống của mình. Việc cộng đồng tạo ra những công cụ hữu ích và việc sử dụng chúng là điều đương nhiên. Tuy nhiên, mọi thứ đều có mặt trái của nó.
Các nhóm và các cá nhân bất cẩn có thể rơi vào tình huống nguy hiểm bằng cách cấu trúc hệ thống của họ xung quanh các công cụ mà họ sử dụng. Các business logic có thể bị lẫn lộn với các chi tiết thực hiện. Điều này có thể dẫn đến một hệ thống khó mở rộng và bảo trì. Những gì nên thay đổi nhanh chóng trong GUI cuối cùng lại biến thành một cuộc truy tìm lỗi kéo dài hàng giờ. Nhưng câu chuyện không cần thiết phải như thế này.
Kiến trúc phần mềm đề xuất các mô hình và quy tắc để xác định cấu trúc (classes, interfaces, và structs) trong một hệ thống và cách chúng liên quan với nhau. Các quy tắc này thúc đẩy khả năng tái sử dụng và sự tách biệt các mối quan tâm đối với các yếu tố này. Điều này giúp dễ dàng thay đổi các chi tiết triển khai như DBMS hoặc thư viện front-end. Trình tái cấu trúc và sửa lỗi ảnh hưởng đến càng ít bộ phận của hệ thống càng tốt. Và thêm các tính năng mới trở nên dễ dàng.
Trong bài viết này, tôi sẽ giải thích một mô hình kiến trúc được đề xuất vào năm 2012 bởi Robert C. Martin, Uncle Bob. Ông là tác giả của những tác phẩm kinh điển như Clean Code và The Clean Coder…vv.
Mô hình có được xây dựng dựa trên các khái niệm đơn giản:
Chia thành phần của hệ thống thành các lớp với các vai trò riêng biệt và được xác định rõ ràng. Và hạn chế các mối quan hệ giữa các entities ở các tầng khác nhau. Không có gì mới trong việc tách ứng dụng của bạn thành các lớp. Nhưng tôi đã chọn cách tiếp cận này vì nó là cách đơn giản nhất để nắm bắt và thực hiện. Và nó làm cho các usecase thử nghiệm trở nên đơn giản.
Chúng tôi chỉ cần đảm bảo Tương tác hoạt động bình thường. Đừng lo lắng nếu từ “Tương tác” có vẻ xa lạ với bạn, chúng ta sẽ tìm hiểu về chúng ngay sau đây.
Từ trong ra ngoài, chúng ta sẽ khám phá từng lớp sâu hơn một chút. Chúng tôi sẽ sử dụng một ứng dụng mẫu khá quen thuộc với chúng ta: counter app. Không mất thời gian để hiểu, vì vậy chúng ta có thể tập trung vào chủ đề của bài viết này.
Entities
Entities trong sơ đồ là Business Rules. Các Entities bao gồm các Business Rules phổ biến cho một công ty. Chúng đại diện cho các entities cơ bản đối với lĩnh vực hoạt động của nó. Chúng là những thành phần có mức độ trừu tượng cao nhất.
Trong ví dụ counter app của chúng tôi, có một Thực thể rất rõ ràng: chính là Counter.
Use Cases
Các trường hợp sử dụng được chỉ ra dưới dạng Application Business Rules. Chúng đại diện cho từng trường hợp sử dụng của một ứng dụng. Mỗi phần tử của lớp này cung cấp một giao diện cho lớp bên ngoài và hoạt động như một trung tâm giao tiếp với các phần khác của hệ thống. Chúng chịu trách nhiệm thực hiện hoàn chỉnh các usecase và thường được gọi là Tương tác.
Trong ví dụ của chúng tôi, chúng tôi có một Trường hợp sử dụng để tăng hoặc giảm bộ đếm của chúng
Lưu ý rằng factory function cho ChangeCounterInteractor nhận một tham số kiểu CounterGateway. Chúng ta sẽ thảo luận về sự tồn tại của loại hình này ở phần sau của bài viết. Nhưng chúng ta có thể nói rằng Gateways là thứ đứng giữa các usecase và layer tiếp theo.
Interface Adapters
Lớp này bao gồm ranh giới giữa các business rules của hệ thống và các công cụ cho phép hệ thống tương tác với các phần bên ngoài, như database và UI. Các phần tử trong lớp này hoạt động như các phần tử trung gian, nhận dữ liệu từ một lớp và chuyển nó sang lớp kia, điều chỉnh dữ liệu khi cần thiết.
Trong ví dụ của chúng tôi, chúng tôi có một vài Interface Adapters. Một trong số đó là React component trình bày Bộ đếm và các điều khiển của nó để tăng và giảm:
Lưu ý rằng component không sử dụng một thể hiện Counter để trình bày giá trị của nó mà thay vào đó là một instance của CounterViewData. Chúng tôi đã thực hiện thay đổi này để tách present logic khỏi business data. Một ví dụ về điều này là logic hiển thị của counter dựa trên view mode. Cách triển khai CounterViewData ở bên dưới:
Một ví dụ khác về Interface Adapter sẽ là triển khai Redux của ứng dụng của chúng tôi. Các mô-đun chịu trách nhiệm về các yêu cầu tới máy chủ và việc sử dụng bộ nhớ cục bộ cũng sẽ nằm trong layer này.
Frameworks and Drivers
Các công cụ mà hệ thống của bạn sử dụng để giao tiếp với các phần bên ngoài tạo nên lớp ngoài cùng. Chúng tôi thường không viết mã trong lớp này, bao gồm các thư viện như React / Redux, browser API, v.v.
The Dependency Rule
Sự phân chia thành các lớp này có hai mục tiêu chính. Một trong số đó là làm rõ trách nhiệm của từng bộ phận trong hệ thống. Hai là đảm bảo vai trò của mỗi lớp độc lập với nhau nhất có thể. Để điều này xảy ra, có một quy tắc nêu rõ các yếu tố phải phụ thuộc vào nhau như thế nào:
Một phần tử không được phụ thuộc vào bất kỳ phần tử nào thuộc một lớp bên ngoài lớp của nó.
Ví dụ: một phần tử trong Use Cases layer không được có bất kỳ thông tin nào về bất kỳ lớp hoặc mô-đun nào liên quan đến GUI hoặc tính ổn định của dữ liệu. Tương tự như vậy, một Entity không thể biết các Use Cases nào sử dụng nó.
Quy tắc này có thể đã đặt ra câu hỏi trong đầu bạn. Lấy ví dụ về một Use Case. Nó được kích hoạt do tương tác của người dùng với UI. Việc thực thi nó liên quan đến việc cập nhật trong một số bộ lưu trữ dữ liệu liên tục như cơ sở dữ liệu. Làm cách nào Interactor có thể thực hiện các lệnh gọi liên quan đến quy trình cập nhật mà không phụ thuộc vào Interface Adapter chịu trách nhiệm về tính ổn định của dữ liệu?
Câu trả lời nằm trong một yếu tố mà chúng tôi đã đề cập trước đây: Gateways. Họ chịu trách nhiệm thiết lập giao diện mà các usecase cần để thực hiện công việc của họ. Sau khi họ đã thiết lập giao diện này, Interface Adapters có thể thực hiện các khía cạnh của chúng, như thể hiện trong sơ đồ ở trên. Chúng tôi có CounterGateway interface và triển khai cụ thể bằng Redux bên dưới:
Có thể bạn không cần nó
Tất nhiên, ứng dụng mẫu này hơi phức tạp đối với counter app tăng / giảm. Và tôi muốn nói rõ rằng bạn không cần tất cả những điều này cho một dự án nhỏ hoặc nguyên mẫu. Nhưng hãy tin tôi, khi ứng dụng của bạn lớn hơn, bạn sẽ muốn tối đa hóa khả năng tái sử dụng và khả năng bảo trì. Kiến trúc phần mềm tốt giúp cho các dự án có khả năng chống chọi với thời gian.
Bộ sưu tập rất hữu ích để nhóm dữ liệu liên quan. Dart bao gồm một số loại bộ sưu tập khác nhau, nhưng hướng dẫn này sẽ bao gồm hai loại phổ biến nhất: List và Map.
Lists
Lists trong Dart tương tự như arraystrong các ngôn ngữ khác. Bạn sử dụng chúng để duy trì một danh sách các giá trị có thứ tự. Danh sách dựa trên 0, vì vậy mục đầu tiên trong danh sách ở chỉ mục 0:
Dưới đây là danh sách các món tráng miệng khác nhau:
List desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
Bạn đặt các phần tử của danh sách trong dấu ngoặc vuông [ ] . Bạn sử dụng dấu phẩy để phân tách các phần tử.
Ở đầu dòng, bạn có thể thấy rằng loại là List. Bạn sẽ nhận thấy không có loại nào được bao gồm. Dart suy luận rằng danh sách có loại List
Đây là danh sách các số nguyên:
final numbers = [42, -1, 299792458, 100];
Nhập numbers vào DartPad và bạn sẽ thấy rằng Dart nhận dạng loại List của int.
Làm việc với các phần tử trong List
Để truy cập các phần tử của danh sách, hãy sử dụng ký hiệu chỉ số con bằng cách đặt số chỉ mục giữa dấu ngoặc vuông sau tên biến danh sách. Ví dụ:
final firstDessert = desserts[0];
print(firstDessert); // cookies
Vì chỉ số danh sách dựa trên số không, desserts[0]là phần tử đầu tiên của danh sách.
Thêm và xóa các phần tử với add và remove tương ứng:
Trước đó, bạn đã học về forvòng lặp. for-inVòng lặp của Dart hoạt động đặc biệt tốt với các danh sách. Hãy thử nó:
for (final dessert in desserts) {
print('I love to eat $dessert.');
}
// I love to eat cookies.
// I love to eat cupcakes.
// I love to eat pie.
// I love to eat cake.
Bạn không cần sử dụng chỉ mục. Dart chỉ lặp qua mọi phần tử của dessertsvà gán nó mỗi lần cho một biến có tên dessert.
Maps
Khi bạn muốn một danh sách các giá trị được ghép nối, Map là một lựa chọn tốt. Dart Map tương tự như dictionariestrong Swift và mapstrong Kotlin.
Bạn bao quanh Maps bằng dấu ngoặc nhọn { }. Sử dụng dấu phẩy để phân tách các phần tử của bản đồ.
Các phần tử của bản đồ được gọi là cặp khóa-giá trị , trong đó khóa ở bên trái dấu hai chấm và giá trị ở bên phải.
Bạn tìm thấy một giá trị bằng cách sử dụng khóa để tra cứu nó, như sau:
final donutCalories = calories['donuts'];
print(donutCalories); // 150
Từ khóa 'donuts'nằm trong dấu ngoặc vuông sau tên bản đồ. Trong trường hợp này, nó ánh xạ tới một giá trị 150.
Nhập donutCalories vào DartPad và bạn sẽ thấy rằng kiểu được suy luận thì int? đúng hơn int. Đó là bởi vì, nếu một bản đồ không chứa khóa mà bạn đang tìm kiếm, nó sẽ trả về một giá trị null.
Thêm một phần tử mới vào bản đồ bằng cách chỉ định khóa và gán cho nó một giá trị:
Chạy mã đó và bạn sẽ thấy bản đồ được in ở định dạng ngang với món tráng miệng mới của bạn ở cuối.
Functions
Các hàm cho phép bạn đóng gói nhiều dòng mã liên quan vào một nội dung duy nhất. Sau đó, bạn triệu hồi hàm để tránh lặp lại những dòng mã đó trong ứng dụng Dart của mình.
Một hàm bao gồm các phần tử sau:
Kiểu trả về
Tên chức năng
Danh sách tham số trong ngoặc đơn
Nội dung hàm được đặt trong dấu ngoặc
Defining Functions
Đoạn mã bạn đang chuyển thành một hàm nằm trong dấu ngoặc nhọn. Khi bạn gọi hàm, bạn truyền vào các đối số phù hợp với loại tham số của hàm.
Tiếp theo, bạn sẽ viết một hàm mới trong DartPad sẽ kiểm tra xem một chuỗi đã cho có phải là Banana hay không :
Hàm sử dụng return để trả về kiểu bool. Đối số bạn truyền vào hàm sẽ xác định bool.
Hàm này sẽ luôn trả về cùng một kiểu giá trị cho bất kỳ đầu vào nhất định nào. Nếu một hàm không cần trả về giá trị, bạn có thể đặt kiểu trả về void. main làm điều này, chẳng hạn.
Làm việc với các Function
Bạn có thể gọi hàm bằng cách truyền vào một chuỗi. Sau đó, bạn có thể chuyển kết quả của cuộc gọi đó tới print:
void main() {
var fruit = 'apple';
print(isBanana(fruit)); // false
}
Các Function lồng vào nhau
Thông thường, bạn xác định các hàm bên ngoài các hàm khác hoặc bên trong các lớp Dart. Tuy nhiên, bạn cũng có thể lồng các hàm Dart. Ví dụ, bạn có thể làm tổ isBanana bên trong main.
void main() {
bool isBanana(String fruit) {
return fruit == 'banana';
}
var fruit = 'apple';
print(isBanana(fruit)); // false
}
Bạn cũng có thể thay đổi đối số thành một hàm, sau đó gọi lại đối số đó bằng đối số mới:
fruit = 'banana';
print(isBanana(fruit)); // true
Kết quả của việc gọi hàm phụ thuộc hoàn toàn vào các đối số bạn truyền vào.
Optional Parameters
Nếu một tham số cho một hàm là tùy chọn, bạn có thể bao quanh nó bằng dấu ngoặc vuông và đặt kiểu là vô hiệu:
Trong chức năng này, title là tùy chọn. Nó sẽ mặc định thành null nếu bạn không chỉ định nó.
Bây giờ, bạn có thể gọi hàm có hoặc không có tham số tùy chọn:
print(fullName('Joe', 'Howard'));
// Joe Howard
print(fullName('Albert', 'Einstein', 'Professor'));
// Professor Albert Einstein
Named Parameters and Default Values
Khi bạn có nhiều tham số, bạn có thể nhầm lẫn khi nhớ cái nào là cái nào. Dart giải quyết vấn đề này với các tham số được đặt tên, mà bạn nhận được bằng cách bao quanh danh sách tham số bằng dấu ngoặc nhọn { }.
Các tham số này là tùy chọn theo mặc định, nhưng bạn có thể cung cấp cho chúng các giá trị mặc định hoặc yêu cầu chúng bằng cách sử dụng từ khóa required:
bool withinTolerance({required int value, int min = 0, int max = 10}) {
return min <= value && value <= max;
}
valuelà bắt buộc, trong khi minvà maxlà tùy chọn với các giá trị mặc định.
Với các tham số được đặt tên, bạn có thể chuyển vào các đối số theo một thứ tự khác bằng cách cung cấp các tên tham số bằng dấu hai chấm:
print(withinTolerance(value: 5)); // true
Chạy mã của bạn để xem các chức năng mới của bạn đang hoạt động.
Anonymous Functions
Dart hỗ trợ first-class functions , nghĩa là nó xử lý các hàm giống như bất kỳ kiểu dữ liệu nào khác. Bạn có thể gán chúng cho các biến, chuyển chúng dưới dạng đối số và trả lại chúng từ các hàm khác.
Để chuyển các hàm này xung quanh dưới dạng giá trị, hãy bỏ qua tên hàm và kiểu trả về. Vì không có tên nên loại hàm này được gọi là hàm ẩn danh .
Bạn có thể gán một hàm ẩn danh cho một biến có tên onPressed như sau:
final onPressed = () {
print('button pressed');
};
onPressedcó giá trị kiểu Function. Các dấu ngoặc trống cho biết hàm không có tham số. Giống như các hàm thông thường, mã bên trong dấu ngoặc nhọn là phần thân của hàm.
Để thực thi mã bên trong thân hàm, hãy gọi tên biến như thể nó là tên của hàm:
onPressed(); // button pressed
Bạn có thể đơn giản hóa các hàm có nội dung chỉ chứa một dòng duy nhất bằng cách sử dụng cú pháp mũi tên . Để thực hiện việc này, hãy xóa dấu ngoặc nhọn và thêm một mũi tên =>.
Dưới đây là so sánh của hàm ẩn danh ở trên và phiên bản đã cấu trúc lại:
// original anonymous function
final onPressed = () {
print('button pressed');
};
// refactored
final onPressed = () => print('button pressed');
Sử dụng Anonymous Functions
Bạn sẽ thường thấy các hàm ẩn danh trong Flutter, giống như các hàm ở trên, được chuyển xung quanh dưới dạng lệnh gọi lại cho các sự kiện giao diện người dùng. Điều này cho phép bạn chỉ định mã chạy khi người dùng làm điều gì đó, chẳng hạn như nhấn một nút.
Một nơi phổ biến khác mà bạn sẽ thấy các chức năng ẩn danh là với các collection. Bạn có thể cung cấp cho tập hợp một hàm ẩn danh sẽ thực hiện một số tác vụ trên mỗi phần tử của tập hợp. Ví dụ:
.map nhận tất cả các giá trị danh sách và trả về một tập hợp mới với chúng.
Một hàm ẩn danh được chuyển dưới dạng một tham số. Trong hàm ẩn danh đó, bạn có một drink đối số đại diện cho từng phần tử của danh sách.
Phần thân của hàm ẩn danh chuyển đổi từng phần tử thành chữ hoa và trả về giá trị. Vì danh sách ban đầu là danh sách các chuỗi, nên drink cũng có kiểu String.
Sử dụng một hàm ẩn danh và kết hợp nó với .maplà một cách thuận tiện để chuyển đổi một bộ sưu tập này thành một bộ sưu tập khác. Lưu ý : Đừng nhầm giữa .mapmethod với Maptype.
Chạy mã để xem bộ sưu tập kết quả.
Xin chúc mừng, bạn đã hoàn thành phần hướng dẫn. Bây giờ bạn sẽ hiểu rõ hơn về mã Dart mà bạn thấy khi học cách xây dựng ứng dụng Flutter!
Ở số trước mình đã giới thiệu một vài widget hữu ích, anh em có thể tìm đọc lại tại đây.
Sang phần 2 này, mình sẽ tiếp tục giới thiệu đến anh em một vài widget ít phổ biến nhưng cũng khá thú vị. Bắt đầu ngay nhé 😀
1. Chip widgets
Đây là một loạt các widget có hình dạng mặc định là hình chữ nhật được bo tròn bốn góc (Stadium shape), có thể có avatar đằng trước để thể hiện hình ảnh. Vì có khá nhiều loại nên mình sẽ không đi sâu cụ thể mà chỉ giới thiệu qua cách sử dụng của chúng.
Chip widget
Chip: Đây là loại cơ bản nhất, chỉ đơn giản thể hiện thông tin, kèm theo avatar đằng trước nếu cần.
ActionChip: Widget này có thêm thuộc tính onPressed nên có thể click được. Đây có thể là một lựa chọn thay thế cho ElevatedButton, TextButton hay OutlinedButton. Nhưng cần lưu ý là widget này sẽ không có trạng thái disabled (tức thuộc tính onPressed không nhận giá trị null), cho nên widget này được khuyên dùng cho các item không cố định trong UI => use case sử dụng: khi người dùng tìm kiếm thông tin, dùng widget này để đưa ra cho người dùng các kết quả gợi ý.
FilterChip: Cũng giống như ActionChip nhưng có thêm thuộc tính selected, widget này được sử dụng như CheckBox hay Switch, phù hợp khi đặt filter tìm kiếm thông tin. Ví dụ tại đây
FilterChip
ChoiceChip: Gần tương tự FilterChip nhưng sử dụng khi muốn chọn 1 trong nhiều lựa chọn.
InputChip: Là sự kết hợp của những loại Chip bên trên, phù hợp khi thực hiện những tác vụ phức tạp.
Thực ra đây cũng không hẳn là một widget nhưng nó khá là hay nên mình cũng xin giới thiệu cho những ai chưa biết 😀
Đôi khi anh em muốn thêm ảnh vào trong ứng dụng của mình để cho thêm sinh động, nhưng nếu ở những vị trí nhỏ mà chúng ta thêm những ảnh có size lớn vào thì sẽ khá là lãng phí bộ nhớ. ResizeImage sẽ giúp chúng ta giảm thiểu điều này
Cách sử dụng khá đơn giản, ResizeImage là một ImageProvider và cũng nhận vào một ImageProvider nên ta có thể sử dụng như sau:
Hầu hết các ứng dụng đều sẽ có phần cài đặt ứng dụng, và ở trong đó đôi khi sẽ có những lựa chọn theo kiểu bật / tắt, chúng ta sẽ nghĩ ngay đến widget Switch phải không nào?
Nhưng với góc nhìn của một người dùng, việc phải click đúng vào ô switch bé tí để có thể sử dụng khiến ta đôi khi cảm thấy hơi bất tiện. Đừng lo vì chúng ta đã có SwitchListTile :)))
Có thể hiểu nôm na đây chính là sự kết hợp của ListTile và Switch, chúng ta có thể click lên mọi điểm trong widget này để có thể kích hoạt nút switch, như vậy trải nghiệm người dùng (UX) sẽ được cải thiện thêm 😀
Tất cả mọi người khi làm việc với Dart để phát triển ứng dụng bằng cách sử dụng Flutter framework thường xuyên gặp phải các cách sử dụng khác nhau của cách từ khóa: implements, extends và with. Trong Dart, một lớp có thể kế thừa một lớp khác, tức là Dart có thể tạo một lớp mới từ một lớp hiện có. Chúng ta sử dụng các từ khóa để làm như vậy. Trong bài viết này, chúng ta sẽ xem xét 3 trong số các từ khóa được sử dụng cho cùng một mục đích và so sánh chúng, đó là:
extends
with
implements
1. “extends” keyword.
Trong Dart, từ khóa “extends” thường được sử dụng để thay đổi hành vi của super class bằng cách sử dụng Inheritance. Một lớp mới sử dụng được các thuộc tính và đặc điểm của lớp hiện có khác được gọi là Tính kế thừa. Nói một cách đơn giản hơn, chúng ta có thể nói rằng, chúng ta sử dụng key word extends để tạo Child class và super để chỉ Parent Class. Class có các thuộc tính được kế thừa bởi child class được gọi là Parent Class. Parent Class còn được gọi là base class or super class. Class kế thừa các thuộc tính từ lớp khác được gọi là child class. Child class còn được gọi là derived class, heir class, or subclass. “extends” keyword là tính kế thừa của OOP điển hình. Ngoài ra, chúng ta có thể ghi đè các phương thức.
Chúng ta sử dụng “extends” keyword nếu bạn muốn tạo một phiên bản cụ thể hơn của một lớp. Ví dụ: nếu class Apple extends từ lớp Fruit, điều đó có nghĩa là tất cả các thuộc tính, biến và hàm được định nghĩa trong class Fruit sẽ có sẵn trong class Apple.
Ví dụ:
Output:
2. “implements’ keyword
Interfaces định nghĩa thiết lập của các phương thức trên một đối tượng. Dart không có cú pháp miêu tả interfaces. Mọi class được ngầm định nghĩa là một interfaces chưa tất cả các instance members của class và bất kỳ interfaces nào nó thể hiện. Nếu bạn muốn tạo class A hỗ trợ API của class B mà không kế thừa B, class A nên “implements” interface B. Chúng ta sử dụng keyword “implements” để làm điều này. Đặc biệt, để sử dụng tính trừu tượng toàn phần trong dart, chúng ta sử dụng “abstract” phía trước class và sẽ không thể khởi tạo được nó.
Ví dụ 1:
Output:
Ví dụ 2:
Kết quả:
3. “with” keyword
Mixins là con đường tái sử dụng các phương thức của các class. Mixins được hiểu như abstract class để tái sử dụng trong nhiều class có chức năng và thuộc tính tương tự. Mixins là con đường để trừu tượng và tái sử dụng các phép toán và trạng thái. Nó tương tự việc tái sử dụng chúng ta là để mở rộng class nhưng không có đa kế thừa. Vẫn chỉ tồn tại một superClass.
“with” keyword được sử dụng để bao gồm Mixins. Mixin là một kiểu cấu trúc khác, chỉ được sử dụng với “with” keyword.
Trong Dart, một lớp có thể giữ vai trò như mixin nếu class đó không có constructor. Điều quan trọng cần lưu ý là mixin không bắt buộc hạn chế kiểu cũng như không áp đặt hạn chế sử dụng đối với các phương thức của class.
Ví dụ:
Kết quả:
4. Tổng kết.
Trên đây là một số chia sẻ về sự khác nhau của extends, implemens, và with keywords trong Dart nhằm giúp mọi người sử dụng đúng mục đích. Mong rằng qua bài viết sẽ giúp ích cho các bạn phần nào đó.
Thực hiện các xử lý trên background của iOS và Android.
II. Cách sử dụng Sqlite trong Flutter
1. Thêm thư viện
1.1. Thêm thư viện sqflitevà path (dùng để xác định vị trí lưu database trong bộ nhớ) vào phần dependencies trong file pubspec.yaml
2. Cài đặt thư viện
2.1. Đầu tiên chúng ta cần tạo một class model Student, đây sẽ là dữ liệu chúng ta dùng trong quá trình lưu trữ.
class Student {
int id;
String name;
int grade;
Student({required this.id, required this.name, required this.grade});
}
Nếu bạn chưa biết rõ về Sql cũng như các câu lệnh của chúng, hãy tìm hiểu về chúng trước khi tiếp tục: SQL Introduction (w3schools.com)
2.2. Tiếp theo chúng ta khởi tạo database
class StudentDatabase {
static Database? _database;
static Future<Database> getInstance() async {
_database ??= await openDatabase(
/// use join to create path for db, then the path will be path/student.db
join(await getDatabasesPath(), "student.db"),
/// This function will be called in the first time database is created
onCreate: (db, version) {
return db.execute(
"CREATE TABLE student(id INTEGER PRIMARY KEY, name TEXT, grade INTEGER)");
},
/// This version will use when you want to upgrade or downgrade the database
version: 1,
singleInstance: true);
return _database!;
}
}
Hàm onCreate() được gọi tại lần đầu mà database được khởi tạo
Version chính là phiên bản của database, nếu bạn muốn thay đổi cấu trúc của database thì chúng ta phải thay đổi version.
Tiếp theo ta thêm các hàm phục vụ cho việc chuyển đổi dữ liệu khi muốn thêm vào database cũng như lấy dữ liệu ra từ database
Hàm insert() có hai tham số truyền vào là tên của bảng và dữ liệu chúng ta muốn thêm vào bảng, dữ liệu này đã được chuyển thành một map để có thể thực hiện thêm vào database.
Tham số conflictAlgorithm: được sử dụng để xác định các xử lý khi có sự trùng lặp dữ liệu xảy ra, ở đây tham số này có giá trị là ConflictAlgorithm.replace có nghĩa là nếu trùng primary key thì giá trị cũ sẽ được thay thế bằng giá trị mới.
Nếu hàm insert thực hiện thành công sẽ trả về id của hàng vừa được thêm.
Nếu không thành công sẽ trả về giá trị 0.
2.4. Lấy thông tin của tất cả các trường
Trong Android Studio có một công cụ hỗ trợ chúng ta xem được các cơ sở dữ liệu đang có trong app, đó chính là App Inspection
Tại đây chúng ta có thể thấy được bảng “student” đã được thêm 33 trường dữ liệu, vậy bây giờ chúng ta muốn lấy tất cả những dữ liệu này ra thì phải làm thế nào?
Future<DataResult> getAllStudent() async{
try{
Database db = await StudentDatabase.getInstance();
final List<Map<String,dynamic>> maps = await db.query("student");
List<Student> students = maps.map((e) => Student.fromMap(e)).toList();
return DataResult.success(students);
}catch(ex){
return DataResult.failure(DatabaseFailure(ex.toString()));
}
}
Chúng ta chỉ cần sử dụng hàm query, truyền vào tham số là bảng mà chúng ta cần lấy dữ liệu.
Dữ liệu trả về sẽ ở dạng List<Map<String,dynamic>>, vì vậy chúng ta cần chuyển đổi chúng sang dạng của lớp Student sử dụng hàm fromMap() đã được thêm trong class Student