Load more và Pull to refresh là hai tính năng gần như không thể thiếu khi chúng ta muốn hiển thị dữ liệu dưới dạng list. Mọi người hẳn đã quá quen thuộc trong việc apply các tính năng này lên ListView rồi. Vậy còn GridView thì sao? Bài này mình xin giới thiệu một trong những phương pháp để có thể đem hai tính năng này vào GridView nhé.
Chuẩn bị
Project khởi đầu: https://github.com/scitbiz/flutter_gridview_pulltorefresh_loadmore_example (checkout sang branch starter
nhé)
Sau khi clone về, chuyển qua branch starter
và chạy app lên thì giao diện sẽ như thế này:
Project này tính năng chỉ đơn giản là lấy màu từ kho data (mình có thêm chút delay để giả lập việc lấy data từ server) gồm 255 mã màu và hiển thị tên cũng như mã màu lên GridView. Hiện tại mới chỉ đang lấy và hiển thị 20 mã màu đầu tiên. Công việc của chúng ta là thêm load more để lấy thêm màu và pull to refresh để reload lại dữ liệu.
Các bạn nghía qua code một chút để nắm rõ hơn nhé
Sơ qua về code hiện tại
Chúng ta tập trung vào file lib/main.dart
, nơi sẽ render GridView ra màn hình chính. Mình có comment bên dưới ý nghĩa các thông số cho bạn nào mới làm quen với GridView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> GridView<span class="token punctuation">.</span><span class="token function">builder</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> gridDelegate<span class="token punctuation">:</span> <span class="token function">SliverGridDelegateWithFixedCrossAxisCount</span><span class="token punctuation">(</span> childAspectRatio<span class="token punctuation">:</span> <span class="token number">1.6</span><span class="token punctuation">,</span> <span class="token comment">// Tỉ lệ chiều-ngang/chiều-rộng của một item trong grid, ở đây width = 1.6 * height</span> crossAxisCount<span class="token punctuation">:</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token comment">// Số item trên một hàng ngang </span> crossAxisSpacing<span class="token punctuation">:</span> <span class="token number">16</span><span class="token punctuation">,</span> <span class="token comment">// Khoảng cách giữa các item trong hàng ngang</span> mainAxisSpacing<span class="token punctuation">:</span> <span class="token number">16</span><span class="token punctuation">,</span> <span class="token comment">// Khoảng cách giữa các hàng (giữa các item trong cột dọc)</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> itemCount<span class="token punctuation">:</span> _colors<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token comment">// Số lượng item </span> itemBuilder<span class="token punctuation">:</span> _buildColorItem<span class="token punctuation">,</span> <span class="token comment">// Hàm render item</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> |
Pull to refresh
Pull to refresh của GridView thực chất giống hệt như ListView, chỉ việc bọc Grid/List trong RefreshIndicator
và thêm hàm chạy khi refresh vào onRefresh
là xong. Tuy nhiên bài này mình xin giới thiệu một widget nữa cũng khá hay, và đồng thời tiện setup luôn để chúng ta chuẩn bị cho việc implement load more. Widget mình muốn nói tới đó là CupertinoSliverRefreshControl
. Widget này sẽ hiển thị loading giống như bên iOS, và đặc biệt hơn chúng ta có thể custom icon khi pull và khi refreshing một cách dễ dàng.
Vì Widget này là một Sliver widget, vì vậy chúng ta sẽ bọc GridView trong CustomScrollView
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> GridView<span class="token punctuation">.</span><span class="token function">builder</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> gridDelegate<span class="token punctuation">:</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">,</span> itemBuilder<span class="token punctuation">:</span> _buildColorItem<span class="token punctuation">,</span> itemCount<span class="token punctuation">:</span> _colors<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> |
Tuy nhiên lúc này IDE sẽ báo lỗi không thể sử dụng được GridView do GridView không phải là một sliver widget. Vì vậy ta đổi GridView.builder
sang SliverGrid
. SliverGrid
có thay đổi tham số một chút, thay vì truyền thẳng itemCount
, itemBuilder
, ta sẽ truyền vào cho nó một delegate
là SliverChildBuilderDelegate
. Delegate này cần truyền vào tham số là function để build item (chúng ta đã có _buildColorItem
) và số lượng item sẽ render childCount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> <span class="token function">SliverGrid</span><span class="token punctuation">(</span> gridDelegate<span class="token punctuation">:</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">,</span> delegate<span class="token punctuation">:</span> <span class="token function">SliverChildBuilderDelegate</span><span class="token punctuation">(</span> _buildColorItem<span class="token punctuation">,</span> childCount<span class="token punctuation">:</span> _colors<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> |
Vậy cái padding đâu mất rồi? SliverList không cho truyền padding, chúng ta cũng không thể đơn giản bọc nó trong Padding
được vì Padding
không phải là một sliver widget. Thay vào đó chúng ta có SliverPadding
, cách dùng tương tự Padding
thôi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> <span class="token function">SliverPadding</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> sliver<span class="token punctuation">:</span> <span class="token function">SliverGrid</span><span class="token punctuation">(</span> gridDelegate<span class="token punctuation">:</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">,</span> delegate<span class="token punctuation">:</span> <span class="token function">SliverChildBuilderDelegate</span><span class="token punctuation">(</span> _buildColorItem<span class="token punctuation">,</span> childCount<span class="token punctuation">:</span> _colors<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> |
Save và chạy lại phát, nếu trên màn hình không có gì thay đổi so với khi clone về, chúng ta đã thành công trong việc chuyển GridView sang dùng sliver rồi
Bây giờ việc thêm pull to refresh khá đơn giản, chúng ta chỉ việc thêm CupertinoSliverRefreshControl
lên trên SliverGrid và viết thêm hàm để refresh data thôi. Bạn có thể đọc thêm các thuộc tính của CupertinoSliverRefreshControl
nếu muốn custom các icon khi pull xuống/loading nhé
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | Future<span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token function">_refresh</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token punctuation">{</span> <span class="token comment">// Clear hết data cũ đi</span> _colors<span class="token punctuation">.</span><span class="token function">clear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Reset page về 1</span> _nextPage <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token comment">// Lấy data mới</span> <span class="token keyword">await</span> <span class="token function">_getColors</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token metadata symbol">@override</span> Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> <span class="token function">CupertinoSliverRefreshControl</span><span class="token punctuation">(</span> onRefresh<span class="token punctuation">:</span> _refresh <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">SliverPadding</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> sliver<span class="token punctuation">:</span> <span class="token function">SliverGrid</span><span class="token punctuation">(</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> |
Load more
Với cách làm của mình, thực chất chúng ta sẽ luôn hiển thị icon loading ở cuối của Grid, chỉ khi không còn data để load nữa thì sẽ ẩn nó đi. Vì vậy chúng ta sẽ phải làm các bước như sau:
- Thêm một state tên
loading
dạngbool
, sẽ mang giá trịtrue
nếu đang lấy data từ server vềm ngược lại làfalse
, để ngăn không cho load thêm nếu như đang load rồi - Thêm một state tên
canLoadMore
dạngbool
, sẽ mang giá trịtrue
nếu server vẫn còn data để có thể load tiếp, ngược lại làfalse
, từ đó sẽ ẩn/hiện icon loading ở cuối Grid - Thêm một
ScrollController
vàoCustomScrollView
để nắm được trạng thái scroll của Grid, từ đó sẽ quyết định có load more hay không - Hiển thị icon loadmore phía cuối Grid
Đầu tiên, có thể dễ dàng thêmloading
và canLoadMore
, set trạng thái khi lấy dữ liệu về cũng như khi refresh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span class="token keyword">class</span> <span class="token class-name">_MyHomePageState</span> <span class="token keyword">extends</span> <span class="token class-name">State</span><span class="token operator"><</span>MyHomePage<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> int _nextPage <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> bool _loading <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> bool _canLoadMore <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> Future<span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token function">_getColors</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token punctuation">{</span> _loading <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token function">setState</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>newColors<span class="token punctuation">.</span>length <span class="token operator">>=</span> _itemsPerPage<span class="token punctuation">)</span> <span class="token punctuation">{</span> _nextPage<span class="token operator">++</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> _canLoadMore <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> _loading <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> Future<span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token function">_refresh</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">async</span> <span class="token punctuation">{</span> _canLoadMore <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> |
Sau đó tạo một ScrollController
và detect khi nào sẽ load thêm dữ liệu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <span class="token keyword">class</span> <span class="token class-name">_MyHomePageState</span> <span class="token keyword">extends</span> <span class="token class-name">State</span><span class="token operator"><</span>MyHomePage<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">const</span> double _endReachedThreshold <span class="token operator">=</span> <span class="token number">200</span><span class="token punctuation">;</span> <span class="token comment">// Khi chỉ còn cách phía dưới Grid 200dp thì sẽ load more</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">final</span> ScrollController _controller <span class="token operator">=</span> <span class="token function">ScrollController</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token metadata symbol">@override</span> <span class="token keyword">void</span> <span class="token function">initState</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> _controller<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>_onScroll<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">_onScroll</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>_controller<span class="token punctuation">.</span>hasClients <span class="token operator">||</span> _loading<span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span> <span class="token comment">// Chỉ chạy những dòng dưới nếu như controller đã được mount vào widget và đang không loading</span> <span class="token keyword">final</span> thresholdReached <span class="token operator">=</span> _controller<span class="token punctuation">.</span>position<span class="token punctuation">.</span>extentAfter <span class="token operator"><</span> _endReachedThreshold<span class="token punctuation">;</span> <span class="token comment">// Check xem đã đạt tới _endReachedThreshold chưa</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>thresholdReached<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Load more!</span> <span class="token function">_getColors</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token metadata symbol">@override</span> Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> controller<span class="token punctuation">:</span> _controller<span class="token punctuation">,</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Cuối cùng, ta thêm icon loading vào bên dưới Grid. Ta dùng SliverToBoxAdapter
để biến widget thường thành sliver widget và dùng state canLoadMore
để ẩn/hiện loading
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token metadata symbol">@override</span> Widget <span class="token function">build</span><span class="token punctuation">(</span>BuildContext context<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> body<span class="token punctuation">:</span> <span class="token function">CustomScrollView</span><span class="token punctuation">(</span> controller<span class="token punctuation">:</span> _controller<span class="token punctuation">,</span> slivers<span class="token punctuation">:</span> <span class="token operator"><</span>Widget<span class="token operator">></span><span class="token punctuation">[</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token function">SliverPadding</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> sliver<span class="token punctuation">:</span> <span class="token function">SliverGrid</span><span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">SliverToBoxAdapter</span><span class="token punctuation">(</span> child<span class="token punctuation">:</span> _canLoadMore <span class="token operator">?</span> <span class="token function">Container</span><span class="token punctuation">(</span> padding<span class="token punctuation">:</span> EdgeInsets<span class="token punctuation">.</span><span class="token function">only</span><span class="token punctuation">(</span>bottom<span class="token punctuation">:</span> <span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span> alignment<span class="token punctuation">:</span> Alignment<span class="token punctuation">.</span>center<span class="token punctuation">,</span> child<span class="token punctuation">:</span> <span class="token function">CircularProgressIndicator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token function">SizedBox</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> |
Vậy là xong rồi. Cũng không có gì phức tạp nhỉ
Bạn có thể tham khảo source code hoàn chỉnh tại https://github.com/scitbiz/flutter_gridview_pulltorefresh_loadmore_example, branch master
nhé