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à StatelessWidget
và StatefulWidget
. Đú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 bool
và counter
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 isLoading
và counter
và run lại app để quan sát UI thay đổi, ta sẽ thấy kết quả sau.
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:
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à isLoading
và counter
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 isLoading
và counter
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 isLoading
và counter
và re-run app để xem UI thay đổi thế nào nhé.
Chúng ta vừa demo về StatelessWidget
và StatefulWidget
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
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 isLoading
và counter
đượ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
và _counter
.
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ị isLoading
và counter
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.
Thử thực hành phân tích UI bên dưới luôn nhé.
Button favorite sẽ thay đổi trạng thái khi được click vào:
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