Se você desenvolve um app white-label, provavelmente já passou por isso: um cliente pede um SDK (Software Development Kit) específico para analytics, deep links ou outras features, e você fica se perguntando como oferecer várias opções de SDK para clientes diferentes sem aumentar o tamanho do app nem comprometer a estabilidade.
Neste artigo, você vai ver como resolver esse problema em projetos cross-platform. Seja com React Native ou Flutter, o objetivo é o mesmo: criar uma arquitetura modular que permita ligar ou desligar SDKs por cliente, mantendo o app white-label leve, estável e fácil de manter.
Os desafios a resolver
Ao montar uma solução assim em um app white-label cross-platform, alguns desafios precisam ser enfrentados:
- Gerenciar dependencies em tempo de build: instalar, remover e configurar cada uma conforme o cliente.
- Garantir que o código nativo não usado fique totalmente de fora, para manter o app leve e evitar complexidade desnecessária.
- Lidar com imports dinâmicos de dependencies, para ligar e desligar features sem quebrar o build nem aumentar o acoplamento.
- Oferecer uma camada de abstração unificada, para que o app converse com analytics, deep links e outras features sem chamar cada SDK individualmente.
Leia também: Como criar um app React Native usando TypeScript
Gerenciar dependencies em tempo de build
Uma forma eficaz de resolver o gerenciamento de dependencies em tempo de build é usar um shell script que instala ou remove SDKs dinamicamente, com base em variáveis de ambiente. Antes de o build começar, o script pode ler flags como ANALYTICS_ENABLED, DEEPLINK_ENABLED e qualquer configuração específica do cliente, e então rodar os comandos yarn add ou yarn remove correspondentes.
Assim, só os SDKs necessários entram no projeto naquele build específico.
Garantir que o código nativo não usado fique de fora
No ecossistema React Native, esse desafio fica bem mais simples graças ao Expo Prebuild e aos Expo Config Plugins. Com eles, você gera um código nativo que inclui apenas os plugins de SDK necessários para aquele build.
Veja um exemplo com o SDK da Klaviyo. Quando o expo prebuild roda, ele gera os projetos nativos de iOS e Android automaticamente, já com toda a configuração da Klaviyo, então você não precisa escrever nem manter código nativo.
Ao envolver a definição do plugin em uma condição que depende do ambiente (process.env.KLAVIYO_ENABLED), o SDK só entra quando aquele build precisa dele. Com isso, o app de cada cliente carrega apenas as integrações nativas de que precisa, sem código extra nem dependencies desnecessárias.
// app.config.js
{
expo: {
name: 'My app',
ios: {...},
android: {...},
plugins: [
process.env.KLAVIYO_ENABLED && [
'klaviyo-expo-plugin',
{
version: process.env.APP_VERSION,
ios: {
buildNumber: process.env.APP_BUILD_NUMBER,
bundleIdentifier: process.env.BUNDLE_ID,
infoPlist: {
UIBackgroundModes: ['remote-notification'],
},
badgeAutoclearing: true,
codeSigningStyle: 'Manual',
devTeam: process.env.APP_TEAM_ID,
},
android: {
package: process.env.PACKAGE_NAME,
openTracking: true,
logLevel: 1,
notificationIconFilePath: './assets/images/icon.png',
},
},
]
],
}
}Code language: JavaScript (javascript)
Se um SDK não oferece um plugin oficial do Expo, você ainda consegue aproveitar esse workflow criando seu próprio plugin. Veja esta documentação sobre mods dos Expo Config Plugins.
Lidar com imports dinâmicos de dependencies
Quando você remove uma dependency em tempo de build, qualquer import direto daquele pacote nos seus arquivos falha, já que a biblioteca não está mais instalada. Isso pode quebrar o build ou gerar erros em runtime se não for tratado.
Uma saída é dar ao seu shell script a capacidade de gerar também um arquivo de mapeamento dinâmico dos SDKs. Por exemplo:
// No SDK installed
const sdks = {
firebase: undefined as any,
klaviyo: undefined as any
}
export const firebase = sdks.firebase
export const klaviyo = sdks.klaviyo
export default sdksCode language: JavaScript (javascript)
// With the SDKs installed
import { Klaviyo } from 'klaviyo-react-native-sdk'
import { getAnalytics, logEvent } from '@react-native-firebase/analytics'
const sdks = {
firebase: { getAnalytics, logEvent },
klaviyo: Klaviyo
}
export const firebase = sdks.firebase
export const klaviyo = sdks.klaviyo
export default sdksCode language: JavaScript (javascript)
Depois, use esse arquivo para tratar os imports dos SDKs com segurança, garantindo que seu código nunca quebre quando uma dependency estiver ausente.
Oferecer uma camada de abstração unificada
Uma camada de abstração unificada mantém seu código limpo e evita que você chame cada SDK diretamente. Se você tem vários SDKs de analytics, por exemplo, dá para criar um adapter abstrato de analytics e uma classe global de analytics que concentra todos os métodos necessários.
A classe principal Analytics decide quais SDKs carregar com base na configuração, enquanto cada SDK (como o da Klaviyo) apenas implementa o contrato compartilhado.
abstract class AnalyticsAdapter {
abstract logEvent(event: string, payload: Record<string, any>): void
abstract initialize(config: any): Promise<void>
}
enum AnalyticsAdapterType {
DEFAULT = 'default',
KLAVIYO = 'klaviyo',
...
}
class Analytics extends AnalyticsAdapter {
type: AnalyticsAdapterType = AnalyticsAdapterType.DEFAULT
private adapters: AnalyticsAdapter[] = []
logEvent(event: string, payload: Record<string, any>): void {
this.adapters.forEach(adapter => adapter.logEvent(event, payload))
}Code language: PHP (php)
async initialize(config: any): Promise<void> {
// create adapter instances
if (config.klaviyo.enabled && process.env.KLAVIYO_ENABLED) {
this.adapters.push(new KlaviyoAnalytics())
}
// initialize all adapters
this.adapters.forEach(adapter =>
adapter.initialize(config[adapter.type]))
}
}Code language: JavaScript (javascript)
import Klaviyo from 'sdks'
class KlaviyoAnalytics extends AnalyticsAdapter {
type: AnalyticsAdapterType = AnalyticsAdapterType.KLAVIYO
logEvent(event: string, payload: Record<string, any>): void {
// ... Klaviyo SDK methods
}
async initialize(config: any): Promise<void> {
Klaviyo.initialize(config.key)
}
}Code language: JavaScript (javascript)
Assim, o resto do seu app white-label conversa com uma interface única.
Como fazer isso no Flutter ou no React Native puro?
Se o seu SDK não exige código nativo customizado, está tudo certo. Basta abstrair os imports e a lógica dos adapters.
Agora, se você precisa mesmo mexer em arquivos nativos, a coisa fica mais complexa. Nesses casos, dá para criar scripts que aplicam patch nos arquivos de Android e iOS, montar um módulo wrapper para encapsular a lógica nativa, apoiar-se em mecanismos como o manifest merging do Android ou até tentar ferramentas como o XcodeGen.
Infelizmente, não existe nada equivalente ao Expo Prebuild para simplificar esse workflow no Flutter ou no React Native puro.
Leia também: Flutter, React Native ou desenvolvimento nativo: o que você precisa saber