Flutter

Một số chia sẻ về Flutter Stateless và StatefulWidget

Tôi đã nói về StatelessWidget và StatefulWidget trong loạt bài hướng dẫn sơ lược về Flutter. Đa số các bạn đã hiểu về State và khi nào nên dùng StatefulWidget, khi nào chỉ cần dùng StatelessWidget là đủ yêu cầu. Tuy nhiên, đa phần các bạn chỉ dùng hai loại Widget trên khi tạo một “màn hình” mới, một vài bạn khác, có lẽ do muốn học kiểu “mì ăn liền” nên đôi khi có những vận dụng “sai be bét” rồi lại mang khắp các chốn phố phường đi hỏi, sau khi nhận được câu trả lời thì lập tức “xóa hết đi em, như chưa từng có bao giờ”. Vì vậy, tôi chia sẻ chút kinh nghiệm của mình về công dụng hữu ích của hai tên Widget này, ngoài việc dùng chúng để tạo một màn hình mới.

1. Nhắc lại về State

Xin nhắc lại phần kiến thức quan trọng nhất về State: Thay vì thực hiện cập nhật nội dung một giao diện (UI)View bằng cách gọi hàm trực tiếp từ chính (UI)View đó, với State, bạn thay đổi trực tiếp phần nội dung luôn. Chẳng hạn, để thay đổi phần text của một (UI)TextView, bạn gọi hàm setText (.text setter) từ (UI)TextView đó, còn với Flutter Text(data), bạn chỉ cần nhẹ nhàng thay đổi data trong setState mà thôi.

Quan trọng hơn hết, tất cả những thay đổi phải nằm trong setState. Chẳng hạn, setState(() => data = “New Text”). Nếu bạn chỉ gán data = “New Text” không nằm trong setState, sẽ không có gì xảy ra trên giao diện cả. Hàm setState có nhiệm vụ thông báo với lớp giao diện là cần phải render lại (ở những nơi nào cần). Hôm trước, có một bạn muốn đổi trang của PageView với các nút nhấn, bạn tăng/giảm trực tiếp PageView.controller.initialPage và kết quả là không có gì xảy ra cả. Dĩ nhiên, bạn ấy mang đi hỏi khắp chợ xem ai có giúp đỡ cho bạn được không. Ở đây, tôi chỉ ra hai cái sai của bạn, mà xuất phát điểm có lẽ là do bạn lười đọc hướng dẫn:

  • PageController.initialPage đúng như tên gọi của nó, là trang mặc định khi PageView được khởi tạo. Chi tiết về nó, xin bạn tham khảo bài viết về PageController. Điều đáng nói ở đây, là giá trị này chỉ sử dụng một lần duy nhất khi PageView được khởi tạo mà thôi. Về sau, bạn có tăng giảm initialPage cả trăm lần thì PageController và PageView cũng có gì thay đổi, vì có ai truy cập tới nó nữa đâu.
  • Giả sử bạn có thể cuộn trang với initialPage, thì để giao diện (tức Widget tree) cập nhật lại, bao gồm thao tác chuyển trang, thì bạn phải thực hiện tăng giảm trong setState. Rõ ràng, PageController không có State và bản thân nó cũng không phải là Widget. Vậy, đào đâu ra setState để bạn gọi?

Tóm lại, nếu bạn muốn học và làm việc với Flutter, State là phần lí thuyết bạn không thể không hiểu. Vì vậy, bạn vui lòng học lại lí thuyết nếu cảm thấy mình chưa hiểu rõ về nó.

2. StatelessWidget và StatefulWidget

Cái tên nói lên tất cả. StatelessWidget là Widget không có State, còn StatefulWidget là Widget có State. Điểm chung của hai Widget này, là bản thân chúng có hàm build(BuildContext) return Widget. Như vậy, bản thân StatelessWidget và StatefulWidget có vai trò chứa đựng tập hợp các Widgets khác bên trong, và dĩ nhiên chúng được tổ chức theo hướng Widget tree. Vai trò này thể hiện rõ nhất với StatelessWidget. Chẳng hạn, bạn có một tổ hợp Widget khá phức tạp như sau, và cần được tái sử dụng nhiều lần:

