欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的旅行推荐轮播卡片应用。项目中使用 Flutter 第三方库 carousel_slider 实现卡片轮播效果,让多个旅行地点以横向滑动卡片的形式展示。

最终运行效果如下:

在这里插入图片描述
在这里插入图片描述

页面主要包含以下内容:

  • 顶部标题栏;
  • 旅行推荐说明卡片;
  • 自动播放轮播卡片;
  • 当前轮播位置指示器;
  • 上一张、下一张控制按钮;
  • 当前景点详情展示;
  • 推荐地点列表;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

本文重点是演示如何在 Flutter for OpenHarmony 项目中使用 Flutter 第三方库 carousel_slider。项目代码写在 lib/main.dart 中,依赖配置写在 pubspec.yaml 中,符合 Flutter for OpenHarmony 第三方库实践方向。


前言

在移动应用开发中,轮播图是非常常见的页面组件。例如首页 Banner、商品推荐、活动宣传、旅游景点展示、新闻头条、课程推荐等,都可以使用轮播卡片来实现。

如果只使用普通列表展示内容,页面会比较平铺直叙。轮播组件可以在有限空间内展示多条重点信息,并且可以通过自动播放吸引用户注意力。

本文选择使用 Flutter 第三方库 carousel_slider 实现轮播卡片。它可以快速创建横向滑动轮播,并支持自动播放、无限滚动、居中放大、自定义卡片内容等功能。

本项目以“旅行推荐轮播卡片应用”为例,使用 carousel_slider 展示多个旅行地点,并通过 Flutter 页面状态更新展示当前选中的景点详情。


一、项目目标

本次实践主要实现以下目标:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 carousel_slider
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 carousel_slider
  • 使用 CarouselSlider.builder() 构建轮播卡片;
  • 使用 CarouselOptions 配置自动播放和页面切换;
  • 使用 CarouselSliderController 控制上一张和下一张;
  • 实现轮播指示器;
  • 实现当前景点详情展示;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 carousel_slider
功能场景 轮播图 / 推荐卡片 / 首页展示
核心组件 CarouselSlider / CarouselOptions / CarouselSliderController
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 carousel_slider

在实际应用中,轮播组件的使用场景非常多。例如:

  • 首页 Banner;
  • 活动宣传页;
  • 商品推荐页;
  • 新闻头条页;
  • 课程推荐页;
  • 旅行景点推荐;
  • 音乐专辑推荐;
  • 电影海报展示;
  • 应用功能介绍页。

如果完全自己使用 PageView 实现轮播,也可以完成基本滑动效果。但如果还要加入自动播放、无限滚动、居中放大、控制按钮和页面回调,就需要写更多逻辑。

carousel_slider 已经封装了常见轮播功能,可以让开发者更快完成轮播类页面。

在本项目中,carousel_slider 主要完成以下工作:

  • 构建横向轮播卡片;
  • 支持自动播放;
  • 支持无限循环;
  • 支持当前卡片居中放大;
  • 支持页面切换回调;
  • 支持通过控制器切换上一张和下一张。

四、创建 Flutter for OpenHarmony 项目

在已经配置好 Flutter for OpenHarmony 开发环境的前提下,可以创建一个 Flutter 项目。

示例项目名称:

flutter create travel_carousel_demo

进入项目目录:

cd travel_carousel_demo

项目创建完成后,主要关注两个文件:

travel_carousel_demo
 ├── pubspec.yaml
 └── lib
     └── main.dart

其中:

文件 作用
pubspec.yaml 配置 Flutter 项目依赖
lib/main.dart 编写 Flutter 页面和业务逻辑

五、添加 carousel_slider 第三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中添加 carousel_slider

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  carousel_slider: ^5.1.2

完整结构大致如下:

name: travel_carousel_demo
description: A Flutter for OpenHarmony carousel slider demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  carousel_slider: ^5.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

添加完成后,在终端执行:

flutter pub get

执行成功后,就可以在 Dart 代码中使用 carousel_slider 了。


六、项目结构

本项目主要修改 lib/main.dart 文件:

lib
 └── main.dart

本项目不需要编写 OpenHarmony 原生 ArkTS 页面,也不需要修改 Index.ets

因为这是 Flutter for OpenHarmony 项目,页面主体应该是 Flutter 代码。审核重点会看:

  • 是否使用 pubspec.yaml 添加 Flutter 第三方库;
  • 是否在 Dart 文件中 import package
  • 是否在 lib/main.dart 中实际调用第三方库;
  • 是否属于 Flutter for OpenHarmony 项目。

