当前位置:   article > 正文

前端福音! 一篇文章说明鸿蒙、Flutter、Vue基础组件与基础方法_鸿蒙与vue结合

鸿蒙与vue结合

简介

本篇文章将深入探讨HarmonyOS、Flutter和Vue的基础组件与基础方法。我们将从语言、框架和UI设计理念等方面入手,分析它们的共同之处和差异。HarmonyOS采用ArkTS作为前端开发语言,基于TypeScript演进,其UI则由方舟开发框架ArkUI实现。与之类似,Flutter和Vue也采用声明式编程风格,但它们在组件布局和属性设置上有所不同。以此文章,抛砖引玉,以我个人的视角说明它们的特点.欢迎指正和批评

HarmonyOS前端开发语言为 ArkTS,基于 TypeScript 演进而来。因此,熟悉 TypeScript 是必备的。

HarmonyOS的UI是通过方舟开发框架 ArkUI 实现的,其中又分为Stage模型和FA模型。主要应用开发采用Stage模型。本文将重点介绍Stage模型。

HarmonyOS、Flutter、VUE都采用 声明式编程 风格。

ArkTS 和 Flutter 都是通过组件进行布局,布局和逻辑交叉在一起,而 VUE 则明确区分了HTML、JavaScript、CSS等模块。

Flutter的设计理念为万物皆为Widget,设置组件的属性(例如:widthheightmarginpaddingonClick)都是需要使用对应的Widget实现。

而AskTS通过抽取组件间公共属性的方式可以直接设置属性,相较于Flutter,设置属性方便了很多,也稍微缓解了组件嵌套问题,

但由于两者的特性,嵌套问题一定会存在。

因此,强烈建议在编写AskTS UI时,多抽象业务组件以提高代码的可读性。

此外,相对于Vue和Flutter有一个好大的改善就是DEV Studio还新增了UI预览器,可直接预览UI布局情况,无需通过手机或模拟器查看。

另外,我非常不理解,为何在构建布局方法build时要去掉return关键词,这一点确实令人困惑。

关于DevStudio的一些注意事项(MAC):

正文

学习步骤

在学习前端语言时,个人建议从以下几个方面入手:

  1. 项目结构
  2. 资源引用
  3. 页面、组件生命周期
  4. 路由跳转与路由数据获取
  5. 页面、组件间通信组件间的通信以及响应式UI
  6. 状态管理
  7. 布局
  8. 网络请求
  9. 数据持久化存储

了解以上内容的实现方式,基本上就能掌握前端基础开发所需的技能。

我将以这几个方面来说明三种技术如何完成以上功能

1. 项目结构
Vue
my-vue-project/
│
├── public/
│   ├── index.html         # 主HTML文件
│   └── ...
│
├── src/
│   ├── assets/            # 静态资源如图片、样式等
│   │   └── ...
│   │
│   ├── components/        # 组件文件夹
│   │   └── ...
│   │
│   ├── views/             # 视图文件夹
│   │   └── ...
│   │
│   ├── App.vue            # 根组件
│   └── main.js            # 入口文件
│
├── babel.config.js        # Babel配置文件
├── package.json           # 项目配置文件
├── vue.config.js          # Vue CLI 配置文件

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
Flutter
flutter_project/
│
├── android/                  # Android项目文件夹
│   └── ...
│
├── ios/                      # iOS项目文件夹
│   └── ...
│
├── lib/                      # Dart代码文件夹
│   ├── main.dart             # 应用程序入口文件
│   └── ...
│
├── test/                     # 测试文件夹
│   └── ...
│
├── assets/                   # 静态资源文件夹
│   └── ...
│
├── pubspec.yaml              # 依赖管理和项目配置文件
└── ...

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
HarmonyOS
ArkTS(Stage模型)

harmony_project/
│
├── app.json5                   # 应用的全局配置信息
├── entry/                      # HarmonyOS工程模块,编译构建生成一个HAP包
│   └── src/                    # 在entry中的src文件夹
│       ├── main/               # 主要代码
│       │   └── ets/            # 用于存放ArkTS源码
│       │       ├── entryability/   # 应用/服务的入口
│       │       └── pages/          # 应用/服务包含的页面
│       └── resources/          # 用于存放应用/服务所用到的资源文件
│   └── module.json5            # Stage模型模块配置文件
├── build-profile.json5         # 当前的模块信息、编译信息配置项
├── hvigorfile.ts               # 模块级编译构建任务脚本
├── oh_modules/                 # 用于存放三方库依赖信息
└── build-profile.json5         # 应用级配置信息

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
2.设备图片适配
Vue

资源目录:

 /src/assets/images/...

  • 1
  • 2

资源使用

<template>
  <div>
    <img src="../assets/logo.png" alt="Logo">
  </div>
</template>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

图片多分辨率适配:

资源存放方式:

/src/assets/images/user_bg@2x.png
/src/assets/images/user_bg@3x.png

  • 1
  • 2
  • 3

资源使用:

step1:定义mixin.scss文件:
/* 根据设备的像素密度选择合适的图片资源 */
@mixin bg-image($url) {
    background-image: url($url + "@2x.png");
    @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) {
        background-image: url($url + "@3x.png");
    }
}

step2:在组件中使用mixin.scss

