[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.
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.
Ở 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 😀
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
[FLUTTER] Cách viết Unit Test trong Flutter (Phần 2)
Lời ngỏ
Mỗi ngôn ngữ, hay framework đều có các cách để triển khai Unit Test khác nhau. Tuy nhiên trong bài viết này mình sẽ chú trọng vào Unit Test trong Flutter.
B. Cách triển khai Unit Test trong Flutter
1. Cài đặt thư viện
Để có thể viết Unit Test trong Flutter, chúng ta cần thêm thư viện test, nhớ thêm vào phần dev_dependencies nhé.
2. Cấu hình và giải thích
Đầu tiên chúng ta tạo một hàm xử lý để giả định cho một chức năng trong app
class Calculation{
int add(int a, int b){
return a+b;
}
int subtract(int a, int b){
return a-b;
}
}
Trong class Calculation, chúng ta có hàm xử lý là hàm “add”,”subtract”, đây chính là hàm chúng ta cần viết unit test. Bước tiếp theo là tạo một class để viết unit test.
Ở trong phần package tree của project chúng ta sẽ thấy một package tên là “test”.
Đây sẽ là nơi chúng ta viết các unit test cho ứng dụng.
Có thể thấy ở bên trong folder này đã có một class được viết sẵn có tên là “widget_test.dart”, đây sẽ là class dùng trong việc test các widget của ứng dụng.
Chúng ta tạo thêm một file đặt tên là “calculation_test” (Hãy đặt tên của file theo những chức năng, lớp mà chúng ta muốn test để dễ trong việc phân biệt và tìm kiếm.
Chúng ta tạo một class “main”, bên trong sẽ viết các hàm unit test
import 'package:flutter_test/flutter_test.dart';
import 'package:unit_test_sample/calculation.dart';
void main(){
/// Init class which needs tested
Calculation calculation = Calculation();
/// Test function add
test("Sum of two integer ", () {
int result = calculation.add(5, 4);
expect(result, 9);
});
}
Ở đây ta sử dụng hàm “test” để viết các unit test cho từng chức năng.
Hàm “test” này có 2 tham số cần truyền vào đó là:
Mô tả của test, nơi chúng ta có thể mô tả xem hàm test này đang test chức năng nào, hoặc có thể bổ sung thêm một vài mô ta cho input chúng ta truyền vào (ví dụ như input đó bị lỗi, sai, hay đúng).
Tham số còn lại chính là một hàm, ở đây chúng ta sẽ viết các bước để có thể gọi được đến hàm cần test (ở đây là hàm “add”), cuối cùng chúng ta sử dụng hàm “expect” để kiểm tra kết quả của hàm test với kết quả mà chúng ta mong đợi. Tham số đầu tiên sẽ là kết quả của hàm được test, tham số còn lại là kết quả mà chúng ta mong muốn.
Như vậy phần chuẩn bị hàm test đã xong, giờ việc chúng ta cần làm là chạy thôi:
Ấn chuột phải rồi chọn “run test in calculation” hoặc ấn tổ hợp phím Ctrl + Shift + F10:
Sau một lúc chạy thì ở phía dưới sẽ hiển thị một màn hình kết quả test:
Nếu có tick xanh ở trước mô tả “Sum of two integer”, có nghĩa là test của chúng ta đã chạy đúng như mong đợi.
Thử thay đổi kết quả mong đợi để xem chuyện gì xảy ra:
test("Sum of two integer ", () {
int result = calculation.add(5, 4);
expect(result, 8);
});
Ngay lập tức test của chúng ta đã chạy sai, trong phần log lỗi cũng đã chỉ ra cho chúng ta biết chúng ta sai ở đâu.
Expected (kết quả mong đợi) là 8 trong khi Actual (kết quả được tính toán) là 9, từ đó chúng ta sẽ rất dễ dàng trong việc fix lỗi.
Khi có nhiều hàm test cùng liên quan đến một vấn đề, chúng ta có thể cho chúng vào trong một “group” để dễ quản lý hơn.
void main() {
/// Init class which needs tested
Calculation calculation = Calculation();
group(
"Calculate with integer",
() {
/// Test function add
test("Sum of two integer ", () {
int result = calculation.add(5, 4);
expect(result, 9);
});
/// Test function subtract
test("Subtraction of two integer ", () {
int result = calculation.subtract(5, 4);
expect(result, 1);
});
},
);
}
[FLUTTER] Một số widget hữu ích trong Flutter (Phần 1)
Giới thiệu
Khi làm việc với Flutter có lẽ ai cũng biết câu nói:
Trong Flutter, (gần như) mọi thứ đều là widget.
Nhưng cũng chính vì có quá nhiều widget, lập trình viên đôi khi cảm thấy khó khăn trong việc lựa chọn, thậm chí là không biết đến sự tồn tại của một widget đã được viết sẵn phục vụ cho những yêu cầu thường gặp, thành ra phải tự code lại tốn nhiều thời gian mà chưa chắc đã thực sự tốt.
Vì vậy, trong bài viết này, mình sẽ giới thiệu một số widget có thể sẽ giúp ích trong quá trình anh em làm việc với Flutter.
À, nhưng trước hết, mình vẫn muốn recommend anh em kênh youtube chính thức của Flutter, có playlist Widgets of the Week(có lẽ nhiều người đều biết đến list này), mình thường xuyên vào đây xem khi có thời gian rảnh 😀
Chắc hẳn khi đọc tên thì anh em cũng đã tưởng tượng được ra tác dụng của widget này. Cũng giống như khi sử dụng các widget liên quan tới image, chúng ta có thuộc tính fit để thể hiện cách vẽ widget con (điều này chắc ai cũng biết rồi).
Nhưng có một cách sử dụng khá hay mà mình mới được biết gần đây là làm cho các text trở nên responsive, khi độ lớn của widget cha thay đổi, text bên trong cũng thay đổi theo, ví dụ mình hoạ tại đây.
Widget này gần tương tự với Flex (lớp cha của Row và Column), nhưng khác ở điểm có thể tự sắp xếp các widget con sang cột / hàng mới tuỳ vào kích thước còn lại. Ví dụ
Ví dụ về Wrap
Một số thuộc tính trong Wrap như sau:
direction: xác định trục chính (vertical / horizontal)
spacing: khoảng cách giữa các widget trong trục chính (Main Axis).
runSpacing: khoảng cách giữa các widget trong trục phụ (Cross Axis).
alignment: vị trí theo trục chính của các widget con khi được wrap sang một hàng / cột mới (start, center, spaceBetween, …)
runAlignment: vị trí theo trục phụ của các widget con khi được wrap sang một hàng / cột mới.
Ngoài ra còn một số thuộc tính nữa anh em có thể đọc thêm tại link doc của Flutter.
3. Flexible, Expanded và Spacer
Tại sao mình lại đưa ra một lúc 3 widget? Bởi vì 3 widget này khá tương đồng nhau về cách sử dụng, để mình giải thích cho nhé.
Hãy tưởng tượng, anh em muốn làm một giao diện có các widget con bên trong có thể thay đổi kích thước theo widget cha (không nên nhầm lẫn với việc size màn hình thay đổi nhé), như vậy thì không thể sử dụng các widget có size cố định, chúng ta hãy nghĩ ngay đến việc sử dụng 3 widget trên xem sao ^^
Ví dụ, chúng ta có 1 Row, bên trong gồm có 4 Container chứa Text, vậy làm thế nào để:
Container 1 chiếm 10%
Container 2 chiếm 20%
Container 3 chiếm 30%
Container 4 chiếm 40%
Rất đơn giản, anh em chỉ cần wrap từng widget con vào trong widget Flexible và cho chúng hệ số flex như sau:
Tính tổng hệ số flex của các widget có trong parent (Bao gồm Flexible, Spacer và Expanded): ví dụ ở trên sẽ là 1 + 2 + 3 + 4 = 10
Đặt kích thước của widget theo tỉ lệ hệ số flex / tổng, Ví dụ Container 1 sẽ chiếm 1/10 tức 10% width của row
Vậy là chúng ta đã có một giao diện động mà không cần sử dụng các kích thước cố định 😀 :D.
Nhưng nếu giữa các widget Flexible mà có widget kích thước cố định thì sao? Sẽ không có vấn đề gì cả, flutter sẽ trừ đi phần cố định chia theo hệ số flex như trên
Expanded cũng giống như Flexible, chỉ khác là nó sẽ luôn muốn chiếm hết không gian còn lại nếu có thể, còn Flexible thì mặc định sẽ chỉ chiếm vừa đủ không gian bằng widget con của nó, và có một điều cần lưu ý:
Flexible(
fit: FlexFit.tight,
...
),
sẽ tương đương với:
Expanded(
...
)
Chỉ là đặt tên như vậy sẽ tường minh hơn, dễ trong việc sử dụng hơn 😀 😀
Hãy so sánh hiệu năng FPS, CPU, Memory và GPU của các công cụ phát triển thiết bị di động phổ biến.
Câu chuyện đằng sau việc nghiên cứu
inVerita và nhóm phát triển mobile của mình liên tục nghiên cứu hiệu năng của các giải pháp mobile đa nền tảng hiện có để trả lời câu hỏi công nghệ nào tốt nhất Flutter hoặc React Native (hoặc Native) cho sản phẩm của bạn, đó là cách Flutter vs React Native vs Native Part I nổi lên. Điều đó gây ra nhiều tranh cãi vì người ta nói rằng không sử dụng React Native để thực hiện phép tính (perform multiple calculations) hàng ngày – có thể đúng như vậy – nhưng trong trường hợp này, các task nặng của CPU được ứng dụng Flutter hoặc Native thực hiện tốt hơn.
Đó là lý do tại sao trong bài viết này, chúng tôi quyết định nghiên cứu hiệu năng của UI có tác động lớn hơn nhiều đến daily user của mobile app.
Việc đo lường hiệu năng UI rất phức tạp và yêu cầu kỹ sư triển khai cùng chức năng theo cùng một cách trên mọi nền tảng. Chúng tôi đã sử dụng GameBench, công cụ kiểm tra toàn cầu để đảm bảo sự khách quan (nó không thay đổi sự thật là chúng tôi thực sự yêu thích Flutter ở nhiều khía cạnh 🙂 và vẫn chạy rất nhiều dự án React Native và Native). GameBench có rất nhiều không gian để cải tiến, nhưng chúng tôi đã cố gắng đưa mọi ứng dụng vào một môi trường single testing với sự trợ giúp của nó, đó là mục tiêu của chúng tôi.
Source code mở vì vậy hãy thử nghiệm và chia sẻ suy nghĩ của bạn với chúng tôi nếu bạn muốn. UI animation chủ yếu sử dụng các công cụ khác nhau trên các nền tảng khác nhau, vì vậy chúng tôi thu hẹp mọi thứ vào các thư viện được hỗ trợ bởi mọi nền tảng (trừ một trường hợp) hoặc ít nhất chúng tôi đã làm mọi thứ để hoàn thành điều đó. Kết quả test có thể khác nhau và tùy thuộc vào phương pháp triển khai, chúng tôi tin rằng bạn có thể đẩy bộ tool đến giới hạn mà nó vượt trội so với các con số của chúng tôi. Bây giờ, chúng ta hãy xem xét các trường hợp.
Thông tin thiết bị phần cứng:
Đối với mục đích thử nghiệm của chúng tôi, chúng tôi đã sử dụng một chiếc Xiaomi Redmi Note 5 và iPhone 6s giá cả phải chăng.
Chúng tôi đã triển khai cùng một UI trên cả Android và iOS ,sử dụng Native, React Native và Flutter. Chúng tôi cũng tự động hóa tốc độ scroll bằng cách sử dụng RecyclerView.SmoothScroller trên Android. Trên iOS và React Native, chúng tôi đã sử dụng cách tiếp cận với timer và lập trình scroll đến vị trí. Trên Flutter, chúng tôi đã sử dụng ScrollController để scroll qua danh sách một cách trơn tru. Trong mỗi trường hợp, chúng tôi có 1000 phần tử trong list view và cùng một thời gian scroll để đến element cuối cùng. Trong mỗi trường hợp này, chúng tôi đã sử dụng hình ảnh trong bộ nhớ đệm (image caching) với các lib khác nhau trên mỗi nền tảng. Xem thông tin chi tiết trong source code.
Android — GPU tests results are not supported by the benchmark (unfortunately, with the devices we have, and we have many:)) )
Kết quả kiểm tra Android – GPU không được hỗ trợ bởi benchmark
Kết quả kiểm tra
Tất cả các thử nghiệm đều cho thấy FPS xấp xỉ như nhau.
Android Native sử dụng một nửa memory so với Flutter và React Native.
React Native yêu cầu khai thác CPU nhiều nhất. Lý do là việc sử dụng JSBridge giữa mã JS và Native kích động sự lãng phí tài nguyên khi serialization và deserialization.
Về khai thác pin, Android Native có kết quả tốt nhất. React-native đang tụt hậu so với cả Android và Flutter. Chạy các animation liên tục sẽ tiêu tốn nhiều pin hơn trên React Native.
Test trên iPhone 6s
Kết quả kiểm tra
FPS: Kết quả của React Native kém hơn so với Flutter và Swift. Lý do là không thể sử dụng biên dịch (compilation) IoT trên iOS.
Memory: Flutter gần như khớp với nguyên bản (native) về mức tiêu thụ Memory nhưng vẫn nặng hơn trên CPU. React Native thua xa Flutter và native trong thử nghiệm này.
Sự khác biệt giữa Flutter và Swift. Flutter đang tích cực sử dụng CPU khi iOS Native đang tích cực sử dụng GPU. Đối chiếu trong Flutter làm tăng tải trên CPU.
Use case 2 — Heavy animations test
Ngày nay hầu hết các điện thoại chạy trên Android và iOS đều có phần cứng rất mạnh. Trong hầu hết các trường hợp sử dụng các ứng dụng kinh doanh, có thể thấy không có sự sụt giảm số khung hình/giây nào. Đó là lý do tại sao chúng tôi quyết định thực hiện một số thử nghiệm với animation nặng. Đủ nặng để giảm số khung hình/giây. Chúng tôi đã sử dụng animation animated vector với Lottie trên Android, iOS, React Native và sử dụng các animation tương tự để sử dụng với Flare on Flutter.
Android và React Native có những điểm tương đồng về hiệu năng của chúng. Đó là điều hiển nhiên vì Lottie cho React Native sử dụng phương tiện Native (16–19% CPU, 30–29 FPS).
Kết quả của Flutter là một bất ngờ, mặc dù nó có một chút trục trặc trong một performance. (12% CPU và 9 FPS).
Android yêu cầu ít bộ nhớ nhất (205 Mb); React Native cần 280 Mb và Flutter cần 266 Mb.
Khởi động lại app. Theo chỉ số này, Flutter là người dẫn đầu (2 giây). Đối với Android Native và React Native, mất khoảng 4 giây.
Chúng tôi phát hiện ra rằng việc xóa một animation cụ thể khỏi lưới (grid) sẽ tăng FPS lên đến 40% trên Flutter. Chúng tôi cho rằng Flare nặng hơn và không được tối ưu hóa cho loại task này, đó là lý do tại sao Flutter lại bị sụt FPS như vậy.
Flare và Flutter sẽ không ngừng khiến bạn ngạc nhiên. Flare chắc chắn có một con đường để đi 😀
iOS Native yêu cầu ít bộ nhớ nhất (48 Mb). React Native cần 135 Mb và Flutter cần 117 Mb.
Khởi động cold app. Theo chỉ số này, Flutter là người dẫn đầu (2 giây). Đối với iOS và React Native, mất khoảng 10 giây.
Lưu ý: chúng tôi đã sử dụng một thư viện khác cho trường hợp này với Flutter nặng hơn nhiều so với những thư viện đã sử dụng cho các nền tảng khác và nó có thể là lý do khiến fps giảm.
Use case 3 — Kiểm tra animation thậm chí còn nặng hơn với các rotation, scaling và fade.
Trong thử nghiệm này, chúng tôi đã so sánh hiệu năng trong khi tạo animation cho 200 hình ảnh. Các animation xoay tỷ lệ và mờ dần được thực hiện cùng một lúc.
200 hình ảnh
Android
Kết quả kiểm tra
Native cho thấy hiệu năng cao nhất và tiêu thụ bộ nhớ hiệu quả nhất.
Flutter cho thấy hiệu năng vừa đủ để làm việc thoải mái nhưng chi phí bộ nhớ cao hơn gấp đôi so với Native.
React Native đã cho thấy hiệu năng thấp trong trường hợp này.
IOS
Kết quả kiểm tra
iPhone 6s đủ mạnh để không giảm fps trong cả 3 trường hợp.
Native sử dụng ít tài nguyên hơn và GPU được sử dụng gần hết.
React Native chủ yếu sử dụng CPU để hiển thị trong khi Flutter sử dụng GPU.
React Native đã sử dụng nhiều bộ nhớ hơn một chút.
Tóm lại
Đối với các ứng dụng thông thường có animation nhỏ và vẻ ngoài lấp lánh, công nghệ không thành vấn đề. Nhưng nếu bạn sẽ thực hiện một số animation nặng, hãy nhớ rằng shiny Native có sức mạnh hiệu năng cao nhất để làm điều đó. Tiếp theo, hãy đến với Flutter và React Native. Chúng tôi chắc chắn không khuyên bạn nên sử dụng React Native trong một hoạt động quá nặng về CPU, trong khi Flutter rất phù hợp cho các task như vậy từ cả quan điểm CPU và Memory.
Công cụ bạn chọn tùy thuộc vào sản phẩm và business case cụ thể của bạn. Trong trường hợp bạn đang tìm cách phát triển MVP một nền tảng – hãy sử dụng các phương tiện gốc, nhưng hãy nhớ rằng các ứng dụng Flutter có thể được xây dựng cho cả môi trường mobile, web, desktop và có vẻ như Flutter có thể trở thành Vua phát triển đa nền tảng trong tương lai không xa, vì hiện tại Flutter đã tạo ra một cuộc cạnh tranh cho các công cụ phát triển native, đặc biệt nếu ngân sách phát triển không hạn chế mà bạn vẫn đang tìm kiếm hiệu năng tốt cho ứng dụng của mình trên các nền tảng khác nhau.
Chúng tôi phải đối mặt với thực tế là có thể có nhiều yếu tố ảnh hưởng đến việc triển khai và benchmark của từng công nghệ và nhiều người trong số các bạn có thể là chuyên gia thực sự của một nền tảng cụ thể có thể khai thác nhiều hơn nữa bộ tool yêu thích. Chúng tôi đã cố gắng giải thích bằng cách tạo ra một môi trường duy nhất cho mỗi ứng dụng để thử nghiệm và một bộ công cụ duy nhất để đo lường hiệu năng và tôi hy vọng bạn thích kết quả này.
Ta có thể thêm Widget dùng để tách các Widget con qua separatorBuilder
Sử dụng ListView.custom
Đây là cách xây dựng ListView giúp bạn có thể tùy chỉnh nhiều hơn cho các model con. Ví dụ: một model con tùy chỉnh có thể kiểm soát thuật toán được sử dụng để ước tính kích thước của các mô hình con không thực sự hiển thị.
[FLUTTER] Cách viết Unit Test trong Flutter (Phần 1)
Lời ngỏ
Unit Test là một phần rất quan trọng trong quá trình phát triển phần mềm, tuy nhiên nó thường xuyên bị lãng quên với một lập trình viên mới vào nghề hoặc chưa có nhiều kinh nghiệm. Mong rằng bài viết sẽ giúp bạn có cái nhìn trực quan hơn về Unit Test tron phát triển phầm mềm, đặc biệt là trong Flutter.
A. Đôi điều về Unit Test
1. Unit Test là gì?
1.1 Ảnh unit test
Unit Test là một loại kiểm thử phần mềm trong đó các đơn vị hay thành phần riêng lẻ của phần mềm được kiểm thử. Kiểm thử đơn vị được thực hiện trong quá trình phát triển ứng dụng. Mục tiêu của Kiểm thử đơn vị là cô lập một phần code và xác minh tính chính xác của đơn vị đó.
Nếu khái niệm trên vẫn còn khá khó hiểu thì hãy thử tách nghĩa từng từ ra một nhé:
Unit là một thành phần Phần mềm nhỏ nhất mà ta có thể kiểm tra được như các hàm (Function), thủ tục (Procedure), lớp (Class), hoặc các phương thức (Method).
Test thì là kiểm thử, kiểm tra tính chính xác của một cái gì đó.
Đến đây thì chắc các bạn cũng đã có cho mình một chút khái niệm về Unit Test rồi đúng không nhỉ.
Thường các lập trình viên khi nghe về một khái niệm mà trong đó có từ “Test” thì điều mọi người sẽ nghĩ đến ngay đó là “Test là công việc của Tester đâu phải việc của mình nên mình không cần quan tâm :v”.
Nhưng KHÔNG, các bạn đã nhầm to. Unit Test sẽ phải được viết bởi các lập trình viên, bởi chính những người viết ra những dòng code đó.
2. Vòng đời của Unit Test
Vòng đời của Unit Test gồm 3 giai đoạn:
Fail (trạng thái lỗi).
Ignore (tạm ngừng thực hiện).
Pass (trạng thái làm việc).
Ba giai đoạn này sẽ thay phiên nhau làm việc khi một Unit Test được chạy tự động.
3. Unit Test quan trọng không và khi nào thì cần viết Unit Test?
Unit Test là một phần không thể thiếu trong quá trình phát triển phần mềm. Unit Test đem lại cho chúng ta rất nhiều lợi ích:
Tạo ra một môi trường để kiểm tra bất kỳ đoạn code nào, duy trì sự ổn định của phần mềm. Unit Test giúp chúng ra kiểm tra những kết quả trả về mong muốn cũng như những ngoại lệ mong muốn.
Phát hiện các lỗi, các xử lý không hiệu quả trong code, các vấn đề về thiết kế.
Việc viết Unit Test có thể coi như việc tạo một người dùng đầu tiên cho ứng dụng, từ đó chúng ta có thể biết được những vấn đề mà trong quá trình sử dụng ứng dụng người dùng có thể gặp phải.
Giúp cho quá trình phát triển phần mềm trở nên nhanh hơn, số lượng test case khi được test cũng sẽ pass nhiều hơn. Điều này giúp cho các bộ phận khác như QA, Tester làm việc sẽ nhàn hơn. Và trên hết đối với những coder chúng ta, việc ít phải đối mặt với Tester cũng làm cho buổi làm việc “bớt sóng gió” hơn đúng không nào?
Note
Viết Unit Test càng sớm càng tốt trong giai đoạn viết code và xuyên suốt chu kỳ Phát triển phần mềm.
4. Như nào là một Unit Test có giá trị?
Muốn viết một Unit Test hiệu quả, đem lại nhiều lợi ích nhất cho bản thân cũng như dự án thì cần chú ý những điểm sau:
Unit Test chạy nhanh, sử dụng dữ liệu dễ hiểu, dễ đọc.
Hãy làm cho mỗi test độc lập với những phần khác. Mỗi test chỉ nên liên quan đến một hàm, thủ tục, … Điều này sẽ giúp chúng ta dễ dàng hơn trong quá trình quản lý unit test, cũng như đáp ứng được các thay đổi trong code.
Giả lập tất cả các dịch vụ và trạng thái bên ngoài. Ví dụ: Nếu bạn có làm việc với Database, thì KHÔNG nên sử dụng database thật của ứng dụng để viết Unit Test, bởi vì giá trị trong đó sẽ có thể thay đổi và ảnh hưởng đến các kết quả mong đợi của bạn. Thay vào đó hay tự vào cho mình một fake database, với dự liệu có sẵn và chỉ sử dụng các hàm, thủ tục để làm việc với nó.
Nên đặt tên các đơn vị kiểm thử rõ ràng và nhất quán với nhau để đảm bảo rằng test case dễ đọc. Để bất kỳ ai cũng có thể khởi chạy test case mà không gặp phải trở ngại.
Triển khai Unit Test bao quát hết tất cả các ngoại lệ, các test case.
B. Cách triển khai Unit Test trong Flutter (Còn tiếp)