七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 carousel_slider
  2. main.dart 中引入第三方库;
  3. 定义旅行地点数据模型;
  4. 使用 CarouselSlider.builder() 构建轮播卡片;
  5. 使用 CarouselOptions 设置自动播放、无限滚动和居中放大;
  6. 使用 onPageChanged 记录当前轮播下标;
  7. 使用小圆点展示当前轮播位置;
  8. 使用 CarouselSliderController 控制上一张和下一张;
  9. 根据当前下标展示景点详情。

第三方库引入代码如下:

import 'package:carousel_slider/carousel_slider.dart';

轮播组件核心代码如下:

CarouselSlider.builder(
  itemCount: _places.length,
  itemBuilder: (context, index, realIndex) {
    return _buildCarouselItem(_places[index]);
  },
  options: CarouselOptions(
    height: 230,
    autoPlay: true,
    enlargeCenterPage: true,
    onPageChanged: (index, reason) {
      setState(() {
        _currentIndex = index;
      });
    },
  ),
)

八、main.dart 完整代码

打开文件:

lib/main.dart

将其中内容替换为下面代码:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Travel Carousel Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const TravelHomePage(),
    );
  }
}

class TravelPlace {
  const TravelPlace({
    required this.title,
    required this.city,
    required this.tag,
    required this.description,
    required this.icon,
    required this.color,
  });

  final String title;
  final String city;
  final String tag;
  final String description;
  final IconData icon;
  final Color color;
}

class TravelHomePage extends StatefulWidget {
  const TravelHomePage({super.key});

  
  State<TravelHomePage> createState() => _TravelHomePageState();
}

class _TravelHomePageState extends State<TravelHomePage> {
  final CarouselSliderController _carouselController =
      CarouselSliderController();

  final List<TravelPlace> _places = const [
    TravelPlace(
      title: '海边日落',
      city: '厦门',
      tag: '治愈 / 拍照',
      description: '适合傍晚散步、看海、拍照,也适合安排轻松的短途旅行。',
      icon: Icons.wb_twilight,
      color: Colors.orange,
    ),
    TravelPlace(
      title: '古镇小巷',
      city: '苏州',
      tag: '人文 / 慢生活',
      description: '适合体验江南水乡氛围,感受老街、小桥和传统建筑。',
      icon: Icons.house,
      color: Colors.teal,
    ),
    TravelPlace(
      title: '山间露营',
      city: '杭州',
      tag: '自然 / 户外',
      description: '适合周末放松、轻徒步和露营,能暂时远离城市节奏。',
      icon: Icons.terrain,
      color: Colors.green,
    ),
    TravelPlace(
      title: '城市夜景',
      city: '上海',
      tag: '都市 / 夜游',
      description: '适合夜间散步、观景和拍摄城市灯光,交通也比较方便。',
      icon: Icons.location_city,
      color: Colors.indigo,
    ),
    TravelPlace(
      title: '博物馆路线',
      city: '南京',
      tag: '学习 / 历史',
      description: '适合喜欢历史文化的游客,可以安排一天的城市文化路线。',
      icon: Icons.museum,
      color: Colors.deepPurple,
    ),
  ];

  int _currentIndex = 0;

  TravelPlace get _currentPlace {
    return _places[_currentIndex];
  }

  void _goToPrevious() {
    _carouselController.previousPage(
      duration: const Duration(milliseconds: 350),
      curve: Curves.easeInOut,
    );
  }

