设计理念

在记事本应用中,文件夹是组织笔记的重要工具。创建文件夹对话框提供了一个简洁的界面,让用户可以快速创建新的文件夹来分类管理笔记。本文将详细介绍如何使用Flutter for OpenHarmony实现一个功能完善的创建文件夹对话框。
请添加图片描述

对话框的基础结构

创建文件夹对话框采用AlertDialog组件实现,这是Flutter中最常用的对话框类型。首先定义对话框的基本类结构:

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

class CreateFolderDialog extends StatelessWidget {
  final Function(String) onFolderCreated;

  const CreateFolderDialog({
    super.key,
    required this.onFolderCreated,
  });

这个类继承自StatelessWidget,因为对话框本身不需要维护复杂的内部状态。onFolderCreated是一个回调函数,当用户成功创建文件夹后,会将文件夹名称通过这个回调传递给父组件。这种设计遵循了Flutter的单向数据流原则,将状态管理的责任交给父组件,使得对话框组件更加纯粹和可复用。使用Function(String)类型明确了回调的参数类型,提高了代码的类型安全性。

对话框的UI实现

接下来实现对话框的build方法,构建用户界面的核心部分:

  
  Widget build(BuildContext context) {
    final nameController = TextEditingController();
    
    return AlertDialog(
      title: const Text('新建文件夹'),
      content: SizedBox(
        width: 300.w,
        child: TextField(
          controller: nameController,

build方法创建了一个TextEditingController来管理输入框的文本内容。AlertDialog是Material Design风格的对话框组件,它提供了标准的对话框布局。title属性设置对话框的标题为"新建文件夹",使用const优化性能。content区域使用SizedBox包裹TextField,通过flutter_screenutil的300.w设置宽度,确保在不同屏幕尺寸下都有合适的显示效果。这种响应式设计是跨平台应用的重要考虑因素。

          decoration: const InputDecoration(
            labelText: '文件夹名称',
            border: OutlineInputBorder(),
          ),
          autofocus: true,
        ),
      ),

TextField的decoration属性定义了输入框的外观样式。labelText提供了输入提示,告诉用户应该输入什么内容。OutlineInputBorder创建了带边框的输入框样式,这种样式在Material Design中被广泛使用,提供了清晰的视觉边界。autofocus设置为true,使对话框打开时输入框自动获得焦点,用户可以直接开始输入,无需额外点击,这个细节大大提升了用户体验和操作效率。

      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (nameController.text.isNotEmpty) {
              onFolderCreated(nameController.text);
              Navigator.pop(context);
            }
          },
          child: const Text('创建'),
        ),
      ],
    );
  }
}

actions数组定义了对话框底部的操作按钮。TextButton用于"取消"操作,点击后直接关闭对话框。ElevatedButton用于"创建"操作,它会先检查输入是否为空,只有在有内容时才执行创建操作。这种基础的验证防止了创建空名称的文件夹。创建成功后,通过onFolderCreated回调将文件夹名称传递给父组件,然后关闭对话框。这种设计将业务逻辑的处理权交给了父组件,保持了组件的职责单一性。

输入验证功能

为了提供更好的用户体验和数据质量,我们需要添加完善的输入验证功能。首先定义带验证的对话框类:

class ValidatedCreateFolderDialog extends StatefulWidget {
  final Function(String) onFolderCreated;

  const ValidatedCreateFolderDialog({
    super.key,
    required this.onFolderCreated,
  });

  
  State<ValidatedCreateFolderDialog> createState() => 
      _ValidatedCreateFolderDialogState();
}

这里改用StatefulWidget,因为验证功能需要管理错误消息等内部状态。当用户输入内容时,需要实时更新验证结果和错误提示,这些都是组件的内部状态。createState方法创建对应的State对象来管理这些状态。使用StatefulWidget虽然增加了一些复杂度,但换来了更强大的交互能力和更好的用户体验。

class _ValidatedCreateFolderDialogState 
    extends State<ValidatedCreateFolderDialog> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  String? _errorMessage;

  
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('新建文件夹'),