<style lang="scss">
  @import './common/css/mixin.scss';
  .brand{
    width:30px;
    height:30px;
    background-size:30px  30px;
    background-repeat:no-repeat;
    @include bg-image('brand'); 
  }
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
Flutter

资源目录:

/assets/images/...
并在pubspec.yaml文件中注册(可以注册整个assets文件夹)

  • 1
  • 2
  • 3

资源使用

Widget _buildImage(
   return Image.asset("assets/images/user_bg.png",width: 100,height: 100,);
)

  • 1
  • 2
  • 3
  • 4

图片多分辨率适配:

资源存放方式:

/assets/images/...
images文件夹下放默认的图片资源, 再建立不同分辨率3x,4x的文件夹

示例:
/assets/images/user_bg.png
/assets/images/3x/user_bg.png
/assets/images/4x/user_bg.png


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

资源使用:

/* 正常使用即可,Flutter会根据设备的像素密度选择合适的图片资源 */
Widget _buildImage(
   return Image.asset("assets/images/user_bg.png",width: 100,height: 100,);
)

  • 1
  • 2
  • 3
  • 4
  • 5
HarmonyOS

资源目录

/entry/src/main/resources

  • 1
  • 2

资源使用

Image($r('app.media.user_bg'))  // media资源的$r引用

  • 1
  • 2

图片多分辨率适配:

资源存放方式:

使用屏幕密度类型的限定符
包含:sdpi,mdpi,ldpi,xldpi,xxldpi,xxxldpi

/entry/src/main/resources目录中 新建 Resource Dicetory 选择屏幕密度类型.
示例:
/entry/src/main/resources/xxldpi/media/user_bg.png
/entry/src/main/resources/xxxldpi/media/user_bg.png

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

资源使用:

/* 正常使用即可,HarmonyOS会根据设备的像素密度选择合适的图片资源 */
Image($r('app.media.user_bg'))   

  • 1
  • 2
  • 3
3. 页面、组件以及生命周期
Vue

页面与组件的生命周期

  • beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  • created: 实例已经创建完成之后被调用,此时组件的数据观测 (data observer) 和 event/watcher 事件都已经初始化。
  • beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated: 在数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。
  • beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed: Vue 实例销毁后调用。此时,Vue 实例的所有指令已被解绑定,所有事件监听器被移除,所有子实例被销毁。

页面特有的生命周期(Vue Router 中的路由生命周期)

  • beforeRouteEnter: 在路由进入页面前调用,通常用于获取页面数据。
  • beforeRouteUpdate: 在路由更新页面时调用,当页面已经存在且路由发生变化时触发。
  • beforeRouteLeave: 在路由离开页面时调用,通常用于提示用户保存未提交的数据或清理定时器等操作。
Flutter

Flutter的页面也是Widget,所以都是Widget生命周期.生命周期方法包括以下几个:

  • initState():在Widget被创建时调用,通常用于初始化状态或数据。
  • didChangeDependencies():在依赖关系发生变化时调用,比如在Widget首次构建时和之后的每次调用setState()时都会调用。
  • build():构建Widget的主要方法,在初始化时和状态变化时都会调用。
  • didUpdateWidget():在Widget重新构建时调用,通常用于处理Widget属性的变化。
  • setState():用于标记Widget的状态发生变化,会触发build()方法的调用来重新构建Widget。
  • dispose():在Widget被销毁时调用,通常用于释放资源或取消订阅。

此外,Flutter还提供了一些其他生命周期方法,如:

  • didChangeAppLifecycleState():当应用的生命周期发生变化时调用,比如进入后台、返回前台等。
  • reassemble():在开发模式下,当热重载(Hot Reload)时调用,用于重新构建Widget。
  • deactivate():在Widget从树中移除时调用,通常用于清理工作,但并不是Widget被销毁时调用的,后续还可能重新插入树中。
  • mounted属性:Widget是否已经插入到树中的标志,可以用于判断Widget是否处于活动状态。
HarmonyOS

页面和组件的生命周期

  • aboutToAppear: 组件即将出现时回调该函数。在创建自定义组件新实例后,执行build()之前
  • aboutToDisappear: 自定义组件析构销毁之前 不允许在该函数中改变状态变量
  • build :构建组件UI

页面特有生命周期

  • onPageShow: 页面每次显示时触发一次,包括路由过程、应用进入前台等场景
  • onPageHide: 页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景
  • onBackPress: 当用户点击返回按钮时触发
4.路由跳转以及获取路由数据
Vue

路由注册

定义页面

src/views目录下创建页面

<!-- HomePage.vue -->
<template>
  <div>
    <h1>Welcome to Home Page</h1>
  </div>
</template>

<script>
export default {
  name: 'HomePage'
}
</script>

<style scoped>
/* 可选的样式 */
</style>


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

配置路由

router/index.js中创建并注册路由器实例

import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../components/HomePage.vue';

const routes = [
  { path: '/', component: HomePage }
  // 这里可以添加更多的路由配置
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

路由注册给VUE

main.js中将路由器配置应用到Vue实例

import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 引入路由配置

createApp(App).use(router).mount('#app');


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

定义组件(与路由无关)

components目录中创建组件

使用时导入:import TestComponent from "@/components/TestComponent.vue";

  • 1
  • 2
  • 3
  • 4

导航方式

在Vue.js中,编程式导航和声明式导航是两种控制路由跳转的方式。

1. 声明式导航
<router-link to="/about">About</router-link>

2.编程式导航
this.$router.push('about')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

获取路由数据

1.query
this.$route.query

2.params
this.$route.params

3.props
声明props字段


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
Flutter

路由配置

定义页面

路径:lib/pages/HomePage.dart

//HomePage.dart
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Text('Home Page'),
      ),
    );
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

