【maaath】 Flutter for OpenHarmony 实战:图片壁纸应用开发指南

Flutter for OpenHarmony 实战:图片壁纸应用开发指南

作者:maaath


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


一、引言

Flutter 作为谷歌推出的跨平台 UI 框架,凭借其高性能、热重载机制以及丰富的生态系统,已经在 Android、iOS、Web 等平台得到了广泛应用。随着 OpenHarmony 生态的蓬勃发展,Flutter for OpenHarmony(以下简称 FlutterOH)应运而生,为开发者提供了在鸿蒙设备上使用 Dart 语言进行跨平台开发的能力。

本文将通过一个完整的图片壁纸应用实战案例,深入讲解如何利用 FlutterOH 实现网络图片请求、瀑布流布局、收藏功能等核心模块。文章将展示真实可运行的代码,帮助读者快速掌握 FlutterOH 的开发技巧。

二、项目概述

本项目实现的是一个功能完善的图片壁纸应用,主要包含以下功能模块:

  • 网络图片请求:从 Picsum API 获取高质量壁纸图片
  • 瀑布流布局:双列瀑布流展示图片,美观大方
  • 下拉刷新与上拉加载:流畅的列表操作体验
  • 收藏功能:本地持久化存储用户收藏
  • 图片预览:全屏查看高清大图

项目结构

复制代码
lib/
├── main.dart                 # 应用入口
├── model/
│   └── wallpaper_model.dart  # 数据模型
├── network/
│   └── wallpaper_service.dart # 网络服务
├── service/
│   └── storage_service.dart  # 本地存储
├── pages/
│   ├── home_page.dart        # 主页
│   ├── preview_page.dart     # 预览页
│   ├── category_page.dart    # 分类页
│   └── favorite_page.dart    # 收藏页
└── widgets/
    └── waterfall_item.dart   # 瀑布流组件

三、数据模型定义

首先定义壁纸数据模型,这是整个应用的基础。

dart 复制代码
/// 壁纸数据模型
class WallpaperModel {
  String id;
  String url;
  String thumbUrl;
  int width;
  int height;
  String title;
  String category;
  bool isFavorite;
  int downloads;
  String author;

  WallpaperModel({
    this.id = '',
    this.url = '',
    this.thumbUrl = '',
    this.width = 0,
    this.height = 0,
    this.title = '',
    this.category = '',
    this.isFavorite = false,
    this.downloads = 0,
    this.author = '',
  });

  /// 从 JSON 创建对象
  factory WallpaperModel.fromJson(Map<String, dynamic> json) {
    return WallpaperModel(
      id: json['id']?.toString() ?? '',
      url: json['url'] ?? '',
      thumbUrl: json['thumbUrl'] ?? '',
      width: json['width'] ?? 0,
      height: json['height'] ?? 0,
      title: json['title'] ?? '',
      category: json['category'] ?? '',
      isFavorite: json['isFavorite'] ?? false,
      downloads: json['downloads'] ?? 0,
      author: json['author'] ?? '',
    );
  }

  /// 转换为 JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'url': url,
      'thumbUrl': thumbUrl,
      'width': width,
      'height': height,
      'title': title,
      'category': category,
      'isFavorite': isFavorite,
      'downloads': downloads,
      'author': author,
    };
  }
}

四、网络请求服务

网络请求是应用获取数据的关键环节。本项目使用 Picsum Photos API 作为图片数据源,该 API 提供大量免费高质量图片,非常适合作为开发测试使用。

dart 复制代码
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../model/wallpaper_model.dart';

/// 壁纸网络服务
class WallpaperService {
  static const String _baseUrl = 'https://picsum.photos';
  static const int _pageSize = 20;

  /// 获取推荐壁纸列表
  Future<List<WallpaperModel>> getRecommendedWallpapers(int page) async {
    try {
      final response = await http.get(
        Uri.parse('$_baseUrl/v2/list?page=$page&limit=$_pageSize'),
        headers: {'Content-Type': 'application/json'},
      ).timeout(const Duration(seconds: 30));

      if (response.statusCode == 200) {
        final List<dynamic> jsonList = json.decode(response.body);
        return _convertToWallpapers(jsonList, page);
      }
      return [];
    } catch (e) {
      debugPrint('获取推荐壁纸失败: $e');
      return _getMockWallpapers(page);
    }
  }