State类中定义了三个关键的成员变量。_formKey是Form组件的全局键,用于访问表单状态和触发验证。_nameController管理输入框的文本内容。_errorMessage存储当前的错误信息,使用可空类型String?表示可能没有错误。这种设计将验证状态与UI状态分离,使得状态管理更加清晰。GlobalKey的使用是Flutter表单验证的标准模式。

      content: SizedBox(
        width: 300.w,
        child: Form(
          key: _formKey,
          child: TextFormField(
            controller: _nameController,
            decoration: InputDecoration(
              labelText: '文件夹名称',
              border: const OutlineInputBorder(),
              errorText: _errorMessage,
            ),

Form组件包裹TextFormField,提供了表单验证的基础设施。将_formKey绑定到Form上,使得我们可以在外部控制表单的验证行为。TextFormField是TextField的增强版本,支持表单验证功能。decoration中的errorText属性用于显示错误信息,当_errorMessage不为null时,输入框下方会显示红色的错误提示。这种即时反馈机制帮助用户快速发现和纠正输入错误。

            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入文件夹名称';
              }
              if (value.length > 50) {
                return '文件夹名称不能超过50个字符';
              }
              if (value.contains('/') || value.contains('\\')) {
                return '文件夹名称不能包含特殊字符';
              }
              return null;
            },

validator函数定义了完整的验证规则。首先检查输入是否为空,这是最基本的验证。然后限制长度不超过50个字符,防止过长的名称影响UI显示。最后检查是否包含路径分隔符等特殊字符,这些字符在文件系统中有特殊含义,不能用于文件夹名称。返回null表示验证通过,返回字符串表示验证失败并显示该错误信息。这种多层次的验证确保了数据的完整性和系统的稳定性。

            autofocus: true,
            onChanged: (value) {
              setState(() {
                _errorMessage = null;
              });
            },
          ),
        ),
      ),

onChanged回调在用户每次输入时触发,这里用它来清除之前的错误信息。当用户开始修改输入时,旧的错误提示就不再适用,应该被清除。通过setState更新_errorMessage为null,触发UI重新渲染,错误提示消失。这种设计提供了流畅的交互体验,用户不会被过时的错误信息困扰。autofocus保持自动聚焦的特性,确保对话框打开时用户可以立即输入。

      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: _validateAndCreate,
          child: const Text('创建'),
        ),
      ],
    );
  }

actions部分的结构与基础版本类似,但"创建"按钮的onPressed改为调用_validateAndCreate方法。这个方法会先执行验证,只有验证通过才会创建文件夹。将验证逻辑提取到单独的方法中,使代码结构更清晰,也便于后续维护和扩展。这种职责分离的设计是良好编程实践的体现。

  void _validateAndCreate() {
    if (_formKey.currentState!.validate()) {
      widget.onFolderCreated(_nameController.text);
      Navigator.pop(context);
    }
  }

  
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }
}

_validateAndCreate方法通过_formKey访问表单状态并调用validate方法。validate会触发所有TextFormField的validator函数,如果全部通过则返回true。只有验证通过时才执行创建操作和关闭对话框。dispose方法中释放TextEditingController资源,防止内存泄漏。这是Flutter中管理资源的标准做法,确保应用的性能和稳定性。

重复名称检查

在实际应用中,我们需要确保文件夹名称的唯一性,避免创建重复的文件夹。首先定义带重复检查的对话框类:

class UniqueCreateFolderDialog extends StatefulWidget {
  final Function(String) onFolderCreated;
  final List<String> existingNames;

  const UniqueCreateFolderDialog({
    super.key,
    required this.onFolderCreated,
    required this.existingNames,
  });

UniqueCreateFolderDialog在原有基础上增加了existingNames参数,这是一个包含所有已存在文件夹名称的列表。通过这个列表,我们可以在用户输入时检查名称是否已被使用。这种设计将数据源的管理责任交给父组件,对话框只负责验证逻辑,符合单一职责原则。使用List类型明确了参数的数据结构,提高了代码的可读性和类型安全性。

  
  State<UniqueCreateFolderDialog> createState() => 
      _UniqueCreateFolderDialogState();
}

class _UniqueCreateFolderDialogState 
    extends State<UniqueCreateFolderDialog> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  bool _isChecking = false;