路由注册到Flutter

在main.dart的MaterialApp组件 routes字段中注册.

//main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 设置路由表
      routes: {
        '/': (context) => HomePage(), // 主页路由
      },
      initialRoute: '/', // 初始路由
    );
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

导航方式

Navigator.pushNamed(context, '/about', arguments: 'arguments');

  • 1
  • 2

获取路由数据

arguments可以是任意类型
final String data = ModalRoute.of(context).settings.arguments as String;

  • 1
  • 2
  • 3

定义组件(与路由无关)

lib目录下想建在哪里就建在哪里! 可以任性.
使用时导包即可.

  • 1
  • 2
  • 3
HarmonyOS

路由配置

定义页面

entry/src/main/ets/entryability/pages/下创建组件

使用@Entry注解装饰。注意:一个页面有且只有一个@Entry
示例:
  @Entry
  @Component
  struct TestPage {

  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

路由注册

entry/src/main/resources/base/profile/main_pages.json文件中注册路由

DevStudio中 创建file时,选择 会自动注册

  • 1
  • 2
  • 3
  • 4

导航方式

import router from '@ohos.router';

router.pushUrl({url:"pages/AboutPage",params:{"id":"1"}}) //打开一个新的页面,覆盖在原页面智商
router.replaceUrl() //关闭当前页面,打开一个新页面
router params参数可以是任意类型,传什么类型过去接收的时就是什么类型

页面打开模式:
  standard 标准模式,每次跳转同一页面都会在栈内添加一个路由,默认模式
  single 单实例模式,如果栈内包含同一路由,那么就把最近的一个相同路由置为栈顶(不会删除该路由的之上的路由) 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

路由数据

const params = router.getParams();
console.log("router=aboutToAppear==",params["id"])

  • 1
  • 2
  • 3

定义组件)(与路由无关

1. 使用@Component注解装饰的UI类
示例:
@Component
struct TestComponent {

}

2.自定义组件方法,只能在当前类中使用
@Builder _buildImageSwiper() {
  Swiper(this.swiperController) {
    ForEach(this.imageList, (item) => {
      Image(item)
        .width('100%')
        .height('100%')
      })
    }
    .height(500)
    .width("100%")
    .loop(true)
    .indicatorStyle({
      size: 15,
      color: "#1A000000",
      selectedColor: "#6236FF"
    })
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
5.页面、组件间通信组件间的通信以及响应式UI
Vue

响应式UI(MVVM)

在Vue3中,ref用于跟踪单个响应式值,而reactive用于跟踪一个对象。

以下是使用refreactive的简单示例:

<template>
  <div>
    <!-- 使用ref的示例 -->
    <input v-model="messageRef" />
    <p>{{ messageRef }}</p>
 
    <!-- 使用reactive的示例 -->
    <input v-model="state.name" />
    <p>{{ state.name }}</p>
  </div>
</template>
 
<script>
import { ref, reactive } from 'vue';
 
export default {
  setup() {
    // 使用ref
    const messageRef = ref('Hello, Vue!');
 
    // 使用reactive
    const state = reactive({
      name: 'Reactive State'
    });
 
    // 返回到模板
    return {
      messageRef,
      state
    };
  }
};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

组件间通信

  1. 父子组件通信:

    使用props 和 events

  2. 子父组件通信:

    使用 emit和emit 和 emit和on

  3. 兄弟组件通信/父孙之间通信:

    使用事件总线(Event Bus)或Vuex

  4. 跨多层级的组件通信:

    使用事件总线或Vuex,或者provide/inject

示例:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <Child :message="parentMessage" @notify="handleNotify" />
    <p>来自子组件的消息: {{ childMessage }}</p>
  </div>
</template>

<script>
import { ref } from 'vue';
import Child from './Child.vue';

export default {
  components: {
    Child,
  },
  setup() {
    const parentMessage = ref('父组件的消息');
    const childMessage = ref('');

    const handleNotify = (message) => {
      childMessage.value = message;
    };

    return {
      parentMessage,
      childMessage,
      handleNotify,
    };
  },
};
</script>


<!-- 子组件 Child.vue -->

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="sendMessage">向父组件发送消息</button>
  </div>
</template>

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
  props: {
    message: String,
  },
  setup(props, { emit }) {
    const sendMessage = () => {
      emit('notify', '子组件的消息');
    };

    return {
      sendMessage,
    };
  },
});
</script>


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
Flutter

响应式UI(MVVM)

Flutter 中UI可变化的组件是StatefulWidget ,更新组件状态并重新构建其 UI 的方法是setState 。当 StatefulWidget 的状态发生变化时,需要调用 setState 来通知 Flutter 框架,以便框架可以重新构建该组件的 UI。

