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ò đó. Bắt đầu với hàm build
nhé
1. Data của Widget
Ở bài trước, chúng ta đã hình dung được Widget
là gì, Widget là những cái Text
, những cái Button
. Bản thân mỗi Widget, nó sẽ mang trong mình những thông tin để có thể hiển thị trên UI. Và trong lập trình thì thông tin sẽ được hình thành từ data mà data thì được lưu trữ trong các biến (variable) hoặc hằng (constant). Ví dụ widget Text
thì có những thông tin nào nhỉ, tất nhiên là một biến text
kiểu String
để lưu trữ cái text rồi, bên cạnh đó chắc chắn cũng sẽ có những biến kiểu như textColor
lưu trữ thông tin về màu sắc của text, fontSize
thể hiện cho kích thước font chữ, bla, bla.
Chúng ta cũng được biết trong bài trước, Widget
đơn giản là những class
. Ví dụ: class Text
. À ra là như vậy, nếu liên kết những yếu tố này lại ta có thể hình dung một class Widget có những property lưu trữ và thể hiện những thông tin của Widget như thế này:
class Text { // Widget chỉ là class String text; // các property lưu trữ thông tin của widget Color textColor; // thông tin về màu sắc của text int fontSize; // thông tin về kích cỡ của font
}
Well, thông tin là những gì user nhìn thấy và đọc được trên màn hình. Dưới góc độ lập trình viên thì ta hay sử dụng từ Data
hơn. Vì vậy, từ bây giờ trở đi mình sẽ dùng từ Data
của Widget. Các bạn sẽ hiểu là từ những Data
này tạo nên thông tin của Widget nhé.
2. Hàm build
Bạn có biết công thức toán học quen thuộc y = f(x)
. Đó là một hàm số, khi ta có giá trị của x
, dựa vào một function f
ta sẽ tính được giá trị y
. Bất cứ khi nào x
thay đổi thì cũng cho ta một giá trị mới của y
đúng ko. Flutter cũng tương tự như vậy, nó sử dụng công thức là:
UI = f(Data)
Khi Data của Widget thay đổi, UI sẽ được update theo công thức hàm f
.
Các bạn thấy khi mình click vào button màu xanh đó thì UI được thay đổi hiển thị con số khác đúng ko. Con số 0, 1, 2, 3, ... đó chính là Data
của Widget. Data
đó được lưu trữ trong một biến có kiểu int
. Khi Data
thay đổi nó sẽ thay đổi UI theo công thức hàm f
.
Công thức hàm f
ở đây chính là hàm build
trong StatelessWidget
trong đoạn kết của bài 1
class CounterTextWidget extends StatelessWidget { int counter; // Data của widget được lưu trữ trong property có kiểu int Widget build(BuildContext context) { // function f chính là hàm build này // hàm build là một công thức, nhận Data từ biến counter // mỗi lần biến counter thay đổi từ 0 lên 1, 2, 3, 4,... thì ta sẽ hiển thị 1 UI khác return Text('Tui là widget Text. Data của tui hiện tại là $counter'); }
}
Như vậy, chúng ta đã hình dung được Data
của Widget và hàm build
và mối quan hệ UI = build(Data)
. Đây chính là nguyên tắc hoạt động của Flutter. Chúng ta sẽ đi đến định nghĩa kế tiếp về State
.
3. State là gì
Giả sử như bây giờ, bạn tự tạo ra một cái Widget là cột đèn giao thông đi. Cột đèn giao thông sẽ có những thông tin nào nhỉ:
- Chiều cao của cái cột được lưu trữ trong một biến có kiểu
int
. Thông tin này không bao giờ thay đổi. Ví dụ cái cột đèn giao thông ban đầu cao 3m, thì 10 năm sau cũng sẽ cao 3m chứ nó không thể tự cao lên theo năm tháng được. Nếu nó bị lùn đi thì có chăng nó bị đập phá (Widget die) =)) - Màu sắc đang hiển thị trên đèn kiểu
Color
, lúc đang hiển thị màu đỏ, lúc thì đèn vàng bật, lúc thì đèn xanh. Đây là một thông tin có thể thay đổi, cứ 1 vài giây hay vài chục giây thì nó thay đổi một lần. Nếu mà màu đèn nó không thay đổi thì có chăng là đèn bị hư (Widget die) =))
Như vậy cái class được mình đặt tên là TrafficLightWidget
này có 2 property là height
(chiều cao của cột đèn giao thông) và currentLight
(màu đèn hiện tại). Biến height
có 1 giá trị mãi mãi không thay đổi nên sẽ khai báo final height
, biến currentLight
có thể thay đổi được bằng cách gán lại giá trị mới nên khai báo var currentLight
hoặc Color currentLight
.
class TrafficLightWidget { final height = 3; // height này cố định ko thay đổi nên để final Color currentLight = Colors.red; // màu đèn sẽ thay đổi từ đỏ, vàng, xanh
}
Như vậy, có thể phân chia data của Widget thành 2 loại: Thông tin có thể thay đổi và thông tin không thể thay đổi.
Còn đây là định nghĩa đơn giản, ngắn gọn nhất về State
:
State là thông tin thể hiện trên Widget mà có thể thay đổi trong suốt thời gian sống sót trên đời của Widget
Trong cái widget cột đèn giao thông đó thì cái data currentLight
đó chính là state vì nó thay đổi được. Khi nó thay đổi thì widget TrafficLightWidget
phải build lại giao diện khác. Ví dụ currentLight = Colors.red
thì widget hiển thị đèn đỏ. Khi currentLight
thay đổi thành currentLight = Colors.green
thì widget phải build lại để hiển thị đèn xanh. Còn cái height
chỉ là data bình thường, không phải là state vì nó luôn không đổi, cái cột đèn sẽ mãi mãi cao 3 mét.
Các data có thể thay đổi được này, được quản lý trong một class có tên là State tách biệt với class Widget
giống như thế này:
class TrafficLightWidget { final height = 3; // height này cố định ko thay đổi nên để final
} // State được quản lý trong class riêng này
class State { Color currentLight = TrafficLightColor.red; // màu đèn sẽ thay đổi từ đỏ, vàng, xanh
}
Vì sao cần có sự tách biệt này, đọc hết bài 5 các bạn sẽ rõ. Giải thích ra dài dòng lắm, bây giờ chỉ tạm hiểu là Flutter tuân thủ một nguyên tắc trong lập trình là: "mỗi class mỗi trách nhiệm" (Single-responsibility principle)
Okay, mình đã hiểu có Data, có công thức là hàm build
thì mình sẽ xây được UI. Khi Data thay đổi, hàm build
sẽ được gọi lại để update UI (chúng ta gọi đây là rebuild Widget). Có 2 loại widget là StatefulWidget
và StatelessWidget
, loại nào cũng có hàm build
nhưng cách chúng gọi hàm build
để update UI là khác nhau:
- Một là:
StatefulWidget
, bản thân Widget này sẽ chủ động update UI. - Hai là:
StatelessWidget
, bản thân Widget này sẽ thụ động update UI, hay nói cách khác là bị Widget khác ép phải update UI.
Như thế nào là chủ động, như thế nào là bị ép. Chúng ta sẽ đến với cách đầu tiên, chủ động update UI sử dụng StatefulWidget
.
4. StatefulWidget
Khi đã hiểu khái niệm về State
rồi thì StatefulWidget
là chỉ đơn giản là một Widget mà CÓ State tức là nó có data có thể thay đổi được. Khi state thay đổi, nó sẽ gọi lại hàm build
để rebuild widget. Nhờ đó mà UI thay đổi.
Đây là cấu tạo của một StatefulWidget
:
class TrafficLightWidget extends StatefulWidget { // 1 final height = 3; // mọi data trong class Widget phải immutable // data nào mutable xin mời qua class khác, là class State bên dưới :D // khi StatefulWidget được khởi tạo nó sẽ gọi hàm createState để tạo 1 object State State<StatefulWidget> createState() { return TrafficLightState(); }
} // khi object Widget gọi hàm createState thì object State ra đời
class TrafficLightState extends State<TrafficLightWidget> { // 2 var currentLight = 'đỏ'; // hàm build Widget build(BuildContext context) { return Text('Đèn cao ${widget.height} mét và đèn đang có màu $currentLight'); // 3 }
}
Cấu tạo của StatefulWidget
có gì:
- Một class là
TrafficLightWidget
kế thừaStatefulWidget
. Mình gọi class này là class Widget. Trong class Widget, mọi data phải khai báo immutable. Mấy thằng data mà mutable thì đẩy qua classState
quản lý như ở mục trên mình đã nói. - Một class là
TrafficLightState
kế thừa classState
. Class này dùng để quản lý cácState
củaStatefulWidget
đó. Trong class này override lại hàmbuild()
. Hàmbuild
này trả về mộtWidget
, nó giúp Flutter biết phải vẽ widgetTrafficLightWidget
như thế nào trên screen. Nói chung hàmbuild
này công dụng giống y chang hàmbuild
trongStatelessWidget
đã được giới thiệu ở trên. Như vậy mộtStatefulWidget
cần tới 2 class là: class Widget và class State. - Trong class
State
có một biến làwidget
, nhờ biến này ta có thể get data từ classWidget
. Như trong VD trên mình đã get được biếnheight
của classTrafficLightWidget
hết sức đơn giản, chỉ cần gọiwidget.height
. Đây là cách giao tiếp giữa 2 classWidget
vàState
Vậy làm thế nào để nói Flutter biết nó cần update UI?. Bên trong class State
có hàm setState
. Hàm setState
cho phép bạn set lại giá trị state
mới (VD set state sang màu vàng, màu xanh). Sau đó nó sẽ gọi lại hàm build
để rebuild lại StatefulWidget
đó bao gồm cả subwidgets lun. Vâng đúng vậy, nó vẽ lại từ widget đó đến hết subtree của widget đó luôn. Cụ thể thế nào thì đến phần StatelessWidget
bạn sẽ rõ nhé
setState(() { // sử dụng hàm setState để set state mới và ra lệnh rebuild Widget currentLight = 'vàng'; // state đã thay đổi từ 'đỏ' sang 'vàng'.
}); // hàm setState đã gọi lại hàm build để build lại UI mới rồi đó!
Tóm lại, ban đầu khi khởi tạo StatefulWidget
, hàm build
đã được gọi một lần đầu tiên, và nó nhận default state
để update UI: UI = build(default state)
. Mỗi lần chúng ta gọi hàm setState
thì nó sẽ gọi lại hàm build
để update UI mới: UI = build(new state)
Well, mọi lý thuyết chỉ là màu xám nếu không có thực hành. Giờ ta sẽ code demo cái App Counter ở mục 1 xem. Thật ra ứng dụng Counter này lúc tạo mới project, Flutter đã code sẵn rồi, mình chỉ xóa bớt code cho nó dễ hiểu hơn thôi. Có giải thích code trong code lun nhé
import 'package:flutter/material.dart'; void main() { runApp(MyApp());
} class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: MyHomePage(), ); }
} // Đây là một StatefulWidget
class MyHomePage extends StatefulWidget { MyHomePageState createState() => MyHomePageState();
} // Đây là class State của StatefulWidget MyHomePage
class MyHomePageState extends State<MyHomePage> { int counter = 0; // Data của Widget Widget build(BuildContext context) { // hàm build return Scaffold( body: Center( // data là biến counter được truyền vào hàm build - công thức UI = build(Data) child: Text('Tui là widget Text. Data của tui hiện tại là: $counter') ), floatingActionButton: FloatingActionButton( onPressed: () { // khi click button màu xanh blue setState(() { // ta sẽ gọi hàm setState counter++; // để gán lại giá trị mới cho biến counter // bên trong hàm setState này sẽ tự động gọi lại hàm build nên UI được update (rebuild) }); }, child: Icon(Icons.add), ), ); }
}
Đây là thành quả:
Ví dụ trên của mình cũng tương tự như ví dụ App Counter mà khi tạo 1 project, Flutter đã code sẵn cho bạn. Bạn thử khám phá xem code App Counter đó hoạt động thế nào nhé
Như vậy, mình vừa trình bày xong một trong 2 cách để update UI khi data thay đổi. Sử dụng StatefulWidget
, chúng ta có thể chủ động update UI bằng hàm setState
Bây giờ, chúng ta tiếp tục với cách 2, Widget bị ép phải update UI, ta sẽ sử dụng StatelessWidget
.
5. StatelessWidget
Bây giờ thử extract widget Text trong ví dụ trên ra một class khác đặt tên là MyText
. Sau khi extract thì MyText
sẽ là một StatelessWidget
như thế này.
// Extract widget Text ra được 1 StatelessWidget
class MyText extends StatelessWidget { // constructor để nhận data từ widget cha MyText({this.counter}); // data nhận được qua constructor là không thể thay đổi (bất biến) final int counter; Widget build(BuildContext context) { // data nhận được sẽ update UI return Text('Tui là widget Text. Data của tui hiện tại là: $counter'); }
}
Well, nếu StatefulWidget
là widget mà CÓ State thì StatelessWidget
đơn giản là một Widget KHÔNG CÓ State. Tức là nó chỉ có data không thể thay đổi (data bất biến như final data, const, ...). Như trong code trên thì nó chỉ có 1 data không thể thay đổi là final int counter
.
Thật ra mình dùng từ chỉ có data bất biến ở đây không đúng lắm. Vì bạn ko thích khai báo final
mà thích khai báo var
để data đó có thể thay đổi thì cũng được luôn nhé. Nhưng data đó vô dụng thôi, data đó có thay đổi cũng vô ích thôi vì bản thân StatelessWidget
không có hàm setState
để nói widget chủ động rebuild lại UI đâu. Nó chỉ biết nhận data từ Widget ngoài truyền vào constructor của nó rồi nhận lấy data đó để show lên UI mà thôi. Đó gọi là thụ động update vì data của StatelessWidget
không thể tự update được mà được thằng khác tryền qua constructor như thế này:
class MyHomePageState extends State<MyHomePage> { int counter = 0; // Data của Widget Widget build(BuildContext context) { return Scaffold( body: Center( // MyText là một StatelessWidget, nó nhận data trong biến counter qua constructor child: MyText(counter: counter), ),
Full source code trong link này: https://dartpad.dev/6a9c0a13992f47226ea04920d2945d19
Kết quả y hệt code demo trong phần StatefulWidget
.
Giải thích đoạn code trong link full source code:
- Kỹ thuật sử dụng constructor để truyền data từ widget cha xuống widget con người ta gọi là Passing state down.
StatelessWidget
nhận đượcState
củaStatefulWidget
thông qua constructor. - Bạn có thấy rằng, khi widget
MyHomePage
là mộtStatefulWidget
, khi nósetState
mới mà thằng widget con của nó làMyText
được rebuild lại để update UI mới. Đúng như trong phầnStatefulWidget
mình đã nói là nó sẽ rebuild cả sub widgets. - Lý do mình vẫn giữ
MyHomePage
làStatefulWidget
trong đoạn code trên là vì như mình đã nóiStatelessWidget
bản chất ko thể tự thay đổi UI mà bị widget cha ép phải thay đổi hay nói cách khác là update UI một cách thụ động. Nếu trong app chỉ toànStatelessWidget
, toàn do bị ép, vậy thì ai là người ép. Trong ví dụ này thìStatefulWidget
là người ép nên cần phải giữ lại ít nhất 1StatefulWidget
ở trên top như vậy. NếuMyHomePage
trong đoạn code trên cũng là mộtStatelessWidget
, thì app này ko có bất kỳ mộtStatefulWidget
nào nên ko sử dụng được hàmsetState
. Vì vậy, khi đó app này sẽ không thể nào thay đổi được UI và biếncounter
sẽ mãi mãi hiển thị là 0 trên màn hình.
Túm cái quần lại là:
StatefulWidget
là Widget cóState
, cònStatelessWidget
là widget không cóState
. VìStatefulWidget
cóState
nên data trongStatefulWidget
có thể thay đổi được và nó có thể chủ động update UI bằng hàmsetState
. Còn data trongStatelessWidget
không thể thay đổi được nên nó muốn update được UI thì phải nhờ thằng cha (có thể là mộtStatefulWidget
nào đó) có khả năng thay đổi data giúp nó rồi truyền data xuống lại cho nó thông qua constructor.
Chính vì, StatelessWidget
không có State
, còn StatefulWidget
thì đã có 1 class khác quản lý State
rồi nên:
Mọi data trong cả class
StatelessWidget
vàStatefulWidget
đều phải khai báo immutable nhé
Well, trong ví dụ trên nếu MyText
không phải là một StatelessWidget
mà là một StatefulWidget
thì nó có hoạt động đúng không. Đúng chứ sao lại không!. StatefulWidget
nó cũng có constructor vậy nên nó cũng biết nhận data từ Widget cha chứ
Check this link này để kiểm tra khi MyText
là một StatefulWidget
: https://dartpad.dev/dcebc401809ad860721768d03aa0937b
Có thể bạn sẽ thắc mắc rằng, đã có StatefulWidget
đầy sức mạnh, có thể cân luôn mọi tính năng của StatelessWidget
, thì cần gì StatelessWidget
nữa. Sao không chỉ sử dụng StatefulWidget
hết cho khỏe, đỡ phải suy nghĩ khi nào thì dùng cái nào. Câu hỏi này cũng tương tự như là hỏi: Tại sao lại cần ImmutableList
trong khi MutableList
có thể làm được tất cả mọi thao tác về List
. Thứ nhất khi một thứ gì đó như data chẳng hạn mà bạn chỉ muốn nó read-only thì tuyệt đối đừng sử dụng mutable vì nó rất dễ sinh bug, nhiều khi mình code làm cho cái data đó thay đổi lúc nào ko hay dẫn đến bug. Thứ 2, bạn thấy code của StatefulWidget
khá là rắc rối so với StatelessWidget
, phải tạo tới 2 class, nói cách khác nếu tạo nhiều StatefulWidget
sẽ có nhiều boilerplate code. Điều này sẽ làm ứng dụng của bạn khó đọc code hơn rất nhiều. Tóm lại, hãy cố gắng sử dụng StatelessWidget hết mức có thể - các đồng nghiệp của bạn sẽ rất biết ơn bạn =)). Sau đây là một cái tip giúp bạn ghi điểm với các đồng nghiệp
6. Khi nào thực sự cần sử dụng StatefulWidget
Đây là sơ đồ khối quyết định 1 data có phải là 1 state trong Widget hay không. Nếu không phải là state thì tất nhiên Widget đó nên là StatelessWidget
rồi.
Giờ mình đi từng ô hình thoi được mình đánh số từ 1 đến 5 đó để giải thích vì sao nó không phải là state nha:
- Data đó không thay đổi tất nhiên không phải state rồi. Quá rõ ràng, không cần giải thích
- Trong ví dụ trong phần StatelessWidget cũng thể hiện rõ điều này. Chắc chắn đó là StatelessWidget và data đó là do widget cha truyền xuống mà thôi. Ko phải state
- State là biến số được truyền vào hàm build để update lại UI. Đằng này biến này không cần thể hiện trên UI nên nó không phải state. Chỉ là một biến private var dùng để tính toán.
- Nếu chỉ hiển thị đúng 1 lần như mấy cái Icon, Logo thì cần gì sử dụng State. Sử dụng StatelessWidget nhé.
- Nếu nó là kết quả của một phép tính, ví dụ màn hình hiển thị 2 TextField cho phép nhập 2 số a và b, tính tổng và show kết quả lên Screen thì
a
vàb
là state còn cái tổng kia thậm chí không cần tạo biến làm gì cho phí bộ nhớ, hàmbuild
sẽ nhận 2 state làa
vàb
rồi tính tổng luôn trong đó show kết quả lên là được rồi
Bạn vẫn còn cảm thấy khó hiểu lắm đúng không . Hãy hình dung khuôn mặt bạn là cái màn hình hiển thị UI của app nhé. Khuôn mặt thì có khi nào bất biến không nhỉ. KHÔNG, nếu có bất biến thì chắc là lúc ngủm củ tỏi rồi. Khuôn mặt phải có TRẠNG THÁI (State
) như: cười, giận giữ, nghiêm túc, ngủ..., khi trạng thái thay đổi thì sẽ build
ra những giao diện khuôn mặt khác nhau. Như vậy khuôn mặt là một root Widget
mà vì nó có State
nên nó là StatefulWidget
. Lúc mới mở app lên, khuôn mặt phải có trạng thái default (default State
) đúng không, chứ ko lẽ khuôn mặt hiển thị trắng xóa như Slenderman =)). Bên trong có những child Widget
như mắt, mũi, miệng,... Bên trong Widget Mắt có những data: màu mắt (ko phải là state vì nó bất biến), trạng thái nhắm mắt hay mở mắt (biến có kiểu bool có thể thay đổi được). Như vậy Widget Mắt cũng có State
, nhưng nó không nên là StatefulWidget
nữa, vì thằng cha của nó là Widget Khuôn Mặt đã là StatefulWidget
rồi. Vậy Widget Mắt nên là StatelessWidget
và Widget Khuôn Mặt là widget cha của nó sẽ truyền biến bool này cho nó để ép nó update UI.
7. Tổng kết lại kiến thức
Đây là những kiến thức quan trọng trong bài, mình sẽ tổng kết ngắn gọn lại như sau:
- Scene = f(Data) trong đó
f
là hàmbuild
có trong mỗiStatelessWidget
hayStatefulWidget
. Flutter hoạt động theo công thức đó: Khi thay đổi Data thì UI sẽ được update. State
chính là Mutable data của widget nênState
là data của Widget mà có thể thay đổi được.StatefulWidget
là widget cóState
, cònStatelessWidget
là widget không cóState
.StatefulWidget
có thể chủ động update UI bằng cách gọi hàmsetState
.- Data trong
StatelessWidget
không thể thay đổi được nên nó muốn update được UI thì phải nhờ thằng cha là mộtStatefulWidget
nào đó có khả năng thay đổi data giúp nó rồi truyền data xuống cho nó thông qua constructor. - Mọi data trong class
StatefulWidget
vàStatelessWidget
đều phải là immutable - Hãy sử dụng
StatelessWidget
hết mức có thể - các đồng nghiệp sẽ yêu mến bạn.
Kết thúc mở
Bây giờ, các bạn có thắc mắc rằng: nếu cái Widget Tree kia nó rất là sâu thì khi ta muốn truyền data từ Widget ông tổ xuống tận cháu, chắt, chút, chít phải tạo sử dụng constructor từ ông tổ xuống ông cố, rồi xuống tiếp ông nội, xuống tiếp bố, xuống tiếp con, ... Sao phải cực thế, trong khi ta muốn truyền thẳng từ ông tổ xuống cháu, chắt luôn. Có cách nào không?. Có đó, câu trả lời đã có trong phần 3 và phần 4
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
Nguồn tham khảo: Beginning App Development with Flutter: Create Cross-Platform Mobile Apps của tác giả Rap Payne