Rybená Logo

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

  1. WebView: Componente que permite exibir conteúdo web dentro do aplicativo
  2. Token de Autenticação: Chave única para identificar e autenticar sua aplicação
  3. API Programática: Conjunto de funções para controlar a Rybená via código
  4. Ponte com o aplicativo: Canal explícito para o aplicativo nativo receber eventos enviados pelo JavaScript da WebView

Passos Básicos

  1. Criar um objeto WebView (há componentes desse tipo para diversas linguagens mobile)
  2. Carregar https://repository.rybena.com.br/webview/index.html?token=SEU-TOKEN-AQUI na WebView e substituir o token pelo valor fornecido pela Rybená. Essa é a página HTTPS oficial; ela acrescenta internamente os parâmetros mode=api e transparent=true ao script da Rybená.
  3. Aguardar a WebView e a API ficarem prontas usando handleLoaded
  4. Manipular o tamanho da Rybená com setSize (largura em pixels)
  5. Abrir o player da Rybená quando for conveniente
  6. 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-webview

Implementaçã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.0

Implementaçã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

  1. Entre em contato com a equipe da Rybená
  2. Forneça informações sobre seu aplicativo
  3. Receba seu token de acesso único
  4. 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() e handleTranslate() 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

Recursos Adicionais

Conteúdo Relacionado

Nesta página