案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy

写在前面

消息通知设置这个功能,说实话用户不会经常用,但一定要有。为什么?因为有些用户真的很烦推送通知。

我自己手机上装了几十个 App,如果每个都疯狂推送,一天能收到上百条通知,根本看不过来。所以我会把大部分 App 的通知关掉,只留几个重要的。

作为开发者,我们当然希望用户开着通知,这样能提高用户活跃度。但如果不给用户关闭的选项,用户可能直接把整个 App 的通知权限关掉,或者干脆卸载。所以,给用户控制权反而是更好的选择。

这篇文章记录一下消息通知设置页面的实现过程,主要是用 Switch 组件来控制各种通知的开关。
请添加图片描述

页面要实现什么功能

消息通知设置页面需要控制这些开关:

总开关:

  • 推送通知:控制是否接收任何推送

分类开关:

  • 订单通知:订单状态变更提醒
  • 促销活动:优惠活动和促销信息
  • 系统通知:系统公告和服务通知

逻辑上,如果总开关关了,分类开关应该全部禁用。这个交互很常见,微信、支付宝的通知设置都是这么做的。

引入需要的依赖

import React from 'react';

这个页面不需要 useState,因为设置数据存在全局状态里,直接用全局状态的读写方法就行。

import {View, Text, StyleSheet, Switch} from 'react-native';

Switch 是 React Native 内置的开关组件,就是那种左右滑动的开关。iOS 和 Android 上的样式会有点不一样,但功能是一样的。

import {useApp} from '../store/AppContext';
import {Header} from '../components/Header';

useApp 里有 settings 对象和 updateSettings 方法,用来读取和更新设置。

获取设置数据