State类中新增了_isChecking布尔变量,用于标记是否正在进行重复检查。这个状态变量控制UI的显示,比如在检查时显示加载动画、禁用创建按钮等。通过这种状态管理,我们可以向用户清晰地传达当前的操作状态,避免用户在检查过程中重复提交。这种设计考虑了异步操作的用户体验,是现代应用开发的重要实践。

  
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('新建文件夹'),
      content: SizedBox(
        width: 300.w,
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [

build方法的结构与之前类似,但content部分使用Column来容纳可能的多个子组件。mainAxisSize设置为MainAxisSize.min,使Column只占用必要的垂直空间,避免对话框过大。这种布局设计为未来可能添加的额外UI元素(如提示信息、图标等)预留了空间,同时保持了当前界面的紧凑性。

              TextFormField(
                controller: _nameController,
                decoration: InputDecoration(
                  labelText: '文件夹名称',
                  border: const OutlineInputBorder(),
                  suffixIcon: _isChecking
                      ? const SizedBox(
                          width: 16,
                          height: 16,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                          ),
                        )
                      : null,
                ),

decoration中的suffixIcon属性根据_isChecking状态动态显示加载动画。当正在检查时,输入框右侧会显示一个小型的CircularProgressIndicator,告诉用户系统正在验证输入。SizedBox限制了加载动画的大小为16x16像素,strokeWidth设置为2使其更加精致。这种视觉反馈让用户了解系统的工作状态,减少了等待时的焦虑感,是优秀用户体验设计的体现。

                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '请输入文件夹名称';
                  }
                  if (value.length > 50) {
                    return '文件夹名称不能超过50个字符';
                  }
                  if (widget.existingNames.contains(value)) {
                    return '文件夹名称已存在';
                  }
                  return null;
                },

validator函数在原有验证规则基础上增加了重复检查。通过widget.existingNames.contains(value)判断输入的名称是否已存在。这个检查放在最后执行,因为只有在基本验证通过后,重复检查才有意义。这种分层验证的设计提高了验证效率,也使错误信息的优先级更加合理。用户会先看到格式错误,修正后才会看到重复错误。

                onChanged: (value) {
                  _validateUniqueness(value);
                },
                autofocus: true,
              ),
            ],
          ),
        ),
      ),

onChanged回调调用_validateUniqueness方法,实现实时的重复检查。每当用户输入内容时,都会触发这个检查,提供即时反馈。这种设计虽然会增加一些性能开销,但大大提升了用户体验,用户不需要等到点击创建按钮才知道名称是否可用。autofocus保持自动聚焦特性,确保良好的输入体验。

      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: _isChecking ? null : _validateAndCreate,
          child: const Text('创建'),
        ),
      ],
    );
  }

创建按钮的onPressed使用三元运算符,当_isChecking为true时设置为null,这会自动禁用按钮并改变其外观。这种设计防止用户在检查过程中点击创建按钮,避免了可能的并发问题和重复提交。按钮的禁用状态配合加载动画,形成了完整的状态反馈系统,让用户清楚地知道何时可以进行下一步操作。

  void _validateUniqueness(String value) async {
    if (value.isEmpty) return;
    
    setState(() {
      _isChecking = true;
    });

    await Future.delayed(const Duration(milliseconds: 500));

    setState(() {
      _isChecking = false;
    });
  }

_validateUniqueness方法模拟了异步的重复检查过程。首先检查输入是否为空,空值无需检查。然后设置_isChecking为true,触发UI更新显示加载状态。Future.delayed模拟了网络请求或数据库查询的延迟,实际应用中这里应该是真实的检查逻辑。最后将_isChecking设置回false,恢复正常状态。这种防抖设计避免了频繁的检查操作,提高了性能。

  void _validateAndCreate() {
    if (_formKey.currentState!.validate()) {
      widget.onFolderCreated(_nameController.text);
      Navigator.pop(context);
    }
  }

  
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }
}

_validateAndCreate方法的实现与之前相同,通过表单验证确保所有规则都通过后才执行创建操作。dispose方法负责清理资源,释放TextEditingController。这种资源管理是Flutter开发的基本要求,忽略它可能导致内存泄漏。整个重复检查功能的实现展示了如何在保证数据完整性的同时,提供流畅的用户体验。

动画效果增强

为了提升用户体验,我们可以为对话框添加动画效果,使其出现和消失更加自然流畅。首先定义带动画的对话框类:

class AnimatedCreateFolderDialog extends StatefulWidget {
  final Function(String) onFolderCreated;

  const AnimatedCreateFolderDialog({
    super.key,
    required this.onFolderCreated,
  });

  
  State<AnimatedCreateFolderDialog> createState() => 
      _AnimatedCreateFolderDialogState();
}

AnimatedCreateFolderDialog是对话框的动画包装器,它本身不包含对话框的具体内容,而是负责提供动画效果。这种设计遵循了装饰器模式,将动画逻辑与对话框内容分离,使得代码更加模块化和可复用。我们可以轻松地为任何对话框添加相同的动画效果,而不需要修改对话框本身的代码。

class _AnimatedCreateFolderDialogState 
    extends State<AnimatedCreateFolderDialog>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