  /// JSON 数据转换为壁纸模型
  List<WallpaperModel> _convertToWallpapers(
      List<dynamic> jsonList, int page) {
    final wallpapers = <WallpaperModel>[];
    final startId = (page - 1) * _pageSize + 1;

    for (var i = 0; i < jsonList.length; i++) {
      final item = jsonList[i] as Map<String, dynamic>;
      final id = item['id']?.toString() ?? (startId + i).toString();

      wallpapers.add(WallpaperModel(
        id: id,
        url: item['download_url'] ?? '$_baseUrl/id/$id/1080/1920',
        thumbUrl: '$_baseUrl/id/$id/400/600',
        width: item['width'] ?? 1080,
        height: item['height'] ?? 1920,
        title: item['author'] ?? '壁纸 $id',
        category: _getRandomCategory(),
        isFavorite: false,
        downloads: (page * 100 + i) % 10000,
        author: item['author'] ?? 'Unknown',
      ));
    }
    return wallpapers;
  }

  /// 获取随机分类
  String _getRandomCategory() {
    final categories = ['风景', '人物', '动物', '建筑', '植物', '抽象', '城市', '自然'];
    return categories[DateTime.now().millisecond % categories.length];
  }

  /// 生成模拟数据
  List<WallpaperModel> _getMockWallpapers(int page) {
    final wallpapers = <WallpaperModel>[];
    final categories = ['风景', '人物', '动物', '建筑', '植物', '抽象', '城市', '自然'];

    for (var i = 0; i < _pageSize; i++) {
      final id = page * 100 + i;
      final category = categories[id % categories.length];

      wallpapers.add(WallpaperModel(
        id: id.toString(),
        url: '$_baseUrl/id/$id/1920/1080',
        thumbUrl: '$_baseUrl/id/$id/400/600',
        width: 1080,
        height: 1920,
        title: '$category 壁纸 $id',
        category: category,
        isFavorite: false,
        downloads: (id * 7) % 10000,
        author: '摄影师 ${(id % 5) + 1}',
      ));
    }
    return wallpapers;
  }

  /// 按分类获取壁纸
  Future<List<WallpaperModel>> getWallpapersByCategory(
      String category, int page) async {
    final wallpapers = await getRecommendedWallpapers(page);
    return wallpapers.map((w) {
      w.category = category;
      w.id = '${category}_${w.id}';
      return w;
    }).toList();
  }

  /// 搜索壁纸
  Future<List<WallpaperModel>> searchWallpapers(String keyword, int page) async {
    final wallpapers = await getRecommendedWallpapers(page);
    if (keyword.isEmpty) return wallpapers;

    final lowerKeyword = keyword.toLowerCase();
    return wallpapers.where((w) {
      return w.title.toLowerCase().contains(lowerKeyword) ||
          w.category.toLowerCase().contains(lowerKeyword);
    }).toList();
  }
}

五、本地存储服务

收藏功能需要本地持久化存储,我们使用 SharedPreferences 实现。

dart 复制代码
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/wallpaper_model.dart';

/// 本地存储服务 - 管理收藏壁纸
class StorageService {
  static const String _favoritesKey = 'wallpaper_favorites';

  /// 获取所有收藏
  Future<List<WallpaperModel>> getFavorites() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final data = prefs.getString(_favoritesKey);
      if (data != null) {
        final List<dynamic> jsonList = json.decode(data);
        return jsonList
            .map((item) => WallpaperModel.fromJson(item as Map<String, dynamic>))
            .toList();
      }
    } catch (e) {
      debugPrint('获取收藏失败: $e');
    }
    return [];
  }

  /// 保存收藏列表
  Future<void> saveFavorites(List<WallpaperModel> favorites) async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final jsonStr = json.encode(favorites.map((w) => w.toJson()).toList());
      await prefs.setString(_favoritesKey, jsonStr);
    } catch (e) {
      debugPrint('保存收藏失败: $e');
    }
  }

  /// 添加收藏
  Future<bool> addFavorite(WallpaperModel wallpaper) async {
    try {
      final favorites = await getFavorites();
      if (!isFavorite(wallpaper.id, favorites)) {
        wallpaper.isFavorite = true;
        favorites.add(wallpaper);
        await saveFavorites(favorites);
        return true;
      }
    } catch (e) {
      debugPrint('添加收藏失败: $e');
    }
    return false;
  }

  /// 移除收藏
  Future<bool> removeFavorite(String wallpaperId) async {
    try {
      final favorites = await getFavorites();
      final index = favorites.indexWhere((w) => w.id == wallpaperId);
      if (index != -1) {
        favorites.removeAt(index);
        await saveFavorites(favorites);
        return true;
      }
    } catch (e) {
      debugPrint('移除收藏失败: $e');
    }
    return false;
  }

  /// 检查是否已收藏
  bool isFavorite(String wallpaperId, [List<WallpaperModel>? favorites]) {
    favorites ??= _cachedFavorites;
    return favorites.any((w) => w.id == wallpaperId);
  }

  List<WallpaperModel> _cachedFavorites = [];

  /// 切换收藏状态
  Future<bool> toggleFavorite(WallpaperModel wallpaper) async {
    if (isFavorite(wallpaper.id)) {
      await removeFavorite(wallpaper.id);
      return false;
    } else {
      await addFavorite(wallpaper);
      return true;
    }
  }
}

六、主页面实现

主页采用底部导航栏设计,包含推荐、分类、收藏三个 Tab。列表使用 GridView 实现瀑布流布局。

dart 复制代码
import 'package:flutter/material.dart';
import '../model/wallpaper_model.dart';
import '../network/wallpaper_service.dart';
import '../service/storage_service.dart';

/// 主页面 - 底部选项卡
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;
  final _tabs = ['推荐', '分类', '收藏'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: const [
          RecommendTab(),
          CategoryTab(),
          FavoriteTab(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        selectedItemColor: const Color(0xFFFF6B6B),
        unselectedItemColor: Colors.grey,
        onTap: (index) => setState(() => _currentIndex = index),
        items: _tabs
            .map((name) => BottomNavigationBarItem(
                  icon: const Icon(Icons.home_outlined),
                  activeIcon: const Icon(Icons.home),
                  label: name,
                ))
            .toList(),
      ),
    );
  }
}

/// 推荐 Tab
class RecommendTab extends StatefulWidget {
  const RecommendTab({super.key});

  @override
  State<RecommendTab> createState() => _RecommendTabState();
}

class _RecommendTabState extends State<RecommendTab> {
  final _wallpaperService = WallpaperService();
  final _storageService = StorageService();
  List<WallpaperModel> _wallpapers = [];
  bool _isLoading = false;
  int _currentPage = 1;
  bool _hasMore = true;

  @override
  void initState() {
    super.initState();
    _loadWallpapers();
  }

  Future<void> _loadWallpapers() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);

    try {
      final newWallpapers = await _wallpaperService.getRecommendedWallpapers(_currentPage);
      final favorites = await _storageService.getFavorites();

      for (var wp in newWallpapers) {
        wp.isFavorite = favorites.any((f) => f.id == wp.id);
      }

      setState(() {
        if (_currentPage == 1) {
          _wallpapers = newWallpapers;
        } else {
          _wallpapers.addAll(newWallpapers);
        }
        _hasMore = newWallpapers.length >= 20;
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _onRefresh() async {
    _currentPage = 1;
    await _loadWallpapers();
  }

  void _onLoadMore() {
    if (!_hasMore || _isLoading) return;
    _currentPage++;
    _loadWallpapers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('发现壁纸', style: TextStyle(fontWeight: FontWeight.bold)),
        backgroundColor: Colors.white,
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => Navigator.pushNamed(context, '/search'),
          ),
        ],
      ),
      body: _wallpapers.isEmpty && _isLoading
          ? const Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _onRefresh,
              child: NotificationListener<ScrollNotification>(
                onNotification: (scrollInfo) {
                  if (scrollInfo.metrics.pixels >=
                      scrollInfo.metrics.maxScrollExtent - 200) {
                    _onLoadMore();
                  }
                  return false;
                },
                child: GridView.builder(
                  padding: const EdgeInsets.all(8),
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    childAspectRatio: 0.75,
                    crossAxisSpacing: 8,
                    mainAxisSpacing: 8,
                  ),
                  itemCount: _wallpapers.length + (_hasMore ? 1 : 0),
                  itemBuilder: (context, index) {
                    if (index >= _wallpapers.length) {
                      return const Center(child: CircularProgressIndicator());
                    }
                    return _buildWallpaperItem(_wallpapers[index]);
                  },
                ),
              ),
            ),
    );
  }

  Widget _buildWallpaperItem(WallpaperModel wallpaper) {
    return GestureDetector(
      onTap: () => _openPreview(wallpaper),
      child: Stack(
        fit: StackFit.expand,
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(12),
            child: Image.network(
              wallpaper.thumbUrl,
              fit: BoxFit.cover,
              loadingBuilder: (context, child, progress) {
                if (progress == null) return child;
                return Container(
                  color: Colors.grey[200],
                  child: const Center(child: CircularProgressIndicator()),
                );
              },
              errorBuilder: (context, error, stack) {
                return Container(
                  color: Colors.grey[200],
                  child: const Icon(Icons.broken_image, size: 48),
                );
              },
            ),
          ),
          Positioned(
            top: 8,
            right: 8,
            child: GestureDetector(
              onTap: () => _toggleFavorite(wallpaper),
              child: Container(
                padding: const EdgeInsets.all(6),
                decoration: BoxDecoration(
                  color: Colors.black38,
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Icon(
                  wallpaper.isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: wallpaper.isFavorite ? Colors.red : Colors.white,
                  size: 20,
                ),
              ),
            ),
          ),
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: Container(
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [Colors.transparent, Colors.black54],
                ),
                borderRadius: const BorderRadius.only(
                  bottomLeft: Radius.circular(12),
                  bottomRight: Radius.circular(12),
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    wallpaper.title,
                    style: const TextStyle(color: Colors.white, fontSize: 12),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  Text(
                    wallpaper.author,
                    style: const TextStyle(color: Colors.white70, fontSize: 10),
                    maxLines: 1,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _openPreview(WallpaperModel wallpaper) {
    Navigator.pushNamed(context, '/preview', arguments: wallpaper);
  }

  Future<void> _toggleFavorite(WallpaperModel wallpaper) async {
    final isFav = await _storageService.toggleFavorite(wallpaper);
    setState(() => wallpaper.isFavorite = isFav);
  }
}

七、图片预览页面

预览页面展示高清大图,支持设置壁纸等操作。

dart 复制代码
import 'package:flutter/material.dart';
import '../model/wallpaper_model.dart';

/// 图片预览页面
class PreviewPage extends StatefulWidget {
  final WallpaperModel wallpaper;

  const PreviewPage({super.key, required this.wallpaper});

  @override
  State<PreviewPage> createState() => _PreviewPageState();
}

class _PreviewPageState extends State<PreviewPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _fadeAnimation;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
    );
    _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
    );
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        fit: StackFit.expand,
        children: [
          // 图片
          GestureDetector(
            onTap: () => Navigator.pop(context),
            child: Center(
              child: AnimatedBuilder(
                animation: _animationController,
                builder: (context, child) {
                  return Opacity(
                    opacity: _fadeAnimation.value,
                    child: Transform.scale(
                      scale: _scaleAnimation.value,
                      child: child,
                    ),
                  );
                },
                child: InteractiveViewer(
                  minScale: 0.5,
                  maxScale: 4.0,
                  child: Image.network(
                    widget.wallpaper.url,
                    fit: BoxFit.contain,
                  ),
                ),
              ),
            ),
          ),
          // 顶部操作栏
          Positioned(
            top: 0,
            left: 0,
            right: 0,
            child: Container(
              padding: EdgeInsets.only(
                top: MediaQuery.of(context).padding.top,
                left: 8,
                right: 8,
              ),
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [Colors.black54, Colors.transparent],
                ),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  IconButton(
                    icon: const Icon(Icons.arrow_back, color: Colors.white),
                    onPressed: () => Navigator.pop(context),
                  ),
                  Row(
                    children: [
                      IconButton(
                        icon: const Icon(Icons.download, color: Colors.white),
                        onPressed: _downloadImage,
                      ),
                      IconButton(
                        icon: Icon(
                          widget.wallpaper.isFavorite
                              ? Icons.favorite
                              : Icons.favorite_border,
                          color: widget.wallpaper.isFavorite
                              ? Colors.red
                              : Colors.white,
                        ),
                        onPressed: _toggleFavorite,
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          // 底部信息栏
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: Container(
              padding: EdgeInsets.only(
                bottom: MediaQuery.of(context).padding.bottom + 16,
                left: 16,
                right: 16,
                top: 32,
              ),
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.bottomCenter,
                  end: Alignment.topCenter,
                  colors: [Colors.black54, Colors.transparent],
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    widget.wallpaper.author,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    widget.wallpaper.title,
                    style: const TextStyle(color: Colors.white70, fontSize: 14),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '${widget.wallpaper.width} × ${widget.wallpaper.height}',
                    style: const TextStyle(color: Colors.white54, fontSize: 12),
                  ),
                  const SizedBox(height: 16),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton.icon(
                      onPressed: _setAsWallpaper,
                      icon: const Icon(Icons.wallpaper),
                      label: const Text('设为壁纸'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: const Color(0xFFFF6B6B),
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(vertical: 12),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(25),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _downloadImage() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('下载功能开发中...')),
    );
  }

  void _toggleFavorite() {
    setState(() {
      widget.wallpaper.isFavorite = !widget.wallpaper.isFavorite;
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          widget.wallpaper.isFavorite ? '已添加到收藏' : '已取消收藏',
        ),
      ),
    );
  }

  void _setAsWallpaper() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('设置壁纸功能开发中...')),
    );
  }
}

八、截图运行板块

8.1 应用启动界面

应用启动后首先显示带有品牌 Logo 的启动页,经过动画效果后进入主页。

8.2 推荐壁纸列表

双列瀑布流展示所有推荐壁纸,每张图片支持点击预览和收藏操作。

8.3 分类浏览

点击底部导航栏的"分类" Tab,进入分类页面,展示 8 个常用分类(风景、人物、动物、建筑、植物、抽象、城市、自然),点击任意分类可查看该分类下的壁纸列表。

8.4 收藏管理

点击底部导航栏的"收藏" Tab,进入收藏页面,展示用户收藏的所有壁纸,支持一键删除和预览操作。

8.5 图片预览

点击任意壁纸进入预览页面,支持手势缩放查看高清细节,底部可进行下载、收藏和设为壁纸操作。

运行效果展示:

图1:推荐壁纸列表界面

图2:分类浏览界面

图3:收藏管理界面

九、技术总结

通过本次实战项目,完整地实现了:

  1. FlutterOH 网络请求 :使用 http 包进行 API 调用,展示了异步请求、错误处理、模拟数据降级等实用技巧。

  2. 本地持久化存储 :使用 shared_preferences 实现收藏功能的增删改查,确保用户数据不丢失。

  3. 响应式状态管理 :通过 StatefulWidgetsetState 管理 UI 状态,实现了数据的动态更新。

  4. 动画效果 :使用 AnimationController 实现图片预览页面的渐入和缩放动画,提升用户体验。

  5. 手势交互 :使用 InteractiveViewer 实现图片的缩放查看,GestureDetector 处理点击事件。

十、代码仓库

本文完整代码已托管至 AtomGit 仓库:

复制代码
https://atomgit.com/maaath/flutter_wallpaper_app

仓库包含完整的 FlutterOH 项目代码,开发者可直接克隆后导入 DevEco Studio 运行测试。

十一、结语

Flutter for OpenHarmony 为跨平台开发带来了全新的可能性。本文通过图片壁纸应用这一实战案例,展示了 FlutterOH 在鸿蒙设备上的开发流程和关键技术点。相信随着 OpenHarmony 生态的持续发展,FlutterOH 将成为越来越多开发者的选择。

感谢各位阅读!


参考链接:

相关推荐
maaath7 小时前
【maaath】Flutter for OpenHarmony:跨平台天气应用开发指南
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 宠物社区应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 实战:健身运动应用的跨平台开发指南
flutter·华为·harmonyos
Swift社区7 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
maaath7 小时前
【maaath】 Flutter for OpenHarmony 新闻资讯应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 跨平台图书阅读应用开发实践
flutter·华为·harmonyos
xmdy58667 小时前
Flutter+开源鸿蒙实战|智联邻里Day2 首页UI开发+全局组件封装+鸿蒙多端适配
flutter·开源·harmonyos
特立独行的猫a8 小时前
移植 vcpkg 到鸿蒙 PC:vcpkg-tool 交叉编译与实践手记
华为·harmonyos·vcpkg·鸿蒙pc·vcpkg-tool
911hzh9 小时前
Flutter 音视频通话集成实战:WebSocket 做信令,WebRTC 传音视频,附详细事件时序图
websocket·flutter·音视频