Blog

  • KIẾN THỨC CƠ BẢN VỀ DART (PHẦN 4)

    KIẾN THỨC CƠ BẢN VỀ DART (PHẦN 4)

    Collections

    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ư arrays trong 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:

    desserts.add('cake');
    print(desserts); 
    // [cookies, cupcakes, donuts, pie, cake]
    
    desserts.remove('donuts');
    print(desserts); 
    // [cookies, cupcakes, pie, cake]

    Chạy mã để xem kết quả.

    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ư dictionaries  trong Swift và maps trong Kotlin.

    Đây là một ví dụ về map trong Dart:

    Map<String, int> calories = {
      'cake': 500,
      'donuts': 150,
      'cookies': 100,
    };

    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ị:

    calories['brownieFudgeSundae'] = 1346;
    print(calories);
    // {cake: 500, donuts: 150, cookies: 100, brownieFudgeSundae: 1346}

    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 :

    bool isBanana ( String fruit) {
       return fruit == 'banana' ; 
    }

    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:

    String fullName(String first, String last, [String? title]) {
      if (title == null) {
        return '$first $last';
      } else {
        return '$title $first $last';
      }
    }

    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ụ:

    // 1
    final drinks = ['water', 'juice', 'milk'];
    // 2
    final bigDrinks = drinks.map(
      // 3
      (drink) => drink.toUpperCase()
    );
    // 4
    print(bigDrinks); // (WATER, JUICE, MILK)

    Hãy xem xét từng bước:

    1. Xác định danh sách drinks có các chữ cái thường.
    2. .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.
    3. 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.
    4. 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!

  • [FLUTTER] Một số widget hữu ích trong Flutter (Phần 2)

    [FLUTTER] Một số widget hữu ích trong Flutter (Phần 2)

    Ở 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.

    2. ResizeImage

    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:

    Image(
        image: ResizeImage(
          NetworkImage(imageUrl)),
         ),

    Như vậy là chúng ta có thể tiết kiệm được một chút bộ nhớ rồi 😀

    3. SwitchListTile

    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 😀

    Anh em có thể xem ví dụ tại đây.

    SwitchListTile

    Lưu ý: CheckBox hay Radio cũng có lựa chọn tương đương là CheckBoxListTileRadioListTile

    Mình xin được phép kết thúc phần 2 tại đây, mong rằng những thông tin mình chia sẻ sẽ giúp ích được anh em trong quá trình làm việc 😀

  • Dart – extends Vs with Vs implements

    Mở đầu:

    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 classsuper để 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 đó.

    Cảm ơn các bạn đã đọc bài viết của mình.

    Author: DuongVT19

  • SQLite trong Flutter

    [FLUTTER] Tìm hiểu về cách sử dụng SQLite trong Flutter

    Để lưu những dữ liệu theo dạng bảng dễ dàng cho việc truy vấn và thay đổi, chúng ta cần sử dụng SQLite

    I. Khái quát về Sqlite trong Flutter

    Trong Flutter chúng ta sử dụng thư viện sqflite, thư viện này sẽ hỗ trợ chúng ta:

    • Sử dụng trên đa nền tảng Android, iOS, MacOS.
    • Hỗ trợ transactions và batches.
    • Tự động quản lý phiên bản (version) trong khi mở.
    • Hỗ trợ các câu lệnh truy vấn CRUD (Create – Read – Update – Delete) đầy đủ.
    • 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 sqflite 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

    class Student{
    ...
    
    Map<String, dynamic> toMap() => {'id': id, 'name': name, 'grade': grade};
    
    factory Student.fromMap(Map<String, dynamic> map) {
      return Student(id: map['id'], name: map['name'], grade: map['grade']);
    }
    ...
    }

    Ở đây mình có viết thành một project CRUD, với các phân tần giữa data và UI rõ ràng, để có thể hiểu hơn về project này hãy đọc đến cuối bài viết.

    2.3. Thêm một trường dữ liệu vào bảng

    Future<DataResult> insertStudent(Student student, String tableName) async {
        try{
          Database db = await StudentDatabase.getInstance();
          int lastInsertedRow = await db.insert(tableName, student.toMap(),
              conflictAlgorithm: ConflictAlgorithm.replace);
          return DataResult.success(lastInsertedRow);
        }catch(ex){
          return DataResult.failure(DatabaseFailure(ex.toString()));
        }
      }
    • 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

    2.5. Thay đổi thông tin của một trường.

    Future<DataResult> updateStudent(Student student) async{
        try{
          Database db = await StudentDatabase.getInstance();
          int numberOfChanges = await db.update("student", student.toMap(),where: "id = ?",whereArgs: [student.id]);
          return DataResult.success(numberOfChanges);
        }catch(ex){
          return DataResult.failure(DatabaseFailure(ex.toString()));
        }
      }

    Để thực hiện được việc thay đổi dữ liệu của một trường chúng ta cần sử dụng hàm update()

    • Các tham số truyền vào gồm có tên bảng, dữ liệu thay đổi (được chuyển sang map), điều kiện thay đổi và tham số cho điều kiện đó.
    • Có thể thấy phần thay đổi dữ liệu này khá giống với phần thêm dữ liệu, chỉ khác chúng ta có thêm hai tham số khá quan trọng là “where” và “whereArgs”.
    • Giá trị trả về của hàm update() là số lượng thay đổi đã diễn ra.

    2.6. Xóa thông tin của một trường

    Future<DataResult> deleteStudent(int id) async{
        try{
          Database db = await StudentDatabase.getInstance();
          int numberOfRowEffected = await db.delete("student",where: "id = ?",whereArgs: [id]);
          return DataResult.success(numberOfRowEffected);
        }catch(ex){
          return DataResult.failure(DatabaseFailure(ex.toString()));
        }
      }

    Để thực hiện việc xóa dữ liệu chúng ta sẽ cần dùng đến hàm delete()

    • Gần giống với hàm update chúng ta cũng cần có hai tham số “where” và “whereArgs”
    • Giá trị trả về sẽ là hàng bị thay đổi giá trị.

    III. Tổng kết

    Phần Sql này khá là khó và khô khan đối với những người mới học, mới tiếp cận, vì vậy mọi người cần luyện tập thêm để trở nên master phần này nhé.

    Link github của project mình tự làm cho mọi người tham khảo: lamdev99/sqflite_crud (github.com)

    Tài liệu tham khảo:

    Author: LamNT59

  • Dependency Injection

    Dependency Injection

    Dependency Injection là một mẫu thiết kế được sử dụng để triển khai Inversion of Control. Nó cho phép tạo các đối tượng phụ thuộc bên ngoài một lớp và cung cấp các đối tượng đó cho một lớp thông qua các cách khác nhau. Sử dụng DI, chúng ta di chuyển việc tạo và ràng buộc các đối tượng phụ thuộc ra bên ngoài lớp phụ thuộc vào chúng. Điều này mang lại mức độ linh hoạt cao hơn, phân tách và kiểm tra dễ dàng hơn.

    Khi mà class A sử dụng một số chức năng của class class B, thì có thể nói là class A có quan hệ phụ thuộc với class B.

    Trong java, trước khi ta có thể sử dụng method của class khác, ta phải khởi tạo một đối tượng của class đấy.

    Tại sao cần sử dụng Dependency Injection?

    Ví dụ chúng ta có một class Car, trong đó có chứa đối tượng khác như Wheel.

    Ở đây, class Car chịu trách nhiệm khởi tạo tất cả các dependency object. Nhưng chuyện gì sẽ xảy ra nếu chúng ta muốn bỏ Wheel và thay thế bằng SteelWheel hoặc PlasticWheel.

    Để giải quyết vấn đề trên thì chúng ta phải tạo một class Car mới với SteelWheel hoặc PlasticWheel. Tuy nhiên khi sử dụng dependency injection, chúng ta có thể đổi Wheel trong thời gian chương trình chạy (Runtime) vì dependency có thể được đẩy vào ở Runtime thay vì Compiletime.

    Có 3 loại Dependency Injection

    1. Constructor injection: Các dependency được cung cấp thông qua constructor của class.
    2. Setter Injection: Khách hàng tạo ra một setter method để các class khác có thể sử dụng chúng để cấp dependency.
    3. Interface injection: Dependency sẽ cung cấp một hàm injector để inject nó vào bất kỳ khách hàng nào được truyền vào. Các khách hàng phải implement một interface mà có một setter method dành cho việc nhận dependency.

    Trách nhiệm của dependency injection là:

    1. Tạo ra các object.
    2. Biết class nào cần object đấy.
    3. Cung cấp cho những class đó object chúng cần.

    Bằng cách này, nếu trong tương lai object đó có sự thay đổi thì dependency injection có nhiệm vụ cấp lại những object cần thiết cho class.

    Điểm mạnh

    1. Giúp unit test dễ hơn.
    2. Giảm thiểu được code mẫu (boilerplate code) vì việc khởi tạo dependency được làm bởi một component khác.
    3. Mở dụng dự án dễ dàng hơn.
    4. Giúp ích trong việc liên kết lỏng giữa các thành phần trong dự án

    Điểm yếu

    1. Khá phức tạp để học.
    2. Rất nhiều các lỗi ở compile time có thể đẩy sang runtime.
    3. Có thể làm ảnh hường tới chức năng auto-complete hay Find references của một số IDE.

    Nguồn tham khảo:

    1. https://viblo.asia/p/dependency-injection-la-gi-va-khi-nao-thi-nen-su-dung-no-LzD5d0d05jY
  • Extension trong Dart và Flutter

    Với những bạn nào đã từng sử dụng Kotlin khi lập trình Android và có tìm hiểu sâu một chút, chắc hẳn các bạn đã biết và cũng sẽ rất ấn tượng với extension của Kotlin. Mình cũng đã từng sử dụng và thấy nó giúp ích rất nhiều trong quá trình code bởi chúng ta có thể viết thêm các phương thức cho các class mà không phải sửa trực tiếp vào class. Việc này rất hữu dụng trong trường hợp bạn muốn viết thêm vào một class nào đó nhưng class đó lại nằm trong một thư viện nào đó hoặc thậm chí là class trong core của Kotlin như String, List,… Một ví dụ (viết bằng dart để cho mọi người dễ nắm bắt):

    Theo cách truyền thống :

    Giả sử bài toán cho một list số nguyên mà tìm trung bình cộng :

    Áp dụng extension :

    Cú pháp ở đây là: extension <TênCủaExtension> on <ClassMuốnExtend> {}.

    Trong đó tên của extension là do bạn chọn, miễn là không trùng với các class đã có sẵn để tránh nhầm lẫn khi sử dụng.

    Nhiều bạn sẽ thắc mắc, this ở đây là gì, tại sao lại có this này nhỉ? Thì this ở đây chính là instance của class String mà chúng ta dùng function này (ở đây là [1,2,3,4,5]). Khi viết function trong extension, chúng ta có thể access trực tiếp vào các member variable của class mà chúng ta đang extend, ví dụ như String thì chúng ta sẽ có thể dùng this.length, this.substring(), this.split() (có thể bỏ chữ this nếu như không bị trùng tên với params truyền vào trong function) , list thì có thể list.leght , list. , list.isEmpty

    Cá nhân mình thì hiểu extention dùng để custom các phương thức mà mình muốn khi không có ở mặc định giúp code gọn gàng .

    Một số extension mà mình áp dụng extention

    Xử lý onclick liên tục

    Mình đã giới thiệu qua về extension trong Dart và Flutter. Đây là một tính năng rất hay mà mình muốn mọi người nên biết và sử dụng, hi vọng các bạn thấy hữu ích và áp dụng tính năng này một cách hiệu quả

  • Cách viết Unit Test trong Flutter

    [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);
          });
        },
      );
    }

    Và khi chạy lên chúng ta sẽ được kết quả như sau:

    3. Tài liệu tham khảo

    Chúc các bạn luôn nhìn thấy một màu xanh lá khi chạy Unit Test

    Tác giả: LamNT59

  • KIẾN THỨC CƠ BẢN VỀ DART (PHẦN 3)

    KIẾN THỨC CƠ BẢN VỀ DART (PHẦN 3)

    Sang phần 3 này, chúng ta sẽ tìm hiểu về Control Flow.

    Control flow cho phép bạn ra lệnh khi nào thực thi, bỏ qua hoặc lặp lại một số dòng mã nhất định. Bạn sử dụng các điều kiện (conditionals ) và vòng lặp (loops ) để xử lý luồng điều khiển trong Dart.

    Trong phần này, bạn sẽ tìm hiểu thêm về:

    • Conditionals
    • While Loops
    • Continue and Break
    • For Loops

    Đây là những gì bạn cần biết về các phần tử luồng điều khiển trong Dart.

    Conditionals

    Hình thức cơ bản nhất của luồng điều khiển là quyết định thực thi hay bỏ qua các phần nhất định của mã của bạn, tùy thuộc vào các điều kiện xảy ra khi chương trình của bạn chạy.

    Cấu trúc ngôn ngữ để xử lý các điều kiện là câu lệnh ifelseifelse trong Dart trông gần giống với việc sử dụng nó trong các ngôn ngữ giống C khác.

    Câu lệnh If

    Giả sử bạn có một biến animal đó hiện là một con cáo. Nó trông như thế này:

    var animal = 'fox';
    

    Bạn có thể sử dụng một if câu lệnh để kiểm tra xem đó animal là mèo hay chó, sau đó chạy một số mã tương ứng.

    if (animal == 'cat' || animal == 'dog') {
      print('Animal is a house pet.');
    }

    Ở đây, bạn đã sử dụng các toán tử equality OR để tạo bool điều kiện bên trong cho if câu lệnh.

    Câu lệnh else

    Với một else mệnh đề, bạn có thể chạy mã thay thế nếu điều kiện sai:

    else {
      print('Animal is NOT a house pet.');
    }
    // Animal is NOT a house pet.

    Bạn cũng có thể kết hợp nhiều câu lệnh ifelse thành một hàm ifelse ifelse:

    if (animal == 'cat' || animal == 'dog') {
      print('Animal is a house pet.');
    } else if (animal == 'rhino') {
      print('That\'s a big animal.');
    } else {
      print('Animal is NOT a house pet.');
    }
    // Animal is NOT a house pet.

    Bạn có thể có nhiều else if nhánh giữa if và else tùy theo nhu cầu.

    While Loops

    Vòng lặp cho phép bạn lặp lại mã một số lần nhất định hoặc dựa trên các điều kiện nhất định. Bạn xử lý sự lặp lại dựa trên điều kiện bằng cách sử dụng vòng lặp while .

    Có hai dạng vòng lặp while trong Dart: while và do-while. Sự khác biệt là với while, điều kiện lặp xảy ra trước khối mã. Trong khi do-while, điều kiện xảy ra sau. Điều đó có nghĩa là do-while các vòng lặp đảm bảo khối mã chạy ít nhất một lần.

    Ví dụ While Loop

    Để thử điều này, hãy tạo một biến được i khởi tạo thành 1:

    var i = 1 ;
    

    Tiếp theo, sử dụng một while vòng lặp để in i trong khi tăng dần. Đặt điều kiện thành i nhỏ hơn 10:

    while (i < 10) {
      print(i);
      i++;
    }
    // 1
    // 2
    // 3
    // 4
    // 5
    // 6
    // 7
    // 8
    // 9

    Chạy mã và bạn sẽ thấy rằng while vòng lặp in các số từ 1 đến 9.

    Ví dụ Do-While

    Đặt lại i trong DartPad, sau đó thêm vòng lặp do-while:

    i = 1;
    do {
      print(i);
      i++;
    } while (i < 10);
    // 1
    // 2
    // 3
    // 4
    // 5
    // 6
    // 7
    // 8
    // 9
    

    Kết quả vẫn giống như trước đây. Tuy nhiên, lần này, phần thân của vòng lặp đã chạy một lần trước khi kiểm tra điều kiện thoát khỏi vòng lặp.

    Continue and Break

    Dart sử dụng continue và break từ khóa trong các vòng lặp và các nơi khác. Đây là những gì họ làm:

    • continue : Bỏ qua mã còn lại bên trong một vòng lặp và ngay lập tức chuyển sang lần lặp tiếp theo.
    • break : Dừng vòng lặp và tiếp tục thực hiện sau phần thân của vòng lặp.

    Hãy cẩn thận khi sử dụng continue trong mã của bạn. Ví dụ: nếu bạn thực hiện do-while vòng lặp từ trên và bạn muốn tiếp tục khi i bằng 5, điều đó có thể dẫn đến một vòng lặp vô hạn tùy thuộc vào vị trí bạn đặt câu lệnh continue :

    i = 1;
    do {
      print(i);
      if (i == 5) {
        continue;
      }            
      ++i;
    } while (i < 10);
    // 1
    // 2
    // 3
    // 4
    // 5
    // 5
    // 5
    // 5
    // 5
    // 5
    // 5
    // 5
    // 5
    // 5
    // ...

    Vòng lặp vô hạn xảy ra bởi vì, một khi i là 5, bạn không bao giờ tăng nó nữa, vì vậy điều kiện luôn đúng.

    Nếu bạn chạy điều này trong DartPad, vòng lặp vô hạn sẽ khiến trình duyệt bị treo. Thay vào đó, hãy sử dụng break, để vòng lặp kết thúc sau khi i đạt đến 5:

    i = 1;
    do {
      print(i);
      if (i == 5) {
        break;
      }
      ++i;
    } while (i < 10);
    // 1
    // 2
    // 3
    // 4
    // 5

    Chạy mã. Bây giờ, vòng lặp kết thúc sau năm lần lặp.

    For Loops

    Trong Dart, bạn sử dụng các vòng lặp for để lặp lại một số lần được xác định trước. Vòng lặp for bao gồm khởi tạo, một điều kiện vòng lặp và một hành động. Chúng tương tự như for các vòng lặp trong các ngôn ngữ khác.

    Dart cũng cung cấp một for-in vòng lặp, lặp lại trên một tập hợp các đối tượng. Bạn sẽ tìm hiểu thêm về những điều này sau.

    Để xem cách for hoạt động của một vòng lặp, hãy tạo một biến cho sum:

    var sum = 0 ;
    

    Tiếp theo, sử dụng một vòng lặp for để khởi tạo bộ đếm vòng lặp từ i đến 1. Sau đó, bạn sẽ kiểm tra xem i nhỏ hơn hoặc bằng 10 và tăng dần i sau mỗi vòng lặp.

    Bên trong vòng lặp, sử dụng phép gán ghép để thêm i vào tổng đang chạy:

    for (var i = 1; i <= 10; i++) {
      sum += i;  
    }
    print("The sum is $sum"); // The sum is 55
    
    

    Chạy mã của bạn trong DartPad để xem tổng.

    Kết thúc phần 3!

  • [FLUTTER] Một số widget hữu ích trong Flutter (Phần 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 😀

    1. FittedBox

    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.

    FittedBox(
        fit: BoxFit.fitWidth,
        child: Text("Fitted Box"),
    )
    Text Responsive với FittedBox

    2. Wrap

    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é.

    3.1. Flexible

    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:

    SizedBox(
              height: 100,
              child: Row(
                children: [
                  Flexible(
                      flex: 1, child: Container(color: Colors.red.shade100)),
                  Flexible(
                      flex: 2,
                      child: Container(
                        color: Colors.red.shade200,
                      )),
                  Flexible(
                      flex: 3,
                      child: Container(
                        color: Colors.red.shade300,
                      )),
                  Flexible(
                      flex: 4,
                      child: Container(
                        color: Colors.red.shade400,
                      )),
                ],
              ),
            ),

    Kích thước của children sẽ được tính 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

    3.2. Expanded

    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 😀 😀

    3.3. Spacer

    Đúng như cái tên của nó, widget này đơn giản là chỉ tạo ra khoảng trắng, được sử dụng giống 2 widget trên, cũng có hệ số flex

    Lưu ý:

    • Các widget này phải được sử dụng trực tiếp dưới widget cha là các widget flex như Flex, Row, Column.
    • Mặc định hệ số flex sẽ luôn là 1

    (Còn tiếp…)

    Tham khảo

    widgets library – Dart API (flutter.dev)

    My 10 Most Favorite Flutter Widgets – YouTube

  • Flutter vs React Native vs Native: So sánh chi tiết hiệu năng

    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.

    Repo link:

    Source code

    Use case 1 — List view benchmarking

    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.

    iOS

    • Tải và lưu hình ảnh vào bộ nhớ đệm – Nuke

    Android

    • Tải và lưu hình ảnh vào bộ nhớ đệm – Glide

    React Native

    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
    1. Tất cả các thử nghiệm đều cho thấy FPS xấp xỉ như nhau.
    2. Android Native sử dụng một nửa memory so với Flutter và React Native.
    3. 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.
    4. 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
    1. 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.
    2. 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.
    3. 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.

    Thử nghiệm animation với Lottie cho Android, iOS, React Native và Flare cho Flutter.

    Lottie cho Android

    Android

    Kết quả kiểm tra
    1. 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).
    2. 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).
    3. Android yêu cầu ít bộ nhớ nhất (205 Mb); React Native cần 280 Mb và Flutter cần 266 Mb.
    4. 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.

    IOS

    Kết quả kiểm tra
    1. Kết quả của iOS và React Native trong bài kiểm tra này gần giống như Lottie đối với React Native.
    2. 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 😀
    3. 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.
    4. 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
    1. Native cho thấy hiệu năng cao nhất và tiêu thụ bộ nhớ hiệu quả nhất.
    2. 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.
    3. 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
    1. iPhone 6s đủ mạnh để không giảm fps trong cả 3 trường hợp.
    2. Native sử dụng ít tài nguyên hơn và GPU được sử dụng gần hết.
    3. React Native chủ yếu sử dụng CPU để hiển thị trong khi Flutter sử dụng GPU.
    4. 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.

    Bài viết này được dịch từ đây.