openplanning

Flutter GridView với SliverGridDelegate tuỳ biến

  1. Fixed Child Height + Min Child Width
  2. Fixed Child Height + Fixed Column Count
  3. CityData + CityWidget
GridView có một vài constructor với tham số gridDelegate. Đây là một đối tượng uỷ quyền để kiểm soát việc các widget con được bố trí thế nào trên GridView.
GridView({
  required SliverGridDelegate gridDelegate,
  List<Widget> children = const <Widget>[], 
  …
});


GridView.custom({
  required SliverGridDelegate gridDelegate,
  required SliverChildDelegate childrenDelegate,
  …
});

GridView.builder({ 
  required SliverGridDelegate gridDelegate,
  required Widget? Function(BuildContext, int) itemBuilder,
  …
});
Một gridDelegate tuỳ biến có thể kiểm soát kích thước của các widget con và số lượng cột hoặc hàng của GridView. Trong bài viết này chúng ta sẽ tạo một vài ví dụ với các gridDelegate tuỳ biến.

1. Fixed Child Height + Min Child Width

Trong ví dụ này chúng ta sẽ tạo một GridView thẳng đứng với các widget con có chiều cao cố định và chiều rộng lớn hơn một giá trị được chỉ định. Số cột của GridView sẽ tự động thay đổi theo chiều rộng của nó. Đây là trường hợp sử dụng phổ biến của GridView.
Chúng ta sẽ tạo một SliverGridDelegate với các thuộc tính quan trọng:
childMainAxisExtent
Chiều cao cố định của các widget con (Trường hợp GridView thẳng đứng).
Chiều rộng cố định của các widget con (Trường hợp GridView nằm ngang).
minChildCrossAxisExtent
Chiều rộng nhỏ nhất của các widget con (Trong trường hợp GridView thẳng đứng).
Chiều cao nhỏ nhất của các widget con (Trong trường hợp GridView nằm ngang).
custom_slivergriddelegate_1.dart
import 'package:flutter/material.dart';
import 'package:flutter/src/rendering/sliver.dart';
import 'package:flutter/src/rendering/sliver_grid.dart';

class ChildFixedMainExtentAndMinCrossExtentSGD extends SliverGridDelegate {
  const ChildFixedMainExtentAndMinCrossExtentSGD({
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    required this.minChildCrossAxisExtent,
    required this.childMainAxisExtent,
  })  : assert(minChildCrossAxisExtent != null && minChildCrossAxisExtent > 0),
        assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
        assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
        assert(childMainAxisExtent != null && childMainAxisExtent > 0);

  /// The number of logical pixels between each child along the main axis.
  final double mainAxisSpacing;

  /// The number of logical pixels between each child along the cross axis.
  final double crossAxisSpacing;

  /// This is height of child widget (Vertical GridView)
  /// or width of child widget (Horizontal GridView).
  final double childMainAxisExtent;

  /// This is min width of child widget (Vertical GridView).
  /// or min height of child widget (Horizontal GridView).
  final double minChildCrossAxisExtent;

  bool _debugAssertIsValid() {
    assert(minChildCrossAxisExtent > 0);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childMainAxisExtent > 0.0);
    return true;
  }

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    assert(_debugAssertIsValid()); 

    int crossAxisCount = (constraints.crossAxisExtent + crossAxisSpacing) ~/
        (minChildCrossAxisExtent + crossAxisSpacing);

    final double usableCrossAxisExtent =
        constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);

    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    //
    return SliverGridRegularTileLayout(
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

  @override
  bool shouldRelayout(ChildFixedMainExtentAndMinCrossExtentSGD oldDelegate) {
    return oldDelegate.mainAxisSpacing != mainAxisSpacing ||
        oldDelegate.crossAxisSpacing != crossAxisSpacing ||
        oldDelegate.childMainAxisExtent != childMainAxisExtent ||
        oldDelegate.minChildCrossAxisExtent != minChildCrossAxisExtent;
  }
}
Ví dụ sử dụng SliverGridDelegate tuỳ biến ở trên:
main_ex1.dart
import 'package:flutter/material.dart';

import 'custom_slivergriddelegate_1.dart';
import 'city_data.dart';
import 'city_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Cites"),
        backgroundColor: Colors.indigo.withAlpha(180),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
        child: _buildGridView(context),
      ),
    );
  }

  Widget _buildGridView(BuildContext context) {
    return GridView.builder(
      itemCount: allCities.length,
      padding: const EdgeInsets.all(10.0),
      gridDelegate:
          const ChildFixedMainExtentAndMinCrossExtentSGD(
        crossAxisSpacing: 5,
        mainAxisSpacing: 5,
        childMainAxisExtent: 200.0,
        minChildCrossAxisExtent: 180,
      ),
      itemBuilder: (BuildContext context, int index) {
        return CityWidget(allCities[index]);
      },
    );
  }
}

2. Fixed Child Height + Fixed Column Count