  void _goToNext() {
    _carouselController.nextPage(
      duration: const Duration(milliseconds: 350),
      curve: Curves.easeInOut,
    );
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('旅行推荐轮播'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildIntroCard(theme),
            const SizedBox(height: 16),
            _buildCarouselCard(theme),
            const SizedBox(height: 16),
            _buildDetailCard(theme),
            const SizedBox(height: 16),
            _buildPlaceListCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildIntroCard(ThemeData theme) {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Container(
              width: 76,
              height: 76,
              decoration: BoxDecoration(
                color: theme.colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                Icons.travel_explore,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            Text(
              'Flutter for OpenHarmony',
              style: theme.textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              '使用 carousel_slider 构建旅行地点推荐轮播页面',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCarouselCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20),
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      '热门旅行推荐',
                      style: theme.textTheme.titleLarge?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  Text(
                    '${_currentIndex + 1} / ${_places.length}',
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 16),
            CarouselSlider.builder(
              carouselController: _carouselController,
              itemCount: _places.length,
              itemBuilder: (context, index, realIndex) {
                return _buildCarouselItem(theme, _places[index]);
              },
              options: CarouselOptions(
                height: 230,
                viewportFraction: 0.82,
                enlargeCenterPage: true,
                enlargeFactor: 0.22,
                enableInfiniteScroll: true,
                autoPlay: true,
                autoPlayInterval: const Duration(seconds: 3),
                autoPlayAnimationDuration:
                    const Duration(milliseconds: 800),
                autoPlayCurve: Curves.fastOutSlowIn,
                onPageChanged: (index, reason) {
                  setState(() {
                    _currentIndex = index;
                  });
                },
              ),
            ),
            const SizedBox(height: 16),
            _buildIndicator(theme),
            const SizedBox(height: 16),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20),
              child: Row(
                children: [
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: _goToPrevious,
                      icon: const Icon(Icons.arrow_back),
                      label: const Text('上一张'),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _goToNext,
                      icon: const Icon(Icons.arrow_forward),
                      label: const Text('下一张'),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCarouselItem(ThemeData theme, TravelPlace place) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 4),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(24),
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            place.color.withOpacity(0.88),
            place.color.withOpacity(0.58),
          ],
        ),
        boxShadow: [
          BoxShadow(
            color: place.color.withOpacity(0.22),
            blurRadius: 18,
            offset: const Offset(0, 10),
          ),
        ],
      ),
      child: Padding(
        padding: const EdgeInsets.all(22),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(
              place.icon,
              size: 52,
              color: Colors.white,
            ),
            const Spacer(),
            Text(
              place.title,
              style: theme.textTheme.headlineSmall?.copyWith(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 6),
            Text(
              place.city,
              style: theme.textTheme.titleMedium?.copyWith(
                color: Colors.white.withOpacity(0.92),
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 10),
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 6,
              ),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.18),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                place.tag,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: Colors.white,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildIndicator(ThemeData theme) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(_places.length, (index) {
        final bool active = index == _currentIndex;

        return AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          width: active ? 22 : 8,
          height: 8,
          margin: const EdgeInsets.symmetric(horizontal: 4),
          decoration: BoxDecoration(
            color: active
                ? theme.colorScheme.primary
                : theme.colorScheme.surfaceContainerHighest,
            borderRadius: BorderRadius.circular(8),
          ),
        );
      }),
    );
  }

