Học Flutter từ cơ bản đến nâng cao. Phần 5: Cô nàng Flutter hoạt động như thế nào?
Bài đăng này đã không được cập nhật trong 3 năm
Lời mở đầu
Trong đoạn kết của phần 1, mình còn nợ các bạn một lời giải thích về Key
trong Flutter và ở bài này các bạn cho mình chây ỳ nợ tiếp nhé :v. Vì bài này liên quan đến việc giải thích về Key
nên để hiểu được bài Key
, ta cần phải tìm hiểu đôi chút về cách Flutter hoạt động.
1. Widget chỉ là bản thiết kế
Chúng ta là dev Flutter với công việc chính là code nên các Widget tree để tạo ra những App Flutter siêu đẹp. Từng cái Text
là Widget
, Padding
cũng là Widget
, nói chung "Mọi thứ trong Flutter đều là Widget". Well, đó là câu nói nổi tiếng trong Document của Flutter chứ không phải mình chém . Và câu nói này không hoàn toàn là sự thật, nó chỉ là bề nổi của tảng băng chìm mà thôi. Sự thật đến tận hôm nay mới được bật mí: Widget chính là một bản vẽ, một bản thiết kế như bản thiết kế "siêu xe" trong hình dưới đây. Flutter sẽ dựa vào bản thiết kế này và render ra cái hình ảnh UI hiển thị lên màn hình. Từng cái Text
chúng ta đọc, từng cái Button
để chúng ta click, cái Icon
, cái Logo
được hiển thị trên màn hình đó không phải là Widget
mà chính xác là nó được dựng lên một bản thiết kế gọi là Widget
.
Well, Widget
chỉ là một bản thiết kế lên những mảnh nhỏ UI như Text
, Image
. Từ thiết kế đó mà Flutter render ra sản phẩm thật, chính là những mảnh UI mà chúng ta nhìn thấy trên app. Bây giờ chúng ta sẽ đi tìm hiểu những mảnh UI thật đó qua 2 class trong Flutter là Element
và RenderObject
.
2. Element và RenderObject là gì
Ở bài 1, chúng ta đã biết Widget
chỉ đơn giản là Dart class do Flutter thiết kế sẵn cho chúng ta như class Text
, Column
, Image
, ... hoặc do chúng ta định nghĩa ra như class SofaWidget
.
Còn đây là khái niệm liên quan đến Element
:
Element
represents a specific instance of aWidget
in a given location of the tree hierarchy - Flutter documentation
Tạm dịch:
Element
đại diện cho mộtinstance
của mộtWidget
tại một vị trí cụ thể trên hệ thống cây.
Nghe nó giống khái niệm về class
và instance
trong lập trình hướng đối tượng vậy. Cũng có thể hiểu như vậy . Nếu như nói Widget
là một bản thiết kế của một mảnh UI, thì Element
đại diện cho cái thành phẩm, tức là một mảnh UI thật sự được sản xuất từ bản thiết kế đó và mảnh UI này được gắn vào một vị trí cụ thể trên hệ thống cây.
Trong cái quá trình tạo ra mảnh UI thật để hiển thị trên màn hình có sự đóng góp của RenderObject
- nhân vật chịu trách nhiệm căn chỉnh kích thước, sắp xếp vị trí trên 1 layout và vẽ, tô màu cho cái mảnh UI đó. Vì vậy, có thể nói gọn lại cả 3 khái niệm Widget
, Element
và RenderObject
như sau:
Widget
chỉ là một bản thiết kế cho các mảnh UI hiển thị trên màn hình,Element
là đại diện cho cái mảnh UI đó ở một vị trí nào đó trên cây vàRenderObject
đóng góp vào tô vẽ, căn chỉnh cho cái mảnh UI đó.
Hai cái dấu mũi tên từ FooElement
trỏ đến FooWidget
và RenderFoo
trong ảnh đó chính là reference. Mỗi Element
sẽ nắm giữ reference của Widget
và RenderObject
và quản lý cả App. Bây giờ chúng ta sẽ tìm hiểu mối quan hệ này giữa 3 đứa nó: Element
& Widget
& RenderObject
3. Quan hệ giữa Widget, Element và RenderObject
Vì nội dung nhiều lý thuyết nên mình đã cố gắng tách chúng ra thành 2 chặng hành trình tìm hiểu. Mỗi chặng chỉ tìm hiểu ở mức vừa đủ để hiểu được bài tiếp theo. Nào cùng bắt đầu chặng 1.
Chặng 1: Mối quan hệ giữa Element, Widget, State
Bắt đầu chặng 1, để biết được Flutter đã sử dụng bản thiết kế Widget Tree do chúng ta thiết kế để render ra App như thế nào thì trước tiên hãy cùng mình tìm hiểu điều gì đã xảy ra khi ta chạy hàm runApp()
bên trong hàm main()
.
Trong hàm runApp
chúng ta truyền vào nguyên một Widget Tree với MyApp
là root Widget đúng ko nào. Flutter sẽ walk down cái Tree đó, từ root Widget đến hết cây. Từng Widget trên cây sẽ gọi hàm createElement()
để tạo ra từng Element
. Quá trình từ một Widget
tạo ra một Element
này người ta gọi là inflation
. Và cứ inflate từng Widget
như thế, một Widget Tree sẽ tạo được một Element Tree. StatelessWidget
sẽ tạo ra StatelessElement
và StatefulWidget
sẽ tạo ra StatefulElement
.
Cái mũi tên nét đứt đó là reference mình đã nói ở trên đấy. Bây giờ mình sẽ giải thích. Vào xem code của class StatefulElement
và StatelessElement
xem nó được hình thành thế nào là biết ngay á mà
Khám phá class StatelessElement
class StatelessElement extends ComponentElement {
StatelessElement(StatelessWidget widget) : super(widget); // 1
// hàm build
Widget build() => widget.build(this); // 2
}
// StatelessElement extends ComponentElement extends Element
class Element {
Element(this.widget);
Widget widget;
}
Khám phá được gì từ 2 dòng code được mình đánh dấu 1 và 2 đó:
- Constructor của class
StatelessElement
nhận mộtStatelessWidget
là tham số. Như vậy, mộtStatelessElement
sẽ giữ tham chiếu củaStatelessWidget
qua biếnwidget
. Biếnwidget
ở bên trong class cha của nó là classElement
ấy. - Hàm
build
trong classStatelessWidget
mà chúng ta đã biết ở những bài trước là do chínhStatelessElement
gọi
Như vậy, code đã chứng minh được: StatelessElement
nắm giữ một reference của StatelessWidget
qua biến widget
. Giờ chúng ta vào bên trong class StatefulElement
khám phá tiếp xem nhé:
Khám phá class StatefulElement
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget) : super(widget) { // 1
state = widget.createState(); // 2
state.widget = widget; // 3
}
// hàm build
Widget build() => state.build(this); // 4
State<StatefulWidget> state;
}
4 dòng code được mình đánh dấu 1, 2, 3 và 4 đó đã nói lên được những gì:
- Cũng tương tự như
StatelessElement
, constuctor củaStatefulElement
cũng nhận một làStatefulWidget
là tham số. Như vậy mộtStatefulElement
sẽ giữ tham chiếu củaStatefulWidget
qua biếnwidget
- Hàm
createState
trong classStatefulWidget
kìa, thấy quen không. Thì ra thằngStatefulElement
đã bảoStatefulWidget
làm giúp nó một việc: "Hey, StatefulWidget, chú gọi hàmcreateState
để tạo ra mộtState
object rồi để anh giữ một tham chiếu đếnState
object đó thông qua biếnstate
được ko" . VàStatefulWidget
đã nghe lời và làm theo . - Hóa ra trong bài 2, mình nói thằng
State
object có một tham chiếu của thằngStatefulWidget
qua biếnwidget
là nhờ dòng code sử dụng lệnh gán này đây các bạn. - Hàm
build
trong classState
là doStatefulElement
gọi.
Kết thúc chặng khám phá code thứ nhất. Tất cả những phân tích rườm rà ở trên được đúc kết quả một tấm ảnh. Và thật sự, chặng 1 mình chỉ muốn các bạn thấy được quan hệ giữa Widget Tree Và Element Tree và các State object qua như tấm ảnh này đây. Các mũi tên đó là reference đó. Như vậy, StatefulElement
có reference của StatefulWidget
và State
, còn StatelessElement
thì có reference của StatelessWidget
Chặng 1 chỉ cần hiểu được cái ảnh này là đủ . Nào chúng ta cùng tiếp tục chặng 2.
Chặng 2: Quan hệ giữa Widget, Element và RenderObject
Nếu để ý, bạn sẽ thấy 2 class StatelessElement
và StatefulElement
được mình trích ra ở trên đều kế thừa ComponentElement
. Trong Flutter, class Element
có 2 class con quan trọng là ComponentElement
và RenderObjectElement
.
RenderObjectElement
: mỗiRenderObjectElement
khi được gắn lên Element Tree sẽ nhờwidget
mà nó đang nắm giữ gọi hàmcreateRenderObject()
để tạo ra objectrenderObject
và nó sẽ nắm giữ tham chiếu củarenderObject
này luôn.ComponentElement
giống như là một tổ hợp (compose) nhiềuElement
, nó có khả năng tạo ra nhữngRenderObject
một cách gián tiếp thông qua việc tạo ra nhữngRenderObjectElement
hoặc nhữngComponentElement
khác.
Bài viết này mình sẽ không đi sâu vào các class đó. Chặng 2 này mình muốn chúng ta tạm hiểu một cách ngắn gọn:
Thằng
Element
nào cũng tạo ra một hoặc nhiềuRenderObject
để vẽ UI, không tạo trực tiếp thì cũng tạo gián tiếp. Như vậy một Element Tree cũng sẽ tạo được một Render Tree và chúng ta có đến 3 cái cây là: Widget Tree, Element Tree và Render Tree.
Như vậy, ở chặng 1 ta đã biết Element
có tham chiếu của Widget
và State
, chặng 2 ta còn biết thêm Element
có tham chiếu của RenderObject
. Vậy thì không còn nghi ngờ gì nữa, Element
là thằng quản lý cây. Tất cả quan hệ được thể hiện qua một tấm ảnh sau:
Mình mới vừa trả lời xong 3 dấu hỏi trong cái ảnh ở mục 2, thì bây giờ lại mọc lên 3 dấu hỏi mới. Chúng khẳng định là:
Trong khi các
Widget
liên tục bị rebuild, tức là bị destroy rồi build lại thì cácElement
chỉ tạo ra đúng 1 lần và nó chỉ được update chứ nó rất hiếm khi phải bị đập đi xây lạiElement
mới.
Thật sự đúng là như vậy đó các bạn. Đó là ý đồ của Flutter để giữ cho App Flutter luôn có performance tốt. Chúng ta đã biết, Widget
là bản thiết kế, một nơi cung cấp thông tin về size, màu sắc, .... Và ẩn sâu bên trong, Flutter đã sử dụng Element
để ra lệnh RenderObject
dựa vào bản thiết kế đó để vẽ nó bằng rất nhiều hàm như paint
, performLayout
, ... Các thuật toán render hay khởi tạo một RenderObject
được Flutter viết rất phức tạp, phức tạp hơn rất nhiều so với những Widget
chúng ta code. Vậy nên tốt nhất là nên giữ các object RenderObject
trong bộ nhớ càng lâu càng tốt chứ không nên destroy chúng rồi sau phải tạo lại vì chúng khá tốn kém khi khởi tạo lại. Nói cách khác, RenderObject
và Element
rất là đắt giá, đắt hơn nhiều Widget
nên Flutter mới để cho Widget
bị rebuild liên tục còn Element
và RenderObject
thì hạn chế bị rebuild, hạn chế đập đi xây lại, chúng chỉ nên được update mà thôi.
- Như thế nào là Element được update?.
- Khi nào thì
Element
được update, khi nào thìElement
bị rebuild?
Đây là 2 câu hỏi lớn trong bài. Cũng là mục đích chính để mình viết bài này. Và để trả lời được 2 câu hỏi này, ta sẽ cần phải tìm hiểu: Mỗi lần rebuild tree, Fluter thực sự đã làm những gì?
4. Nàng Flutter đã làm gì mỗi lần rebuild
Mỗi lần Widget bị rebuild, tức là có một Widget
mới thay cho Widget
cũ, Element
sẽ so sánh thằng Widget
mới đó với thằng cũ xem cái bản hiện tại có khác gì bản thiết kế mới hay không rồi đưa một quyết định quan trọng.
Cụ thể, Element
sẽ xem xét cái Widget Type, nếu nó thấy Widget
cũ và mới có cùng Type (ví dụ như cái Widget
cũ là Text
, cái Widget
mới cũng là Text
) thì Element
đó sẽ không bị rebuild mà Element
đó chỉ update bản thân nó bằng cách cho biến widget
vốn đang trỏ đến Widget cũ, chuyển sang trỏ đến cái Widget mới, rồi renderObject
tiếp tục công việc đọc các thông số trong bản thiết kế mới và vẽ lại thôi. Quả là thông minh, nhờ thế mà UI được update mà không cần phải tái tạo lại Element
và RenderObject
vốn rất tốn kém mỗi lần khởi tạo mới. Triệu lời giải thích cũng không thể bằng tấm ảnh dưới đây:
Cái ảnh đã nói lên tất cả. Bạn đã hiểu Mỗi lần rebuild thì Element được update là như thế nào chưa. Nhìn hình cũng thấy Widget cũ mà Element
đang trỏ tới là Text('Trip A')
bị đập đi và biến widget
trong Element
chuyển sang trỏ đến Widget
mới là Text('Trip B')
. Như vậy: Element
và State
được update, chứ không phải rebuild. Widget
mới bị rebuild, vì nó bị đập đi cái cũ, tạo ra cái mới thay thế.
Sau khi so sánh xong 1 Widget
tại vị trí đó, Element
sẽ tiếp tục chạy xuống Widget
kế tiếp trên tree để tiếp tục công việc so sánh. Nếu quá trình so sánh gặp trường hợp Widget cũ và Widget mới khác Widget Type thì sẽ rất căng. Tại vị trí đó, thằng Widget
mới đó sẽ gọi lại hàm createElement
để tạo Element
mới và các RenderObject
mới cũng được tạo ra. Cứ như thế, sub Element Tree
kéo theo sub Render Tree
bị rebuild, dẫn đến performance của app sẽ rất kém. Vì vậy nên tránh các trường hợp thế này nhá
Code demo thử phát biết ngay nó tạo lại Element
thế nào. Sử dụng code của app Counter nhưng sửa lại code 1 tí và có đặt log vào để quan sát: khi _counter
là số chẵn sẽ hiển thị 1 Column
, ngược lại _counter
là số lẻ sẽ hiển thị 1 Row
:
Center(child: _counter % 2 == 0? Column() : Row())
Full source code để vào xem log và run app trải nghiệm: https://dartpad.dev/e2553214e383480d0af1e2c15c809588
Xem log ta sẽ thấy, mới run app tức là first build thì từng Widget
gọi hàm createElement
. Nhưng khi click 1 lần, 2 lần để rebuild MyHomePage
. Ta sẽ thấy MyText
gọi lại hàm createElement
như này:
MyText createElement
Như vậy, mình xin túm lại một câu chốt trả lời cho câu: Khi nào thì Element
được update, khi nào thì Element
bị rebuild:
Mỗi lần rebuild, Flutter walk down the Element Tree, từng Element sẽ so sánh Widget cũ (cái mà nó đang nắm giữ trong biến
widget
) và Widget mới. Nếu nó thấy Widget cũ và Widget mới có cùng Widget Type nó sẽ update biến widget trỏ đếnWidget
mới đó. Ngược lại, tức là khác Widget Type, nó sẽ bị rebuild.
Và đó cũng là mục đích chính của bài viết này. Chỉ cần hiểu được câu chốt này thì sẽ dễ dàng hiểu được khái niệm về Key
trong bài tiếp theo.
Kết bài
Thật ra qúa trình so sánh ở trên, Element
ngoài so sánh Widget Type
, nó còn so sánh thêm một yếu tố nữa đó là Key
. Đó cũng là lý do mình cần phải viết bài này một cách căn bản nhất có thể. Những nội dung trong bài chỉ vừa đủ để hiểu được những lý thuyết về Key
chứ không đi quá sâu vào việc Flutter render hình ảnh như thế nào. Nếu các bạn tò mò có thể tự tìm hiểu bằng các link tham khảo bên dưới nhé
Xuất hiện từ bài 1, đến giờ nhân vật Key
mới chịu come out trong bài viết tiếp theo. Hy vọng các bạn cùng đón đọc.
Đọc tiếp phần 6: Key là gì, có mở khóa trái tim nàng được không?
Đọc tiếp phần 7: Lột trần trụi GlobalKey
Tham khảo:
https://medium.com/flutter-community/the-layer-cake-widgets-elements-renderobjects-7644c3142401
https://www.youtube.com/watch?v=996ZgFRENMs&ab_channel=Flutter
https://flutter.dev/docs/resources/architectural-overview#build-from-widget-to-element
Flutter Recipes của tác giả Fu Cheng
Beginning Flutter của tác giả Marco L. Napoli
All rights reserved