Flutter & OpenHarmony 运动App运动挑战组件开发

前言

运动挑战是激励用户突破自我、保持运动热情的有效方式。通过设定有时限的挑战目标,用户可以在竞争和自我超越中获得成就感。本文将详细介绍如何在Flutter与OpenHarmony平台上实现运动挑战组件,包括挑战创建、进度追踪、排行榜、奖励机制等功能模块的完整实现方案。

Flutter挑战数据模型

dart 复制代码
class Challenge {
  final String id;
  final String name;
  final String description;
  final ChallengeType type;
  final double targetValue;
  final double currentValue;
  final DateTime startDate;
  final DateTime endDate;
  final String? imageUrl;
  final int participantCount;
  final bool isJoined;
  
  Challenge({
    required this.id,
    required this.name,
    required this.description,
    required this.type,
    required this.targetValue,
    this.currentValue = 0,
    required this.startDate,
    required this.endDate,
    this.imageUrl,
    this.participantCount = 0,
    this.isJoined = false,
  });
  
  double get progress => (currentValue / targetValue * 100).clamp(0, 100);
  bool get isCompleted => currentValue >= targetValue;
  bool get isActive => DateTime.now().isAfter(startDate) && DateTime.now().isBefore(endDate);
  int get daysRemaining => endDate.difference(DateTime.now()).inDays;
  
  String get targetText {
    switch (type) {
      case ChallengeType.distance: return '${targetValue.toInt()} 公里';
      case ChallengeType.duration: return '${targetValue.toInt()} 分钟';
      case ChallengeType.workouts: return '${targetValue.toInt()} 次';
      case ChallengeType.steps: return '${targetValue.toInt()} 步';
      case ChallengeType.calories: return '${targetValue.toInt()} 千卡';
    }
  }
}

enum ChallengeType { distance, duration, workouts, steps, calories }

挑战数据模型定义了挑战的完整结构。支持距离、时长、次数、步数和卡路里五种挑战类型。progress计算完成百分比,isActive判断挑战是否在进行中,daysRemaining计算剩余天数。participantCount记录参与人数,增加社交竞争感。targetText根据类型返回格式化的目标文字。这种模型支持多样化的挑战形式。

OpenHarmony挑战服务

typescript 复制代码
import http from '@ohos.net.http';

class ChallengeService {
  async getActiveChallenges(): Promise<Array<object>> {
    let httpRequest = http.createHttp();
    
    try {
      let response = await httpRequest.request(
        'https://api.fitness.com/challenges/active',
        { method: http.RequestMethod.GET }
      );
      
      if (response.responseCode === 200) {
        let result = JSON.parse(response.result as string);
        return result['challenges'] || [];
      }
      return [];
    } finally {
      httpRequest.destroy();
    }
  }
  
  async joinChallenge(challengeId: string, userId: string): Promise<boolean> {
    let httpRequest = http.createHttp();
    
    try {
      let response = await httpRequest.request(
        'https://api.fitness.com/challenges/join',
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({ challengeId: challengeId, userId: userId }),
        }
      );
      return response.responseCode === 200;
    } finally {
      httpRequest.destroy();
    }
  }
  
  async updateProgress(challengeId: string, userId: string, value: number): Promise<void> {
    let httpRequest = http.createHttp();
    
    try {
      await httpRequest.request(
        'https://api.fitness.com/challenges/progress',
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({ challengeId: challengeId, userId: userId, value: value }),
        }
      );
    } finally {
      httpRequest.destroy();
    }
  }
}

挑战服务与服务器交互管理挑战数据。getActiveChallenges获取当前进行中的挑战列表,joinChallenge让用户加入挑战,updateProgress在运动结束后更新挑战进度。这种服务设计支持社交化的挑战功能,用户可以参与官方挑战或好友发起的挑战。

Flutter挑战卡片组件

dart 复制代码
class ChallengeCard extends StatelessWidget {
  final Challenge challenge;
  final VoidCallback onTap;
  final VoidCallback? onJoin;
  
  const ChallengeCard({Key? key, required this.challenge, required this.onTap, this.onJoin}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (challenge.imageUrl != null)
              Image.network(challenge.imageUrl!, height: 120, width: double.infinity, fit: BoxFit.cover),
            Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(child: Text(challenge.name, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
                      Container(
                        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: challenge.isActive ? Colors.green : Colors.grey,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(challenge.isActive ? '进行中' : '已结束', style: TextStyle(color: Colors.white, fontSize: 12)),
                      ),
                    ],
                  ),
                  SizedBox(height: 8),
                  Text(challenge.description, style: TextStyle(color: Colors.grey), maxLines: 2),
                  SizedBox(height: 12),
                  Row(
                    children: [
                      Icon(Icons.flag, size: 16, color: Colors.grey),
                      SizedBox(width: 4),
                      Text('目标: ${challenge.targetText}'),
                      Spacer(),
                      Icon(Icons.people, size: 16, color: Colors.grey),
                      SizedBox(width: 4),
                      Text('${challenge.participantCount}人参与'),
                    ],
                  ),
                  if (challenge.isJoined) ...[
                    SizedBox(height: 12),
                    LinearProgressIndicator(value: challenge.progress / 100),
                    SizedBox(height: 4),
                    Text('进度: ${challenge.progress.toStringAsFixed(0)}%  剩余${challenge.daysRemaining}天'),
                  ] else if (challenge.isActive && onJoin != null) ...[
                    SizedBox(height: 12),
                    SizedBox(
                      width: double.infinity,
                      child: ElevatedButton(onPressed: onJoin, child: Text('加入挑战')),
                    ),
                  ],
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

挑战卡片组件展示挑战的详细信息。顶部显示挑战封面图,下方显示名称、状态标签、描述、目标和参与人数。已加入的挑战显示进度条和剩余天数,未加入的显示加入按钮。状态标签使用绿色表示进行中,灰色表示已结束。这种卡片设计信息丰富,引导用户参与挑战。

Flutter挑战排行榜

dart 复制代码
class ChallengeLeaderboard extends StatelessWidget {
  final List<LeaderboardEntry> entries;
  final String currentUserId;
  
  const ChallengeLeaderboard({Key? key, required this.entries, required this.currentUserId}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      itemCount: entries.length,
      itemBuilder: (context, index) {
        var entry = entries[index];
        bool isCurrentUser = entry.userId == currentUserId;
        
        return Container(
          margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          padding: EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: isCurrentUser ? Colors.blue.withOpacity(0.1) : Colors.white,
            borderRadius: BorderRadius.circular(12),
            border: isCurrentUser ? Border.all(color: Colors.blue, width: 2) : null,
          ),
          child: Row(
            children: [
              Container(
                width: 32,
                height: 32,
                decoration: BoxDecoration(
                  color: _getRankColor(index + 1),
                  shape: BoxShape.circle,
                ),
                child: Center(child: Text('${index + 1}', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
              ),
              SizedBox(width: 12),
              CircleAvatar(child: Text(entry.userName[0])),
              SizedBox(width: 12),
              Expanded(child: Text(entry.userName, style: TextStyle(fontWeight: isCurrentUser ? FontWeight.bold : FontWeight.normal))),
              Text('${entry.value.toStringAsFixed(1)}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            ],
          ),
        );
      },
    );
  }
  
  Color _getRankColor(int rank) {
    if (rank == 1) return Colors.amber;
    if (rank == 2) return Colors.grey;
    if (rank == 3) return Colors.brown;
    return Colors.blue;
  }
}

class LeaderboardEntry {
  final String userId;
  final String userName;
  final double value;
  
  LeaderboardEntry({required this.userId, required this.userName, required this.value});
}

挑战排行榜展示参与者的排名情况。前三名使用金银铜颜色的徽章,当前用户使用蓝色边框高亮。显示排名、头像、用户名和完成数值。排行榜增加了挑战的竞争性,激励用户努力提升排名。这种社交化的竞争机制能够显著提升用户的参与度。

OpenHarmony挑战通知服务

typescript 复制代码
import notificationManager from '@ohos.notificationManager';

class ChallengeNotificationService {
  async notifyProgress(challengeName: string, progress: number): Promise<void> {
    if (progress >= 50 && progress < 51) {
      await this.sendNotification('挑战进度', `「${challengeName}」已完成50%,继续加油!`);
    } else if (progress >= 100) {
      await this.sendNotification('🎉 挑战完成', `恭喜完成「${challengeName}」挑战!`);
    }
  }
  
  async notifyDeadline(challengeName: string, daysRemaining: number): Promise<void> {
    if (daysRemaining <= 3 && daysRemaining > 0) {
      await this.sendNotification('挑战即将结束', `「${challengeName}」还剩${daysRemaining}天,抓紧时间!`);
    }
  }
  
  private async sendNotification(title: string, content: string): Promise<void> {
    let notificationRequest: notificationManager.NotificationRequest = {
      id: Date.now(),
      content: {
        notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
        normal: { title: title, text: content }
      }
    };
    await notificationManager.publish(notificationRequest);
  }
}

挑战通知服务在关键节点发送提醒。进度达到50%时鼓励用户继续,完成时发送庆祝通知。临近截止日期时提醒用户抓紧时间。这种通知机制帮助用户保持对挑战的关注,提高完成率。

Flutter创建挑战表单

dart 复制代码
class CreateChallengeForm extends StatefulWidget {
  final Function(Challenge) onCreate;
  
  const CreateChallengeForm({Key? key, required this.onCreate}) : super(key: key);
  
  @override
  State<CreateChallengeForm> createState() => _CreateChallengeFormState();
}

class _CreateChallengeFormState extends State<CreateChallengeForm> {
  final _nameController = TextEditingController();
  ChallengeType _type = ChallengeType.distance;
  double _targetValue = 50;
  int _durationDays = 7;
  
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: EdgeInsets.all(16),
      children: [
        TextField(
          controller: _nameController,
          decoration: InputDecoration(labelText: '挑战名称', border: OutlineInputBorder()),
        ),
        SizedBox(height: 16),
        Text('挑战类型', style: TextStyle(fontWeight: FontWeight.bold)),
        Wrap(
          spacing: 8,
          children: ChallengeType.values.map((type) => ChoiceChip(
            label: Text(_getTypeName(type)),
            selected: _type == type,
            onSelected: (_) => setState(() => _type = type),
          )).toList(),
        ),
        SizedBox(height: 16),
        Text('目标: ${_targetValue.toInt()} ${_getTypeUnit(_type)}'),
        Slider(value: _targetValue, min: 10, max: 500, onChanged: (v) => setState(() => _targetValue = v)),
        SizedBox(height: 16),
        Text('持续时间: $_durationDays 天'),
        Slider(value: _durationDays.toDouble(), min: 1, max: 30, divisions: 29, onChanged: (v) => setState(() => _durationDays = v.toInt())),
        SizedBox(height: 24),
        ElevatedButton(onPressed: _createChallenge, child: Text('创建挑战')),
      ],
    );
  }
  
  String _getTypeName(ChallengeType type) {
    switch (type) {
      case ChallengeType.distance: return '距离';
      case ChallengeType.duration: return '时长';
      case ChallengeType.workouts: return '次数';
      case ChallengeType.steps: return '步数';
      case ChallengeType.calories: return '卡路里';
    }
  }
  
  String _getTypeUnit(ChallengeType type) {
    switch (type) {
      case ChallengeType.distance: return '公里';
      case ChallengeType.duration: return '分钟';
      case ChallengeType.workouts: return '次';
      case ChallengeType.steps: return '步';
      case ChallengeType.calories: return '千卡';
    }
  }
  
  void _createChallenge() {
    // 创建挑战
  }
}

创建挑战表单让用户自定义挑战。输入挑战名称,选择类型,设置目标值和持续天数。这种表单支持用户创建个人挑战或邀请好友参与的社交挑战,增加了应用的互动性和趣味性。

总结

本文全面介绍了Flutter与OpenHarmony平台上运动挑战组件的实现方案。从挑战模型到服务接口,从卡片展示到排行榜,从通知提醒到创建表单,涵盖了挑战功能的各个方面。通过有时限的目标和社交竞争,我们可以有效激励用户保持运动热情,提升应用的用户粘性。欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
程序员Ctrl喵19 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难20 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡21 小时前
flutter列表中实现置顶动画
flutter
始持1 天前
第十二讲 风格与主题统一
前端·flutter
始持1 天前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持1 天前
第十三讲 异步操作与异步构建
前端·flutter
新镜1 天前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴1 天前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区1 天前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎1 天前
树形选择器组件封装
前端·flutter