State类混入了SingleTickerProviderStateMixin,这是使用AnimationController的必要条件。AnimationController是Flutter动画系统的核心,它控制动画的播放、暂停和时长。duration设置为300毫秒,这是一个经过实践验证的合适时长,既不会太快让用户感觉突兀,也不会太慢影响操作效率。vsync参数使用this,将动画与组件的生命周期绑定,确保在组件不可见时停止动画,节省资源。

    _scaleAnimation = Tween<double>(
      begin: 0.8,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticOut,
    ));
    
    _controller.forward();
  }

Tween定义了动画的起始值和结束值,这里从0.8缩放到1.0,产生一个放大的效果。CurvedAnimation应用了Curves.elasticOut曲线,这会产生一个弹性的动画效果,对话框会稍微超过目标大小然后回弹,给人一种生动活泼的感觉。最后调用forward()开始播放动画。这种弹性动画在现代UI设计中非常流行,能够吸引用户注意力并提供愉悦的视觉体验。

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: CreateFolderDialog(
        onFolderCreated: widget.onFolderCreated,
      ),
    );
  }
}

dispose方法中释放AnimationController资源,这是必须的,否则会导致内存泄漏。build方法使用ScaleTransition包裹原始的CreateFolderDialog,ScaleTransition会根据_scaleAnimation的值自动更新缩放比例。这种组合方式展示了Flutter组件的强大组合能力,我们可以轻松地为现有组件添加新功能而不修改其内部实现。整个动画实现简洁优雅,充分利用了Flutter的动画框架。

实际使用示例

了解了对话框的实现原理后,让我们看看如何在实际应用中使用这些对话框。首先创建一个文件夹管理页面:

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

  
  State<FolderPage> createState() => _FolderPageState();
}

class _FolderPageState extends State<FolderPage> {
  final List<String> _folders = [
    '工作文档', '学习资料', '个人项目', '图片收藏', '视频备份'
  ];

FolderPage是一个完整的文件夹管理页面示例,使用StatefulWidget因为需要管理文件夹列表的状态。_folders列表存储了当前已存在的文件夹名称,这个列表会在用户创建新文件夹时动态更新。在实际应用中,这个列表通常来自数据库或本地存储,这里为了演示使用硬编码的初始数据。这种设计展示了状态管理的基本模式。

  void _showBasicDialog() {
    showDialog(
      context: context,
      builder: (context) => CreateFolderDialog(
        onFolderCreated: (name) {
          setState(() {
            _folders.add(name);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('文件夹"$name"创建成功')),
          );
        },
      ),
    );
  }

_showBasicDialog方法展示了如何使用基础版本的对话框。showDialog是Flutter显示对话框的标准方法,builder参数返回要显示的对话框组件。onFolderCreated回调中,我们使用setState更新_folders列表,添加新创建的文件夹。然后使用SnackBar显示成功提示,给用户即时反馈。这种模式是Flutter中处理用户操作的标准做法,清晰地展示了数据流向和UI更新机制。

  void _showValidatedDialog() {
    showDialog(
      context: context,
      builder: (context) => ValidatedCreateFolderDialog(
        onFolderCreated: (name) {
          setState(() {
            _folders.add(name);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('文件夹"$name"创建成功')),
          );
        },
      ),
    );
  }

_showValidatedDialog方法使用带验证功能的对话框,使用方式与基础版本完全相同。这展示了良好的API设计,不同版本的对话框保持了一致的接口,使用者可以轻松切换而不需要修改回调逻辑。验证功能在对话框内部处理,对外部调用者透明,这种封装提高了代码的可维护性和可测试性。

  void _showUniqueDialog() {
    showDialog(
      context: context,
      builder: (context) => UniqueCreateFolderDialog(
        onFolderCreated: (name) {
          setState(() {
            _folders.add(name);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('文件夹"$name"创建成功')),
          );
        },
        existingNames: _folders,
      ),
    );
  }

_showUniqueDialog方法使用带重复检查的对话框,需要额外传入existingNames参数。这个参数传递当前的文件夹列表,对话框会用它来检查名称是否重复。这种设计将数据源的控制权保留在父组件,对话框只负责验证逻辑,符合关注点分离的原则。每次打开对话框时都会传入最新的文件夹列表,确保检查的准确性。

  void _showAnimatedDialog() {
    showDialog(
      context: context,
      builder: (context) => AnimatedCreateFolderDialog(
        onFolderCreated: (name) {
          setState(() {
            _folders.add(name);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('文件夹"$name"创建成功')),
          );
        },
      ),
    );
  }
}

_showAnimatedDialog方法使用带动画效果的对话框,同样保持了一致的接口。四个方法展示了不同场景下的使用方式,开发者可以根据实际需求选择合适的对话框版本。基础版本适合快速开发,验证版本适合需要数据质量保证的场景,重复检查版本适合需要唯一性约束的场景,动画版本适合注重用户体验的场景。这种分层设计提供了灵活性和可扩展性。

