Học Flutter từ cơ bản đến nâng cao. Phần 4: Lột trần InheritedWidget
Lời mở đầu
Trong đoạn kết của phần 2, chúng ta đã đối mặt với 1 bài toán: Làm thế nào để truyền data từ một widget cha nào đó xuống thẳng widget chắt mà không phải sử dụng constructor để truyền xuống từ từ từng widget một. Và trong phần 3 thì cách giải bài toán này đã được hé lộ. Đó là sử dụng BuildContext
kết hợp với InheritedWidget
. BuildContext
đã được giải thích trong phần 3. Bây giờ chúng ta sẽ tìm hiểu InheritedWidget
1. Đặt vấn đề
Quay trở lại ví dụ app Counter ở phần 2: https://dartpad.dev/?id=157b19bb180a0d7ea4d738946c4e9bf1
Chúng ta sẽ thấy để truyền được data từ MyHomePage
xuống child widget của nó là CounterWidget
thì trong class CounterWidget
, ta phải tạo 1 constructor có vẻ giống hệt constructor của MyHomePage
Kỹ thuật truyền data từ widget cha xuống Widget con thông qua constructor như vậy được gọi là "Passing state down".
Giả sử, trong đoạn code trên, nếu chúng ta extract widget Center
ra một Widget đặt tên là MyCenterWidget
thì ta cũng phải tạo một constructor với 2 property là counter
và isLoading
để nó có thể nhận data từ MyHomePage
truyền xuống cho CounterWidget
.
Nếu cứ làm theo cách như vậy, từ một widget ông muốn truyền data xuống widget cháu, ta phải truyền sang tay từng người một, từ ông → ba → con → cháu. Chúng ta đã thấy vấn đề nghiêm trọng ở đây chưa?
Có 2 vấn đề xảy ra ở đây là:
- Nếu khoảng cách giữa
MyHomePage
vàCounterWidget
trên Widget tree càng xa thì chúng ta sẽ càng cực khổ để truyền được data từMyHomePage
xuốngCounterWidget
qua constructor của nhiều Widget trung gian. - Khi
MyHomePage
gọi hàmsetState
, nó sẽ gọi lại hàmbuild
khiến cho cảMyCenterWidget
vàCounterWidget
đều được khởi tạo lại và gọi hàmbuild
. Đây là 1 sự lãng phí vì ta chỉ cần widgetCounterWidget
được rebuild mà thôi.
Vậy làm thế nào để khắc phục được 2 nhược điểm trên. Có một loại Widget có thể giúp ta làm điều đó là InheritedWidget
2. InheritedWidget
InheritedWidget là một nơi lưu trữ data và cung cấp data cho widget con trong widget tree. Tất cả widget con của InheritedWidget
đều có thể truy cập vào InheritedWidget
để lấy data. Tức là từ vị trí InheritedWidget
, bạn không cần thiết phải truyền data xuống từng 1 widget con một nữa mà Widget con ở bất kỳ vị trí nào trên widget tree muốn lấy data từ InheritedWidget
, sẽ giơ cao cánh tay chộp lấy data mà nó muốn từ InheritedWidget luôn.
Bây giờ, chúng ta sẽ tạo một InheritedWidget
bằng cách tạo ra 1 class extends nó. Vì nó là Data provider nên tất nhiên nó sẽ chứa các data counter
và isLoading
để truyền đến widget con của nó. Tất nhiên, chúng ta cũng cần phải tạo constructor cho nó.
class MyInheritedWidget extends InheritedWidget {
final int counter;
final bool isLoading;
MyInheritedWidget({
required this.isLoading,
required this.counter,
});
}
Nó bắt chúng ta phải override lại hàm updateShouldNotify
trả về kiểu bool
. Well, tạm thời chúng ta chưa hiểu nó để làm gì nên mình sẽ tạm return false
.
class MyInheritedWidget extends InheritedWidget {
...
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return false;
}
}
Nó lại bắt chúng ta phải add super constructor super(...) invocation mà super constructor này lại yêu cầu phải có parameter child
. Như vậy, chúng ta buộc phải tạo thêm 1 property là Widget child
nữa để truyền vào super constructor.
class MyInheritedWidget extends InheritedWidget {
final int counter;
final bool isLoading;
final Widget child;
MyInheritedWidget({
required this.isLoading,
required this.counter,
required this.child,
}) : super(child: child);
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return false;
}
}
Đến đây vẫn chưa có cách nào để các widget con có thể tìm thấy MyInheritedWidget
để truy cập vào lấy data. Khoan, idea "tìm thấy Widget" này rất quen. Chúng ta đã gặp nó ở trong phần 3, cụ thể là hàm Scaffold.of(context)
. Như vậy, chúng ta cũng sẽ tạo 1 hàm static tên là of
cho truyền context
vào. Các Widget con sẽ cần gọi MyInheritedWidget.of(context)
để dựa vào context
đó (tức vị trí của widget con), hàm of
sẽ đi tìm thằng widget cha có type là MyInheritedWidget
và trả về MyInheritedWidget
đó.
class MyInheritedWidget extends InheritedWidget {
...
static MyInheritedWidget? of(BuildContext context) { }
}
Để tìm kiếm chúng ta sử dụng hàm context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>
. Nó sẽ giúp ta get được Widget cha gần vị trí context nhất có type là MyInheritedWidget
:
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
Sau tất cả thì chúng ta cũng đã tạo xong một custom InheritedWidget
:
class MyInheritedWidget extends InheritedWidget {
final int counter;
final bool isLoading;
final Widget child;
MyInheritedWidget({
required this.isLoading,
required this.counter,
required this.child,
}) : super(child: child);
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return false;
}
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
Bây giờ, chúng ta sẽ cần đặt widget MyInheritedWidget
ở vị trí cha của widget CounterWidget
và MyCenterWidget
. Khi đó ta được quyền xóa luôn constructor và data của kẻ trung gian MyCenterWidget
. Thậm chí xoá luôn constructor của CounterWidget
, chúng ta không cần thiết phải nhận data qua constructor nữa mà sẽ nhận data bằng hàm MyInheritedWidget.of(context)
...
class MyHomePageState extends State<MyHomePage> {
...
Widget build(BuildContext context) {
return Scaffold(
body: MyInheritedWidget( // NEW
isLoading: _isLoading,
counter: _counter,
child: MyCenterWidget(),
),
...
);
}
...
}
class CounterWidget extends StatelessWidget {
Widget build(BuildContext context) {
final myInheritedWidget = MyInheritedWidget.of(context); // NEW
if (myInheritedWidget == null) {
return Text('MyInheritedWidget was not found');
}
return myInheritedWidget.isLoading
? CircularProgressIndicator()
: Text('${myInheritedWidget.counter}');
}
}
...
Full source code: https://dartpad.dev/?id=e1d67137e8815a7b877b7aa119887a89
Thử run app lên lại xem. It works like a charm
Đoạn code trên, mình nợ các bạn một lời giải thích về hàm updateShouldNotify
đúng ko nào. Nhưng trước khi mình giải thích hàm này, cho mình xin thú tội với các bạn.
3. Lời thú tội ngọt ngào
Những gì chúng ta vừa code ở trên chỉ giải quyết được một vấn đề đầu tiên trong 2 vấn đề đã nêu ra ở đầu bài. Bạn thử đặt lệnh print
vào hàm build
của MyCenterWidget
và CounterWidget
sẽ thấy nó được gọi lại mỗi khi ta click button.
Button clicked!. Call setState method
rebuild MyHomePage
rebuild MyCenterWidget
rebuild CounterWidget
Bởi vì khi bạn click button
, hàm setState
sẽ khiến cho hàm build
của MyHomePage
sẽ được gọi và nó khiến cho cả MyCenterWidget
và CounterWidget
bị khởi tạo mới. Oh no, như vậy là lãng phí, chúng ta ko muốn MyCenterWidget
bị khởi tạo lại.
Thay cho lời xin lỗi, mình sẽ chỉ ra cách fix bằng cách:
Trong MyHomePage
tạo ra 1 property là Widget child
và mình sẽ truyền MyCenterWidget
vào paramenter child
này:
void main() {
runApp(
MaterialApp(
home: MyHomePage(
isLoading: false,
counter: 0,
child: MyCenterWidget(), // code thêm dòng này
),
),
);
}
class MyHomePage extends StatefulWidget {
final bool isLoading;
final int counter;
final Widget child; // code thêm dòng này
const MyHomePage({
required this.isLoading,
required this.counter,
required this.child, // code thêm dòng này
});
...
}
Vì chúng ta đã tạo ra MyCenterWidget
và truyền nó vào MyHomePage
nên child
của MyInheritedWidget
không cần phải tạo mới MyCenterWidget
nữa mà chúng ta sẽ sử dụng chính instance được truyền vào MyHomePage
thông qua biến widget
class MyHomePageState extends State<MyHomePage> {
...
Widget build(BuildContext context) {
return Scaffold(
body: MyInheritedWidget(
isLoading: _isLoading,
counter: _counter,
child: widget.child, // code thêm dòng này
),
...
);
}
...
}
Full source code: https://dartpad.dev/?id=0a54da9b69a47e341a16873a62fc6bcb
Bây giờ thử chạy và click button lại xem:
Tuyệt vời, hàm build
của cả MyCenterWidget
và CounterWidget
đều đã không được gọi lại. Khoan đã, có gì đó sai sai. Chúng ta chỉ muốn thằng MyCenterWidget
không bị rebuild thôi mà. Nếu thằng CounterWidget
cũng không được rebuild thì có nghĩa là UI của CounterWidget
cũng sẽ không được update??? Sinh ra bug khủng rồi =)). Á cái thằng này, mài bảo ta rằng sẽ chuộc lỗi bằng cách chỉ ta cách fix, ai dè fix bug nhỏ sinh ra bug khủng lun hả. Lôi đầu nó ra chém!
Khoooang, tại hạ xin khai sự thật ạ: "Bệ hạ nghĩ kĩ xem, Flutter đâu có ngu như vậy, nó cung cấp hàm updateShouldNotify
để chúng ta sử dụng MyInheritedWidget
điều khiển child
của nó". Đây chính là hàm mà thần nợ bệ hạ 1 lời giải thích từ đầu, là hàm mà khi chúng ta kế thừa InheritedWidget
nó sẽ bắt override ấy. Trong đống child của MyInheritedWidget
, chúng ta muốn thằng nào update, thằng đó sẽ update, chúng ta muốn thằng nào ko được update, nó sẽ ko được update: "Phụ sử tử build, tử bất build bắt bug". Thế có toẹt vời không bệ hạ =))
4. Giải thích hàm updateShouldNotify
Hàm updateShouldNotify
được gọi ngay sau khi InheritedWidget
bị rebuild. Nếu hàm updateShouldNotify
return true
thì một khi InheritedWidget
rebuild, nó cũng bắt các widget con đang phụ thuộc vào nó phải rebuild. Ngược lại, nếu hàm updateShouldNotify
return false
thì nó sẽ không rebuild mấy thằng con phụ thuộc nó. Như thế nào được gọi là phụ thuộc?. Các widget con của MyInheritedWidget
, nếu sử dụng hàm MyInheritedWidget.of(context)
thì ta sẽ nói Widget đó sẽ phụ thuộc vào MyInheritedWidget
.
Ah, ta hiểu rồi. Bởi vì hàm updateShouldNotify
của chúng ta đang trả về false
nên CounterWidget
sẽ không thể gọi lại hàm build
. Bây giờ chỉ cần đổi lại trả về true
thì vấn đề sẽ được giải quyết. Nhưng để giảm bớt những lần rebuild CounterWidget
không cần thiết, ta nên check xem data (isLoading
and counter
) của InheritedWidget có thay đổi hay ko bằng cách:
updateShouldNotify(MyInheritedWidget oldWidget) {
return isLoading != oldWidget.isLoading || counter != oldWidget.counter;
}
bool
Full source code: https://dartpad.dev/?id=82db2fd77528fa02b3bc7a4799ba1ff4
Run app lên và kiểm chứng thôi nào. Khi click vào button
, ta nhận được log:
Button clicked!. Call setState method
rebuild MyHomePage
rebuild CounterWidget
Tuyệt vời, hàm build
của MyCenterWidget
không được gọi lại còn hàm build
của CounterWidget
đã được gọi lại. Như vậy chúng ta đã giải quyết xong vấn đề thứ 2 được nêu ra từ đầu bài viết, đó là chỉ rebuild những widget cần được rebuild.
Kết luận
Tại sao cần phải biết InheritedWidget
và cách nó hoạt động. Vì đây là nền tảng, là gốc gác, là một phương pháp quản lý state low-level. Nhờ đó mà chúng ta có thể học lên các phương pháp quản lý state level cao hơn như provider
, bloc
Click follow để nhận thông báo khi có bài viết mới nhé 500 anh em cây khế
Đọc tiếp phần 5: Cô nàng Flutter hoạt động như thế nào?
All rights reserved