expo在安卓端设置徽标是可以的但是清除、获取当前徽标数均有问题。下面通过注册插件方式加载原生代码解决
import { ConfigPlugin, withAndroidManifest, AndroidConfig } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;
const withAndroidBadge: ConfigPlugin = (config: ExpoConfig) => {
return withAndroidManifest(config, async (config) => {
const mainApplication = getMainApplicationOrThrow(config.modResults);
// 添加 me.leolin.shortcutbadger
addMetaDataItemToMainApplication(
mainApplication,
'me.leolin.shortcutbadger.impl.xiaomi.XiaomiHomeBadger',
'me.leolin.shortcutbadger.impl.xiaomi.XiaomiHomeBadger'
);
// 添加其他厂商的实现...
addMetaDataItemToMainApplication(
mainApplication,
'me.leolin.shortcutbadger.impl.huawei.HuaweiHomeBadger',
'me.leolin.shortcutbadger.impl.huawei.HuaweiHomeBadger'
);
addMetaDataItemToMainApplication(
mainApplication,
'me.leolin.shortcutbadger.impl.oppo.OPPOHomeBader',
'me.leolin.shortcutbadger.impl.oppo.OPPOHomeBader'
);
return config;
});
};
export default withAndroidBadge;
import { ConfigPlugin, withDangerousMod, AndroidConfig } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
import * as fs from 'fs';
import * as path from 'path';
const createBadgeModuleContents = (packageName: string) => `
package ${packageName};
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import me.leolin.shortcutbadger.ShortcutBadger;
import android.content.SharedPreferences;
import android.content.Context;
public class BadgeModule extends ReactContextBaseJavaModule {
private static final String BADGE_COUNT_KEY = "badge_count";
private final SharedPreferences preferences;
public BadgeModule(ReactApplicationContext reactContext) {
super(reactContext);
preferences = reactContext.getSharedPreferences("badge_preferences", Context.MODE_PRIVATE);
}
@Override
public String getName() {
return "BadgeModule";
}
@ReactMethod
public void getBadgeCount(Promise promise) {
try {
int count = preferences.getInt(BADGE_COUNT_KEY, 0);
promise.resolve(count);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void setBadgeCount(int count, Promise promise) {
try {
preferences.edit().putInt(BADGE_COUNT_KEY, count).apply();
boolean success = ShortcutBadger.applyCount(getReactApplicationContext(), count);
promise.resolve(success);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void clearBadge(Promise promise) {
try {
preferences.edit().putInt(BADGE_COUNT_KEY, 0).apply();
boolean success = ShortcutBadger.removeCount(getReactApplicationContext());
promise.resolve(success);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
}
`;
const createBadgePackageContents = (packageName: string) => `
package ${packageName};
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BadgePackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new BadgeModule(reactContext));
return modules;
}
}
`;
const withAndroidBadgeModule: ConfigPlugin = (config: ExpoConfig) => {
return withDangerousMod(config, [
'android',
async (config) => {
const packageName = config.android?.package || 'club.rubicone.dev';
const packagePath = packageName.replace(/\./g, '/');
const javaPath = path.join(
config.modRequest.platformProjectRoot,
'app',
'src',
'main',
'java',
packagePath
);
await fs.promises.mkdir(javaPath, { recursive: true });
await fs.promises.writeFile(
path.join(javaPath, 'BadgeModule.java'),
createBadgeModuleContents(packageName)
);
await fs.promises.writeFile(
path.join(javaPath, 'BadgePackage.java'),
createBadgePackageContents(packageName)
);
// 修改 MainApplication.kt 添加 BadgePackage
const mainApplicationPath = path.join(javaPath, 'MainApplication.kt');
let mainApplicationContent = await fs.promises.readFile(mainApplicationPath, 'utf8');
if (!mainApplicationContent.includes('BadgePackage')) {
// 匹配 getPackages 函数
const packagePattern = /override fun getPackages\(\): List<ReactPackage> \{[\s\S]*?val packages = PackageList\(this\)\.packages[\s\S]*?return packages/;
mainApplicationContent = mainApplicationContent.replace(
packagePattern,
(match) => {
const insertPoint = match.indexOf('return packages');
return match.slice(0, insertPoint) +
' packages.add(BadgePackage())\n ' +
match.slice(insertPoint);
}
);
// 添加导入语句
if (!mainApplicationContent.includes('import ' + packageName + '.BadgePackage')) {
const lastImportIndex = mainApplicationContent.lastIndexOf('import ');
const insertIndex = mainApplicationContent.indexOf('\n', lastImportIndex) + 1;
mainApplicationContent =
mainApplicationContent.slice(0, insertIndex) +
`import ${packageName}.BadgePackage\n` +
mainApplicationContent.slice(insertIndex);
}
await fs.promises.writeFile(mainApplicationPath, mainApplicationContent);
}
// 修改 build.gradle 添加依赖
const buildGradlePath = path.join(config.modRequest.platformProjectRoot, 'app', 'build.gradle');
let buildGradleContent = await fs.promises.readFile(buildGradlePath, 'utf8');
if (!buildGradleContent.includes('me.leolin:ShortcutBadger')) {
buildGradleContent = buildGradleContent.replace(
/dependencies\s*{/,
`dependencies {\n implementation "me.leolin:ShortcutBadger:1.1.22@aar"`
);
await fs.promises.writeFile(buildGradlePath, buildGradleContent);
}
return config;
},
]);
};
export default withAndroidBadgeModule;
import { ConfigPlugin, withGradleProperties } from '@expo/config-plugins';
const withAndroidGradleProperties: ConfigPlugin = (config) => {
return withGradleProperties(config, (config) => {
config.modResults = config.modResults.filter(
(item) => !(item.type === 'property') || item.key !== 'shortcutBadgerDependency'
);
config.modResults.push({
type: 'property',
key: 'shortcutBadgerDependency',
value: 'me.leolin:ShortcutBadger:1.1.22@aar',
});
return config;
});
};
export default withAndroidGradleProperties;
然后统一封装一下在IOS使用expo-notifications原有的,在安卓端使用我们自己注册的原生插件
```
// 角标管理
export const BadgeManager = {
// 设置角标数
setBadgeCount: async (count: number): Promise<boolean> => {
try {
if (Platform.OS === 'android') {
if (NativeModules.BadgeModule) {
await NativeModules.BadgeModule.setBadgeCount(count);
}
} else {
await Notifications.setBadgeCountAsync(count);
}
return true;
} catch (error) {
Logger.getInstance().error("Failed to set badge count", "角标设置", error);
return false;
}
},
// 清除角标
clearBadge: async (): Promise<boolean> => {
try {
console.log("Clearing badge count")
if (Platform.OS === 'android') {
if (NativeModules.BadgeModule) {
await NativeModules.BadgeModule.clearBadge();
}
} else {
await Notifications.setBadgeCountAsync(0);
}
return true;
} catch (error) {
Logger.getInstance().error("Failed to clear badge count", "角标清除", error);
return false;
}
},
// 增加角标数
incrementBadge: async (): Promise<boolean> => {
try {
let currentBadge=0;
if (Platform.OS === 'android') {
if (NativeModules.BadgeModule) {
currentBadge = await NativeModules.BadgeModule.getBadgeCount();
}
} else {
currentBadge = await Notifications.getBadgeCountAsync();
}
return await BadgeManager.setBadgeCount(currentBadge + 1);
} catch (error) {
Logger.getInstance().error("Failed to increment badge count", "角标增加", error);
return false;
}
}
};
自动加载注册插件目录下插件
import { ExpoConfig, ConfigContext } from 'expo/config';
import { ConfigPlugin } from '@expo/config-plugins';
import ts from 'typescript';
import fs from 'fs';
import path from 'path';
// 添加缓存机制
const pluginCache = new Map<string, ConfigPlugin<any>>();
const compiledFiles = new Set<string>();
// 修改 compilePlugin 函数,添加错误处理
const compilePlugin = (filePath: string): ConfigPlugin<any> => {
try {
// 检查缓存
const cacheKey = `${filePath}:${fs.statSync(filePath).mtimeMs}`;
if (pluginCache.has(cacheKey)) {
return pluginCache.get(cacheKey)!;
}
const source = fs.readFileSync(filePath, 'utf8');
const result = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2016,
esModuleInterop: true,
},
});
const compiledDir = path.join(process.cwd(), 'compiled-plugins');
if (!fs.existsSync(compiledDir)) {
fs.mkdirSync(compiledDir, { recursive: true });
}
const fileName = path.basename(filePath, '.ts');
const tempFile = path.join(compiledDir, `${fileName}.js`);
console.log('Compiling plugin:', fileName);
fs.writeFileSync(tempFile, result.outputText);
// 记录已编译的文件
compiledFiles.add(tempFile);
const plugin = require(tempFile);
// 存入缓存
pluginCache.set(cacheKey, plugin.default);
return plugin.default;
} catch (error) {
console.error(`Error loading plugin ${path.basename(filePath)}:`, error);
throw error;
}
};
// 添加清理函数
const cleanupCompiledPlugins = () => {
if (process.env.EAS_BUILD) {
// 在 EAS 构建过程中不清理文件
return;
}
compiledFiles.forEach(file => {
try {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
} catch (error) {
console.warn(`Failed to cleanup file ${file}:`, error);
}
});
compiledFiles.clear();
};
// 修改 loadPlugins 函数
const loadPlugins = (): ConfigPlugin<any>[] => {
const pluginsDir = path.join(process.cwd(), 'plugins');
const files = fs.readdirSync(pluginsDir);
// 在加载新插件前清理旧的编译文件
cleanupCompiledPlugins();
const plugins = files
.filter(file => file.startsWith('with') && file.endsWith('.ts'))
.map(file => compilePlugin(path.join(pluginsDir, file)));
// 注册进程退出时的清理函数
if (!process.env.EAS_BUILD) {
process.on('exit', cleanupCompiledPlugins);
process.on('SIGINT', () => {
cleanupCompiledPlugins();
process.exit();
});
}
return plugins;
};
// 加载所有插件
const customPlugins = loadPlugins();
// 定义插件类型
type ExpoPlugin = string | [string, any] | [string] | [] | ConfigPlugin<any>;
// 配置类型
type AppConfig = Omit<ExpoConfig, 'plugins'> & {
plugins: ExpoPlugin[];
};
评论区