+174

Học Flutter từ cơ bản đến nâng cao. Phần 2: StatefulWidget vs StatelessWidget. Khi nào thì cần sử dụng cái nào?

Lời mở đầu

bài trước, chúng ta đã dừng lại ở một kết thúc mở. Từ đó mọc lên trong đầu ta biết bao nhiêu câu hỏi: State là gì?, StatefulWidget là gì?, StatelessWidget là gì?, hàm build trong StatelessWidget đó là gì. Okay, hôm nay ta sẽ đi tìm câu trả lời thỏa mãn sự tò mò đó.

1. StatelessWidget, State và hàm build

Chúng ta đã biết Widget chỉ đơn giản là những Dart class. Nếu chúng ta vào trong xem các class Text, Scaffold, ... chúng ta sẽ thấy chúng sẽ extends một trong hai class là StatelessWidgetStatefulWidget. Đúng vậy, Flutter chia Widget làm 2 nhóm chính: một nhóm sẽ extends class StatelessWidget và một nhóm sẽ extends class StatefulWidget và đặc điểm chung của cả 2 class này là chúng đều có một method có tên là build.

Bây giờ chúng ta thử tạo ra một custom StatelessWidget xem thế nào nhé.

Đầu tiên chúng ta sẽ tạo 1 class có tên là CounterWidget và extends StatelessWidget.

class CounterWidget extends StatelessWidget {    
}

Nó bắt chúng ta phải override hàm build. Well, ok thôi.

class CounterWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
     
  }
}

Bây giờ chúng ta sẽ tạo ra 2 property có tên là isLoading có kiểu boolcounter có kiểu int. Tất nhiên chúng ta cũng sẽ cần phải tạo constructor cho nó:

class CounterWidget extends StatelessWidget {
  final bool isLoading;
  final int counter;  
  
  const CounterWidget({
    required this.isLoading,
    required this.counter,
  });  
  
  
  Widget build(BuildContext context) {
    return isLoading ? CircularProgressIndicator() : Text('$counter');
  }
}

Well, tin được không chúng ta vừa tạo ra 1 Widget tương tự cách Flutter tạo ra widget Text trong thư viện material đấy. Bây giờ, chúng ta sẽ cùng tìm hiểu cách chúng hoạt động qua source code sau.

Full source code: https://dartpad.dev/?id=dc336f1ee4e9b00936b671ca126df5a0

Bằng cách chỉnh sửa lại giá trị của biến isLoadingcounter và run lại app để quan sát UI thay đổi, ta sẽ thấy kết quả sau.

image.png

Oh như vậy:

  • State chỉ là những data. Những data này được truyền vào hàm build.
  • Hàm build giống như một công thức/hàm số nhận State như một input để cho ra output là UI tương ứng hiển thị trên màn hình: UI = build(state)
  • Hàm build này được gọi khi chúng ta khởi tạo 1 object StatelessWidget

Như vậy, chúng ta đã code xong 1 custom StatelessWidget rồi. Bây giờ chúng ta sẽ thử code 1 custom StatefulWidget.

2. StatefulWidget

Tương tự như StatelessWidget, StatefulWidget cũng chỉ là 1 class mà extends class StatefulWidget. Chúng ta sẽ tạo 1 class có tên là MyHomePage extends StatefulWidget. Nó sẽ bắt chúng ta override lại hàm createState và hàm này trả về object State<MyHomePage>.

class MyHomePage extends StatefulWidget {
  
  State<MyHomePage> createState() {
    
  }
}

Vậy ta cần phải tạo 1 class kế thừa State<MyHomePage>:

class MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
      
  }
}

Chà, chúng ta đã thấy hàm build của StatefulWidget đây rồi. Như vậy để tạo ra 1 custom StatefulWidget ta cần phải tạo đến 2 class : một class extends class StatefulWidget và một class extends class State. Tại sao Flutter lại tách ra 2 class như vậy? Câu trả lời sẽ có trong các bài kế tiếp khi chúng ta tìm hiểu How Flutter work under the hood. Tạm thời chúng ta chỉ cần quan tâm đến flow code sẽ chạy thế này:

image.png

Khi chúng ta tạo 1 object MyHomePage bằng cách MyHomePage(), nó sẽ gọi hàm createState() tạo ra object MyHomePageState. Khi object MyHomePageState được khởi tạo, nó sẽ gọi hàm build. Như vậy, trường hợp này giống với StatelessWidget, hàm build sẽ được gọi khi ta khởi tạo 1 StatefulWidget.

Thử code tương tự phần StatelessWidget xem thế nào, chúng ta cũng sẽ tạo ra 2 property là isLoadingcounter cho MyHomePage và tạo cả constructor nữa.

