Integração com Aplicativos Móveis
A Rybená pode ser integrada em aplicativos móveis através de uma WebView, permitindo controle programático completo através da API. Esta abordagem oferece flexibilidade máxima para integrar a solução de acessibilidade em seu aplicativo.
Visão Geral
O fluxo de integração entre a Rybená e aplicativos é realizado através de incorporação de uma WebView com comandos facilitados para manipular a aplicação.
Nas integrações com aplicativos, entendemos que a forma de interação entre o usuário e o sistema é bastante diversa e dinâmica, muito diferente do cenário web, em que o usuário controla suas ações por seleções e cliques.
Por isso, entendemos que a melhor forma de entregar essa integração é de maneira programática, para que cada aplicativo diga à Rybená quando traduzir e qual texto deve ser traduzido. Os callbacks de carregamento e de tradução são executados dentro da WebView; para notificar o aplicativo nativo, configure explicitamente a ponte JavaScript da plataforma, como mostrado mais adiante.
Integração por Incorporação
Na integração por incorporação, o aplicativo de origem incorpora o aplicativo Rybená dentro dele, ou seja, diferentemente da outra solução, não é necessário abrir outro aplicativo ou ter o aplicativo Rybená instalado.
Conceitos Básicos
- WebView: Componente que permite exibir conteúdo web dentro do aplicativo
- Token de Autenticação: Chave única para identificar e autenticar sua aplicação
- API Programática: Conjunto de funções para controlar a Rybená via código
- Ponte com o aplicativo: Canal explícito para o aplicativo nativo receber eventos enviados pelo JavaScript da WebView
Passos Básicos
- Criar um objeto WebView (há componentes desse tipo para diversas linguagens mobile)
- Carregar
https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUIna WebView e substituir o token pelo valor fornecido pela Rybená. Essa é a página HTTPS oficial; ela acrescenta internamente os parâmetrosmode=apietransparent=trueao script da Rybená. - Aguardar a WebView e a API ficarem prontas usando
handleLoaded - Manipular o tamanho da Rybená com
setSize(largura em pixels) - Abrir o player da Rybená quando for conveniente
- Manipular as funcionalidades disponíveis na documentação da API
Integração por Framework
React Native
A integração com React Native é simples e direta, utilizando o componente WebView.
Instalação
Primeiro, instale o pacote react-native-webview:
npm install react-native-webview
# ou
yarn add react-native-webviewImplementação Básica
import React, { useRef } from 'react';
import { View, StyleSheet, Button } from 'react-native';
import { WebView } from 'react-native-webview';
export default function RybenáIntegration() {
const webViewRef = useRef<WebView>(null);
// URL da WebView da Rybená com seu token de autenticação
const rybenaUrl = 'https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI';
// Toda chamada espera o player terminar de carregar.
const runWhenReady = (script: string) => {
webViewRef.current?.injectJavaScript(`
if (window.RybenaApi) {
RybenaApi.getInstance().handleLoaded(() => {
${script}
});
}
true;
`);
};
// setSize recebe a largura do player em pixels.
const setSize = (width: number) => {
runWhenReady(`RybenaApi.getInstance().setSize(${Math.round(width)});`);
};
// Funções de controle do player.
const openPlayer = () => {
runWhenReady('RybenaApi.getInstance().openPlayer();');
};
const closePlayer = () => {
runWhenReady('RybenaApi.getInstance().closePlayer();');
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: rybenaUrl }}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
/>
<View style={styles.controls}>
<Button title="Definir Tamanho (300px)" onPress={() => setSize(300)} />
<Button title="Abrir Player" onPress={openPlayer} />
<Button title="Fechar Player" onPress={closePlayer} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
webview: {
flex: 1,
},
controls: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#f5f5f5',
},
});Exemplo Completo
Temos um exemplo de integração da Rybená em um aplicativo mobile utilizando React Native e Expo. Você pode acessar o repositório do exemplo aqui.
Se o código do exemplo ainda usar uma URL do repositório com porta explícita, substitua essa referência pela URL HTTPS sem porta indicada nos passos acima.
Funções da API Disponíveis
// JSON.stringify evita quebrar o JavaScript quando o texto vier do app nativo.
const translate = (text: string) => {
const safeText = JSON.stringify(text);
runWhenReady(`RybenaApi.getInstance().translate(${safeText});`);
};
const setLanguage = (lang: string) => {
const safeLanguage = JSON.stringify(lang);
runWhenReady(`RybenaApi.getInstance().setLanguage(${safeLanguage});`);
};
const switchToLibras = () => {
runWhenReady('RybenaApi.getInstance().switchToLibras();');
};
const switchToVoz = () => {
runWhenReady('RybenaApi.getInstance().switchToVoz();');
};
const pause = () => runWhenReady('RybenaApi.getInstance().pause();');
const play = () => runWhenReady('RybenaApi.getInstance().play();');
const stop = () => runWhenReady('RybenaApi.getInstance().stop();');
const setSpeed = (speed: number) => {
runWhenReady(`RybenaApi.getInstance().setSpeed(${speed});`);
};
// Consulta pontual; o resultado pode ser enviado pela ponte onMessage.
const checkTranslation = () => {
webViewRef.current?.injectJavaScript(`
const translating = window.RybenaApi
? RybenaApi.getInstance().isTranslating()
: false;
window.ReactNativeWebView?.postMessage(JSON.stringify({
type: "translation-state",
value: translating
}));
true;
`);
};handleLoaded evita chamadas antes da inicialização. Para informar o aplicativo quando uma tradução terminar, registre handleTranslate e envie uma mensagem pela ponte onMessage, conforme a seção de comunicação explícita abaixo.
Flutter
A integração com Flutter utiliza o pacote webview_flutter.
Instalação
Adicione o pacote ao seu pubspec.yaml:
dependencies:
webview_flutter: ^4.0.0Implementação Básica
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class RybenáIntegration extends StatefulWidget {
@override
_RybenáIntegrationState createState() => _RybenáIntegrationState();
}
class _RybenáIntegrationState extends State<RybenáIntegration> {
late WebViewController _controller;
// A página oficial inicializa o modo API e transparente.
final String rybenaUrl = 'https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI';
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
// WebView carregada com sucesso
},
),
)
..loadRequest(Uri.parse(rybenaUrl));
}
Future<void> _runWhenReady(String script) async {
await _controller.runJavaScript('''
if (window.RybenaApi) {
RybenaApi.getInstance().handleLoaded(() => {
$script
});
}
true;
''');
}
// setSize recebe a largura do player em pixels.
Future<void> setSize(int width) =>
_runWhenReady('RybenaApi.getInstance().setSize($width);');
Future<void> openPlayer() =>
_runWhenReady('RybenaApi.getInstance().openPlayer();');
Future<void> closePlayer() =>
_runWhenReady('RybenaApi.getInstance().closePlayer();');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Rybená Integration')),
body: Column(
children: [
Expanded(
child: WebViewWidget(controller: _controller),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () => setSize(300),
child: Text('Definir Tamanho'),
),
ElevatedButton(
onPressed: openPlayer,
child: Text('Abrir Player'),
),
ElevatedButton(
onPressed: closePlayer,
child: Text('Fechar Player'),
),
],
),
),
],
),
);
}
}Funções da API Disponíveis
// jsonEncode protege textos arbitrários recebidos do aplicativo nativo.
Future<void> translate(String text) => _runWhenReady(
'RybenaApi.getInstance().translate(${jsonEncode(text)});',
);
Future<void> setLanguage(String lang) => _runWhenReady(
'RybenaApi.getInstance().setLanguage(${jsonEncode(lang)});',
);
Future<void> switchToLibras() =>
_runWhenReady('RybenaApi.getInstance().switchToLibras();');
Future<void> switchToVoz() =>
_runWhenReady('RybenaApi.getInstance().switchToVoz();');
Future<void> pause() => _runWhenReady('RybenaApi.getInstance().pause();');
Future<void> play() => _runWhenReady('RybenaApi.getInstance().play();');
Future<void> stop() => _runWhenReady('RybenaApi.getInstance().stop();');
Future<void> checkTranslation() async {
final result = await _controller.runJavaScriptReturningResult(
'window.RybenaApi?.getInstance().isTranslating() ?? false;',
);
print('Está traduzindo: $result');
}
// Registre handleTranslate para enviar um evento pela ponte nativa.
Future<void> registerTranslationCallback() => _controller.runJavaScript('''
if (window.RybenaApi) {
RybenaApi.getInstance().handleTranslate(() {
RybenaChannel.postMessage(
JSON.stringify({"type": "translation-finished"})
);
});
}
true;
''');handleLoaded evita chamadas antes da inicialização. A ponte RybenaChannel é configurada na seção de comunicação explícita abaixo.
Android Nativo (Kotlin)
A integração com Android nativo utiliza o componente WebView.
Implementação Básica
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
class RybenáIntegration : AppCompatActivity() {
private lateinit var webView: WebView
// A página oficial inicializa o modo API e transparente.
private val rybenaUrl = "https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rybena)
webView = findViewById(R.id.webview)
setupWebView()
}
private fun setupWebView() {
webView.apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
webViewClient = WebViewClient()
loadUrl(rybenaUrl)
}
}
private fun runWhenReady(script: String) {
webView.evaluateJavascript(
"""
if (window.RybenaApi) {
RybenaApi.getInstance().handleLoaded(() => {
$script
});
}
true;
""".trimIndent(),
null
)
}
// setSize recebe a largura do player em pixels.
fun setSize(width: Int) {
runWhenReady("RybenaApi.getInstance().setSize($width);")
}
fun openPlayer() {
runWhenReady("RybenaApi.getInstance().openPlayer();")
}
fun closePlayer() {
runWhenReady("RybenaApi.getInstance().closePlayer();")
}
fun translate(text: String) {
val safeText = JSONObject.quote(text)
runWhenReady("RybenaApi.getInstance().translate($safeText);")
}
}Layout XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Definir Tamanho"
android:onClick="setSize" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Abrir Player"
android:onClick="openPlayer" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Fechar Player"
android:onClick="closePlayer" />
</LinearLayout>
</LinearLayout>iOS Nativo (Swift)
A integração com iOS nativo utiliza o componente WKWebView.
Implementação Básica
import UIKit
import WebKit
class RybenáIntegration: UIViewController {
private var webView: WKWebView!
// URL da WebView da Rybená com seu token de autenticação
// A página oficial inicializa o modo API e transparente.
private let rybenaUrl = "https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI"
override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
setupUI()
}
private func setupWebView() {
let contentController = WKUserContentController()
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = self
if let url = URL(string: rybenaUrl) {
webView.load(URLRequest(url: url))
}
}
private func setupUI() {
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100)
])
// Adicionar botões de controle
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 10
let setSizeButton = UIButton(type: .system)
setSizeButton.setTitle("Definir Tamanho", for: .normal)
setSizeButton.addTarget(self, action: #selector(setSize), for: .touchUpInside)
let openPlayerButton = UIButton(type: .system)
openPlayerButton.setTitle("Abrir Player", for: .normal)
openPlayerButton.addTarget(self, action: #selector(openPlayer), for: .touchUpInside)
let closePlayerButton = UIButton(type: .system)
closePlayerButton.setTitle("Fechar Player", for: .normal)
closePlayerButton.addTarget(self, action: #selector(closePlayer), for: .touchUpInside)
stackView.addArrangedSubview(setSizeButton)
stackView.addArrangedSubview(openPlayerButton)
stackView.addArrangedSubview(closePlayerButton)
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
stackView.heightAnchor.constraint(equalToConstant: 50)
])
}
private func runWhenReady(_ script: String) {
webView.evaluateJavaScript("""
if (window.RybenaApi) {
RybenaApi.getInstance().handleLoaded(() => {
\(script)
});
}
true;
""")
}
// Converte texto nativo em um literal JavaScript seguro.
private func javaScriptString(_ value: String) -> String? {
guard
let data = try? JSONSerialization.data(withJSONObject: [value]),
let json = String(data: data, encoding: .utf8)
else {
return nil
}
return String(json.dropFirst().dropLast())
}
// setSize recebe a largura do player em pixels.
@objc func setSize() {
runWhenReady("RybenaApi.getInstance().setSize(300);")
}
@objc func openPlayer() {
runWhenReady("RybenaApi.getInstance().openPlayer();")
}
@objc func closePlayer() {
runWhenReady("RybenaApi.getInstance().closePlayer();")
}
func translate(_ text: String) {
guard let safeText = javaScriptString(text) else { return }
runWhenReady("RybenaApi.getInstance().translate(\(safeText));")
}
}
extension RybenáIntegration: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// WebView carregada com sucesso
}
}Autenticação
Token de Acesso
Para integrar a Rybená em seu aplicativo, você precisa de um token de acesso único. Este token autentica a WebView e garante que apenas aplicativos autorizados possam acessar a Rybená.
Como Obter o Token
- Entre em contato com a equipe da Rybená
- Forneça informações sobre seu aplicativo
- Receba seu token de acesso único
- Use o token na URL da WebView
Exemplo de Uso do Token
// A página HTTPS oficial inicializa o modo API e transparente.
const rybenaUrl =
"https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI";Nunca compartilhe seu token de acesso publicamente. Mantenha-o seguro e use apenas em seu aplicativo.
API Programática
Funções Principais
A Rybená oferece funções para controle programático através de RybenaApi.getInstance().
Tamanho e Posição
const api = RybenaApi.getInstance();
// setSize recebe a largura do player em pixels.
api.setSize(300);
// setPosition recebe um valor válido da propriedade CSS position.
api.setPosition("fixed");
// Para posicionar o player, informe coordenadas em pixels.
api.setCoordinates(20, 20);Controle do Player
const api = RybenaApi.getInstance();
api.openPlayer();
api.closePlayer();
// Se o aplicativo precisar de um botão "alternar", mantenha o estado no host.
let playerAberto = false;
function alternarPlayer() {
playerAberto = !playerAberto;
if (playerAberto) {
api.openPlayer();
} else {
api.closePlayer();
}
}Tradução e Reprodução
const api = RybenaApi.getInstance();
api.switchToLibras();
api.translate("Texto para traduzir");
api.switchToVoz();
api.translate("Texto que será lido em voz alta");
api.pause();
api.play();
api.stop();
api.setSpeed(1.25);Idioma
// O aplicativo deve manter o idioma selecionado caso precise reutilizá-lo.
api.setLanguage("ptBR");Prontidão e Andamento
api.handleLoaded(() => {
api.openPlayer();
api.translate("A chamada só acontece depois que a WebView estiver pronta.");
});
api.handleTranslate(() => {
console.log("Tradução concluída dentro da WebView.");
});
const translating = api.isTranslating();
console.log("Está traduzindo:", translating);Comunicação com o aplicativo
A API não envia mensagens ao código nativo automaticamente. handleLoaded e handleTranslate executam dentro da WebView; para expor esses eventos ao aplicativo, registre os callbacks e chame a ponte JavaScript oferecida pela plataforma.
React Native
const registerRybenaCallbacks = () => {
webViewRef.current?.injectJavaScript(`
if (window.RybenaApi) {
const api = RybenaApi.getInstance();
api.handleLoaded(() => {
window.ReactNativeWebView?.postMessage(
JSON.stringify({ type: "rybena-ready" })
);
});
api.handleTranslate(() => {
window.ReactNativeWebView?.postMessage(
JSON.stringify({ type: "translation-finished" })
);
});
}
true;
`);
};
<WebView
ref={webViewRef}
source={{ uri: rybenaUrl }}
onLoadEnd={() => registerRybenaCallbacks()}
onMessage={(event) => {
try {
const message = JSON.parse(event.nativeEvent.data);
console.log("Evento da Rybená:", message);
} catch {
console.warn("Mensagem inválida recebida da WebView");
}
}}
/>;Flutter
late final WebViewController controller;
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'RybenaChannel',
onMessageReceived: (JavaScriptMessage message) {
final data = jsonDecode(message.message);
print('Evento da Rybená: $data');
},
)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) async {
await controller.runJavaScript('''
if (window.RybenaApi) {
const api = RybenaApi.getInstance();
api.handleLoaded(() => {
RybenaChannel.postMessage(
JSON.stringify({"type": "rybena-ready"})
);
});
api.handleTranslate(() => {
RybenaChannel.postMessage(
JSON.stringify({"type": "translation-finished"})
);
});
}
true;
''');
},
),
)
..loadRequest(Uri.parse(rybenaUrl));No Android e no iOS, configure respectivamente uma interface JavaScript (addJavascriptInterface) ou um WKScriptMessageHandler e invoque-a a partir dos mesmos callbacks. Sem essa ponte explícita, não há retorno automático de mensagens ou de estado para o aplicativo.
Solução de Problemas
Melhores Práticas
Gerenciamento de Estado
- Mantenha no aplicativo o estado que ele controla, como a visibilidade esperada do player, e use
openPlayer()/closePlayer()explicitamente - Use
isTranslating()ehandleTranslate()para acompanhar a tradução; eventos só chegam ao app nativo quando a ponte JavaScript for configurada - Implemente tratamento de erros adequado
Performance
- Carregue a WebView apenas quando necessário
- Use lazy loading para conteúdo pesado
- Implemente cache quando apropriado
Segurança
- Nunca exponha seu token de acesso
- Valide todas as mensagens recebidas da Rybená
- Implemente autenticação adicional quando necessário
Experiência do Usuário
- Forneça feedback visual para ações do usuário
- Implemente estados de carregamento
- Trate erros de forma amigável