🎨 开源鸿蒙 Flutter 实战|用户详情页骨架屏加载效果完整实现

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

【摘要】本文面向开源鸿蒙跨平台开发新手,基于 Flutter 框架实现用户详情页的骨架屏加载效果,包含骨架屏组件封装、页面状态管理、点击事件修复、深色模式适配等核心内容,完整覆盖功能实现、问题排查、鸿蒙适配要点与虚拟机实机运行验证,代码可直接复制复用,有效提升应用用户

之前给首页列表加了骨架屏,这次直接把用户详情页也安排上了!不仅实现了完整的详情页骨架屏布局,还踩坑修复了用户卡片点击无响应的问题,全程用的都是 OpenHarmony 官方兼容库,已经在鸿蒙虚拟机完整验证,点击用户卡片就能跳转,骨架屏加载超丝滑!

先给大家汇报一下这次的核心成果✨:
✅ 封装用户详情页专属骨架屏组件
✅ 完整模拟真实页面布局结构
✅ 修复用户卡片点击无响应的问题
✅ 深色 / 浅色模式自动适配
✅ 加载完成后平滑过渡到实际内容
✅ 鸿蒙虚拟机实机验证,交互完全正常
✅ 代码结构清晰,新手也能看懂、能修改

一、技术选型说明
全程使用 OpenHarmony 官方兼容清单内的稳定库,无额外依赖,完全规避兼容风险:
兼容清单
二、核心功能实现
2.1 第一步:扩展骨架屏组件库
在之前的lib/widgets/shimmer_skeleton.dart基础上,新增用户详情页专属骨架屏组件:

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

/// 基础骨架屏组件
class ShimmerSkeleton extends StatelessWidget {
  final double width;
  final double height;
  final double radius;
  final bool isDarkMode;

  const ShimmerSkeleton({
    super.key,
    required this.width,
    required this.height,
    this.radius = 8,
    required this.isDarkMode,
  });

  
  Widget build(BuildContext context) {
    return Shimmer.fromColors(
      baseColor: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!,
      highlightColor: isDarkMode ? Colors.grey[700]! : Colors.grey[100]!,
      period: const Duration(milliseconds: 1500),
      child: Container(
        width: width,
        height: height,
        decoration: BoxDecoration(
          color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
          borderRadius: BorderRadius.circular(radius),
        ),
      ),
    );
  }
}

/// 用户详情页骨架屏
class UserDetailSkeleton extends StatelessWidget {
  final bool isDarkMode;
  const UserDetailSkeleton({super.key, required this.isDarkMode});

  
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // 头部区域
          Container(
            height: 200,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: isDarkMode
                    ? [Colors.grey[800]!, Colors.grey[900]!]
                    : [Colors.purple[100]!, Colors.purple[50]!],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // 头像骨架
                ShimmerSkeleton(
                  width: 100,
                  height: 100,
                  radius: 50,
                  isDarkMode: isDarkMode,
                ),
                const SizedBox(height: 16),
                // 用户名骨架
                ShimmerSkeleton(
                  width: 150,
                  height: 24,
                  radius: 12,
                  isDarkMode: isDarkMode,
                ),
                const SizedBox(height: 8),
                // 简介骨架
                ShimmerSkeleton(
                  width: 200,
                  height: 16,
                  radius: 8,
                  isDarkMode: isDarkMode,
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),
          // 信息卡片区域
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: GridView.count(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              crossAxisCount: 2,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
              children: List.generate(4, (index) {
                return Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        ShimmerSkeleton(
                          width: 40,
                          height: 40,
                          radius: 20,
                          isDarkMode: isDarkMode,
                        ),
                        const SizedBox(height: 12),
                        ShimmerSkeleton(
                          width: 60,
                          height: 16,
                          radius: 8,
                          isDarkMode: isDarkMode,
                        ),
                        const SizedBox(height: 4),
                        ShimmerSkeleton(
                          width: 40,
                          height: 20,
                          radius: 10,
                          isDarkMode: isDarkMode,
                        ),
                      ],
                    ),
                  ),
                );
              }),
            ),
          ),
          const SizedBox(height: 24),
          // 个人简介区域
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    ShimmerSkeleton(
                      width: 80,
                      height: 20,
                      radius: 10,
                      isDarkMode: isDarkMode,
                    ),
                    const SizedBox(height: 12),
                    ...List.generate(3, (index) {
                      return Padding(
                        padding: const EdgeInsets.only(bottom: 8),
                        child: ShimmerSkeleton(
                          width: double.infinity,
                          height: 14,
                          radius: 7,
                          isDarkMode: isDarkMode,
                        ),
                      );
                    }),
                  ],
                ),
              ),
            ),
          ),
          const SizedBox(height: 24),
        ],
      ),
    );
  }
}

2.2 第二步:修改用户详情页
将用户详情页从StatelessWidget改为StatefulWidget,添加加载状态控制:

// 导入骨架屏组件
import 'widgets/shimmer_skeleton.dart';

// 用户详情页
class UserDetailPage extends StatefulWidget {
  final GitHubUser user;
  const UserDetailPage({super.key, required this.user});

  
  State<UserDetailPage> createState() => _UserDetailPageState();
}

class _UserDetailPageState extends State<UserDetailPage> {
  bool _isLoading = true;

  
  void initState() {
    super.initState();
    // 模拟网络请求延迟
    Future.delayed(const Duration(milliseconds: 800), () {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    });
  }

  
  Widget build(BuildContext context) {
    final isDarkMode = Theme.of(context).brightness == Brightness.dark;
    
    // 加载中:显示骨架屏
    if (_isLoading) {
      return Scaffold(
        appBar: AppBar(title: const Text("用户详情")),
        body: UserDetailSkeleton(isDarkMode: isDarkMode),
      );
    }

    // 加载完成:显示实际内容
    return Scaffold(
      appBar: AppBar(title: Text(widget.user.login)),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 头部区域
            Container(
              height: 200,
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: isDarkMode
                      ? [Colors.grey[800]!, Colors.grey[900]!]
                      : [Colors.purple[100]!, Colors.purple[50]!],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Hero(
                    tag: 'avatar-${widget.user.id}',
                    child: CircleAvatar(
                      radius: 50,
                      backgroundImage: NetworkImage(widget.user.avatarUrl),
                    ),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    widget.user.login,
                    style: const TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    widget.user.htmlUrl,
                    style: TextStyle(
                      fontSize: 14,
                      color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            // 信息卡片区域
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: GridView.count(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                crossAxisCount: 2,
                mainAxisSpacing: 12,
                crossAxisSpacing: 12,
                children: [
                  _buildInfoCard(Icons.people, "关注者", "1.2k", isDarkMode),
                  _buildInfoCard(Icons.person_add, "正在关注", "256", isDarkMode),
                  _buildInfoCard(Icons.folder, "仓库", "42", isDarkMode),
                  _buildInfoCard(Icons.location_on, "位置", "中国", isDarkMode),
                ],
              ),
            ),
            const SizedBox(height: 24),
            // 个人简介区域
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        "个人简介",
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 12),
                      Text(
                        "这是一位热爱开源的开发者,专注于Flutter和开源鸿蒙跨平台开发,喜欢分享技术心得,欢迎一起交流学习~",
                        style: TextStyle(
                          fontSize: 14,
                          color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
                          height: 1.6,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoCard(IconData icon, String label, String value, bool isDarkMode) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, size: 32, color: Theme.of(context).primaryColor),
            const SizedBox(height: 12),
            Text(
              label,
              style: TextStyle(
                fontSize: 14,
                color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
              ),
            ),
            const SizedBox(height: 4),
            Text(
              value,
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

2.3 第三步:修复用户卡片点击无响应问题
问题原因:
之前的代码中,_UserCardContent组件里的AnimatedCard的onTap回调是空的() {},这会拦截点击事件,导致外层的OpenContainer无法接收到点击事件,从而无法打开用户详情页。

修复方案:
1.给_UserCardContent添加onTap参数
2.将OpenContainer的openContainer回调传递给_UserCardContent
3.在AnimatedCard的onTap中正确调用传入的回调

// 修改前
closedBuilder: (context, openContainer) => _UserCardContent(user: user),

class _UserCardContent extends StatelessWidget {
  final GitHubUser user;
  
  const _UserCardContent({required this.user});
  
  
  Widget build(BuildContext context) {
    return AnimatedCard(
      onTap: () {},  // 空回调拦截了点击事件
      // ...
    );
  }
}

// 修改后
closedBuilder: (context, openContainer) => _UserCardContent(
  user: user,
  onTap: openContainer,  // 传递 openContainer 回调
),

class _UserCardContent extends StatelessWidget {
  final GitHubUser user;
  final VoidCallback onTap;  // 新增参数
  
  const _UserCardContent({required this.user, required this.onTap});
  
  
  Widget build(BuildContext context) {
    return AnimatedCard(
      onTap: onTap,  // 正确调用 openContainer
      // ...
    );
  }
}

三、开源鸿蒙平台适配要点
3.1 动画性能优化
使用shimmer库的官方稳定版 3.0.0,在鸿蒙设备上渲染性能最好
骨架屏动画时长控制在 1500ms,符合开源鸿蒙系统视觉规范
加载完成后直接切换到实际内容,避免不必要的动画叠加

3.2 深色模式适配
通过Theme.of(context).brightness判断当前主题模式,自动调整骨架屏的baseColor和highlightColor,确保在深色模式下有足够的对比度,无视觉异常。

3.3 状态管理注意事项
用户详情页改为StatefulWidget后,要在initState中模拟加载延迟
使用mounted判断,避免页面销毁后调用setState导致的异常
加载延迟控制在 800ms 左右,既能展示骨架屏效果,又不会让用户等待太久

3.4 权限说明
所有功能均为纯 UI 实现,无需申请任何开源鸿蒙系统权限,直接接入即可使用。

四、开源鸿蒙虚拟机运行验证
一键运行命令

cd ohos
hvigorw assembleHap -p product=default -p buildMode=debug
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b com.example.demo1

Flutter 开源鸿蒙用户详情页骨架屏 - 虚拟机全屏运行验证

运行验证截图

效果:应用在开源鸿蒙虚拟机全屏稳定运行,所有功能正常,无闪退、无渲染异常

五、新手学习总结
作为大一新生,这次用户详情页骨架屏的实现真的让我收获满满!不仅学会了如何封装复杂页面的骨架屏,还踩坑修复了点击事件拦截的问题,而且全程都能完美兼容开源鸿蒙平台,成就感直接拉满🥰

这次开发也让我明白了:骨架屏的布局要尽量和真实页面一致,这样用户体验才会连贯。手势事件的嵌套一定要注意,内层的空回调会拦截外层的点击事件,这个坑在鸿蒙平台上会更明显。页面状态管理要注意mounted判断,避免页面销毁后调用setState导致的异常

后续我还会继续优化:
✅ 实现更多页面的骨架屏
✅ 优化骨架屏的动画效果
✅ 对接真实后端接口实现动态加载
✅ 增加更多用户详情页的功能

也会持续给大家分享我的鸿蒙 Flutter 新手实战内容,和大家一起在开源鸿蒙的生态里慢慢进步✨

如果这篇文章有帮到你,或者你也有更好的骨架屏实现思路,欢迎在评论区和我交流呀!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