Tự học Flutter 2023

New Flutter cơ bản

Tìm hiểu về Widget trong Flutter

Trong bài này chúng ta sẽ tìm hiểu về Widget trong Flutter, hiểu được nó là gì, sử dụng như thế nào, phân biệt được stateless và stateful widget.
  • Widget là gì?
  • Phân loại widget
  • Một số loại widget cơ bản
  • Text
  • Container
  • Button
  • Column
  • Row
  • Stack
  • SingleChildScrollView
  • Cách sử dụng widget
  • Danh sách tất cả các loại widget
  • Stateless và Stateful widget
  • State là cái gì?
  • Stateless Widget
  • Stateful Widget
  • Lựa chọn Stateful hay Stateless widget?
  • MaterialApp, CupertinoApp, WidgetsApp
  • WidgetsApp là gì
  • MaterialApp là gì
  • CupertinoApp là gì
  • CupertinoApp hay MaterialApp
  • Tiếp theo là gì?
  • Widget là gì

    Theo định nghĩa của chính chủ thì Widget là một mô tả bất biến của một phần giao diện người dùng. Với định nghĩa như thế này, mình nghĩ là những ai chưa từng viết dòng code Flutter nào sẽ khó mà hiểu được. Vì thế mình sẽ đưa thêm một vài định nghĩa "kiểu khác" cho các bạn có nhiều góc nhìn hơn.

    Mỗi phần tử trên màn hình của ứng dụng Flutter là một widget. Nếu bạn mở một ứng dụng được viết bằng Flutter lên, những gì hiển thị trên màn hình đều là widget: các dòng chữ, nút bấm, hộp thoại...

    Widget là một cách để khai báo và xây dựng giao diện người dùng. Nếu bạn đã quen với việc phát triển Android hoặc iOS thì bạn có thể liên tưởng ngay lập tới View (trên Android) hoặc UIViews (trên iOS).

    Bạn cũng có thể xem Widget là một bản thiết kế (blueprint), Flutter sử dụng bản thiết kế này để tạo ra các phần tử trên màn hình.

    ...

    Trên đây là một số cách định nghĩa khác nhau, hi vọng đã cho bạn mường tượng ra được chút gì. Đến khi chúng ta thực hành nó sẽ rõ như ban ngày thôi. Trước mắt, mình sẽ lấy cho bạn một ví dụ về widget. Đoạn code bên dưới là một widget, tên nó là Text, dùng để hiển thị text lên màn hình.

    
    Text
      "Hello world",
      textDirection: TextDirection.ltr,
      style: TextStyle(color: Colors.green),
    )
                          

    Widget trên sẽ tạo ra đại loại như thế này trên màn hình điện thoại của bạn:

    
    Hello world
                          

    Một vài điểm lưu ý tại thời điểm này:

  • Trong Flutter tất cả mọi thứ (thực ra là đa số thôi) đều là Widget, từ từ rồi bạn sẽ thấm.
  • Widget có thể lồng nhau, tức là một widget (cha) có thể chứa các widget (con) khác. Chúng ta gọi một tập hợp widget như vậy là một widget tree. (Xem hình mô tả bên dưới, nếu chưa hiểu bạn cứ xem tiếp phần phân loại widget sau đó xem lại hình)
  • Phân loại widget

    Đã là phân loại thì phải có tiêu chí để phân loại chứ nhỉ. Thường thì đa số các bài viết đều phang ngay vào mặt bạn rằng Widget có 2 loại là Stateless WidgetStateful Widget. Điều này thì đúng đấy, nhưng mình thì mình chưa muốn nói về 2 cái đó tại thời điểm này.

    Mình muốn đưa các bạn trải nghiệm thêm một chút về widget chung chung cái đã. Để bạn cảm nhận được widget là cái gì, cách xài nó như thế nào. Vì vậy mình sẽ phân loại widget theo chức năng, chúng ta sẽ có thể có một số loại widget như sau:

  • Widget để hiển thị nội dung: text, image, button...
  • Widget để bố cục: column, row, stack
  • ...
  • Chúng ta sẽ tìm hiểu sơ qua một vài loại widget đã kể tên ở trên trước khi đi vào Stateless WidgetStateful Widget.

    Một số loại widget cơ bản

    Text

    Widget này mình đã lấy ví dụ ở trên. Khi bạn muốn hiển thị text, bạn sẽ sử dụng widget này.

    
    Text
      "Hello world", // text cần hiển thị
      textDirection: TextDirection.ltr, // hướng từ trái qua phải
      style: TextStyle(color: Colors.green), // tùy chỉnh màu sắc cho text
    )
                          

    Container

    Là widget dùng để làm vật chứa các widget khác, và nó thường được dùng để trang trí. Như ví dụ dưới, chúng ta có thể đặt một Text widget bên trong một container, và đặt màu nền cho container là màu xanh để tạo ra một dòng text nằm trong một hình chữ nhật màu xanh. Container chỉ có thể chứa một widget con. (Column và Row là các widget có thể chứa nhiều widget con.)

    
    Container(
      width: double.infinity, // rộng nhất có thể
      color: Colors.green, // màu nền
      child: Center( // Center là widget dùng để căn giữa các widget con của nó
          child: Text(
            "Hello", 
            style: TextStyle(color: Colors.white),
        ),
    )
                          

    Và đây là kết quả hiển thị

    Hello

    Button

    Như cái tên, widget này sẽ tạo ra nút bấm. Đoạn code dưới đây sẽ tạo ra trên màn hình một nút bấm với dòng chữ "Click me!".

    
    ElevatedButton(
      onPressed: () {}, // đoạn code này sẽ được thực hiện khi người dùng bấm nút
      child: Text("Click me!"), // nội dung hiển thị trên nút bấm 
    )
                          

    Và đây là kết quả tạo ra:

    Column

    Đây là loại widget để bố trí các widget hiển thị nội dung (text, button,...). Như cái tên của nó, nó sẽ bố trí các widget con của nó theo cột (widget này có thể chứa nhiều widget con).

    
    Column(
      children: [
        Text("Hàng 1", style: TextStyle(color: Colors.blue),),
        Text("Hàng 2", style: TextStyle(color: Colors.green),),
        Text("Hàng 3", style: TextStyle(color: Colors.orange),),
      ],
    ),
                          

    Và đây là kết quả tạo ra:

    
    Hàng 1
    Hàng 2
    Hàng 3
                            

    Row

    Tương tự như column, khác mỗi một điểm là row sẽ bố trí các widget con của nó theo hàng.

    
    Row(
      children: [
        Text("Cột 1", style: TextStyle(color: Colors.blue),),
        Text("Cột 2", style: TextStyle(color: Colors.green),),
        Text("Cột 3", style: TextStyle(color: Colors.orange),),
      ],
    ),
                          

    Và đây là kết quả tạo ra:

    
    Cột 1 Cột 2 Cột 3
                            

    Stack

    Với column và row chúng ta đã có thể bố trí widget theo cột, theo hàng; Stack cho phép chúng ta bố trí widget con chồng lên nhau. Hiện tại mình chỉ muốn giới thiệu qua vậy cho bạn biết, chúng ta sẽ tìm hiểu nó sau.

    SingleChildScrollView

    Thông thường nội dung trên màn hình sẽ dài hơn độ dài của màn hình, lúc này ta cần cho phép người kéo xuống để xem các phần nội dung bị ẩn bên dưới. Đây là widget để làm việc đó.

    Cách sử dụng Widget trong ứng dụng Flutter

    Bây giờ chúng ta đã biết được sơ sơ vài loại widget. Câu hỏi đặt ra là xài như thế nào trong một ứng dụng Flutter, những đoạn code trên sẽ phải viết ở đâu?

    Quay lại với ứng dụng mẫu của Flutter (ứng dụng mà Flutter tạo ra khi bạn tạo dự án mới), trong thư mục lib bạn sẽ thấy có một file tên là main.dart. Bên trong file đó bạn sẽ thấy hàm void main() như bên dưới:

    
    void main() {
      runApp(const MyApp());
    }
                          

    Đây là entry point của ứng dụng chúng ta, khi ứng dụng chạy lên nó sẽ chạy vào đây đầu tiên. Bây giờ trong hàm này bạn để ý có dòng runApp(...), hàm này nhận vào một widget và hiển thị lên màn hình, mặc định trong code tạo sẵn đó, widget nhận vào tên là MyApp. MyApp chẳng qua cũng là một custom widget, tức là một widget được tạo ra từ những widget có sẵn.

    Time to vọc vạch! Bây giờ bạn có thể thử thay đoạn code mẫu ở trên bằng đoạn code bên dưới sau đó chạy thử.

    
    void main() {
      runApp(Text(
        "Hello",
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.white),
      ));
    }
                          

    Woala, dòng text Hello sẽ xuất hiện trên màn hình. Điều này có nghĩa là bạn đã tạo ra được giao diện ứng dụng bằng widget, mặc dù chỉ đơn giản là một dòng text mà thôi. Từ đây bạn có thể tiếp tục tìm hiểu thêm các loại widget và sử dụng chúng để tạo ra giao diện phức tạp hơn.

    Danh sách tất cả widget

    Để xem danh sách tất cả các widget mà Flutter hỗ trợ, bạn có thể truy cập vào Widget Catalog. Tất cả các widget được hỗ trợ sẽ được liệt kê theo từng nhóm, đầy đủ mô tả, ví dụ sử dụng.

    Ngoài ra, nếu bạn muốn xem video, bạn có thể xem serie Flutter Widget of the Week.

    Mình khuyên bạn là nên thử vọc vạch đôi chút với Text, Column, Row, SingleChildScrollView để tạo ra một vài widget đơn giản trên màn hình. Sau đó hẵng chuyển sang phần tiếp theo.

    Stateless và Stateful widget

    Bây giờ khi bạn đã "thấm" một chút về widget, đây là lúc chúng ta quay lại với Stateless và Stateful widget. (đã nhắc đến ở đoạn phân loại widget)

    Nếu bạn đã vọc đủ, có thể bạn sẽ có một câu hỏi giống mình từng thắc mắc tại thời điểm này, đó là làm sao để thay đổi UI (những dòng text) đang hiển thị khi có tương tác (bấm nút chẳng hạn). Với những widget mà chúng ta vừa tìm hiểu ở trên, cơ bản là chúng ta sẽ không làm được bởi vì chúng là StatelessWidget.

    State là cái gì?

    Từ cái tên bạn có thể thấy rằng, 2 loại widget mà chúng ta nói đến đều có một điểm chung đó là State. Vì thế, trước khi nói về StatelessWidget mình muốn làm rõ khái niệm State là gì trước. Để làm điều đó, mình muốn nhắc lại từ tài liệu chính chủ của Flutter: Ý tưởng chính khi nói về Widget đó là người dùng sẽ xây dựng giao diện dựa vào các Widget. Các widget mô tả cách hiển thị của nó tùy thuộc vào cấu hình và trạng thái hiện tại của chúng. Khi trạng thái của widget thay đổi, widget sẽ cập nhật lại cách hiển thị. (Bản dịch không được sát nghĩa, nếu bạn đọc được tiếng Anh, xem trang này).

    Ở đây có một thứ chúng ta cần làm rõ trước khi nói tiếp. "Các widget mô tả cách hiển thị của nó tùy thuộc vào cấu hình và trạng thái hiện tại của chúng". Trạng thái là cái mà chúng ta đang muốn tìm hiểu, vậy CẤU HÌNH ở đây là cái gì? Quay lại với các ví dụ về các loại widget ở trên bạn sẽ thấy rằng widget có các thuộc tính như: textDirection, style (color, size...) những thứ đó chính là cấu hình. O, bây giờ chúng ta đi tiếp.

    Về cơ bản, widget sẽ có một thứ gọi là trạng thái (state), việc nó hiển thị như thế nào phụ thuộc vào state đó của nó. State của một widget đơn giản là bất cứ dữ liệu gì cần thiết dùng để xây dựng cách hiển thị của widget. Dữ liệu state này có thể thay đổi trong suốt vòng đời của ứng dụng.

    Lấy ví dụ, bạn có một Text widget để thông báo cho người dùng biết một chức năng nào đó của ứng dụng đang bật hay đang tắt (giả sử như là VPN chẳng hạn). Widget đó sẽ có một state, state này có thể đơn giản là một biến isActive. Nếu isActivetrue thì widget sẽ hiển thị dòng chữ "Đang được bật" ngược lại nó sẽ hiển thị dòng chữ "Đã tắt". Biến isActive có thể thay đổi liên tục trong quá trình sử dụng ứng dụng (người dùng bấm một nút nào đó để bật tắt kết nối VPN, ứng dụng thay đổi trạng thái của biến isActive), và mỗi lần thay đổi widget sẽ lại cập nhật lại.

    Stateless Widget

    Bây giờ thì mọi thứ có vẻ đã rõ ràng hơn nhiều đúng không? Stateless Widget là những widget mà không có cái state vừa nói ở trên. Và bởi vì không có state nên sẽ không có chuyện nó cập nhật lại dựa theo sự thay đổi từ state. Nói cách khác, một Stateless Widget khi bạn tạo ra nó thì nó sẽ mãi mãi như vậy không thay đổi gì nữa cả. Nếu muốn thay đổi bạn sẽ phải tạo widget mới.

    Một số widget mà chúng ta đã tìm hiểu trước đó là Stateless Widget. Ví dụ như Text Widget chúng ta đã tạo ở phần trước, nó là một Stateless Widget nên dòng chữ "Hello" sẽ không thể đổi thành dòng chữ khác được. Bạn không có cách nào để thay dòng "Hello" thành "Xin chào".

    Lưu ý rằng Stateless Widget không thể thay đổi chính nó, nhưng nếu widget cha của nó thay đổi thì nó sẽ được khởi tạo lại. Vậy làm thế nào mà widget cha của nó thay đổi được? Cha nó phải là Stateful widget :D

    Chúng ta có thể tự tạo ra một StatelessWidget bằng cách kế thừa lớp StatelessWidget. Một StatelessWidget chung chung sẽ như thế này:

    
    class WidgetName extends StatelessWidget {
      const WidgetName({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(); 
        // đây là nơi bạn tạo ra widget với bằng cách kết hợp các widget có sẵn
      }
    }
                          

    Hàm build là nơi mà bạn ra widget theo ý mình bằng việc kết hợp các widget có sẵn. Khi stateless widget được tạo ra, nó sẽ gọi hàm build để build widget tree và hiển thị lên màn hình. Tiến trình sẽ có chút phức tạp hơn, bạn sẽ tìm hiểu sâu hơn ở các bài viết sau này.

    Tới đây các bạn đã hiểu cơ bản về Stateless Widget. Chúng ta sẽ chuyển qua tìm hiểu về Stateful Widget. Thực ra chúng ta vẫn còn nhiều điều để nói thêm về Stateless Widget tỉ như: làm sao tạo ra Stateless Widget, vòng đời của một Stateless Widget... Hẹn các bạn ở một bài viết khác. (Bạn có thể xem mục các bài viết liên quan ở cuối bài.)

    Stateful Widget

    Ừm, Stateful Widget là widget mà có state ấy, thế thôi. Cái quan trọng bây giờ chúng ta đang tò mò là cái state đó, mặt mũi nó thế nào. Nhìn qua một chút nhé:

    
    class PlayerStatusWidget extends StatefulWidget {
      const PlayerStatusWidget({super.key});
    
      @override
      State<PlayerStatusWidget> createState() => _PlayerStatusWidgetState();
    }
    
    class _PlayerStatusWidgetState extends State<PlayerStatusWidget> {
      bool _isPlaying = false;
    
      @override
      Widget build(BuildContext context) {
        if (_isPlaying) {
          return Text("Đang chơi nhạc!");
        } else {
          return Text("Đã tạm dừng!");
        }
      }
    }
                          

    Ở trên là một StatefulWidget, tên nó là PlayerStatusWidget. Khác với StatelessWidget, nó extends lớp StatefullWidget và có một state đi kèm (_PlayerStatusWidgetState).

    State này có 1 biến là _isPlaying để lưu trữ trạng thái của trình chơi nhạc. Hàm build nay không còn nằm ở thân của widget nữa mà thuộc về state. Điều này hợp lý bởi vì mỗi khi state thay đổi (trong trường hợp này là biến _isPlaying) thì hàm build sẽ được gọi và Widget sẽ được cập nhật lại.

    Vậy làm sao để cập nhật lại biến _isPlaying? Câu trả lời đó là sử dụng hàm setState. Chúng ta sẽ tìm hiểu kỹ hơn về nó trong bài viết SetState trong Flutter.

    Cũng tương tự như với Stateless Widget, cũng còn nhiều vấn đề để nói về Statefull Widget như: vòng đời của widget, widget tree / element tree / render tree,... Mình sẽ đề cập tới trong những bài viết khác. Bạn có thể xem mục các bài viết liên quan ở cuối bài.

    Lựa chọn Stateful hay Stateless widget?

    Sau khi tìm hiểu sơ qua về 2 loại widget này, mình nghĩ là bạn cũng đã tự có câu trả lời.

    Stateless widget sẽ hữu ích hơn trong trường hợp mà phần giao diện bạn đang muốn tạo ra không phụ thuộc vào bất cứ dữ liệu gì, nói cách khác là tĩnh không thay đổi. Ngược lại, Stateful widget sẽ hữu ích cho trường hợp mà giao diện bạn muốn tạo ra thay đổi động.

    MaterialApp, CupertinoApp, WidgetsApp

    Trước khi kết thúc bài viết này, mình muốn nói qua một chút về MaterialApp, CupertinoApp và WidgetsApp bởi lẽ trong giai đoạn này khi các bạn vọc vạch những ứng dụng Flutter đầu tiên bạn sẽ gặp những thứ quỷ này rất nhiều và chắc hẳn sẽ phải thắc mắc chúng nó là cái gì sao sểnh ra là lại thấy mặt chúng nó.

    WidgetsApp là gì

    Câu đầu tiên phải nói rằng nó cũng chỉ là một widget. Nó là một lớp tiện ích bao bọc một số các widget và chức năng cơ bản thường sử dụng khi tạo ứng dụng (ví dụ: navigation, localization...). Chúng ta hầu như không bao giờ sử dụng nó mà sử dụng MaterialApp hoặc CupertinoApp thay thế. Nó chỉ là lớp base mà thôi.

    MaterialApp là gì

    MaterialApp cũng là một widget, nó được xây dựng dựa trên WidgetsApp, và cũng là một lớp tiện ích bao bọc/cung cấp một số widget/chức năng thường dùng khi tạo ra ứng dụng theo style Material. Ví dụ như AppBar widget, bạn không thể sử dụng widget này nếu bạn không bọc nó trong một MaterialApp widget.

    Style Material ở đây là đang nói đến Material Design, nôm na là một phong cách thiết kế của Google. Bạn có thể tìm hiểu thêm tại: https://m3.material.io/

    MaterialApp được sử dụng mặc định khi bạn tạo ra một ứng dụng Flutter mới. Cũng chẳng có gì thắc mắc nhỉ, vì Flutter là của Google.

    CupertinoApp là gì

    Tương tự, CupertinoApp cũng là một widget, nó được xây dựng dựa trên WidgetsApp, và cũng là một lớp tiện ích bao bọc/cung cấp một số widget/chức năng thường dùng khi tạo ra ứng dụng, nhưng bây giờ là theo style iOS của Apple.

    CupertinoApp hay MaterialApp

    Bạn có thể sử dụng bất kỳ cái nào bạn thích. Tuy nhiên người dùng của mỗi nền tảng thì thường sẽ quen với những kiểu design của nền tảng đó. Vì vậy, tốt hơn là bạn nên xài MaterialApp cho ứng dụng Android và CupertinoApp cho ứng dụng iOS.

    Tiếp theo là gì?

    Như vậy là chúng ta vừa đi qua chủ đề Widget trong Flutter. Mặc dù chưa hẳn là trọn vẹn (còn chưa nói tới setState - nên là bạn vẫn chưa biết cách xài Stateful widget) nhưng cũng đã cho bạn cái nhìn khái quát về Widget. Những kiến thức trong bài viết này chủ yếu ở mức độ cơ bản, để có thể hiểu sâu thêm về Widget + tự tin mà trả lời phỏng vấn bạn sẽ cần học thêm nhiều. Mình sẽ liệt kê danh sách các bài viết liên quan đến chủ đề Widget này tại đây để bạn có thể tham khảo thêm.

  • Hướng dẫn bằng video
  • Sẽ cập nhật sau