页面UI构建

完成对话框的调用逻辑后,我们需要构建页面的UI来触发这些对话框:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('文件夹管理'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Wrap(
              spacing: 8.0,

build方法构建页面的整体结构。Scaffold提供了标准的Material Design布局,包含AppBar和body。body使用Column垂直排列内容,Padding添加内边距使界面不那么拥挤。Wrap组件用于排列按钮,它会自动换行,适合显示多个操作按钮。spacing设置按钮之间的间距为8像素,确保按钮不会紧贴在一起。这种布局设计考虑了不同屏幕尺寸的适配性。

              children: [
                ElevatedButton(
                  onPressed: _showBasicDialog,
                  child: const Text('基础对话框'),
                ),
                ElevatedButton(
                  onPressed: _showValidatedDialog,
                  child: const Text('验证对话框'),
                ),
                ElevatedButton(
                  onPressed: _showUniqueDialog,
                  child: const Text('重复检查对话框'),
                ),

这里创建了三个按钮,分别对应三种不同的对话框。每个按钮的onPressed回调调用相应的显示方法。按钮文本清晰地说明了每种对话框的特点,便于用户理解和选择。这种设计不仅展示了功能,也提供了很好的学习和测试界面。在实际应用中,通常只会使用其中一种对话框,这里为了演示目的展示了所有版本。

                ElevatedButton(
                  onPressed: _showAnimatedDialog,
                  child: const Text('动画对话框'),
                ),
              ],
            ),
          ),
          const Divider(),
          Expanded(
            child: ListView.builder(
              itemCount: _folders.length,
              itemBuilder: (context, index) {

添加了第四个按钮用于显示动画对话框。Divider组件创建了一条分隔线,将操作区域和列表区域分开,提高了界面的层次感。Expanded使ListView占据剩余的所有垂直空间,确保列表可以充分显示。ListView.builder是构建长列表的高效方式,它只会渲染可见的列表项,节省内存和提高性能。itemCount指定列表项的数量,itemBuilder定义如何构建每个列表项。

                return ListTile(
                  leading: const Icon(Icons.folder),
                  title: Text(_folders[index]),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      setState(() {
                        _folders.removeAt(index);
                      });
                    },
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

ListTile是Material Design中标准的列表项组件,leading显示文件夹图标,title显示文件夹名称,trailing显示删除按钮。删除按钮的onPressed回调使用setState更新列表,移除对应的文件夹。这个完整的示例展示了创建和删除文件夹的完整流程,用户可以通过不同的按钮创建文件夹,然后在列表中看到结果,也可以删除不需要的文件夹。这种交互设计直观明了,符合用户的操作习惯。

总结

创建文件夹对话框是记事本应用中文件夹管理功能的核心组件,它为用户提供了简洁直观的界面来创建新文件夹。通过本文的详细介绍,我们实现了从基础到高级的四种不同版本的对话框,每种都针对特定的使用场景进行了优化。

基础版本的CreateFolderDialog提供了最简单的实现,适合快速开发和原型验证。它只包含必要的UI元素和基本的空值检查,代码简洁易懂。ValidatedCreateFolderDialog在基础版本上增加了完善的输入验证,包括长度限制和特殊字符检查,确保了数据的质量和系统的稳定性。这个版本适合对数据完整性有要求的生产环境。

UniqueCreateFolderDialog进一步增加了重复名称检查功能,通过实时验证确保文件夹名称的唯一性。它展示了如何处理异步验证和状态管理,提供了流畅的用户体验。AnimatedCreateFolderDialog则通过添加动画效果,提升了视觉体验和用户满意度,展示了Flutter强大的动画系统和组件组合能力。

这些实现充分体现了Flutter的设计哲学:组件化、可组合、可复用。每个对话框都是独立的组件,可以单独使用,也可以组合使用。通过统一的回调接口,不同版本的对话框可以无缝切换,降低了维护成本。良好的状态管理和资源管理确保了应用的性能和稳定性。

在实际开发中,开发者应该根据具体需求选择合适的对话框版本。如果只是简单的演示或内部工具,基础版本就足够了。如果是面向用户的产品,建议使用带验证和重复检查的版本,确保数据质量。如果追求极致的用户体验,可以添加动画效果。无论选择哪个版本,都要注意资源管理和错误处理,确保应用的健壮性。

通过持续优化和功能扩展,创建文件夹对话框将成为应用的重要功能模块,为用户提供优秀的文件夹管理体验。


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

Logo

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

更多推荐