以下是setState()的简单使用示例:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
 
  final String title;
 
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
 
  void _incrementCounter() {
    setState(() {
      // 这里更新UI相关的状态
      _counter++;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '点击按钮更新次数:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

Widget间通信

  1. 父子间通信

    • 通过构造函数传递数据:在创建子Widget时,通过构造函数将数据传递给子Widget。
  2. 子父间通信

    • 使用回调函数:在创建子Widget时,将回调函数操作传递给子Widget,子Widget执行操作后通过回调函数将结果传递回父Widget
  3. 使用全局状态管理工具

    • 使用Flutter提供的全局状态管理工具,如Provider、Riverpod、GetX等,在应用的任何地方都可以访问和修改共享的状态数据。
  4. 使用InheritedWidget

    • InheritedWidget是Flutter提供的一种在Widget树中共享数据的方式,父Widget通过InheritedWidget将数据传递给子Widget,在子Widget中可以通过BuildContext获取到共享的数据。

示例:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  String messageFromChild = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('父组件'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('接受的子组件消息: $messageFromChild'),
          ChildWidget(
            initialMessage: '来自父亲的关怀',
            onMessageReceived: (msg) {
              setState(() {
                messageFromChild = msg;
              });
            },
          ),
        ],
      ),
    );
  }
}

class ChildWidget extends StatefulWidget {
  final String initialMessage;
  final Function(String) onMessageReceived;

  ChildWidget({required this.initialMessage, required this.onMessageReceived});

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

class _ChildWidgetState extends State<ChildWidget> {
  late String message;

  @override
  void initState() {
    super.initState();
    message = widget.initialMessage;
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('父亲的传递的关怀: $message'),
          ElevatedButton(
            onPressed: () {
              widget.onMessageReceived('来自儿子的孝心');
            },
            child: Text('喊爸爸'),
          ),
        ],
      ),
    );
  }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
HarmonyOS

在ArKTS中,组件间通信主要有以下几种方式:

  1. 父子间通信

    通过构造函数传递数据:在创建子组件时,通过构造函数将数据传递给子组件。父组件中使用@State注解声明变量

    子组件通过构造函数接受该变量.注解声明变量的方式有两种

    1. 单向传递.使用@Prop注解声明:父组件将变量「拷贝」一份交给子组件使用,子组件不可修改变量,
    2. 双向传递.使用@Link注解声明;父组件会将变量的「引用」交给子组件,相当于子组件可以[直接修改]
      如果是传递对象时,需要@ObjectLink@Observed装饰器用于在涉及嵌套对象或数组为元素对象的场景中进行双向数据同步。
  2. 子父间通信

    使用回调函数:在创建子Widget时,将回调函数操作传递给子Widget,子Widget执行操作后通过回调函数将结果传递回父Widget

  3. 父孙之间通信

    通过@provide@cousume注解声明变量: 父组件中通过@provide 声明变量,发布消息订阅 子组件中通过@cousume接收消息订阅.

    注意:provide和cousume需要变量名字相同或者定义别名

示例:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  @State parentText: string = '我是 parent'
  @State childText: string = 'child-text'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick((e) => {
            console.log(e + "");
            this.message = "点击刷新message"
          }).margin({ bottom: 20 });
        ChildComponent({ parentText: this.parentText, childText: $childText }).margin({ bottom: 20 });
        Text("child text=" + this.childText).fontColor("#787").fontSize(30);

      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component
struct ChildComponent {
  @Prop parentText: string
  @Link childText: string

