Flutter

Flutter PageView & PageController

Trong bài ImageSlider, tôi có giới thiệu sơ lược về PageView, vốn là Widget tương đồng với ViewPager trong Android Support Libs. Hôm nay, tôi sẽ giới thiệu thêm về Widget này, cũng như PageController là class của PageView.controller.

1. PageView

Trong trường hợp bạn chưa biết PageView là gì, bạn hãy xem bài viết trước về ImageSlider. PageView class có 3 constructors, nhưng trong bài viết này thì tôi chỉ nói sơ lược về 2 constructors là PageView và PageView.builder. Thoạt nhìn bạn có thể đoán được chúng tương tự như ListView và ListView.builder. 2 constructors này chỉ khác nhau ở phần định nghĩa các Widget con, còn các props còn lại là hoàn toàn giống nhau.

Với PageView, bạn trực tiếp chỉ định các children với prop PageView.children, còn với PageView.builder, bạn sẽ gián tiếp định nghĩa children Widgets theo từng index chạy từ 0 tới itemCount. Điều này, một lần nữa, là tương tự như ListView.builder. Tôi cho rằng bạn đã quá quen thuộc với thao tác dựng Widget con nên sẽ chỉ giới thiệu các props quan trọng khác:

  • controller: Định nghĩa một PageController có tác dụng “control” PageView hiện tại. Chi tiết về PageController sẽ được trình bày bên dưới.
  • onPageChanged: Là một callback thuộc typedef ValueChanged<int> được gọi khi vị trí hiện tại của PageView thay đổi, hay nói gần gũi hơn là người dùng vuốt chuyển sang trang trước, với tham số duy nhất trong callback là vị trí mới của PageView.
  • pageSnapping: Là một bool (mặc định là true) mà PageView có tự động snap (kéo vừa khít) trang mới hay không.
  • reverse: Là một bool (mặc định là false) chỉ thứ tự các trang. Nếu true, PageView sẽ sắp xếp các trang từ cuối trở về đầu.
  • scrollDirection: Chỉ hướng cuộn, giá trị hoặc là Axis.horizontal (mặc định – cuộn ngang) hoặc Axis.vertical (cuộn dọc).

Ngoài ra, chúng ta còn 2 props nữa là physics và sliverChildDelegate. Phần sliverChildDelegate tương tự như ListView và nếu dùng PageView.custom thì bạn cần trực tiếp chỉ định nó. Còn physics thuộc class ScrollPhysics định nghĩa các giá trị vật lí cho các Widget có thể cuộn được. Prop physics trong đại đa số các trường hợp đều được giữ mặc định, và bạn chỉ nên tự định nghĩa physics khi kiến thức Flutter của bạn đã vô cùng chắc chắn.

Trong bài về ImageSlider, tôi sử dụng constructor PageView và dùng List<String>.map.toList để dựng các children Widgets. Nhưng tôi cũng có thể dùng PageView.builder như sau:

2. PageController

Như các bạn có thể thấy trong các PageView constructors, bạn không có cách nào chỉ định trang mặc định. Chẳng hạn, với ImageSlider, bạn muốn mặc định PageView sẽ hiển thị từ trang số 3 thay vì trang số 0 đầu tiên. Có cách nào thực hiện được không?

Hóa ra trang mặc định được định nghĩa gián tiếp trong PageView controller. Prop này nhận instance thuộc class PageController. Class này có 1 constructor duy nhất chứa các props sau:

  • initialPage: Chỉ số trang mặc định. Nếu bạn muốn mặc định PageView sẽ hiển thị từ trang số 3 thay vì 0, thì bạn định nghĩa ở đây.
  • keepPage: Một bool chỉ định PageView sẽ giữ trạng thái các Widget con của nó sau khi hiển thị lần đầu tiên hay mỗi lần hiển thị lại một Widget con là mỗi lầ PageView sẽ phải render lại từ đầu. Giá trị mặc định, theo bạn có thể nhận ra, là true.
  • viewPortFraction: Tỉ lệ giữa kích thước sẽ hiển thị của mỗi trang so với kích thước của PageView. Mặc định, theo các bạn có thể quan sát được, thì giá trị này là 1.0, tức là kích thước của mỗi trang đúng bằng kích thước của PageView.

Ngoài ra, chúng ta còn một getter page dưới dạng read-only prop. Nó trả về vị trí trang hiện tại của PageView. Tuy nhiên, trên thực tế, bạn ít khi nên sử dụng page, vì thứ nhất, giá trị mà nó trả về là một double (tức có thể là 0.0, 2.0 nhưng cũng có thể là 1.4, 2.7), trong trường hợp bạn muốn xác định chính xác PageView đang hoặc sẽ chuyển sang page nào sẽ khá phức tạp. Vì vậy, nếu bạn chỉ muốn xác định trang hiện tại dưới dạng số nguyên (int), bạn nên sử dụng PageController.initialPage và PageView.onPageChanged: (int newPage) { }.

Tuy nhiên, không phải giá trị page này là hoàn toàn vô ích, bởi nếu chỉ để “trưng bày cho xôm tụ” thì chi bằng Flutter team xóa nó khỏi Flutter Sdk luôn cho đỡ chật đất. Ứng dụng thường gặp nhất của nó là để tạo hiệu ứng khi chuyển trang và nếu không có nó thì sẽ rất khó khăn cho những hiệu ứng phức tạp cần phải đo khoảng cách giữa những lần di chuyển ngón tay.

Do bạn không thực hiện thao tác gọi hàm từ Widget như trong Android và iOS Sdk, vì vậy, để yêu cầu PageView thực hiện bất kì thao tác nào qua code, mà đại đa số các trường hợp là thao tác chuyển trang, bạn phải “nhờ vả” tới PageController này qua các methods của nó, bao gồm:

  • animateToPage(int page, { Duration duration, Curve curve })
  • nextPage({ Duration duration, Curve curve })
  • previousPage({ Duration duration, Curve curve })

Ba hàm trên là các hàm thường gặp nhất để chuyển trang qua code. Ở đây, curve đóng vai trò tương tự như interpolation trong Android: Chỉ định tốc độ theo điểm thời gian theo đồ thị. Vì Curve là một abstract class, nên bạn phải định nghĩa một Curve gián tiếp bằng cách extends Curve hoặc các class đã được impl Curve sẵn mà Flutter Sdk cung cấp như Cubic, ElasticInCurve, ElasticInOutCurve, ElasticOutCurve, FlippedCurve, Interval, SawTooth, Threshold… hoặc thông dụng và dễ dàng hơn nhiều là các constants về Curve được định nghĩa sẵn trong Curves class.

Ngoài ra, nếu bạn muốn chuyển trang mà không dùng hiệu ứng thì có thể dùng method jumpToPage(int page). Còn hai methods khác là attach(ScrollPosition position) và createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) tạm thời bạn không nên thay đổi trừ khi kiến thức về ScrollPhyscis của bạn đã đủ tốt.

Và cuối cùng, nhìn có vẻ PageController khá chi tiết, tuy nhiên khi vào làm thực tế, hầu như tôi phải extends PageController nếu phải chỉ định PageView.controller, mà lí do đầu tiên là cần tính hiệu số giữa vị trí page mới và page cũ. Ngoài ra, nếu PageView đang nằm trong một StatefulWidget, và nội dung mỗi trang có sự thay đổi cần liên quan tới state, thì việc extends PageController để thêm vào nhiều methods để quản lí state của Widget là một ý tưởng không tồi. Chúc các bạn code vui vẻ.

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.