  Widget _buildDetailCard(ThemeData theme) {
    final TravelPlace place = _currentPlace;

    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              width: 58,
              height: 58,
              decoration: BoxDecoration(
                color: place.color.withOpacity(0.16),
                borderRadius: BorderRadius.circular(18),
              ),
              child: Icon(
                place.icon,
                color: place.color,
                size: 30,
              ),
            ),
            const SizedBox(width: 14),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '当前推荐:${place.title}',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 6),
                  Text(
                    '${place.city} · ${place.tag}',
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.primary,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    place.description,
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                      height: 1.5,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPlaceListCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '推荐地点列表',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            ...List.generate(_places.length, (index) {
              final TravelPlace place = _places[index];
              final bool selected = index == _currentIndex;

              return Container(
                margin: const EdgeInsets.only(bottom: 10),
                child: InkWell(
                  borderRadius: BorderRadius.circular(16),
                  onTap: () {
                    _carouselController.animateToPage(
                      index,
                      duration: const Duration(milliseconds: 350),
                      curve: Curves.easeInOut,
                    );
                    setState(() {
                      _currentIndex = index;
                    });
                  },
                  child: Container(
                    padding: const EdgeInsets.all(14),
                    decoration: BoxDecoration(
                      color: selected
                          ? place.color.withOpacity(0.12)
                          : theme.colorScheme.surfaceContainerHighest,
                      borderRadius: BorderRadius.circular(16),
                      border: Border.all(
                        color: selected ? place.color : Colors.transparent,
                        width: 1.2,
                      ),
                    ),
                    child: Row(
                      children: [
                        Icon(
                          place.icon,
                          color: place.color,
                        ),
                        const SizedBox(width: 12),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                place.title,
                                style: theme.textTheme.titleMedium?.copyWith(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              const SizedBox(height: 4),
                              Text(
                                '${place.city} · ${place.tag}',
                                style: theme.textTheme.bodySmall?.copyWith(
                                  color: theme.colorScheme.onSurfaceVariant,
                                ),
                              ),
                            ],
                          ),
                        ),
                        if (selected)
                          Icon(
                            Icons.check_circle,
                            color: place.color,
                          ),
                      ],
                    ),
                  ),
                ),
              );
            }),
          ],
        ),
      ),
    );
  }

  Widget _buildLibraryCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '第三方库说明',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            _buildInfoRow(
              theme,
              title: '库名称',
              value: 'carousel_slider',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value: "import 'package:carousel_slider/carousel_slider.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value: 'CarouselSlider / CarouselOptions / CarouselSliderController',
            ),
            _buildInfoRow(
              theme,
              title: '应用场景',
              value: '首页 Banner、活动推荐、商品展示、旅行推荐、内容轮播',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(
    ThemeData theme, {
    required String title,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 82,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 carousel_slider 第三方库

代码开头引入第三方库:

import 'package:carousel_slider/carousel_slider.dart';

这说明项目确实使用了 Flutter 第三方库,而不是 OpenHarmony 原生库。

本项目中主要使用以下组件和类:

CarouselSlider
CarouselOptions
CarouselSliderController

其中:

组件或类 作用
CarouselSlider 构建轮播组件
CarouselOptions 配置轮播参数
CarouselSliderController 手动控制轮播切换

2. 定义旅行地点数据模型

项目中定义了旅行地点模型:

class TravelPlace {
  const TravelPlace({
    required this.title,
    required this.city,
    required this.tag,
    required this.description,
    required this.icon,
    required this.color,
  });

  final String title;
  final String city;
  final String tag;
  final String description;
  final IconData icon;
  final Color color;
}

字段说明如下:

字段 作用
title 景点标题
city 所在城市
tag 景点标签
description 景点说明
icon 页面图标
color 卡片主题色

这样页面可以根据数据动态生成轮播卡片。


3. 使用 CarouselSlider.builder 构建轮播

轮播组件核心代码如下:

CarouselSlider.builder(
  itemCount: _places.length,
  itemBuilder: (context, index, realIndex) {
    return _buildCarouselItem(theme, _places[index]);
  },
  options: CarouselOptions(...),
)

其中:

参数 作用
itemCount 轮播项数量
itemBuilder 构建每一个轮播卡片
options 配置轮播效果

使用 CarouselSlider.builder() 的好处是可以根据数据列表动态生成页面,不需要手动写多个重复卡片。


4. 使用 CarouselOptions 配置轮播效果

本项目中使用了以下配置:

CarouselOptions(
  height: 230,
  viewportFraction: 0.82,
  enlargeCenterPage: true,
  enlargeFactor: 0.22,
  enableInfiniteScroll: true,
  autoPlay: true,
  autoPlayInterval: const Duration(seconds: 3),
)

参数说明如下:

参数 作用
height 轮播区域高度
viewportFraction 每张卡片占屏幕宽度比例
enlargeCenterPage 是否放大中间卡片
enlargeFactor 中间卡片放大程度
enableInfiniteScroll 是否开启无限循环
autoPlay 是否自动播放
autoPlayInterval 自动播放间隔

通过这些配置,可以让轮播卡片具有自动切换和居中突出效果。


5. 监听当前轮播下标

轮播切换时,通过 onPageChanged 获取当前下标:

onPageChanged: (index, reason) {
  setState(() {
    _currentIndex = index;
  });
}

当轮播页面发生变化时,_currentIndex 会更新。页面中的指示器和详情卡片也会跟着变化。


6. 实现轮播指示器

指示器通过 List.generate() 生成多个小圆点:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: List.generate(_places.length, (index) {
    final bool active = index == _currentIndex;

    return AnimatedContainer(...);
  }),
)

当前选中的圆点会变长,其他圆点保持较短,用来提示用户当前处于第几张卡片。


7. 使用 CarouselSliderController 控制上一张和下一张

页面中创建控制器:

final CarouselSliderController _carouselController =
    CarouselSliderController();

上一张按钮调用:

_carouselController.previousPage(
  duration: const Duration(milliseconds: 350),
  curve: Curves.easeInOut,
);

下一张按钮调用:

_carouselController.nextPage(
  duration: const Duration(milliseconds: 350),
  curve: Curves.easeInOut,
);

这样用户除了等待自动播放,也可以手动切换卡片。


8. 点击列表切换到指定轮播项

推荐地点列表中,点击某个地点后调用:

_carouselController.animateToPage(
  index,
  duration: const Duration(milliseconds: 350),
  curve: Curves.easeInOut,
);

这样可以直接跳转到指定轮播卡片。

同时更新当前下标:

setState(() {
  _currentIndex = index;
});

这样详情区域和选中状态可以同步更新。


十、运行项目

完成代码后,在终端执行:

flutter pub get

然后连接 OpenHarmony 设备或启动 OpenHarmony 模拟器。

查看设备:

flutter devices

运行项目:

flutter run

如果环境配置正确,应用会运行到 OpenHarmony 设备或模拟器中。

运行成功后,页面会显示“旅行推荐轮播”。页面中的旅行卡片会自动播放,也可以点击“上一张”“下一张”按钮进行手动切换。


十一、开发中遇到的问题

1. carousel_slider 依赖没有生效

如果代码中出现找不到 carousel_slider 的问题,可以检查 pubspec.yaml 中是否添加了:

carousel_slider: ^5.1.2

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。


2. import 导入报错

如果下面代码报错:

import 'package:carousel_slider/carousel_slider.dart';

通常有几种原因:

  • pubspec.yaml 中没有添加依赖;
  • 没有执行 flutter pub get
  • YAML 缩进错误;
  • 包名写错;
  • 编辑器没有刷新依赖。

其中 YAML 缩进最容易出问题。依赖必须写在 dependencies 下面,并且缩进要正确。


3. 轮播卡片没有显示

如果轮播卡片没有显示,可以检查:

  • CarouselSlider 是否有固定高度;
  • itemCount 是否大于 0;
  • itemBuilder 是否返回了有效组件;
  • 页面是否被其他组件遮挡;
  • 是否正确导入第三方库。

本项目中通过下面代码设置高度:

CarouselOptions(
  height: 230,
)

如果不给轮播组件足够空间,页面可能无法正常显示。


4. 自动播放没有生效

如果轮播没有自动播放,可以检查是否设置了:

autoPlay: true

同时可以设置自动播放间隔:

autoPlayInterval: const Duration(seconds: 3)

这样轮播会每隔 3 秒自动切换一次。


5. 上一张和下一张按钮无效

如果按钮点击后没有效果,可以检查:

  • 是否创建了 CarouselSliderController
  • 是否把控制器传给了 CarouselSlider
  • 按钮中是否调用了 previousPage()nextPage()

正确绑定方式如下:

CarouselSlider.builder(
  carouselController: _carouselController,
  ...
)

6. 指示器没有同步变化

如果指示器没有跟着轮播变化,可以检查是否在 onPageChanged 中调用了:

setState(() {
  _currentIndex = index;
});

Flutter 页面状态变化后,需要使用 setState() 通知页面刷新。


7. 运行不到 OpenHarmony 设备

如果项目无法运行到 OpenHarmony 设备或模拟器,可以检查:

  • Flutter for OpenHarmony 环境是否配置完成;
  • 设备是否连接成功;
  • flutter devices 是否能识别设备;
  • 是否执行了 flutter pub get
  • 是否选择了正确的运行设备;
  • 项目是否为 Flutter 项目,而不是原生鸿蒙项目。

如果 flutter devices 都识别不到设备,那应该先处理环境问题,而不是盯着轮播代码反复怀疑人生。


十二、本文和原生鸿蒙项目的区别

本文是 Flutter for OpenHarmony 第三方库实践,不是 OpenHarmony 原生 ArkTS 项目。

主要区别如下:

对比项 本文写法 原生鸿蒙写法
UI 技术 Flutter ArkUI
主要语言 Dart ArkTS
页面入口 lib/main.dart Index.ets
依赖配置 pubspec.yaml oh-package.json5
依赖安装 flutter pub get ohpm install
第三方库 carousel_slider OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / CarouselSlider @Entry / @Component

因此本文符合 Flutter for OpenHarmony 第三方库实践方向。


十三、总结

本篇完成了一个基于 carousel_slider 的 Flutter for OpenHarmony 旅行推荐轮播卡片应用。项目通过 Flutter 第三方库实现轮播卡片效果,并结合页面状态更新展示当前景点详情和轮播指示器。

通过本次实践,我主要完成了以下内容:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 carousel_slider 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入 carousel_slider
  • 使用 CarouselSlider.builder() 构建轮播卡片;
  • 使用 CarouselOptions 设置自动播放、无限滚动和居中放大;
  • 使用 CarouselSliderController 控制上一张和下一张;
  • 使用 onPageChanged 监听当前轮播下标;
  • 使用 Flutter Material 组件构建推荐页面;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础轮播应用,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

后续可以在这个基础上继续扩展,例如:

  • 添加真实景点图片;
  • 添加网络图片加载;
  • 添加景点详情页跳转;
  • 添加收藏功能;
  • 添加搜索功能;
  • 添加城市筛选;
  • 添加旅行计划列表;
  • 添加地图定位;
  • 添加暗色主题;
  • 添加本地数据保存。

整体来看,carousel_slider 可以帮助 Flutter 开发者快速实现轮播展示效果。通过这个项目,可以理解 Flutter for OpenHarmony 中第三方库依赖配置、轮播组件使用和页面状态同步之间的基本关系。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