Flutter

Flutter, Bài 7: State và Stateful & Stateless Widgets

Nếu bạn trước giờ chỉ code với Android và iOS SDK, bạn sẽ cần học một hướng tư duy mới để viết ứng dụng Flutter là code theo State. Còn trong trường hợp bạn đã có “gốc gác” React Native rồi, bạn có thể bỏ qua phần State và đi trực tiếp vào StatefulWidgetsStatelessWidget.

Flutter

1. Tư duy về State:

State, theo các từ điển Anh-Việt thông dụng, và cả “chị Google”, được dịch sang tiếng ta là “trạng thái”. Khi viết code, bạn có thể sẽ “lầm lẫn” mà cho rằng State, hay trạng thái, là trạng thái của các biến. Không phải đâu. State ở đây là trạng thái của cả một Widget, hay đối với React Native là <View>, và mở rộng ra là trạng thái của cả ứng dụng.

Vậy, lập trình theo State là dư lào? Vì giải thích lí thuyết sẽ lòng vòng với chữ nghĩa khó hiểu, tôi cho rằng bạn sẽ dễ dàng nắm bắt chính xác hơn qua một ví dụ nho nhỏ. Bây giờ, tôi có một (NS)String là full_name, nó được cấu thành bằng việc nối một (NS)String là first_name và một (NS)String khác là last_name. Về mặt giao diện, first_name sẽ được hiển thị với một (UI)TextView là firstName, và tương tự với last_namelastName. Giả sử, tôi có first_name = “Nghĩa” và last_name = “Nguyễn”. Như vậy, tôi có (tạm thời tôi dùng dấu = cho nhanh gọn và dễ hiểu, thực tế như bạn biết dùng dấu = như vậy là sai) firstName = “Nghĩa” và lastName = “Nguyễn”, và đương nhiên full_name = “Nghĩa Nguyễn”.

Sau khoảng 10 giây thì tôi hứng chí lên và muốn đổi first_name = “Huy”. Đối với việc bạn sử dụng “ếch đi cày” của Android và iOS, việc đổi first_name thành bất kì thứ gì khác đều không làm ảnh hưởng tới full_namefirstName. Tức là dù tôi có đổi first_name = “Huy” hay “Nam” hay “Nguyệt” hay “Thảo” hay “Trang” thì firstName vẫn hiển thị giá trị cũ, là firstName = “Nghĩa”, và tương tự với full_name = “Nghĩa Nguyễn” như thường. Tôi muốn thay đổi chúng, thì phải tự code tay, gọi tới chúng và gán giá trị và text mới. Mà thậm chí, tôi cho firstName = “Ngân” và full_name = “Thúy Nguyễn” thì cũng chẳng nhằm nhò gì. Như bạn thấy, 5 tên full_name, first_name, last_name, firstNamelastName là những đối tượng riêng rẽ, cho dù ban đầu bạn có chỉ định cho chúng nắm tay nhau một lần.

Như vậy, lỡ như chúng ta có 10 ngàn đối tượng riêng rẽ như vậy, nhưng phải cập nhật cho chúng đi cùng nhau, có lẽ bạn sẽ phải “dành tất cả thanh xuân” để code và kiểm tra từng đối tượng một. Và như vậy, một hướng lập trình mới ra đời để giải quyết vấn nạn gây nhức đầu, chóng mặt, hoa mắt, ù tai, đau lưng, mỏi gối, sưng viêm, đau thần kinh tọa cho các developers. Đó là hướng “code theo State“, với hậu phương là ReactiveX programming pattern. Về RX thì đó lại là một câu chuyện khác, sẽ được giới thiệu trong bài khác. Còn ở đây, bạn chỉ chú ý tới phần State mà thôi.

Với việc sử dụng State, thì 5 tên full_name, first_name, last_name, firstNamelastName sẽ có “dây mơ rễ má” với nhau, mãi “nắm tay nhau thật chặt, giữ tay nhau thật lâu” và nếu có một đối tượng thay đổi, các đối tượng liên quan sẽ thay đổi theo. Chẳng hạn, bây giờ, tôi đổi first_name = “Huy” thì lập tức full_name = “Huy Nguyễn” ngay và luôn, đồng thời firstName cũng thay đổi text ngay tắp lự, tức firstName = “Huy”. Dĩ nhiên, last_namelastName không có gì thay đổi. Nhưng nếu bạn muốn đổi text của lastName = “Phạm”, thì lập tức last_name cũng thay đổi, tức last_name = “Phạm” và dĩ nhiên full_name = “Huy Phạm” cho nó vuông.

Total lại, việc dùng State có lợi điểm là như vậy, một đối tượng thay đổi thì các đối tượng liên quan cũng tự thay đổi theo, và điều này thậm chí còn linh động hơn cả con trỏ trong C/C++ nữa. Sự thay đổi này diễn ra ngay lập tức và không cần bất cứ hành động kích hoạt sự kiện (event trigger) nào. Chính vì ưu điểm vô cùng lớn này mà xu hướng State này ngày càng phát triển mạnh mẽ hơn. Sự thật là điều này đã có mặt trên các JavaScript frameworks nổi tiếng như Angular(JS), React, Vue hay Ember với ràng buộc dữ liệu hai chiều (two-way data bindinng), còn trên mobile thì chỉ mới được phổ biến thông qua React Native và giờ là Flutter.

2. State trong Flutter StatefulWidget:

Chỉ cần nghe qua cái tên StatefulWidget là bạn có thể hiểu ngay Widget này có State. Nói nôm na, thì StatefulWidget sẽ cần override một method có tên là <State<SW extends StatefulWidget>> SW createState(), chẳng hạn như trong ứng dụng “cho sẵn” của Flutter khi tạo project, chúng ta có _MyHomePageState createState(), trong đó, _MyHomePageStateState<MyHomePage>, còn MyHomePage extends StatefulWidget có vai trò tương đồng với MainActivity extends Activity trong Android Sdk vậy. MyHomePage đóng vai trò là một “màn hình” của ứng dụng.

Còn với góc độ cụ thể, thì StatefulWidgetWidget – đương nhiên rồi – có các member có thể thay đổi được (từ gốc là mutable). Đó là phần giải thích “chính chủ”. Còn giải thích của tôi là: StatefulWidgetWidget chấp nhận sự thay đổi bên trong nó, và nó chủ động thay đổi theo. Ở thời điểm này, bạn sẽ khá khó hiểu. Mọi thứ sẽ rõ ràng hơn khi so sánh StatefulWidgetStatelessWidget. Còn bây giờ, bạn hãy mở lại project mặc định do Flutter tạo sẵn. Bạn sẽ thấy phần Text thứ hai là Text($_count). Phần data của nó là giá trị của biến $_count. Việc cập nhật data này không thực hiện trực tiếp trên Text widget này theo kiểu findViewById như với Android Sdk, hay một @IBOutlet như iOS Sdk. Bạn sẽ thay đổi nó qua việc cập nhật $_count. Và để cập nhật $_count, bạn cũng không được phép gọi trực tiếp tới nó ngay dưới hàm _incrementCounter, mà buộc phải đặt nó trong setState(VoidCallback).

Vì sao lại phải đặt nó vào đó? Vì nếu bạn đặt nó trực tiếp nằm bên trong của _incrementCounter, thì dẫu $_count có được cập nhật thì ứng dụng cũng không cập nhật lại giao diện. Tức phần data của Text($_counter) vẫn không đổi dù bản thân $_counter có thay đổi đi nữa. Hàm setState này có nhiệm vụ thông báo cho StatefulWidget là có sự thay đổi và cần cập nhật lại giao diện cho khớp. Không có setState thì không có sự thay đổi giao diện.

Tạm thời bạn cần dành từ 30 giây tới 5 phút để đọc hiểu lại 2 đoạn trên cho thật kĩ…

Nếu bạn đang thắc mắc “Tại sao không đặt luôn setState vào trong StatefulWidget cho rồi?” thì bạn đang hiểu đúng vấn đề. Để tìm hiểu kĩ phần này, bạn sẽ cần nhắc lại những kiến thức cơ bản về Activity của Android về phần render giao diện.

Thực chất, khi xoay màn hình, thì coi như ứng dụng sẽ dựng lại các Widget, và xem như là “làm mới” lại Widget đó, sẽ render lại Widget từ những dữ liệu ban đầu. Trong Android Sdk, để khôi phục lại trạng thái cho các TextView, RadioButton, CheckBox, v.v…, bạn sẽ phải thao tác với Bundle savedInstanceState. Với Flutter thì nó làm sẵn cho bạn với tên State này. Điều này có nghĩa là, trong ví dụ mặc định cho sẵn, khi bạn xoay màn hình, thì hệ thống sẽ render lại MyHomePage, nhưng thay vì tạo một State mới toanh, thì nó sẽ tận dụng lại State cũ, lấy tất cả các giá trị của các members trong _MyHomePageState ra để render lại MyHomePage. Như vậy, bạn sẽ hiểu, là class State của một Widget sẽ được giữ nguyên để khi bản thân Widget này cần được, và thực hiện render lại, Widget sẽ sử dụng lại và tiếp State đang liên kết với nó.

Ngoài ra, State “riêng” cũng có giúp StatefulWidget chỉ render lại phần giao diện nào có thay đổi mà thôi, không phải refresh lại toàn bộ cả Widget gây tốn thời gian cũng như làm hỏng cả trải nghiệm người dùng.

Tóm lại, ở phần này, bạn sẽ cần nhớ 2 điều làm kinh nghiệm xương máu như sau:

  • Khi sử dụng một StatefulWidget, bạn cần một State<StatefulWidget> tương ứng với nó, và trỏ vào StatefulWidget#createState. Phần render giao diện sẽ do State quản lí.
  • Trong State class, để cập nhật dữ liệu được hiển thị của một Widget, cũng như những đối tượng liên quan tới một member nào đó, bạn cần dùng setState(VoidCallback).

3. StatelessWidget:

Cái tên nói lên tất cả, StatelessWidget không có State. Nó không chấp nhận sự thay đổi bên trong nó. Còn đối với sự thay đổi tác động từ bên ngoài nó, thì nó sẽ thụ động thay đổi theo. Như vậy là sao?

Có nghĩa là StatelessWidget chỉ đơn thuần nhận dữ liệu, và hiển thị dữ liệu một cách thụ động. Việc tương tác với nó không sinh ra bất kì một event nào để chính bản thân nó phải render lại. Họa chăng, việc nó phải render lại là do tác động từ bên ngoài yêu cầu nó phải render lại. Còn nội tại của nó thì không.

Chính vì vậy, nó không có liên quan gì với State cả. Bản thân nó không có hàm createState, mà thay vào đó, hàm build(BuildContext) nằm trực tiếp trong nó luôn, chứ không phải “ở nhờ” class State nào bên ngoài.

4. So sánh giữa StatefulWidget và StatelessWidget:

Có thể bạn đang thắc mắc là mình có cần tạo một StatelessWidget tương ứng với một “màn hình” nào không, thì trước mắt là không. Bởi Text(data) của bạn đã là một StatelessWidget rồi. Như bạn có thể nhận thấy, việc update data của Text(data), yêu cầu Text phải render lại, có nguyên nhân xuất phát từ bên ngoài, tức từ State của Widget “phụ huynh” của nó là _MyHomePageState yêu cầu, còn bản thân Text này không phát ra một yêu cầu nào để bản thân nó phải render lại.

Tuy nhiên, nếu bạn muốn hiểu thêm về StatelessWidget dưới dạng một “màn hình”, thì bạn có thể hình dung là bạn sẽ tạo một Button trên _MyHomePageState, mà khi được press thì _MyHomePageState sẽ gửi giá trị của $_counter qua “màn hình” mới để hiển thị đúng nó. Nếu “màn hình mới” kia chỉ có mỗi nhiệm vụ hiển thị giá trị được chuyển qua, và không có nhiệm vụ nào khác, thì bạn có thể cho “màn hình mới” extends StatelessWidget. Nhưng nếu bạn còn muốn tương tác với con số kia, xong rồi hiển thị giá trị mới đó và cuối cùng gửi lại MyHomePage hay đúng hơn là _MyHomePageState, bạn buộc phải cho “màn hình mới” extends StatefulWidget.

Tới đây, tôi mong bạn đã có cái nhìn tương đối hoàn chỉnh về State, StatefulWidgetStatelessWidget. Sang bài tiếp theo, tôi sẽ giới thiệu một số Widget thông dụng để dựng giao diện ứng dụng, hay đúng hơn là giao diện của một “màn hình”. Hẹn gặp lại các bạn sau.

2 thoughts on “Flutter, Bài 7: State và Stateful & Stateless Widgets”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.