Trong ví dụ này chúng ta sẽ tạo một GridView thẳng đứng với số cột cố định và các widget con có chiều cao cố định. Chiều rộng của widget con sẽ thay đổi theo chiều rộng của GridView.
Chúng ta sẽ tạo một SliverGridDelegate với các thuộc tính quan trọng:
crossAxisCount
Số lượng cột của GridView (Trong trường hợp GridView thẳng đứng).
Số lượng hàng của GridView (Trong trường hợp GridView nằm ngang).
childMainAxisExtent
Chiều cao của các widget con (Trong trường hợp GridView thẳng đứng).
Chiều rộng của các widget con (Trong trường hợp GridView nằm ngang).
custom_slivergriddelegate_2.dart
import 'package:flutter/material.dart';
import 'package:flutter/src/rendering/sliver.dart';
import 'package:flutter/src/rendering/sliver_grid.dart';

class ChildFixedMainExtentFixedCrossCountSGD extends SliverGridDelegate {
  const ChildFixedMainExtentFixedCrossCountSGD({
    required this.crossAxisCount,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    required this.childMainAxisExtent,
  })  : assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
        assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
        assert(childMainAxisExtent != null && childMainAxisExtent > 0);

  /// The number of children in the cross axis.
  final int crossAxisCount;

  /// The number of logical pixels between each child along the main axis.
  final double mainAxisSpacing;

  /// The number of logical pixels between each child along the cross axis.
  final double crossAxisSpacing;

  /// This is height of child widget (Vertical GridView)
  /// or width of child widget (Horizontal GridView).
  final double childMainAxisExtent;

  bool _debugAssertIsValid() {
    assert(crossAxisCount > 0);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childMainAxisExtent > 0.0);
    return true;
  }

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    assert(_debugAssertIsValid());
    final double usableCrossAxisExtent =
        constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);

    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;

    return SliverGridRegularTileLayout(
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

  @override
  bool shouldRelayout(ChildFixedMainExtentFixedCrossCountSGD oldDelegate) {
    return oldDelegate.crossAxisCount != crossAxisCount ||
        oldDelegate.mainAxisSpacing != mainAxisSpacing ||
        oldDelegate.crossAxisSpacing != crossAxisSpacing ||
        oldDelegate.childMainAxisExtent != childMainAxisExtent;
  }
}
Ví dụ sử dụng SliverGridDelegate tuỳ biến ở trên:
main_ex2.dart
import 'package:flutter/material.dart';

import 'custom_slivergriddelegate_2.dart';
import 'city_data.dart';
import 'city_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Cites"),
        backgroundColor: Colors.indigo.withAlpha(180),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
        child: _buildGridView(context),
      ),
    );
  }

  Widget _buildGridView(BuildContext context) {
    return GridView.builder(
      itemCount: allCities.length,
      padding: const EdgeInsets.all(10.0),
      gridDelegate: const ChildFixedMainExtentFixedCrossCountSGD(
        crossAxisSpacing: 5,
        mainAxisSpacing: 5,
        childMainAxisExtent: 200.0,
        crossAxisCount: 2,
      ),
      itemBuilder: (BuildContext context, int index) {
        return CityWidget(allCities[index]);
      },
    );
  }
}

3. CityData + CityWidget

Dữ liệu các thành phố được sử dụng trong các ví dụ trên.
city_data.dart
class City {
  final String name;
  final String imageName;
  final String population;
  final String country;

  City(
      {required this.name,
      required this.country,
      required this.population,
      required this.imageName});

  String get imageUrl {
    return 'https://raw.githubusercontent.com/o7planning/rs/master/flutter/city/$imageName';
  }
}

List<City> allCities = [
  City(
      name: "Delhi",
      country: "India",
      population: "19 mill",
      imageName: "delhi.png"),
  City(
      name: "London",
      country: "Britain",
      population: "8 mill",
      imageName: "london.png"),
  City(
      name: "Vancouver",
      country: "Canada",
      population: "2.4 mill",
      imageName: "vancouver.png"),
  City(
      name: "New York",
      country: "USA",
      population: "8.1 mill",
      imageName: "newyork.png"),
  City(
      name: "Paris",
      country: "France",
      population: "2.2 mill",
      imageName: "paris.png"),
  City(
      name: "Berlin",
      country: "Germany",
      population: "3.7 mill",
      imageName: "berlin.png"),
];
Thiết kế giao diện cho các widget con của GridView.
city_widget.dart
import 'package:flutter/material.dart';

import 'city_data.dart';

class CityWidget extends StatelessWidget {
  final City city;
  const CityWidget(this.city, {super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: GridTile(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Image.network(
                city.imageUrl,
                fit: BoxFit.cover,
                height: 100.0,
              ),
              Text(
                city.name,
                style: const TextStyle(
                  fontSize: 16.0,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(city.country),
              Text('Population: ${city.population}'),
            ],
          ),
        ),
      ),
    );
  }
}

Các hướng dẫn lập trình Flutter

Show More