Và mỗi lần sử dụng, bạn sẽ phải mất công cóp pát lại. Ngoài ra, có thể một số thành phần trong một Widget con có thể thay đổi, chẳng hạn như TextStyle bên trên, có thể có trường hợp màu chữ phải là màu xanh lá, xanh dương hay vàng, và mỗi lần như vậy là mỗi lần vô cùng vất vả. Để đơn giản hóa, bạn có thể cho chúng vào một StatelessWidget, với phần Widget tree phức tạp trên nằm trong build(BuildContext) của StatelessWidget vừa tạo là được. Do đó, StatefulWidget và StatelessWidget có thể nằm bất kì nơi nào trong Widget tree của bạn, không nhất thiết phải là một “màn hình” mới.

Với StatefulWidget thì mọi chuyện hơi khác một chút. Bản thân StatefulWidget không có hàm build như trên, mà hàm build lại nằm trong một <StateImpl extends State<W extends StatefulWidget>>, chẳng hạn:

Vì sao lại cần phải tạo riêng một class State như vậy? Vì StatefulWidget đóng vai trò “giữ chỗ”, và nội dung của nó do phần State này đảm nhận. Khi cần render lại nội dung, được gọi từ State.setState(VoidCallback), thì phần State sẽ báo cho StatefulWidget những nơi nào cần được thay đổi, và StatefulWidget chỉ thay đổi ở những nơi đó mà thôi.

Vậy, khi nào dùng StatelessWidget và khi nào dùng StatefulWidget? Câu hỏi này khiến nhiều bạn với bắt tay vào Flutter mà không có kinh nghiệm với React và React Native khá khó trả lời. Trong bài trước, tôi có giải thích qua, nhưng có một số bạn vẫn chưa hiểu được ý, nên lần này tôi sẽ nói cụ thể hơn một chút. Và trước khi đọc tiếp, xin mời bạn “ghi lòng tạc dạ” là muốn cập nhật UI thì cần phải gọi hàm State.setState(VoidCallback), tức là nằm trong một class extends State.

Bạn chỉ sử dụng StatefulWidget khi một Widget trong Widget tree (sẽ được nằm ở State.build(BuildContext)) có phát ra (emit) một event để yêu cầu thay đổi giá trị của một biến. Và bao giờ cũng vậy, event đó phải gọi tới State.setState để thông báo cho class State là cần render lại nội dung, và từ đó StatefulWidget sẽ thay đổi. Còn trong những trường hợp không có event, thì bạn dùng StatelessWidget. Chẳng hạn, tôi có một Widget tree như sau:

Nếu bạn không có ý định thay đổi data từ “Text” sang một chuỗi khác thì bạn có thể đặt Widget tree trên vào một StatelessWidget. Tuy nhiên, nếu bạn muốn thay đổi giá trị của data và buộc Text(data) phải render lại, tức chuyển sang hiển thị nội dung data mới, thì bạn bắt buộc phải đặt Widget tree trên vào một State của một StatefulWidget. Chẳng hạn, tôi bỏ cặp dấu comment của RaisedButton, và định nghĩa hàm toggleText như sau:

Thì khi nhấn vào RaisedButton, trên giao diện không có gì xảy ra cả. Vì mặc dù data có thay đổi, nhưng bạn không yêu cầu State thông báo cho StatefulWidget chủ của nó phải render lại. Hệ quả là “trước sau như một” và nhiều bạn sẽ vò đầu bứt tai không hiểu tại sao lại không có gì xảy ra trên giao diện. Để giao diện thay đổi, thì bạn phải cho tất cả những thay đổi nằm trong thân của VoidCallback trong State.setState(VoidCallback) như sau:

Như vậy, thì khi bạn nhấn vào RaisedButton, phần text của Text(data) hiển thị trên màn hình mới có thay đổi theo giá trị của data. Nếu không có setState thì không có thay đổi gì trên giao diện, dù giá trị của data có thay đổi như nào đi nữa.

3. Chia sẻ thêm về State

Trong đại đa số các ví dụ về State và setState, tôi thường đặt trường hợp gán giá trị mới cho một String, chẳng hạn như ví dụ bên trên. Tuy nhiên, không phải lúc nào bạn cũng phải thực hiện việc gán giá trị mới. Chẳng hạn, tôi muốn thêm vào một phần tử vào ListView:

Hi vọng bài viết này giúp được các bạn bớt đi những khó khăn khi lựa chọn giữa StatefulWidget và StatelessWidget. Nếu bạn còn có thêm thắc mắc nào, hãy để lại bình luận bên dưới.

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.