class MyHomePage extends StatefulWidget {
  final bool isLoading;
  final int counter;  
  const MyHomePage({
    required this.isLoading,
    required this.counter,
  });  
  
  State<MyHomePage> createState() {
    return MyHomePageState();
  }
}

Tiếp theo, chúng ta sẽ hoàn thiện hàm build trong class MyHomePageState.

class MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CounterWidget(
          isLoading: widget.isLoading,
          counter: widget.counter,
        ),
      ),
    );
  }
}

Đoạn code trên xuất hiện một variable lạ là widget. Bất kỳ class nào extends class State, đều sẽ có 1 biến tên là widget. Biến này chính là instance của class MyHomePage. Nó giúp ta get được giá trị của 2 property isLoadingcounter của object MyHomePage như thế này: widget.isLoading, widget.counter

Now, chúng ta sẽ gọi hàm runApp và run thử app thôi nào.

import 'package:flutter/material.dart';
void main() {
  runApp(
    MaterialApp(
      home: MyHomePage(isLoading: false, counter: 1),
    ),
  );
}

Full source code: https://dartpad.dev/?id=07a510bfe7622166c057cccb8564937a

Các bạn hãy thử thay đổi giá trị của isLoadingcounter và re-run app để xem UI thay đổi thế nào nhé.

Chúng ta vừa demo về StatelessWidgetStatefulWidget xong. Có thể chúng ta sẽ cảm thấy chúng ko khác nhau nhiều lắm. Cả 2 đều gọi hàm build ngay khi khởi tạo một đối tượng mới. Nếu chỉ như vậy thì chúng ta không việc gì phải sử dụng StatefulWidget để phải tạo đến 2 class?. Thật ra, StatefulWidget còn có 1 cách khác để có thể gọi lại hàm build mà ko cần phải tạo ra đối tượng mới. Đó là sử dụng hàm setState.

3. setState method

Đây là method của class State. Hàm này cần được truyền vào 1 VoidCallback. Khi chúng ta gọi hàm này thì hàm build sẽ được gọi lại ngay lập tức.

void setState(
   VoidCallback fn
)

Để code ví dụ về cách hoạt động của hàm setState. Ta sẽ đặt vào hàm build của Widget MyHomePage một lệnh print và tạo thêm 1 FloatingActionButton trong Widget MyHomePage. Khi click button, ta sẽ kiểm tra xem hàm build có được gọi hay không bằng cách xem log trong console.

Full source code: https://dartpad.dev/?id=ebfa471ca136c6fe5c72a3d8803812cd

1_P6XxK7xts3uGh5XM1jY4NA.gif

Tuyệt vời, hàm build đã được gọi lại. Tuy nhiên, UI chẳng hề thay đổi. Vì sao nhỉ. Nếu chúng ta xem kĩ lại đoạn code trên, chúng ta sẽ thấy giá trị của biến isLoadingcounter được truyền vào MyHomePage không hề thay đổi. Thử nhớ lại xem, các giá trị đó từ đầu bài được chúng ta gọi là State. Và chúng ta có công thức UI = build(state). Như vậy, theo công thức này nếu state không thay đổi thì UI tất nhiên cũng sẽ không thay đổi.

Vậy là rõ rồi, ta muốn UI thay đổi, ta phải đổi giá trị của State. Vậy nên chúng ta phải khai báo các biến dưới dạng mutable variable trong class State và truyền vào hàm build, ko thể sử dụng biến final được nữa:

class MyHomePageState extends State<MyHomePage> {
  late bool _isLoading;
  late int _counter;
  

  void initState() {
    super.initState();
    _isLoading = widget.isLoading;
    _counter = widget.counter;
  }
  
...
  child: CounterWidget(
   isLoading: _isLoading,
   counter: _counter,
  ),
...

Oh, đoạn code trên lại xuất hiện 1 hàm lạ nữa là hàm initState. Đây cũng chính là một điểm khác nhau giữa StatelessWidget và StatefulWidget. Hàm này được gọi ngay sau khi khởi tạo object MyHomePage nhưng nó sẽ được gọi trước hàm build nên nó thường được dùng để khởi tạo các giá trị của initial state của UI. Cụ thể ở đây, chúng ta sử dụng hàm initState để nhận giá trị được truyền vào MyHomePage và gán làm giá trị ban đầu cho biến _isLoading_counter.

image.png

Tiếp theo, chúng ta cần code lại hàm onFloatingButtonClicked để khi click vào chúng ta sẽ update State và rebuild UI mới.

void onFloatingButtonClicked() {
    setState(() {
      _counter++;
      if (_counter % 2 == 0) {
        _isLoading = false;
      } else {
        _isLoading = true;
      }
    });
 }

Cụ thể, mỗi lần click button, chúng ta sẽ update biến counter tăng thêm 1 và nếu counter là số lẻ thì chúng ta sẽ cho show ProgressIndicator.

Full source code: https://dartpad.dev/?id=157b19bb180a0d7ea4d738946c4e9bf1

4. StatelessWidget vs StatefulWidget

Từ những thí nghiệm nhỏ trên, chúng ta dễ dàng rút ra bảng so sánh sau:

Giống nhau:

  • Đều có hàm build
  • Đều hoạt động theo công thức UI = build(state)
  • Đều gọi hàm build sau khi khởi tạo

Khác nhau:

StatelessWidget StatefulWidget
ko có class State có thêm class State
ko có hàm initState có hàm initState (và nhiều hàm khác nữa)
ko có hàm setState có hàm setState

Kì lạ nhỉ, trong example trên, tôi thấy CounterWidget là một StatelessWidget nhưng có thay đổi UI kìa, sao lại nói là never change nhỉ. Trong VD trên, sở dĩ UI có thay đổi là do khi hàm build của MyHomePageState được gọi lại, nó đã tạo ra một object CounterWidget mới. Vâng là do tạo ra object mới replace cái cũ chứ bản thân object cũ ko thể thay đổi. Chính vì object mới có những giá trị isLoadingcounter khác object cũ nên ta sẽ thấy UI có thay đổi khi MyHomePageState gọi lại hàm build.

Chính điểm khác biệt cuối cùng, một bên không thể gọi lại hàm build, một bên có thể gọi lại hàm build sẽ giúp chúng ta dễ dàng ra quyết định khi nào thì nên sử dụng cái nào?

5. Khi nào thì nên sử dụng cái nào?

Từ đầu bài đến giờ, có ai thắc mắc vì sao chúng ta phải tạo ra class custom Widget không nhỉ. Câu trả lời đơn giản là để reuse code. Giả sử, chúng ta có nhiều màn hình có 1 mảnh UI giống nhau như CounterWidget thì chúng ta nên tạo ra class CounterWidget và nhúng vào nhiều màn hình đó để tránh duplicate code, tăng khả năng tái sử dụng code.

Nhưng mà cái khó là chúng ta phải quyết định class custom Widget đó nên extends StatelessWidget hay là StatefulWidget. Dễ thôi, Dù cho Widget đó kích thước lớn hay bé, miễn là nó thể thay đổi UI khi user tương tác với nó (tap/press/vuốt) thì nó là StatefulWidget.

Đúng vậy, chính vì chỉ StatefulWidget mới có khả năng tự rebuild Widget nên khi phân tích một Widget. Chúng ta hãy tự đặt câu hỏi: nó có khả năng tự thay đổi UI khi user tương tác (VD click vào) với nó hay không.

Trong example trên, MyHomePage là một widget chứa cả FloatingActionButton và nó có khả năng thay đổi UI khi click vào FloatingActionButton nên nó nên là StatefulWidget.

Bản thân CheckBox như thế này cũng là một StatefulWidget vì nó có thể tự thay đổi UI khi click vào.

1_X0sk5dTVghqvyzyDvSCFAQ.gif

Thử thực hành phân tích UI bên dưới luôn nhé.

image.png

Button favorite sẽ thay đổi trạng thái khi được click vào:

image.png

Trong UI trên, chỉ có button favorite (ngôi sao) là có thể thay đổi state khi user click vào nó, còn tất cả Widget còn lại như Text, Image, các IconButton Call, Route, Share và thậm chí cả Scaffold chúng ta đều có thể sử dụng StatelessWidget. Đừng hiểu nhầm, cứ Widget gì có khả năng tương tác được như button thì sẽ là StatefulWidget nhé. Bởi vì các IconButton này, tuy có thể tương tác được nhưng khi tương tác thì chúng không cần thay đổi UI nên ta có thể cho chúng là StatelessWidget. Và nên nhớ rằng, cái gì sử dụng StatelessWidget được thì nên sử dụng StatelessWidget để tối ưu performance hơn. Chúng ta sẽ cố gắng sử dụng StatelessWidget nhiều nhất có thể - các đồng nghiệp sẽ yêu mến bạn.

Kết thúc mở

Chúng ta đã đi tìm hiểu về StatelessWidget, StatefulWidget và hàm build. Trong mỗi hàm build đều nhận vào một biến có kiểu BuildContext. Vậy BuildContext là gì, tại sao chúng ta lại cần nó. Câu trả lời sẽ có trong phần tiếp theo 😄

Lại cứ phải note nhẹ: Click follow để nhận thông báo khi có bài viết mới nhé =))

Đọc tiếp phần 3: Lột trần cô nàng Flutter, BuildContext là gì

Đọc tiếp phần 4: Phần 4: Lột trần InheritedWidget


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí