Flutter头像上传:使用Riverpod实现选择上传实时更新完整解决方案

📸 使用 Riverpod 实现头像立即上传

  1. 完整的实现方案

项目结构

复制代码
lib/
├── main.dart
├── providers/
│   ├── avatar_provider.dart
│   └── providers.dart
├── services/
│   └── upload_service.dart
├── widgets/
│   └── avatar_upload_widget.dart
└── pages/
    └── profile_page.dart
  1. Provider 定义

lib/providers/avatar_provider.dart

dart 复制代码
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../services/upload_service.dart';

// 上传状态
enum AvatarUploadStatus {
  idle,       // 空闲
  selecting,  // 选择中
  uploading,  // 上传中
  success,    // 成功
  error,      // 失败
}

// 状态模型
class AvatarState {
  final AvatarUploadStatus status;
  final File? selectedImage;
  final String? imageUrl;
  final double progress;
  final String? errorMessage;

  AvatarState({
    this.status = AvatarUploadStatus.idle,
    this.selectedImage,
    this.imageUrl,
    this.progress = 0,
    this.errorMessage,
  });

  AvatarState copyWith({
    AvatarUploadStatus? status,
    File? selectedImage,
    String? imageUrl,
    double? progress,
    String? errorMessage,
  }) {
    return AvatarState(
      status: status ?? this.status,
      selectedImage: selectedImage ?? this.selectedImage,
      imageUrl: imageUrl ?? this.imageUrl,
      progress: progress ?? this.progress,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }

  bool get isIdle => status == AvatarUploadStatus.idle;
  bool get isSelecting => status == AvatarUploadStatus.selecting;
  bool get isUploading => status == AvatarUploadStatus.uploading;
  bool get isSuccess => status == AvatarUploadStatus.success;
  bool get hasError => status == AvatarUploadStatus.error;
}

// Notifier 类
class AvatarNotifier extends StateNotifier<AvatarState> {
  final UploadService uploadService;
  final ImagePicker imagePicker;

  AvatarNotifier({
    required this.uploadService,
    required this.imagePicker,
  }) : super(AvatarState());

  // 选择图片并立即上传
  Future<void> pickAndUploadImage({
    required ImageSource source,
    required String userId,
  }) async {
    try {
      // 1. 选择图片
      state = state.copyWith(
        status: AvatarUploadStatus.selecting,
        errorMessage: null,
      );

      final XFile? pickedFile = await imagePicker.pickImage(
        source: source,
        maxWidth: 800,
        maxHeight: 800,
        imageQuality: 85,
      );

      if (pickedFile == null) {
        state = state.copyWith(status: AvatarUploadStatus.idle);
        return;
      }

      final File imageFile = File(pickedFile.path);

      // 2. 更新预览状态
      state = state.copyWith(
        status: AvatarUploadStatus.uploading,
        selectedImage: imageFile,
        progress: 0,
      );

      // 3. 立即上传
      final String imageUrl = await uploadService.uploadAvatar(
        imageFile: imageFile,
        userId: userId,
        onProgress: (double progress) {
          // 更新进度
          state = state.copyWith(progress: progress);
        },
      );

      // 4. 上传成功
      state = state.copyWith(
        status: AvatarUploadStatus.success,
        imageUrl: imageUrl,
        progress: 1.0,
        selectedImage: null, // 清除临时文件引用
      );

    } catch (error) {
      // 5. 错误处理
      state = state.copyWith(
        status: AvatarUploadStatus.error,
        errorMessage: error.toString(),
        progress: 0,
      );
      
      // 可选:3秒后重置错误状态
      Future.delayed(const Duration(seconds: 3), () {
        if (state.hasError) {
          state = state.copyWith(status: AvatarUploadStatus.idle);
        }
      });
    }
  }

  // 清除错误
  void clearError() {
    if (state.hasError) {
      state = state.copyWith(
        status: AvatarUploadStatus.idle,
        errorMessage: null,
      );
    }
  }

  // 手动设置头像URL(用于初始加载)
  void setInitialAvatar(String? imageUrl) {
    state = state.copyWith(imageUrl: imageUrl);
  }

  // 重置状态
  void reset() {
    state = AvatarState();
  }
}

// Provider 定义
final avatarProvider = StateNotifierProvider<AvatarNotifier, AvatarState>(
  (ref) {
    return AvatarNotifier(
      uploadService: ref.watch(uploadServiceProvider),
      imagePicker: ImagePicker(),
    );
  },
);

lib/providers/providers.dart

dart 复制代码
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'avatar_provider.dart';

// 导出所有 provider
export 'avatar_provider.dart';

// 其他依赖的 provider
final uploadServiceProvider = Provider<UploadService>((ref) {
  return UploadService();
});
  1. 上传服务

lib/services/upload_service.dart

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

class UploadService {
  // 模拟上传,实际项目中替换为真实API
  Future<String> uploadAvatar({
    required File imageFile,
    required String userId,
    required void Function(double progress) onProgress,
  }) async {
    try {
      // 模拟上传延迟
      await Future.delayed(const Duration(milliseconds: 100));
      
      // 模拟进度更新
      for (int i = 0; i <= 10; i++) {
        await Future.delayed(const Duration(milliseconds: 100));
        onProgress(i / 10);
      }

      // 实际项目中,这里应该是真实的API调用
      /*
      final uri = Uri.parse('https://your-api.com/upload/avatar');
      final request = http.MultipartRequest('POST', uri)
        ..files.add(await http.MultipartFile.fromPath(
          'avatar',
          imageFile.path,
        ))
        ..fields['userId'] = userId;

      final streamedResponse = await request.send();
      
      // 监听进度
      streamedResponse.stream.listen(
        (List<int> chunk) {
          final total = streamedResponse.contentLength ?? 0;
          final uploaded = chunk.length;
          onProgress(uploaded / total);
        },
      );

      final response = await http.Response.fromStream(streamedResponse);
      
      if (response.statusCode == 200) {
        final jsonResponse = json.decode(response.body);
        return jsonResponse['data']['url'];
      } else {
        throw Exception('上传失败: ${response.statusCode}');
      }
      */

      // 模拟返回的图片URL
      await Future.delayed(const Duration(milliseconds: 500));
      return 'https://picsum.photos/400/400?random=${DateTime.now().millisecondsSinceEpoch}';

    } catch (e) {
      throw Exception('上传失败: $e');
    }
  }
}
  1. 上传组件

lib/widgets/avatar_upload_widget.dart

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../providers/avatar_provider.dart';

class AvatarUploadWidget extends ConsumerWidget {
  final String userId;
  final double size;
  final bool showEditButton;
  final VoidCallback? onUploadSuccess;

  const AvatarUploadWidget({
    Key? key,
    required this.userId,
    this.size = 100,
    this.showEditButton = true,
    this.onUploadSuccess,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(avatarProvider);
    
    return Center(
      child: Stack(
        alignment: Alignment.bottomRight,
        children: [
          // 头像显示
          _buildAvatar(state, context),
          
          // 编辑按钮
          if (showEditButton && !state.isUploading)
            _buildEditButton(context, ref),
          
          // 上传进度指示器
          if (state.isUploading) _buildProgressIndicator(state),
          
          // 错误提示
          if (state.hasError) _buildErrorOverlay(state, ref),
        ],
      ),
    );
  }

  Widget _buildAvatar(AvatarState state, BuildContext context) {
    // 如果有选中的图片(正在上传中),显示预览
    if (state.selectedImage != null && state.isUploading) {
      return _buildImagePreview(state.selectedImage!);
    }
    
    // 如果有上传成功的图片URL,显示网络图片
    if (state.imageUrl != null && state.imageUrl!.isNotEmpty) {
      return _buildNetworkImage(state.imageUrl!);
    }
    
    // 默认头像
    return _buildDefaultAvatar();
  }

  Widget _buildImagePreview(File imageFile) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(
          color: Colors.blue.withOpacity(0.3),
          width: 2,
        ),
      ),
      child: ClipOval(
        child: Image.file(
          imageFile,
          fit: BoxFit.cover,
          width: size,
          height: size,
        ),
      ),
    );
  }

  Widget _buildNetworkImage(String imageUrl) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(
          color: Colors.grey.shade300,
          width: 2,
        ),
      ),
      child: ClipOval(
        child: CachedNetworkImage(
          imageUrl: imageUrl,
          width: size,
          height: size,
          fit: BoxFit.cover,
          placeholder: (context, url) => Center(
            child: CircularProgressIndicator(
              strokeWidth: 2,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
            ),
          ),
          errorWidget: (context, url, error) => _buildDefaultAvatar(),
        ),
      ),
    );
  }

  Widget _buildDefaultAvatar() {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.grey.shade200,
        border: Border.all(
          color: Colors.grey.shade300,
          width: 2,
        ),
      ),
      child: Icon(
        Icons.person,
        size: size * 0.5,
        color: Colors.grey.shade400,
      ),
    );
  }

  Widget _buildEditButton(BuildContext context, WidgetRef ref) {
    return GestureDetector(
      onTap: () => _showImageSourceDialog(context, ref),
      child: Container(
        width: size * 0.3,
        height: size * 0.3,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.blue,
          border: Border.all(
            color: Colors.white,
            width: 2,
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              blurRadius: 4,
              offset: Offset(0, 2),
            ),
          ],
        ),
        child: const Icon(
          Icons.camera_alt,
          size: 18,
          color: Colors.white,
        ),
      ),
    );
  }

  Widget _buildProgressIndicator(AvatarState state) {
    return Positioned.fill(
      child: Container(
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.black.withOpacity(0.5),
        ),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: size * 0.4,
                height: size * 0.4,
                child: CircularProgressIndicator(
                  value: state.progress,
                  strokeWidth: 3,
                  backgroundColor: Colors.white30,
                  valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
                ),
              ),
              SizedBox(height: 8),
              Text(
                '${(state.progress * 100).toInt()}%',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 12,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildErrorOverlay(AvatarState state, WidgetRef ref) {
    return Positioned.fill(
      child: GestureDetector(
        onTap: () => ref.read(avatarProvider.notifier).clearError(),
        child: Container(
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Colors.red.withOpacity(0.8),
          ),
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(
                  Icons.error_outline,
                  color: Colors.white,
                  size: 30,
                ),
                SizedBox(height: 4),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  child: Text(
                    '上传失败',
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                    maxLines: 2,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  void _showImageSourceDialog(BuildContext context, WidgetRef ref) {
    showModalBottomSheet(
      context: context,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const SizedBox(height: 16),
            Text(
              '更换头像',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: Colors.grey.shade800,
              ),
            ),
            const SizedBox(height: 16),
            _buildOptionButton(
              context: context,
              icon: Icons.camera_alt,
              text: '拍照',
              onTap: () {
                Navigator.pop(context);
                _uploadFromSource(ImageSource.camera, ref);
              },
            ),
            _buildOptionButton(
              context: context,
              icon: Icons.photo_library,
              text: '从相册选择',
              onTap: () {
                Navigator.pop(context);
                _uploadFromSource(ImageSource.gallery, ref);
              },
            ),
            const SizedBox(height: 16),
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildOptionButton({
    required BuildContext context,
    required IconData icon,
    required String text,
    required VoidCallback onTap,
  }) {
    return ListTile(
      leading: Icon(icon, color: Colors.blue),
      title: Text(text),
      onTap: onTap,
    );
  }

  void _uploadFromSource(ImageSource source, WidgetRef ref) {
    ref.read(avatarProvider.notifier).pickAndUploadImage(
      source: source,
      userId: userId,
    );
    
    // 监听上传成功
    ref.listen<AvatarState>(avatarProvider, (previous, next) {
      if (next.isSuccess && onUploadSuccess != null) {
        onUploadSuccess!();
      }
    });
  }
}
  1. 个人资料页面

lib/pages/profile_page.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/avatar_upload_widget.dart';
import '../providers/avatar_provider.dart';

class ProfilePage extends ConsumerStatefulWidget {
  const ProfilePage({Key? key}) : super(key: key);

  @override
  _ProfilePageState createState() => _ProfilePageState();
}

class _ProfilePageState extends ConsumerState<ProfilePage> {
  final String userId = 'user_123'; // 实际项目中从登录状态获取

  @override
  void initState() {
    super.initState();
    // 页面初始化时加载现有头像
    _loadInitialAvatar();
  }

  Future<void> _loadInitialAvatar() async {
    // 模拟从服务器获取现有头像URL
    await Future.delayed(const Duration(milliseconds: 500));
    
    // 这里应该调用API获取用户头像URL
    // final response = await api.getUserAvatar(userId);
    // ref.read(avatarProvider.notifier).setInitialAvatar(response.avatarUrl);
    
    // 模拟数据
    ref.read(avatarProvider.notifier).setInitialAvatar(
      'https://picsum.photos/400/400?random=123',
    );
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(avatarProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('个人资料'),
        centerTitle: true,
        elevation: 0,
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 头像上传区域
              _buildAvatarSection(state),
              
              const SizedBox(height: 30),
              
              // 个人信息
              _buildInfoSection(),
              
              const SizedBox(height: 30),
              
              // 上传状态显示
              if (state.isUploading) _buildUploadStatus(state),
              if (state.hasError) _buildErrorStatus(state, ref),
              if (state.isSuccess) _buildSuccessStatus(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildAvatarSection(AvatarState state) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Text(
              '个人头像',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: Colors.grey.shade800,
              ),
            ),
            const SizedBox(height: 20),
            AvatarUploadWidget(
              userId: userId,
              size: 120,
              onUploadSuccess: () {
                // 上传成功后的回调
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('头像更新成功!'),
                    backgroundColor: Colors.green,
                  ),
                );
              },
            ),
            const SizedBox(height: 16),
            Text(
              state.isUploading ? '正在上传...' : '点击右下角相机更换头像',
              style: TextStyle(
                fontSize: 14,
                color: Colors.grey.shade600,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoSection() {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '个人信息',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: Colors.grey.shade800,
              ),
            ),
            const SizedBox(height: 20),
            _buildInfoItem('用户名', 'Flutter用户'),
            _buildInfoItem('邮箱', 'user@example.com'),
            _buildInfoItem('注册时间', '2024-01-01'),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoItem(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Expanded(
            flex: 2,
            child: Text(
              label,
              style: TextStyle(
                fontSize: 16,
                color: Colors.grey.shade600,
              ),
            ),
          ),
          Expanded(
            flex: 3,
            child: Text(
              value,
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildUploadStatus(AvatarState state) {
    return Card(
      color: Colors.blue.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            CircularProgressIndicator(
              value: state.progress,
              strokeWidth: 3,
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '正在上传头像...',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.blue.shade800,
                    ),
                  ),
                  const SizedBox(height: 4),
                  LinearProgressIndicator(
                    value: state.progress,
                    backgroundColor: Colors.blue.shade200,
                    valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '${(state.progress * 100).toInt()}%',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.blue.shade600,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildErrorStatus(AvatarState state, WidgetRef ref) {
    return Card(
      color: Colors.red.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Icon(Icons.error_outline, color: Colors.red),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '上传失败',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.red.shade800,
                    ),
                  ),
                  Text(
                    state.errorMessage ?? '未知错误',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.red.shade600,
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
            TextButton(
              onPressed: () => ref.read(avatarProvider.notifier).clearError(),
              child: Text('关闭'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSuccessStatus() {
    return Card(
      color: Colors.green.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Icon(Icons.check_circle, color: Colors.green),
            const SizedBox(width: 16),
            Expanded(
              child: Text(
                '头像更新成功!',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Colors.green.shade800,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  1. 主程序入口

lib/main.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'pages/profile_page.dart';
import 'providers/providers.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '头像上传示例',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        appBarTheme: const AppBarTheme(
          centerTitle: true,
          elevation: 0,
        ),
      ),
      home: const ProfilePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}
  1. Pubspec.yaml 配置
yaml 复制代码
name: avatar_upload_example
description: A Flutter avatar upload example with Riverpod
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=2.19.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  
  # 状态管理
  flutter_riverpod: ^2.3.6
  
  # 图片选择
  image_picker: ^1.0.4
  
  # 网络请求
  http: ^0.13.5
  
  # 网络图片缓存
  cached_network_image: ^3.2.3
  
  # 图片裁剪(可选)
  image_cropper: ^5.0.0

flutter:
  uses-material-design: true
  
  # 配置 assets
  assets:
    - assets/images/
  
  # iOS 配置
  # 在 ios/Runner/Info.plist 中添加:
  # <key>NSCameraUsageDescription</key>
  # <string>需要相机权限来拍摄头像照片</string>
  # <key>NSPhotoLibraryUsageDescription</key>
  # <string>需要相册权限来选择头像照片</string>
  
  # Android 配置
  # 在 android/app/src/main/AndroidManifest.xml 中添加:
  # <uses-permission android:name="android.permission.CAMERA"/>
  # <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  # <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  1. 关键特性总结

✅ 立即上传流程

  1. 选择图片 → 自动进入选择状态
  2. 显示预览 → 立即显示选中图片
  3. 开始上传 → 自动开始上传并显示进度
  4. 上传成功 → 更新头像并显示成功状态
  5. 错误处理 → 显示错误信息并提供重试

🔄 状态管理

· Riverpod 管理整个上传状态

· 实时更新 上传进度

· 自动重置 错误状态

🎯 用户体验

· 流畅的上传进度显示

· 清晰的错误提示

· 成功状态反馈

· 优雅的加载和过渡效果

🛠️ 扩展建议

  1. 添加图片裁剪功能
  2. 实现断点续传
  3. 支持多图片上传
  4. 添加图片压缩功能
  5. 集成云存储(如 Firebase Storage、阿里云 OSS)

这个实现方案:选择相册或拍照 → 立即上传 → 上传成功更新页面。所有的状态都是实时更新的,用户可以看到完整的上传过程。

相关推荐
sunly_3 小时前
Flutter:showModalBottomSheet底部弹出完整页面
开发语言·javascript·flutter
AskHarries3 小时前
Google 登录问题排查指南
flutter·ios·app
500846 小时前
鸿蒙 Flutter 分布式硬件调用:跨设备摄像头 / 麦克风共享
分布式·flutter·华为·electron·wpf·开源鸿蒙
sunly_6 小时前
Flutter:页面级动画弹出
flutter
西西学代码7 小时前
Flutter---通用子项的图片个数不同(1)
flutter
new小码7 小时前
已有Flutter项目适配鸿蒙6
flutter
遝靑7 小时前
Flutter 3.20+ 全平台开发实战:从状态管理到跨端适配(含源码解析)
flutter
500847 小时前
存量 Flutter 项目鸿蒙化:模块化拆分与插件替换实战
java·人工智能·flutter·华为·ocr
装不满的克莱因瓶8 小时前
Windows下安装Dart
android·flutter·dart·移动端