export const NotificationsScreen = () => {
  const {settings, updateSettings} = useApp();

从全局状态里取两个东西:

settings:当前的设置数据,是个对象,里面有各种设置项的值

updateSettings:更新设置的方法,传入 key 和 value 就能更新对应的设置项

这种设计的好处是,设置数据是全局的,在任何页面都能读取到。比如首页要根据 pushEnabled 判断是否显示通知提示,直接从 settings 里取就行。

页面整体结构

return (
  <View style={styles.container}>
    <Header title="消息通知" />

    <View style={styles.content}>
      {/* 总开关 */}
      {/* 分类开关 */}
      {/* 底部提示 */}
    </View>
  </View>
);

结构很简单,一个 Header,下面是设置项列表。

总开关

<View style={styles.item}>
  <View style={styles.itemInfo}>
    <Text style={styles.itemTitle}>推送通知</Text>
    <Text style={styles.itemDesc}>开启后可接收推送消息</Text>
  </View>
  <Switch
    value={settings.pushEnabled}
    onValueChange={(value) => updateSettings('pushEnabled', value)}
    trackColor={{false: '#ddd', true: '#3498db'}}
    thumbColor="#fff"
  />
</View>

这是最重要的开关,控制是否接收任何推送。

value:开关的当前状态,从 settings.pushEnabled 读取

onValueChange:状态变化时的回调,把新值更新到全局状态

trackColor:轨道颜色,false 是关闭时的颜色,true 是开启时的颜色

thumbColor:滑块颜色,我们用白色

Switch 组件的样式在不同平台上有差异。iOS 上开启是绿色的,Android 上是蓝色的。通过 trackColor 可以统一成我们想要的颜色。

item: {
  flexDirection: 'row', 
  alignItems: 'center', 
  justifyContent: 'space-between', 
  backgroundColor: '#fff', 
  padding: 16, 
  borderBottomWidth: 1, 
  borderBottomColor: '#f0f0f0'
},

每个设置项是一行,左边是文字说明,右边是开关。用 justifyContent: 'space-between' 让它们分布在两端。

itemInfo: {flex: 1, marginRight: 16},
itemTitle: {fontSize: 16, color: '#333'},
itemDesc: {fontSize: 12, color: '#999', marginTop: 4},

标题用 16 号字,描述用 12 号灰色字,层次分明。

分类开关的标题

<View style={styles.section}>
  <Text style={styles.sectionTitle}>通知类型</Text>

分类开关上面加个小标题,让用户知道下面这些是具体的通知类型。

section: {marginTop: 24},
sectionTitle: {fontSize: 14, color: '#999', paddingHorizontal: 16, paddingVertical: 12},

标题用灰色小字,和设置项区分开。

订单通知开关

<View style={[styles.item, !settings.pushEnabled && styles.disabledItem]}>
  <View style={styles.itemInfo}>
    <Text style={[styles.itemTitle, !settings.pushEnabled && styles.disabledText]}>
      订单通知
    </Text>
    <Text style={[styles.itemDesc, !settings.pushEnabled && styles.disabledText]}>
      订单状态变更提醒
    </Text>
  </View>
  <Switch
    value={settings.orderNotify}
    onValueChange={(value) => updateSettings('orderNotify', value)}
    trackColor={{false: '#ddd', true: '#3498db'}}
    thumbColor="#fff"
    disabled={!settings.pushEnabled}
  />
</View>

这里有几个关键点:

条件样式:如果总开关关了(!settings.pushEnabled),整个 item 加上 disabledItem 样式,文字加上 disabledText 样式,看起来是灰色禁用状态。

disabled 属性:Switch 组件的 disabled 属性设为 !settings.pushEnabled,总开关关了这个开关就不能点击。

disabledItem: {opacity: 0.5},
disabledText: {color: '#ccc'},

禁用状态用 opacity: 0.5 让整个 item 变淡,文字变成浅灰色。这样用户一眼就能看出这些开关现在不可用。

促销活动开关

<View style={[styles.item, !settings.pushEnabled && styles.disabledItem]}>
  <View style={styles.itemInfo}>
    <Text style={[styles.itemTitle, !settings.pushEnabled && styles.disabledText]}>
      促销活动
    </Text>
    <Text style={[styles.itemDesc, !settings.pushEnabled && styles.disabledText]}>
      优惠活动和促销信息
    </Text>
  </View>
  <Switch
    value={settings.promoNotify}
    onValueChange={(value) => updateSettings('promoNotify', value)}
    trackColor={{false: '#ddd', true: '#3498db'}}
    thumbColor="#fff"
    disabled={!settings.pushEnabled}
  />
</View>

和订单通知一样的结构,只是 key 不同(promoNotify)。

促销活动这个开关,说实话很多用户会关掉。因为促销推送太多了,用户会觉得烦。但对于运营来说,这是触达用户的重要渠道。所以我们把它做成可选的,让用户自己决定。

系统通知开关

<View style={[styles.item, !settings.pushEnabled && styles.disabledItem]}>
  <View style={styles.itemInfo}>
    <Text style={[styles.itemTitle, !settings.pushEnabled && styles.disabledText]}>
      系统通知
    </Text>
    <Text style={[styles.itemDesc, !settings.pushEnabled && styles.disabledText]}>
      系统公告和服务通知
    </Text>
  </View>
  <Switch
    value={settings.systemNotify}
    onValueChange={(value) => updateSettings('systemNotify', value)}
    trackColor={{false: '#ddd', true: '#3498db'}}
    thumbColor="#fff"
    disabled={!settings.pushEnabled}
  />
</View>

系统通知一般是比较重要的信息,比如服务升级、安全提醒等。建议默认开启。

底部提示

<Text style={styles.tip}>
  💡 关闭推送通知后,您将无法收到订单状态变更、促销活动等重要消息
</Text>

在页面底部加一个提示,告诉用户关闭通知的后果。

这个提示的目的是"劝退"用户不要关闭通知。但我们不是强制,只是告知。用户看了这个提示,如果还是要关,那就关吧。

tip: {fontSize: 13, color: '#999', padding: 16, lineHeight: 20},

提示用灰色小字,不要太醒目,但要让用户能看到。

关于 Switch 组件的一些经验

用 Switch 组件有几个注意事项:

1. 平台差异

iOS 和 Android 上的 Switch 样式不一样。iOS 是圆形滑块,Android 是方形滑块。如果想要完全一致的样式,可以自己用 TouchableOpacity + Animated 实现一个。

2. 颜色自定义

trackColor 可以分别设置开启和关闭时的轨道颜色。thumbColor 设置滑块颜色。但在 iOS 上,thumbColor 可能不生效,这是 React Native 的一个已知问题。

3. disabled 状态

Switch 的 disabled 属性可以禁用开关,但禁用后的样式可能不够明显。建议像我们这样,额外加上 opacity 或者改变文字颜色,让禁用状态更明显。

4. 受控 vs 非受控

Switch 应该是受控组件,也就是 value 由外部状态控制。不要用非受控的方式(不传 value),不然状态会乱。

设置数据的存储

我们的设置数据存在全局状态里,但这只是内存中的数据,App 关闭后就没了。实际项目中,设置数据应该持久化存储。

常用的方案有:

1. AsyncStorage

React Native 提供的本地存储方案,类似于 Web 的 localStorage。简单易用,适合存储少量数据。

// 保存设置
await AsyncStorage.setItem('settings', JSON.stringify(settings));

// 读取设置
const data = await AsyncStorage.getItem('settings');
const settings = data ? JSON.parse(data) : defaultSettings;

2. MMKV

比 AsyncStorage 快很多的存储方案,微信团队开源的。如果设置项很多或者读写频繁,建议用这个。

3. 后端同步

如果用户在多个设备上使用 App,设置数据应该同步到后端。用户在手机上改了设置,平板上也能生效。

我们这里为了演示简化了,只用内存存储。实际项目中要根据需求选择合适的方案。

通知权限的问题

这个页面只是控制 App 内部的通知开关,但还有一个更上层的开关:系统的通知权限。

如果用户在系统设置里关闭了 App 的通知权限,不管我们这里怎么设置都没用。所以有些 App 会在这个页面检测系统通知权限,如果关闭了就提示用户去系统设置里打开。

// 伪代码,实际需要用 react-native-permissions 等库
const checkPermission = async () => {
  const status = await checkNotificationPermission();
  if (status === 'denied') {
    Alert.alert(
      '通知权限已关闭',
      '请在系统设置中开启通知权限',
      [{text: '去设置', onPress: openSettings}, {text: '取消'}]
    );
  }
};

这个功能我们这里没实现,但值得了解。

一些产品层面的思考

1. 默认值的选择

通知开关默认应该是开还是关?从用户体验角度,应该让用户自己选择。但从运营角度,默认开启能触达更多用户。

我的建议是:重要通知(订单、系统)默认开启,营销通知(促销)默认关闭或者让用户首次使用时选择。

2. 通知频率控制

除了开关,还可以加上频率控制。比如促销通知,用户可以选择"每天最多1条"或者"每周最多3条"。这样既能触达用户,又不会太打扰。

3. 免打扰时段

让用户设置免打扰时段,比如晚上 10 点到早上 8 点不推送。这个功能很贴心,但实现起来需要后端配合。

4. 通知预览

在设置页面展示各种通知的样例,让用户知道开启后会收到什么样的通知。这样用户能做出更明智的选择。

小结

消息通知设置页面的核心是 Switch 组件的使用:

  • 总开关控制是否接收任何推送
  • 分类开关控制具体类型的通知
  • 总开关关闭时,分类开关要禁用
  • 禁用状态要有明显的视觉反馈

几个关键点:

  • Switch 的 valueonValueChange 实现受控组件
  • disabled 属性配合样式实现禁用效果
  • trackColor 自定义开关颜色
  • 底部提示告知用户关闭通知的后果

设置类页面看着简单,但细节很多。做好了用户会觉得这个 App 很专业。

下一篇写账号安全页面,敬请期待。


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

Logo

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

更多推荐