openplanning

Hướng dẫn và ví dụ Flutter SkeletonLoader

  1. Cài đặt thư viện
  2. SkeletonLoader
  3. Ví dụ Skeleton
  4. Ví dụ đầy đủ
Trong khi phát triển một ứng dụng di động, trải nghiệm của người dùng là vô cùng quan trọng. Thời gian tải chậm và màn hình trống rỗng làm người dùng thất vọng.
Để tạo ra cảm giác mượt mà và hấp dẫn hơn cho người dùng bạn có thể sử dụng Flutter Skeleton Loader. Màn hình sẽ hiển thị một khung xương giao diện trong khi dữ liệu thực đang được tải.
  • Flutter Skeletons

1. Cài đặt thư viện

pubspec.yaml
dependencies: 
  skeleton_loader:

2. SkeletonLoader

SkeletonLoader chỉ có duy nhất một constructor:
SkeletonLoader({
  Key? key,
  int items = 1,
  required Widget builder,
  Color baseColor = const Color(0xFFE0E0E0),
  Color highlightColor = const Color(0xFFF5F5F5),
  SkeletonDirection direction = SkeletonDirection.ltr,
  Duration period = const Duration(seconds: 2),
});

3. Ví dụ Skeleton

Đầu tiên, chúng ta cần thiết kế giao diện cho một "Skeleton Loader Item".
city_skeleton_item_widget.dart
import 'package:flutter/material.dart';

class CitySkeletonItemWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Color bgColor = Colors.white;
    return ListTile(
      leading: Container(
        width: 100,
        height: 80,
        decoration: BoxDecoration(color: bgColor),
      ),
      title: Container(
        margin: const EdgeInsets.symmetric(vertical: 1),
        decoration: BoxDecoration(color: bgColor),
        child: const Text(''),
      ),
      subtitle: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            width: double.infinity,
            margin: const EdgeInsets.symmetric(vertical: 1),
            decoration: BoxDecoration(color: bgColor),
            child: const Text(''),
          ),
          Container(
            margin: const EdgeInsets.symmetric(vertical: 1),
            width: double.infinity,
            decoration: BoxDecoration(color: bgColor),
            child: const Text(''),
          ),
        ],
      ),
    );
  }
}
SkeletonLoader sẽ hiển thị các "Items" của nó trên một cột.
SkeletonLoader(
  items: 3, // <---------------------------------
  highlightColor: Colors.blue.shade200,
  direction: SkeletonDirection.ltr,
  builder: CitySkeletonItemWidget(), // <--------
)
skeleton_screen_ex1.dart
import 'package:flutter/material.dart';
import 'package:skeleton_loader/skeleton_loader.dart';

import 'city_data_model.dart';
import 'city_skeleton_item_widget.dart';
import 'city_widget.dart';

class SkeletonScreenEx1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SkeletonLoader Demo 1"),
        backgroundColor: Colors.indigo.withAlpha(180),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
        child: _buildSkeletonLoader(), // <------------
      ),
    );
  }

  Widget _buildSkeletonLoader() {
    return SingleChildScrollView(
      child: SkeletonLoader(
        items: 3, // <---------------------------------
        highlightColor: Colors.blue.shade200,
        direction: SkeletonDirection.ltr,
        builder: CitySkeletonItemWidget(),
      ),
    );
  }
}
Bạn có thể kết hợp RowSkeletonLoader để mô phỏng một cái gì đó giống một GridView đang trong quá trình tải dữ liệu.
skeleton_screen_ex2.dart
import 'package:flutter/material.dart';
import 'package:skeleton_loader/skeleton_loader.dart';

import 'city_data_model.dart';
import 'city_skeleton_item_widget.dart';
import 'city_widget.dart';

class SkeletonScreenEx2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SkeletonLoader Demo 2"),
        backgroundColor: Colors.indigo.withAlpha(180),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
        child: _buildSkeletonLoader(), // <------------
      ),
    );
  }

  Widget _buildSkeletonLoader() {
    return SingleChildScrollView(
      child: Row(
        children: [
          Expanded(
            child: SkeletonLoader(
              items: 3, // <----------------------------
              highlightColor: Colors.blue.shade200,
              direction: SkeletonDirection.ltr,
              builder: CitySkeletonItemWidget(),
            ),
          ),
          Expanded(
            child: SkeletonLoader(
              items: 3, // <----------------------------
              highlightColor: Colors.blue.shade200,
              direction: SkeletonDirection.ltr,
              builder: CitySkeletonItemWidget(),
            ),
          ),
        ],
      ),
    );
  }
}

4. Ví dụ đầy đủ

Mô hình dữ liệu được sử dụng trong ví dụ này:
city_data_model.dart
class CityModel {
  final String name;
  final String imageName;
  final String population;
  final String country;

  CityModel({
    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<CityModel> allCities = [
  CityModel(
      name: "Delhi",
      country: "India",
      population: "19 mill",
      imageName: "delhi.png"),
  CityModel(
      name: "London",
      country: "Britain",
      population: "8 mill",
      imageName: "london.png"),
  CityModel(
      name: "Vancouver",
      country: "Canada",
      population: "2.4 mill",
      imageName: "vancouver.png"),
  CityModel(
      name: "New York",
      country: "USA",
      population: "8.1 mill",
      imageName: "newyork.png"),
  CityModel(
      name: "Paris",
      country: "France",
      population: "2.2 mill",
      imageName: "paris.png"),
  CityModel(
      name: "Berlin",
      country: "Germany",
      population: "3.7 mill",
      imageName: "berlin.png"),
];
city_widget.dart
import 'package:flutter/material.dart';

import 'city_data_model.dart';

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

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Image.network(
          city.imageUrl,
          fit: BoxFit.cover,
          width: 100.0,
        ),
        title: Text(
          city.name,
          style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
        ),
        subtitle: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(city.country),
            Text('Population: ${city.population}'),
          ],
        ),
      ),
    );
  }
}
city_screen.dart
import 'package:flutter/material.dart';
import 'package:skeleton_loader/skeleton_loader.dart';

import 'city_data_model.dart';
import 'city_skeleton_item_widget.dart';
import 'city_widget.dart';

class CityScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CityScreenState();
}

class _CityScreenState extends State<CityScreen> {
  List<CityModel> cityModels = [];
  bool isLoading = true;

  @override
  void initState() {
    super.initState();
    //
    initCityModelData();
  }

  void initCityModelData() {
    Future.delayed(
      const Duration(seconds: 8),
      () {
        cityModels = allCities;
        isLoading = false;
        setState(() {});
      },
    );
  }

  @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: isLoading // <-------------------------------------
            ? _buildSkeletonLoader(context) // <------------------
            : _buildListView(context), // <-----------------------
      ),
    );
  }

  Widget _buildSkeletonLoader(BuildContext context) {
    return SingleChildScrollView(
      child: SkeletonLoader(
        items: 5,
        highlightColor: Colors.blue.shade200,
        direction: SkeletonDirection.ltr,
        builder: CitySkeletonItemWidget(),
      ),
    );
  }

  Widget _buildListView(BuildContext context) {
    return ListView.builder(
      itemCount: cityModels.length,
      itemBuilder: (BuildContext context, int index) {
        CityModel city = cityModels[index];
        return CityWidget(city);
      },
      padding: const EdgeInsets.all(0.0),
    );
  }
}
main_city.dart
import 'package:flutter/material.dart';
import 'city_screen.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,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: CityScreen(),
    );
  }
}

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

Show More