  build() {
    Text(this.parentText+"点击传递值给parent")
      .fontColor("#333")
      .fontSize(20)
      .onClick((e) => {
        this.childText = "child changed"
      })
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
6.状态管理

在声明式 UI 中,状态管理是通过将应用程序的状态从各个组件中进行状态提审到中央位置上。这样所有组件都可以共享和访问相同的状态数据。状态管理的目的是确保应用程序状态的变化变得可预测和可控,并保持不同部分的 UI 同步。

在前端开发中,登录成功后保存用户信息并且刷新全局UI是一个非常普遍的功能,我们通过用户信息全局状态管理进行讲解示例.

Vue

在Vue3,我们可以使用Vuex或者是provideinject 实现全局状态管理

这里将使用provideinject 进行说明

创建全局状态管理模块

首先,我们创建一个名为 useUserStore.js 的文件,该文件将包含我们的全局用户状态及其相关方法。

<!-- useUserStore.js -->

import { reactive, ref } from 'vue';

export const useUserStore = () => {
  const state = reactive({
    user: ref(null) // 使用 ref 创建一个响应式引用,初始值为 null
  });

  // 设置用户的方法
  const setUser = (newUser) => {
    state.user = ref(newUser); // 当设置用户时,确保它是一个响应式引用
  };

  // 清除用户的方法
  const clearUser = () => {
    state.user = ref(null); // 清除用户信息
  };

  // 提供给子组件的状态和方法
  const provideUserStore = () => ({
    user: state.user,
    setUser,
    clearUser
  });

  return {
    provideUserStore
  };
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在 Vue 应用中使用全局状态管理模块

接下来,在我们的主文件 main.js 中,我们需要使用创建的 useUserStore,并将其提供给根组件,这样任何子孙组件都可以注入它。

<!-- main.js -->

import { createApp } from 'vue';
import App from './App.vue';
import { useUserStore } from './useUserStore';

const app = createApp(App);

// 创建用户状态管理实例
const userStore = useUserStore();

// 将用户状态管理实例提供给根组件
app.provide('userStore', userStore.provideUserStore());

app.mount('#app');


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在组件中使用全局状态管理

现在,在任何子孙组件中,我们都可以使用 inject 来访问用户状态和方法。

登录保存UserInfo

<!-- Login.vue -->

<template>
  <div>
    <!-- 登录表单 -->
    <input type="text" v-model="credentials.username" placeholder="Username" />
    <input type="password" v-model="credentials.password" placeholder="Password" />
    <button @click="login">登录</button>
  </div>
</template>

<script>
import { inject, ref } from 'vue';

export default {
  setup() {
    // 注入用户状态管理实例
    const { user, setUser, clearUser } = inject('userStore');

    // 用于存储登录凭据的响应式对象
    const credentials = ref({
      username: '',
      password: ''
    });

    // 登录方法
    const login = () => {
      // 登录成功,设置用户信息
      setUser(credentials.value);
    };

    // 清除用户信息的方法
    const logout = () => {
      clearUser();
    };

    return {
      credentials,
      login,
      logout
    };
  }
};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

User页面显示信息

<!-- User.vue -->
<template>
  <div>
    <p v-if="user">{{ user.username }},欢迎回来!</p>
    <p v-else>请登录</p>
  </div>
</template>
<script>
import { inject } from 'vue';
export default {
  setup() {
    // 注入用户状态管理实例
    const { user } = inject('userStore');

    return {
      user
    };

  }
};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这个示例中,Login.vue 组件使用 inject 来获取全局状态管理实例,并调用 setUser 方法来设置用户信息。User.vue 组件也使用 inject 来获取用户信息,并显示当前登录用户的用户名。这样,无论用户导航到应用的哪个部分,用户状态都会保持一致,并且可以在需要时轻松更新。

Flutter

在Flutter中,通常使用provider库实现全局状态管理.

下面是一个简单的示例,实现用户登录并保存用户信息以进行全局状态管理

首先,创建一个User类来表示用户信息:

//User.dart
class User {
  final String name;
  final String passwrod;

  User(this.name, this.passwrod);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后,创建一个UserInfoModel类,该类将包含用户登录的逻辑,并更新用户状态:

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

class UserInfoModel with ChangeNotifier {
  User? _currentUser;

  User? get currentUser => _currentUser;

  Future<void> login(String name, String passwrod) async {
    // 模拟异步登录过程
    await Future.delayed(Duration(seconds: 1));
    _currentUser = User(name, passwrod);
    notifyListeners();
  }

  void logout() {
    _currentUser = null;
    notifyListeners();
  }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

接下来,在main.dart中将UserInfoModel注册全局的Provider

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
        providers: [
          ChangeNotifierProvider.value(value: UserInfoModel()),
        ],
        child: MaterialApp(
          home: MyHomePage(),
        ));
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

现在,在任何组件中,我们都可以使用 Provider提供的方法操作的UserInfoModel对象。

Login.dart中登录成功后保存数据

//Login.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './user_info_model.dart';

class Login extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(labelText: 'Username'),
            ),
            SizedBox(height: 16.0), // 添加一些间距
            TextField(
              obscureText: true, // 隐藏密码
              decoration: InputDecoration(labelText: 'Password'),
            ),
            SizedBox(height: 24.0), // 添加一些间距
            ElevatedButton(
              onPressed: () {
                // 获取Provider中的UserModel实例
   						 UserInfoModel userModel = context.watch<UserInfoModel>();
                // 假设我们从输入框中获取了用户名和密码
                final String username = 'test_user';
                final String password = 'test_password';
								userModel.login(username,password)
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

当点击登录按钮时,我们从Provider中获取UserInfoModel的实例,并使用模拟的用户名和密码来调用UserInfoModellogin方法实现用户信息的全局刷新

在User.dart中使用UserInfoModel获取用户信息

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'user_info_model.dart'; 

class User extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
   	UserInfoModel userModel = context.watch<UserInfoModel>();
    return Scaffold(
      appBar: AppBar(
        title: Text('User Profile'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 显示用户名的Text组件
            Text(
              userInfoModel.name,
              style: Theme.of(context).textTheme.headline5,
            ),
            // 退出登录的按钮
            ElevatedButton(
              onPressed: () {
                userInfoModel.logout(); // 调用logout方法
              },
              child: Text(
                'Logout',
                style: Theme.of(context).textTheme.button,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

通过 context.watch<UserInfoModel>() 获取 UserInfoModel 的实例,并显示其 name 字段。ElevatedButton 组件的 onPressed 属性被设置为调用 UserInfoModel 实例的 logout 方法,处理退出登录,销毁用户数据的逻辑。

HarmonyOS

在鸿蒙ArkTS中,使用PersistentStorage 以及AppStorage进行全局状态管理

首先先说明一下PersistentStorage的作用为持久化保存AppStorage的对象,当退出App后,AppStorage保存的值将会被清除,PersistentStorage在初始化时,会将AppStorage的对象进行复原,已达到用户数据持久化保存

首先先建立UserInfoModel

//UserInfoModel.ets
export class UserInfoModel {
  static init() {
    PersistentStorage.PersistProp('userInfo', { "isLogin": false });
  }

  static login(loginInfo) {
    AppStorage.SetOrCreate('userInfo', loginInfo)
  }

  static logout() {
    PersistentStorage.DeleteProp('userInfo');

  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

然后进行初始化UserInfoModel,并且注册PersistentStorage

//Index.ets
//组件即将出现的生命周期方法
  aboutToAppear() {
    UserInfoModel.init()
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

就这样完成了UserInfoModel的全局注册

Login.ets中保存用户信息

import router from '@ohos.router'
import toast from '@ohos.promptAction'

@Entry
@Component
struct LoginPage {
  @State inputPhone: string = "";

  build() {
    Row() {
      Column() {
        this._buildBackIcon()
        this._buildLoginTitle()
        this._buildPhoneTextFiled()
        this._buildSubmitButton()
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
    }
    .alignItems(VerticalAlign.Top)
    .justifyContent(FlexAlign.Start)
    .height('100%').backgroundColor("#ffffff")
  }

  @Builder _buildBackIcon() {
    Row() {
      Image($r('app.media.ic_book_reader_left_arrow'))
        .width(14).height(14)
    }
    .width(40)
    .height(40)
    .margin({
      top: 60,
      left: 20
    })
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .onClick((e => {
      router.back()

    }))

  }

  @Builder _buildLoginTitle() {
    Text("手机号码登录/注册")
      .fontColor("#cc000000")
      .fontSize(26)
      .fontWeight(500)
      .margin({
        top: 40,
        left: 30
      })
  }


  @Builder _buildPhoneTextFiled() {
    Column() {

      TextInput({ placeholder: "请输入手机号码" })

        .height(50)
        .backgroundColor("#ffffff")
        .type(InputType.PhoneNumber)
        .onFocus(() => {
        })
        .onChange((value: string) => {
          this.inputPhone = value
        })
        .onSubmit((e) => {

        })
        .maxLength(11)
        .placeholderColor("#80000000")
        .fontColor("#cc000000")
        .fontWeight(600)
        .margin(0)
        .padding(0)
        .borderRadius(0)
      Divider()
        .color(this.inputPhone.length == 0 ? "#2342" : "#234221")
        .height(0.5)
        .width('100%')

    }
    .padding({ left: 30, right: 30, top: 60 })
    .width('100%')
  }

  @Builder _buildSubmitButton() {
    Row() {
      Button("立即登录")
        .width('100%')
        .height(44)
        .backgroundColor("#6236FF")
        .onClick((e) => {
           if (this.inputPhone.length == 0) {
            toast.showToast({
              message: "请输入电话号码"
            })
            return;
          }
         	//模拟登录
        	const userInfo =  UserApi.login(this.inputPhone)
          //刷新状态管理器
				  UserInfoModel.login(userInfo);

        })

    }.margin({ left: 30, right: 30, top: 60 })
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

User.ets中使用UserInfoModel的信息

//User.ets

  • 1
  • 2
@Entry
@Component
struct User {
  @StorageProp("userInfo") userInfo: string = ""

  build() {
    Row() {
      Column() {
        Text(JSON.stringify(this.userInfo))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick((e) => {
            console.log(e + "");
            this.message = "sss"
          }).margin({ bottom: 20 });
 
        Button("退出登录").onClick(() => {
           UserInfoModel.logout();
        })
       
      }
      .width('100%')
    }
    .height('100%')
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

User.ets中使用@StorageProp注解获取到UserInfo的信息,("userInfo") 中的值需要和UserInfoModel注册的值保持一样. 在退出登录时,调用 UserInfoModel.logout()完成用户数据的清除

7.布局

在日常开发中,我们所使用到的布局样式通常为Flex(横向,纵向)和Position(堆叠),这里主要也就从这2个方面来介绍三种语言的布局以及着重介绍AskTS的布局方式和常用的组件

Vue

在Vue中,实现布局需要使用HTML语言和CSS语言分别提供标签和样式。HTML语言提供标签,而CSS语言提供样式。这些代码分别放置在Vue组件的<template>标签和<style>标签之下。

Flex

<template>
	<div class="flex-container">
    <div class="item">Item 1</div>
    <div class="item">Item 2</div>
    <div class="item">Item 3</div>
  </div>
</template>

<style>
  .flex-container{
    display: flex;
    flex-direction:row; // row表示横向布局,column表示纵向布局
    justify-content: center; //主轴上子元素的对齐方式
    align-items: center; //交叉轴上子元素的对齐方式
  }
</style>

最终UI展示效果 item1 item2 item3横向或者是纵向排列

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Position

<template>
    <div class="flex-container stack-container">
      <div class="item">Item 1</div>
      <div class="item">Item 2</div>
      <div class="item">Item 3</div>
      <div class="item stack-item">Item 4</div>
    </div>
</template>

<style>
.flex-container {
    display: flex;
    flex-direction: row;
 }

.stack-container {
  position: relative;
}

.stack-item {
  position: absolute;
  top: 10px;  //距离顶部距离
  left: 10px; //距离左侧距离
  //注意,同一方向的属性只能出现一个  
  //使用top left 布局从左上计算偏移
  //使用top right 布局从右上计算偏移
  //使用bottom left 布局从左下计算偏移
  //使用bottom right 布局从右下计算偏移

  background-color: white;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
}
</style>

最终UI展示效果 item1 item2 item3横向排列,item4盖在了item1上面


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
Flutter

在Flutter中,万物皆为Widget,所有的布局和组件都由Widget组成。所有需要设置的属性都需要使用Widget,例如设置间距使用Pading组件,设置点击事件使用GestureDetector组件,设置背景颜色使用Container组件.所以在使用Flutter编写布局存在很严重的Widget嵌套过深的问题!

Flutter有两个基础Widget,所有的组件都是集成他俩.分别是StatelessWidgetStatefulWidget

  1. StatelessWidget(无状态组件):

    • StatelessWidget代表着这个widget的UI状态不可变化,即其属性在其生命周期内不会发生变化。
    • StatelessWidget通常用于表示静态UI元素,例如展示文本、图像等不需要交互的组件。
    • StatelessWidget的build方法会在每次需要重建时被调用,但它的状态不会改变。
  2. StatefulWidget(有状态组件):

    • StatefulWidget代表着Widget的UI状态可以变化,即其属性可以在其生命周期内发生变化。

    • StatefulWidget通常用于表示需要动态交互的组件,例如按钮、输入框等。

    • StatefulWidget由两部分组成:一个是StatefulWidget本身,另一个是与之关联的State对象,用于管理Widget的状态。

    • StatefulWidget的build方法会在每次需要重建时被调用,并且可以通过与其关联的State对象来管理和更新状态。

Flex

RowExample.dart
//Row 水平方向布局
class RowExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween, //主轴,当组件是Row时,代表着设置组件水平方向的对齐方式
      crossAxisAlignment: CrossAxisAlignment.center,//交叉轴,当组件是Row时,代表着设置组件垂直方向的对齐方式
      children: [
        Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
        Container(
          width: 50,
          height: 50,
          color: Colors.green,
        ),
        Container(
          width: 50,
          height: 50,
          color: Colors.blue,
        ),
      ],
    );
  }
}
//UI最终展示效果是red  green blue三个色块横向排列

//Column 垂直方向布局
class ColumnExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴,当组件是Column时,代表着设置组件垂直方向的对齐方式
      crossAxisAlignment: CrossAxisAlignment.start,//交叉轴,当组件是Column时,代表着设置组件水平方向的对齐方式
      children: [
        Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
        Container(
          width: 50,
          height: 50,
          color: Colors.green,
        ),
        Container(
          width: 50,
          height: 50,
          color: Colors.blue,
        ),
      ],
    );
  }
}

//UI最终展示效果是red  green blue三个色块纵向排列

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

Position

在Flutter中实现堆叠布局使用的是Stack组件,在Stack组件中定位子Widget的位置有两种方式

  1. Positioned 组件
    • 使用 Positioned 组件可以精确地设置子组件在堆叠中的位置,通过设置 left、top、right、bottom 属性来指定子组件相对于堆叠的位置。
    • 这种方式适用于需要精确控制子组件位置的情况。
  2. Align 组件
    • 使用 Align 组件可以根据指定的 alignment 属性将子组件对齐到堆叠的位置。
    • Align 组件的 alignment 属性可以设置为 Alignment 枚举值或者 FractionalOffset 对象来指定对齐方式。
    • 这种方式适用于相对简单的布局,通过设置对齐方式来快速调整子组件的位置。
//堆叠布局
class StackDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Container(
          width: double.infinity,
          height: double.infinity,
          color: Colors.white,
        ),
        //Stack中间
        Align(
          alignment: Alignment.center,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
        //Stack右上角
         Align(
          alignment: Alignment.topRight,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.yellow,
          ),
        ),
        //距离左侧50,顶部50的位置
        Positioned(
          top: 50,
          left: 50,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.red,
          ),
        ),
       //距离右侧50,底部50的位置
        Positioned(
          bottom: 50,
          right: 50,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.green,
          ),
        ),
  
      ],
    );
  }
}
UI最终展示效果是blue在中间 yellow在右上角,red在距离左侧50,顶部50的位置,绿色在距离右侧50,底部50的位置

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
HarmonyOS(ArkUI)
ArkUI开发框架主要有9种布局方式
布局类型优点适用场景
线性布局(Row、Column)简单,适合线性排列的子元素子元素能够以某种方式线性排列时优先考虑此布局
层叠布局(Stack)能够实现堆叠效果而不影响其他子组件的布局空间需要有堆叠效果时优先考虑此布局,例如弹出式面板
弹性布局(Flex)默认支持子组件的压缩或拉伸,可使得多个容器内子组件有更好的填充效果需要计算拉伸或压缩比例时优先使用此布局
相对布局(RelativeContainer)布局自由,通过设置锚点规则实现子组件位置对齐页面元素分布复杂、需要子元素自由布局时推荐使用
栅格布局(GridRow、GridCol)栅格可根据不同设备灵活布局,降低适配成本,保证一致性和协调性不同设备、不同状态下的布局需求,例如手机、大屏、平板等
媒体查询(@ohos.mediaquery)根据设备类型或状态修改应用样式,提高用户体验根据设备和应用的不同属性信息设计不同的布局,动态更新页面布局
列表(List)轻松高效地显示结构化、可滚动的信息呈现同类数据类型或数据类型集的场景,例如图片和文本列表
网格(Grid)页面均分能力强,子组件占比控制能力强需要按照固定比例或均匀分配空间的布局场景,例如计算器、相册、日历等
轮播(Swiper)实现广告轮播、图片预览等功能广告轮播、图片预览、可滚动应用等场
设置布局位置两种类型
布局类型实现方式使用说明
绝对定位使用 position 实现绝对定位,设置元素左上角相对于父容器左上角偏移位置对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。
相对定位使用 offset 可以实现相对定位,设置元素相对于自身的偏移量相对定位不脱离文档流,即原位置依然保留,不影响元素本身的特性,仅相对于原位置进行偏移。
常用组件
简单实战示例

整体页面布局

页面分为三个模块,顶部功能区UgcDetailHeader,底部功能区UgcDetailBottom,中间内容显示区域this._buildUgcContent(),顶部功能区和底部功能区盖在中间内容显示区域之上

  build() {
    //完成底部模块UgcDetailBottom沉底
    Stack({ alignContent: Alignment.BottomStart }) {
      //完成顶部模块UgcDetailHeade置顶
      Stack({ alignContent: Alignment.TopStart }) {
        //Ugc内容模块
        this._buildUgcContent()
        //顶部模块,包含title,左侧退出按钮,右侧分享按钮
        UgcDetailHeader()

      }.width('100%').height("100%")
      //底部模块
      UgcDetailBottom()
    }
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

顶部模块UgcDetailHeader

//@Component注解表示这是一个组件
@Component
export struct UgcDetailHeader {
  build() {
    Row() {
      //左侧关闭页面的按钮
      Row() {
       Image($r("app.media.ic_book_reader_left_arrow")).width(7.5).height(14)
      }
      .height(36)
      .width(36)
      .margin({ left: 16 })
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .borderRadius(18)
      .backgroundColor("#33000000")
      //右侧分享按钮
      Row() {
        Image($r("app.media.ic_share_copy_link")).width(36).height(36)
      }
      .height(36)
      .width(36)
      .margin({ right: 16 })
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .borderRadius(18)
      .backgroundColor("#33000000")
    }
    .width("100%")
    //主轴组件两端对齐
    .justifyContent(FlexAlign.SpaceBetween)
    .height(60)
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

顶部模块UgcDetailBottom

@Component
export struct UgcDetailBottom {
  @State inputReview: string = "";

  build() {
    Row() {
      Row() {
        Image($r("app.media.ic_ugc_detail_review")).width(20).height(20).margin({ left: 18, right: 4 })
        //输入框
        TextInput({ placeholder: "说点什么..." })
          .onChange((value: string) => {
            this.inputReview = value
            console.log("_buildPhoneTextFiled===onChange" + value)
          })
          .onSubmit((e) => {
            console.log("_buildPhoneTextFiled===onSubmit" + e)

          })
          .fontColor("#cc000000")
          .placeholderColor("#5c000000")
          .placeholderFont({
            size: 12,
          })
          .fontSize(12)
          .width(195)
          .height(30)
          .backgroundColor('#F1F1F1')
          .margin(0)
          .padding(0)

      }
      .width(237)
      .height(30)
      .backgroundColor('#F1F1F1')
      .borderRadius(16)
      .margin({ left: 16 })


      Row() {
        Image($r("app.media.ic_ugc_detail_encourage")).width(20).height(20).margin({ left: 18, right: 4 })
        Text("356次鼓励").fontSize(12).fontWeight(600).fontColor("#6236FF")
      }.margin({right:16})
    }
    //交叉轴垂直居中
    .alignItems(VerticalAlign.Center)
    //主轴两端对齐
    .justifyContent(FlexAlign.SpaceBetween)
    .height(60)
    .backgroundColor("#ffffff")
    .width("100%")
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

中间内容显示区域UgcContent

 //自定义组件方法,只能在当前类中使用 
@Builder _buildUgcContent() {
    //UGC内容模块,整体滑动
    Scroll() {
      //纵向布局
      Column() {
        //自定义组件方法,只能在当前类中使用
        this._buildImageSwiper()
        //用户模块,全局组件
        UgcDetailUserInfo()
        //UGC信息,全局组件
        UgcDetailUgcInfo()
      }
      //交叉轴的排列方式
      .alignItems(HorizontalAlign.Start)
      //主轴的排列方式
      .justifyContent(FlexAlign.Start)

    }.width('100%')
  }

UgcDetailUserInfo.ets

  @Component
  export struct UgcDetailUserInfo {
    build() {
      Row() {
        Row() {
          Image("https://wegymercdn.gymcity.com.cn/coach_archives_3.0/080ae09c812e4f1ef0f5290d23ced62f.png")
            .height(36)
            .width(36).borderRadius(10)
          Column() {
            Text("营养师彭彭").fontSize(13).fontColor("#CC000000")
            Text("betterWE 明星营养师").fontSize(10).fontColor("#66000000").margin({ top: 3 })
          }.margin({
            left: 8
          }).alignItems(HorizontalAlign.Start)
        }

        Row() {
          Text("+ 关注").fontSize(13).fontWeight(600).fontColor("#6236FF")
            .margin({
              left: 3
            })
        }
        .borderRadius(12)
        .backgroundColor('#0D6236FF')
        .width(64)
        .height(24)
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.Center)

      }
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({
        left: 16,
        right: 16 }
      )
      .width("100%")
      .margin({
        top: 20
      })
    }
  }

UgcDetailUgcInfo.ets

@Component
export struct UgcDetailUgcInfo {
  build() {
    Column() {
      Text("营养师推荐 逆袭大女主!").fontSize(13).fontWeight(600).fontColor("#CC000000")
      Text("1⃣早八党粗粮碳水【全麦杂粮包】\n✅0蔗糖0反式脂肪➕全麦占比no.1\n某姆健康版平价替代~\n\n
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/687915
推荐阅读
相关标签