"A IU está incrível. Mas como o Flutter lida com as APIs específicas de cada plataforma?"
O Flutter convida você a desenvolver aplicativos para dispositivos móveis na linguagem de programação Dart e compilá-los para Android e iOS. Mas o Dart não compila no bytecode Dalvik do Android. E você não pode contar com os vínculos Dart/Objective-C no iOS. Isso significa que o código Dart é criado sem acesso direto às APIs específicas do iOS Cocoa Touch e do Android SDK.
Esse não é um problema tão importante, desde que o código Dart se limite a pintar pixels na tela. A biblioteca do Flutter e o seu mecanismo gráfico conseguem fazer isso muito bem. Além de pintar pixels, você também pode usar I/O de arquivos ou rede e a lógica empresarial relacionada sem nenhum problema. A linguagem, o tempo de execução e as bibliotecas do Dart resolvem isso para você.
Mas aplicativos não triviais precisam de maior integração com a plataforma de host:
notificações, ciclo de vida do aplicativo, links diretos...
sensores, câmera, bateria, geolocalização, som, conectividade...
compartilhar informações com outros aplicativos, inicializar outros aplicativos...
preferências persistidas, pastas especiais, informações do dispositivo...
A lista é longa, diversificada e parece crescer a cada nova versão da plataforma.
O acesso a todas essas APIs de plataforma pode ser incorporado à própria biblioteca do Flutter. Mas isso deixaria o Flutter muito maior e seria preciso alterá-lo com frequência. Na prática, isso provavelmente faria o Flutter ficar para trás em relação à versão mais recente da plataforma. Ou disponibilizaria aos autores de aplicativos encapsulamentos de APIs da plataforma insatisfatórios, do tipo "menor denominador comum". Ou confundiria usuários inexperientes com abstrações complexas para disfarçar as diferenças entre as plataformas. Ou geraria fragmentação de versões. Ou bugs.
Se pensarmos bem, todas as alternativas anteriores.
A equipe do Flutter escolheu uma abordagem diferente. Não é uma abordagem amplamente funcional, mas é simples, versátil e deixa você no controle de tudo.
Para começar, o Flutter é hospedado por um aplicativo de ambiente para Android ou iOS. As partes do Flutter do aplicativo são encapsuladas em componentes padrão específicos de cada plataforma, como o View no Android e o UIViewController no iOS. Portanto, embora o Flutter convide você a desenvolver aplicativos em Dart, você pode criar as funcionalidades necessárias usando Java/Kotlin ou Objective-C/Swift no aplicativo host, trabalhando diretamente com as APIs específicas da plataforma.
Depois, os canais de plataforma oferecem um mecanismo simples de comunicação entre o código Dart e o código específico da plataforma do aplicativo host. Isso quer dizer que você pode expor um serviço da plataforma no código do aplicativo host e invocá-lo no código Dart. Ou vice-versa.
E, por fim, os plug-ins permitem criar uma Dart API apoiada por uma implementação do Android escrita em Java ou Kotlin e uma implementação do iOS escrita em Objective-C ou Swift, e empacotar tudo isso criando um trio Flutter/Android/iOS integrado usando os canais de plataforma. Isso significa que você pode reutilizar, compartilhar e distribuir sua ideia sobre como o Flutter deve usar uma determinada API de uma plataforma.
Este artigo é uma introdução detalhada aos canais de plataforma. Começando com os fundamentos de mensagens do Flutter, apresentarei os conceitos de canal de mensagem/método/evento e discutirei algumas considerações de projeto de APIs. Não teremos listagens de APIs, apenas amostras de código pequenas para copiar e colar. Mostrarei uma lista resumida de orientações de uso, baseada na minha experiência de colaborador no repositório flutter/plugins do GitHub como membro da equipe do Flutter. O artigo se encerra com uma lista de recursos adicionais, incluindo links para as APIs de referência DartDoc/JavaDoc/ObjcDoc.
Índice
API dos canais de plataforma Fundamentos: mensagens binárias assíncronas Canais de mensagem: nome + codec Canais de método: envelopes padronizados Canais de evento: streaming
Orientações de uso Prefixe nomes de canal por domínio para garantir a unicidade Considere o uso de canais de plataforma como comunicação intramodular Não simule os canais de plataforma Considere o uso de testes automatizados para interagir com a plataforma Prepare a plataforma para receber chamadas síncronas
Materiais
API dos canais de plataforma
Para a maioria dos casos de uso, você provavelmente usará canais de método para a comunicação com a plataforma. Mas, como muitas das propriedades dos canais são obtidas dos canais de mensagem mais simples e dos fundamentos de mensagens binárias subjacentes, começarei daqui.
Fundamentos: mensagens binárias assíncronas
No nível mais básico, o Flutter se comunica com o código da plataforma usando uma passagem de mensagens assíncronas com mensagens binárias, o que quer dizer que o payload da mensagem é um buffer de bytes. Para distinguir as mensagens usadas de acordo com sua finalidade, cada mensagem é enviada em um "canal" lógico que é simplesmente uma string de nome. Os exemplos abaixo usam o nome de canal "foo".
// Send a binary message from Dart to the platform.
final WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final ByteData message = buffer.done();
await BinaryMessages.send('foo', message);
print('Message sent, reply ignored');
No Android, uma mensagem como essa pode ser recebida (como um java.nio.ByteBuffer) usando este código Kotlin:
// Receive binary messages from Dart on Android.
// This code can be added to a FlutterActivity subclass, typically
// in onCreate.
flutterView.setMessageHandler("foo") { message, reply ->
message.order(ByteOrder.nativeOrder())
val x = message.double
val n = message.int
Log.i("MSG", "Received: $x and $n")
reply.reply(null)
}
A ByteBuffer API oferece suporte à leitura de valores primitivos durante o avanço automático da posição de leitura atual. No iOS, a história é parecida: estou aceitando sugestões para melhorar meu fu simplista no Swift:
// Receive binary messages from Dart on iOS.
// This code can be added to a FlutterAppDelegate subclass,
// typically in application:didFinishLaunchingWithOptions:.
let flutterView =
window?.rootViewController as ! FlutterViewController;
flutterView.setMessageHandlerOnChannel("foo") {
(message: Data!, reply: FlutterBinaryReply) -> Void in
let x : Float64 = message.subdata(in: 0..<8)
.withUnsafeBytes { $0.pointee }
let n : Int32 = message.subdata(in: 8..<12)
.withUnsafeBytes { $0.pointee }
os_log("Received %f and %d", x, n)
reply(nil)
}
A comunicação é bidirecional, ou seja, você também pode enviar mensagens na direção contrária, do Java/Kotlin ou Objective-C/Swift para o Dart. Se você reverter a direção da configuração acima, obterá algo assim:
// Send a binary message from Android.
val message = ByteBuffer.allocateDirect(12)
message.putDouble(3.1415)
message.putInt(123456789)
flutterView.send("foo", message) { _ ->
Log.i("MSG", "Message sent, reply ignored")
}
// Send a binary message from iOS.
var message = Data(capacity: 12)
var x : Float64 = 3.1415
var n : Int32 = 12345678
message.append(UnsafeBufferPointer(start: &x, count: 1))
message.append(UnsafeBufferPointer(start: &n, count: 1))
flutterView.send(onChannel: "foo", message: message) {(_) -> Void in
os_log("Message sent, reply ignored")
}
// Receive binary messages from the platform.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
final ReadBuffer readBuffer = ReadBuffer(message);
final double x = readBuffer.getFloat64();
final int n = readBuffer.getInt32();
print('Received $x and $n');
return null;
});
Os detalhes. Respostas obrigatórias. Cada envio de mensagem envolve uma resposta assíncrona do destinatário. Nos exemplos acima, não há muita utilidade na comunicação reversa, mas a resposta nula é necessária para a conclusão do futuro (future) do Dart e para a execução dos dois callbacks da plataforma.
Threads. Mensagens e respostas são recebidas (e devem ser enviadas) no thread de IU principal da plataforma. No Dart, só há um thread por isolado (isolate) do Dart (ou seja, por visualização do Flutter). Portanto, não existe confusão sobre qual thread deve ser usado nesse caso.
Exceções. Todas as exceções não capturadas geradas em um gerenciador de mensagens Dart ou Android são capturadas pela biblioteca e registradas. Uma resposta nula é retornada ao remetente. As exceções não capturadas geradas em gerenciadores de resposta são registradas.
Vida útil do gerenciador. Os gerenciadores de eventos registrados são mantidos e continuam ativos junto com a visualização do Flutter (ou seja, o isolado do Dart, a instância FlutterView no Android e o FlutterViewController no iOS). Você pode reduzir o tempo de vida do gerenciador cancelando seu registro: é só definir um gerenciador nulo (null), ou diferente, usando o mesmo nome de canal.
Exclusividade do gerenciador. Os gerenciadores são vinculados a um mapa de hash indexado pelo nome do canal. Portanto, cada canal pode ter apenas um gerenciador. Uma mensagem enviada a um canal que não tem um gerenciador de mensagens registrado no lado do destinatário recebe automaticamente uma resposta nula.
Comunicação síncrona. A comunicação com a plataforma só está disponível em modo assíncrono. Dessa forma, você evita fazer chamadas bloqueadoras nos threads e os problemas de sistema que essas chamadas podem causar (mau desempenho, risco de deadlock). No momento em que escrevo este artigo, não está totalmente claro se o Flutter realmente precisa ter comunicação síncrona e, se precisar, de que forma.
Trabalhando com mensagens binárias, você precisa se preocupar com detalhes delicados, como ordem de bytes (endianness) e como representar mensagens de nível hierárquico superior, como strings ou mapas, usando bytes. Você também precisa especificar o nome de canal certo sempre que quiser enviar uma mensagem ou registrar um gerenciador. A necessidade de facilitar tudo isso nos leva aos canais de plataforma:
Um canal de plataforma é um objeto que reúne um nome de canal e um codec para serializar/desserializar mensagens para o formato binário e vice-versa .
Canais de mensagem: nome + codec
Suponha que você queira enviar e receber mensagens de string, e não buffers de byte. Isso pode ser feito com um canal de mensagens, um tipo simples de canal de plataforma, construído com um codec de string. O código abaixo mostra como usar canais de mensagem nas duas direções no Dart, no Android e no iOS:
// String messages
// Dart side
const channel = BasicMessageChannel<String>('foo', StringCodec());
// Send message to platform and receive reply. final String reply = await channel.send('Hello, world');
print(reply);
// Receive messages from platform and send replies.
channel.setMessageHandler((String message) async {
print('Received: $message');
return 'Hi from Dart';
});
// Android side
val channel = BasicMessageChannel<String>(
flutterView, "foo", StringCodec.INSTANCE)
// Send message to Dart and receive reply. channel.send("Hello, world") { reply ->
Log.i("MSG", reply)
}
// Receive messages from Dart and send replies. channel.setMessageHandler { message, reply ->
Log.i("MSG", "Received: $message")
reply.reply("Hi from Android")
}
// iOS side
let channel = FlutterBasicMessageChannel(
name: "foo",
binaryMessenger: controller,
codec: FlutterStringCodec.sharedInstance())
// Send message to Dart and receive reply. channel.sendMessage("Hello, world") {(reply: Any?) -> Void in
os_log("%@", type: .info, reply as ! String)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler {
(message: Any?, reply: FlutterReply) -> Void in
os_log("Received: %@", type: .info, message as ! String)
reply("Hi from iOS")
}
O nome de canal só é especificado na construção no canal. Em seguida, você pode fazer chamadas para enviar uma mensagem ou criar um gerenciador de mensagens sem repetir o nome do canal. E o mais importante é que deixamos que a classe do codec de string decida como interpretar os buffers de byte como strings e vice-versa.
Sem dúvida, essas vantagens são ótimas. Mas acredito que você concorde que o BasicMessageChannel não faz muita coisa. E isso é proposital. O código Dart acima é equivalente a este uso de fundamentos de mensagens binárias:
const codec = StringCodec();
// Send message to platform and receive reply. final String reply = codec.decodeMessage(
await BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);
// Receive messages from platform and send replies.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
print('Received: ${codec.decodeMessage(message)}');
return codec.encodeMessage('Hi from Dart');
});
Esse comentário também se aplica às implementações de canais de mensagem no Android e no iOS . Não há nada de mágico aqui :
Canais de mensagem delegam toda a comunicação para a camada de mensagens binárias.
Canais de mensagem não controlam os gerenciadores registrados por conta própria.
Canais de mensagem são leves e sem estado.
Duas instâncias de canal de mensagens criadas com nome de canal e codec iguais são equivalentes (e interferem na comunicação uma da outra).
Por diversos motivos históricos, a biblioteca do Flutter define quatro codecs de mensagem diferentes:
StringCodec , que codifica strings usando UTF-8. Como acabamos de ver, canais de mensagem com esse codec têm o tipo BasicMessageChannel<String> no Dart.
BinaryCodec , que, na implementação do mapeamento de identidade em buffers de byte, oferece a conveniência dos objetos de canal nos casos em que não é necessário codificar/decodificar. Os canais de mensagem do Dart com esse codec têm o tipo BasicMessageChannel<ByteData>.
JSONMessageCodec , que opera com valores "parecidos com JSON" (strings, números, booleanos, nulos, listas desses valores e mapas indexados por strings desses valores). As listas e mapas são heterogêneos e podem ser aninhados. Durante a codificação, os valores são transformados em strings JSON e, em seguida, em bytes, usando UTF-8. Os canais de mensagem do Dart têm o tipo BasicMessageChannel<dynamic> com esse codec.
StandardMessageCodec , que opera com valores um pouco mais generalizados que o codec do JSON e também oferece suporte a buffers de dados (UInt8List, Int32List, Int64List, Float64List) e mapas com chaves diferentes de string. O gerenciamento dos números difere do JSON. Os ints do Dart chegam à plataforma como números inteiros assinados de 32 ou 64 bits, dependendo da magnitude, e nunca como números de ponto flutuante. Os valores são codificados em formato binário personalizado, razoavelmente compacto e extensível. O codec padrão foi desenvolvido para ser a opção padrão para a comunicação de canais do Flutter. Como no caso do JSON, os canais de mensagem do Dart construídos com o codec padrão têm o tipo BasicMessageChannel<dynamic>.
Como você já deve ter imaginado, os canais de mensagem funcionam com qualquer implementação de codec de mensagem que satisfaça um contrato simples. Assim, você poderá conectar seu próprio codec, se precisar. Você terá de implementar codificação e decodificação compatíveis no Dart, no Java/Kotlin e no Objective-C/Swift.
Os detalhes. Evolução dos codecs . Todos os codecs de mensagem estão disponíveis no Dart como parte da biblioteca do Flutter, bem como nas duas plataformas como parte das bibliotecas expostas pelo Flutter para código Java/Kotlin ou Objective-C/Swift. O Flutter usa os codecs somente para comunicação interna do aplicativo e não como formato de persistência. Isso significa que a forma binária das mensagens pode mudar de uma versão do Flutter para outra, sem aviso prévio. Naturalmente, as implementações de codec do Dart, do Android e do iOS evoluem juntas para garantir que a codificação pelo remetente seja decodificada pelo destinatário nas duas direções.
Mensagens nulas. Todo codec de mensagem precisa oferecer suporte a mensagens nulas e preservá-las, já que elas são a resposta padrão a uma mensagem enviada em um canal que não tem um gerenciador de mensagens registrado no lado do destinatário.
Definição de tipo estática de mensagens no Dart. Um canal de mensagens configurado com o codec de mensagens padrão dinamiza mensagens e respostas. Na maioria das vezes, você torna suas expectativas de tipo explícitas atribuindo uma variável com tipo:
final String reply1 = await channel.send(msg1);
final int reply2 = await channel.send(msg2);
Mas existe uma ressalva quando se trabalha com respostas que envolvem parâmetros de tipo genéricos:
final List<String> reply3 = await channel.send(msg3); // Fails. final List<dynamic> reply3 = await channel.send(msg3); // Works.
A primeira linha falha em tempo de execução, a menos que a resposta seja nula. O codec de mensagens padrão foi desenvolvido para listas e mapas heterogêneos. No lado do Dart, eles têm os tipos de tempo de execução List<dynamic> e Map<dynamic, dynamic>, e o Dart 2 impede que esses valores sejam atribuídos a variáveis com argumentos de tipos mais específicos. Essa situação é parecida com a da desserialização de JSON do Dart, que produz List<dynamic> e Map<String, dynamic>, exatamente como o codec de mensagens do JSON.
Os futuros (futures) podem dar uma dor de cabeça parecida:
Future<String> greet() => channel.send('hello, world'); // Fails. Future<String> greet() async { // Works.
final String reply = await channel.send('hello, world');
return reply;
}
O primeiro método falha em tempo de execução, mesmo que a resposta recebida seja uma string. A implementação de canal cria um Future<dynamic>, independentemente do tipo da resposta, e um objeto como esse não pode ser atribuído a um Future<String>.
Por que o "basic", ou básico, em BasicMessageChannel? Os canais de mensagem parecem ser usados só em situações bem restritas, em que você comunica uma forma de stream de evento homogêneo em um contexto implícito. Como eventos de teclado, talvez. Na maioria das aplicações dos canais de plataforma, você precisará comunicar não só com valores, mas também o que você quer que aconteça com cada valor ou como gostaria que eles fossem interpretados pelo destinatário. Para isso, uma das soluções é fazer a mensagem representar uma chamada de método com o valor como argumento. Assim, o ideal é ter uma forma padronizada de separar o nome do método e o argumento na mensagem. E também é uma boa ideia ter uma forma padronizada de distinguir entre respostas que indicam execução bem-sucedida ou erro. É isso que os canais de método fazem por você. Anteriormente, BasicMessageChannel era denominado MessageChannel, mas o nome mudou para evitar confusão entre MessageChannel e MethodChannel no código. Por serem aplicáveis de forma mais geral, os canais de método ficaram com o nome mais curto.
Canais de método: envelopes padronizados
Canais de método são canais de plataforma criados para invocar parte nomeadas do código no Dart, no Java/Kotlin ou no Objective-C/Swift. Os canais de método usam "envelopes" de mensagem padronizados para transmitir o nome do método e argumentos do remetente ao destinatário e para distinguir resultados de execução bem-sucedida e com erro na resposta associada. Os envelopes e os payloads compatíveis são definidos por classes de codec de método separadas, de forma semelhante ao uso de codecs de mensagem pelos canais de mensagem.
Os canais de método se resumem a isso: combinar um nome de canal com um codec.
Mais especificamente, nenhuma suposição é feita sobre que código é executado no recebimento de uma mensagem de um canal de método. Mesmo que a mensagem represente uma chamada de método, não é preciso invocar um método. Você pode simplesmente aplicar o nome de método e executar algumas linhas de código para cada caso.
Observação. Essa falta de vinculação implícita ou automatizada para métodos e seus parâmetros pode ser frustrante. Isso não é um problema; frustrações podem ser produtivas. Imagino que você possa criar uma solução para isso do zero usando processamento de anotações e geração de código, ou talvez possa reutilizar partes de uma biblioteca de RPC que já existe. O Flutter é um projeto de código aberto. Fique à vontade para contribuir ! Os canais de método estão disponíveis como destino para a geração de código, se forem adequados para o caso. Nesse meio tempo, eles são úteis isoladamente no "modo manual".
Os canais de método foram a resposta da equipe do Flutter ao desafio de definir uma API de comunicação funcional para uso pelo ecossistema de plug-ins que, naquele momento, ainda não existia. Queríamos algo que os criadores de plug-in pudessem usar imediatamente, sem precisar de um grande número de modelos ou configurações de compilação complexas. Acho que o conceito de canal de método é uma boa resposta, mas eu ficaria surpreso se essa for a única resposta.
Veja como usar um canal de método no caso simples de invocação de uma parte do código da plataforma pelo Dart. O código é associado à barra de nome que, neste caso, não é um nome de método, mas poderia ser. Esse código simplesmente cria uma string de boas-vindas e a retorna ao autor da chamada. Portanto, podemos codificar isso com a premissa razoável de que a invocação da plataforma não vai falhar (falaremos sobre tratamento de erros mais tarde):
// Invocation of platform methods, simple case.
// Dart side.
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
// Android side.
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as ! String)")
default : result(FlutterMethodNotImplemented)
}
}
Adicionando casos aos construtores de alternância, podemos ampliar facilmente o código acima para gerenciar diversos métodos. A cláusula padrão lida com a situação em que um método desconhecido é chamado (muito provavelmente por conta de um erro de programação).
O código Dart acima é equivalente ao seguinte:
const codec = StandardMethodCodec();
final ByteData reply = await BinaryMessages.send (
'foo',
codec.encodeMethodCall(MethodCall('bar', 'world')),
);
if (reply == null )
throw MissingPluginException();
else
print(codec.decodeEnvelope(reply));
As implementações de canais de método do Android e do iOS são encapsulamentos leves similares de chamadas aos fundamentos de mensagens binárias. Uma resposta nula é usada para representar um resultado de "não implementado". A vantagem dessa abordagem é que o comportamento no lado do destinatário não é afetado pelo acionamento da invocação por meio da cláusula padrão da estrutura switch ou se nenhum gerenciador de chamadas de método está registrado no canal.
O valor do argumento no exemplo é a string simples "world". Mas o codec do método padrão, adequadamente denominado "codec do método padrão", usa internamente o codec de mensagem padrão para codificar os valores do payload. Isso significa que todos os valores "parecidos com JSON generalizados" que descrevemos são suportados como argumentos e resultados (bem-sucedidos) do método. Em especial, listas heterogêneas oferecem suporte a vários argumentos, enquanto que mapas heterogêneos oferecem suporte a argumentos nomeados. O valor padrão dos argumentos é nulo. Alguns exemplos:
await channel.invokeMethod('bar');
await channel.invokeMethod('bar', <dynamic>['world', 42, pi]);
await channel.invokeMethod('bar', <String, dynamic>{
name: 'world',
answer: 42,
math: pi,
}));
O Flutter SDK tem dois codecs de método:
StandardMethodCodec , que, por padrão, delega a codificação dos valores do payload ao StandardMessageCodec. Como StandardMessageCodec é extensível, StandardMethodCodec também é extensível.
JSONMethodCodec , que delega a codificação dos valores do payload ao JSONMessageCodec.
Você pode configurar canais de método com qualquer codec de método, incluindo os personalizados. Para entender completamente o que é invocado na implementação de um codec, vamos examinar como os erros são tratados pela API do canal de método ampliando o exemplo acima com um método "baz" falível:
// Method calls with error handling.
// Dart side.
const channel = MethodChannel('foo');
// Invoke a platform method. const name = 'bar'; // or 'baz', or 'unknown'
const value = 'world';
try {
print(await channel.invokeMethod(name, value));
} on PlatformException catch (e) {
print('$name failed: ${e.message}');
} on MissingPluginException {
print('$name not implemented');
}
// Receive method invocations from platform and return results.
channel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'bar':
return 'Hello, ${call.arguments}';
case 'baz':
throw PlatformException(code: '400', message: 'This is bad');
default :
throw MissingPluginException();
}
});
// Android side.
val channel = MethodChannel(flutterView, "foo")
// Invoke a Dart method.
val name = "bar" // or "baz", or "unknown"
val value = "world"
channel.invokeMethod(name, value, object: MethodChannel.Result {
override fun success(result: Any?) {
Log.i("MSG", "$result")
}
override fun error(code: String?, msg: String?, details: Any?) {
Log.e("MSG", "$name failed: $msg")
}
override fun notImplemented() {
Log.e("MSG", "$name not implemented")
}
})
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
"baz" -> result.error("400", "This is bad", null)
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
// Invoke a Dart method. let name = "bar" // or "baz", or "unknown"
let value = "world"
channel.invokeMethod(name, arguments: value) {
(result: Any?) -> Void in
if let error = result as ? FlutterError {
os_log("%@ failed: %@", type: .error, name, error.message!)
} else if FlutterMethodNotImplemented.isEqual(result) {
os_log("%@ not implemented", type: .error, name)
} else {
os_log("%@", type: .info, result as ! NSObject)
}
}
// Receive method invocations from Dart and return results. channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as ! String)")
case "baz": result(FlutterError(
code: "400", message: "This is bad", details: nil))
default : result(FlutterMethodNotImplemented)
}
Os erros são trios (código, mensagem, detalhes) quando o código e a mensagem são strings. O objetivo da mensagem é o consumo humano. O objetivo do código é... bem, ser código. Os detalhes do erro são algum valor personalizado, muitas vezes nulo, que é restrito aos tipos de valor compatíveis com o codec.
Os detalhes. Exceções. Toda exceção não capturada gerada em um gerenciador de chamadas de método do Dart ou do Android é capturada pela implementação de canal e registradas. Uma resposta de erro é retornada ao autor da chamada. As exceções não capturadas geradas em gerenciadores de resultado são registradas.
Codificação de envelope. A forma como codec de método codifica envelopes é um detalhe de implementação, assim como a forma como os codecs de mensagem convertem mensagens em bytes. Vejamos um exemplo. Um codec de método pode usar listas: é possível codificar chamadas de método como listas de dois elementos [nome de método, argumentos]; resultados bem-sucedidos, como listas de um elemento [resultado] e resultados de erro, como listas de três elementos [código, mensagem, detalhes]. Esse tipo de codec de método pode ser implementado com uma delegação simples a um codec de mensagem que ofereça suporte, pelo menos, a listas, strings e valores nulos (null). Os argumentos, resultados bem-sucedidos e detalhes de erro da chamada de método seriam valores arbitrários compatíveis com esse codec de mensagens.
Diferenças da API. Os exemplos de código acima destacam que os canais de método geram resultados muito diferentes no Dart, no Android e no iOS:
No Dart, a invocação é gerenciada por um método que retorna um futuro (future). O futuro é complementado com o resultado da chamada nos casos de sucesso, com uma PlatformException nos casos de erro e com uma MissingPluginException nos casos sem implementação.
No Android, a invocação é gerenciada por um método que recebe um argumento de callback. A interface de callback define três métodos. Desses métodos, só um é chamado, dependendo do resultado. O código do cliente implementa a interface de callback para definir o que deve acontecer nos casos de sucesso, erro e sem implementação.
No iOS, a invocação é gerenciada de forma semelhante a um método que recebe um argumento de callback. Mas aqui o callback é uma função de um só argumento que recebe uma instância de FlutterError, a constante FlutterMethodNotImplemented ou, nos casos de sucesso, o resultado da invocação. O código do cliente especifica um bloqueio com lógica condicional para gerenciar os diferentes casos, se necessário.
Essas diferenças, espelhadas também na forma como os gerenciadores de chamada de mensagens são programados, surgiram como concessões aos estilos das linguagens de programação (Dart, Java e Objective-C) usadas para as implementações de canal de método do Flutter SDK. Se você refizer as implementações em Kotlin e Swift, poderá remover algumas das diferenças, mas é preciso ter cuidado para não dificultar ainda mais o uso dos canais de método no Java e no Objective-C.
Canais de evento: streaming
Um canal de evento é um canal de plataforma especializado criado para o caso de uso de exposição de eventos da plataforma ao Flutter como um stream do Dart. No momento, o Flutter SDK não oferece suporte ao caso simétrico de exposição de streams do Dart ao código da plataforma. Essa funcionalidade pode ser criada, se necessária.
Veja como consumir um stream de evento da plataforma no Dart:
// Consuming events on the Dart side.
const channel = EventChannel('foo');
channel.receiveBroadcastStream().listen((dynamic event) {
print('Received event: $event');
}, onError: (dynamic error) {
print('Received error: ${error.message}');
});
O código abaixo mostra como produzir eventos no lado da plataforma usando eventos de sensor no Android, por exemplo. A maior preocupação é garantir que os eventos da origem da plataforma (neste caso, o gerenciador de sensores) sejam detectados e enviados pelo canal de eventos no momento exato em que 1) há pelo menos um ouvinte de stream no Dart; e 2) a Activity de ambiente está em execução. Juntando a lógica necessária em uma única classe, você aumenta a chance de fazer tudo funcionar corretamente:
// Producing sensor events on Android.
// SensorEventListener/EventChannel adapter. class SensorListener(private val sensorManager: SensorManager) :
EventChannel.StreamHandler, SensorEventListener {
private var eventSink: EventChannel.EventSink? = null
// EventChannel.StreamHandler methods
override fun onListen(
arguments: Any?, eventSink: EventChannel.EventSink?) {
this .eventSink = eventSink
registerIfActive()
}
override fun onCancel(arguments: Any?) {
unregisterIfActive()
eventSink = null
}
// SensorEventListener methods. override fun onSensorChanged(event: SensorEvent) {
eventSink?.success(event.values)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
eventSink?.error("SENSOR", "Low accuracy detected", null)
}
// Lifecycle methods.
fun registerIfActive() {
if (eventSink == null) return
sensorManager.registerListener(
this ,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun unregisterIfActive() {
if (eventSink == null) return
sensorManager.unregisterListener(this )
}
}
// Use of the above class in an Activity. class MainActivity: FlutterActivity() {
var sensorListener: SensorListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super .onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this )
sensorListener = SensorListener(
getSystemService(Context.SENSOR_SERVICE) as SensorManager)
val channel = EventChannel(flutterView, "foo")
channel.setStreamHandler(sensorListener)
}
override fun onPause() {
sensorListener?.unregisterIfActive()
super .onPause()
}
override fun onResume() {
sensorListener?.registerIfActive()
super .onResume()
}
}
Se você usa o pacote android.arch.lifecycle no aplicativo, pode tornar o SensorListener mais autossuficiente transformando-o em um LifecycleObserver.
Os detalhes. A vida de um gerenciador de streams. O gerenciador de streams do lado da plataforma tem dois métodos, onListen e onCancel, que são invocados sempre que o número de ouvintes do stream do Dart passa de zero a um e volta a zero, respectivamente. Isso pode acontecer muitas vezes. A implementação do gerenciador de streams deve começar a encaminhar os eventos ao coletor de eventos quando onListen é chamado e parar quando onCancel é chamado. Além disso, ela deve pausar quando o componente do aplicativo de ambiente não está em execução. O código acima mostra um exemplo comum. Internamente, um gerenciador de streams é obviamente apenas um gerenciador de mensagens binárias, registrado na visualização do Flutter, que usa o nome do canal de eventos.
Codec. Um canal de eventos é configurado com um codec de método, o que nos permite distinguir eventos de sucesso e de erro da mesma forma que os canais de método conseguem distinguir resultados de sucesso e de erro.
Argumentos e erros do gerenciador de streams. Os métodos onListen e onCancel do gerenciador de streams são invocados com invocações de canal de método. Assim, controlamos as chamadas de método do Dart para a plataforma e as mensagens de evento na direção oposta, tudo isso no mesmo canal lógico. Essa configuração permite que argumentos sejam encaminhados aos métodos de controle e que os erros sejam comunicados. No Dart, os argumentos, se existentes, são passados na chamada de receiveBroadcastStream. Isso significa que eles são especificados apenas uma vez, independentemente do número de invocações de onListen e onCancel que acontecem durante a vida útil do stream. Todos os erros comunicados são registrados.
Fim do stream. Um coletor de eventos tem um método endOfStream que pode ser invocado para sinalizar que não serão enviados mais eventos de sucesso ou erro. A mensagem binária nula é usada exatamente para isso. Quando recebida pelo Dart, o stream é fechado.
Vida útil de um stream . O stream do Dart é apoiado por um controlador de streams alimentado pelas mensagens de canal recebidas pela plataforma. Um gerenciador de mensagens binárias é registrado usando o nome do canal de eventos para receber mensagens enviadas somente enquanto o stream tiver ouvintes.
Orientações de uso
Prefixe nomes de canal por domínio para garantir a unicidade
Nomes de canal são apenas strings, mas precisam ser exclusivos em todos os objetos do canal usados para coisas diferentes no seu aplicativo. Para fazer isso, você pode usar qualquer nomenclatura adequada. Porém, a abordagem recomendada para canais usados em plug-ins é aplicar um nome de domínio e um prefixo do nome do plug-in, como some.body.example.com/sensors/foo para o canal "foo" usado pelo plug-in "sensors" desenvolvido por "some.body" em "example.com". Se você fizer isso, os consumidores do plug-in poderão combinar quantos plug-ins quiserem nos aplicativos sem risco de conflitos de nome de canal.
Considere o uso de canais de plataforma como comunicação intramodular
O código para invocar chamadas de procedimento remoto em sistemas distribuídos é, superficialmente, semelhante ao código que usa canais de método: você invoca um método definido por uma string e serializa os argumentos e resultados. Como os componentes do sistema distribuído são, muitas vezes, desenvolvidos e implantados de forma independente, é essencial verificar rigorosamente as solicitações e as respostas. Normalmente, isso é feito no estilo "verificar e registrar" nos dois lados da rede.
Por outro lado, os canais de plataforma agrupam três códigos desenvolvidos e implantados juntos em um único componente.
Java/Kotlin ↔ Dart ↔ Objective-C/Swift
Na verdade, muitas vezes faz sentido criar uma tríade como essa em um único módulo de código , como, por exemplo, um plug-in do Flutter. Isso significa que a necessidade de verificar argumentos e resultados nas invocações de canal de método deve ser equivalente à necessidade dessas verificações em chamadas de método normais dentro do mesmo módulo.
Nos módulos, a maior preocupação é proteger contra erros de programação não detectados pelas verificações estáticas do compilador e em tempo de execução até que geram erros graves não locais em termos de tempo ou espaço. Um estilo de codificação razoável é tornar as suposições explícitas usando tipos ou declarações, o que permite processar falhas com rapidez e simplicidade como, por exemplo, com uma exceção. Obviamente, os detalhes variam de acordo com a linguagem de programação. Exemplos:
Se um valor recebido por um canal de plataforma tiver que ter um tipo específico, atribua-o imediatamente a uma variável desse tipo.
Se é esperado que um valor recebido por um canal de plataforma não seja nulo, faça com que referência ao seja cancelada imediatamente ou assegure que o valor não é nulo antes de armazená-lo para uso posterior. Dependendo da linguagem de programação, você poderá atribuí-lo a uma variável do tipo não anulável para evitar isso.
Dois exemplos simples:
// Dart: we expect to receive a non-null List of integers.
for (final int n in await channel.invokeMethod('getFib', 100)) {
print(n * n);
}
// Android: we expect non-null name and age arguments for
// asynchronous processing, delivered in a string-keyed map.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> {
val name : String = call.argument("name")
val age : Int = call.argument("age")
process(name, age, result)
}
else -> result.notImplemented()
}
}
:
fun process(name: String, age: Int, result: Result) { ... }
O código do Android explora um método de argumento de tipo <T> T genérico (String key) de MethodCall, que procura a chave nos argumentos (que se presume ser um mapa) e encaminha o valor encontrado ao tipo de destino (local da chamada). Se ocorrer uma falha, será gerada uma exceção adequada. Por ser gerada por um gerenciador de chamadas de método, ela seria registrada e um resultado de erro seria enviado ao Dart.
Não simule os canais de
plataforma. Durante a criação de testes de unidade para código Dart que usa canais de plataforma, uma reação comum é simular o objeto de canal, como seria feito com uma conexão de rede.
Com certeza você pode fazer isso, mas os objetos de canal não precisam ser simulados para funcionar bem com os testes de unidade. Em vez disso, você pode registrar os gerenciadores de método ou mensagem simulados para que eles façam o papel da plataforma em um determinado teste. Veja a seguir um teste de unidade de uma função "hello" que deve invocar o método "bar" no canal "foo":
test('gets greeting from platform', () async {
const channel = MethodChannel('foo');
channel.setMockMethodCallHandler((MethodCall call) async {
if (call.method == 'bar')
return 'Hello, ${call.arguments}';
throw MissingPluginException();
});
expect(await hello('world'), 'Platform says: Hello, world');
});
Para testar o código que cria gerenciadores de mensagem ou método, você pode sintetizar mensagens recebidas usando BinaryMessages.handlePlatformMessage. No momento, esse método não é espelhado nos canais de plataforma, mas isso pode ser feito facilmente, como mostrado no código abaixo. O código define um teste de unidade de uma classe "Hello", que deve coletar argumentos recebidos de chamadas feitas ao método "bar" no canal "fo", e retornar uma mensagem de boas-vindas:
test('collects incoming arguments', () async {
const channel = MethodChannel('foo');
final hello = Hello();
final String result = await handleMockCall(
channel,
MethodCall('bar', 'world'),
);
expect(result, contains('Hello, world'));
expect(hello.collectedArguments, contains('world'));
});
// Could be made an instance method on class MethodChannel.
Future<dynamic > handleMockCall(
MethodChannel channel,
MethodCall call,
) async {
dynamic result;
await BinaryMessages.handlePlatformMessage (
channel.name,
channel.codec.encodeMethodCall(call),
(ByteData reply) {
if (reply == null )
throw MissingPluginException();
result = channel.codec.decodeEnvelope(reply);
},
);
return result;
}
Os dois exemplos acima declaram o objeto de canal no teste de unidade. E isso funciona muito bem, a menos que você se preocupe com a duplicidade de nomes de canal e codec, pois todos os objetos de canal com nome e codec iguais são equivalentes. Você pode evitar a duplicação declarando o canal como uma const em algum lugar visível no código de produção e no teste.
Não é necessário criar uma forma de injetar um canal simulado no código de produção.
Considere o uso de testes automatizados para interagir com a plataforma
Os canais de plataforma são bastante simples, mas fazer tudo funcionar na IU do Flutter por meio de uma Dart API personalizada apoiada por uma implementação Java/Kotlin e Objective-C/Swift individual não é tão simples assim. Além disso, na prática, para manter o esquema funcionando à medida que o aplicativo é alterado, você terá que automatizar os testes para evitar regressões. Não é possível fazer isso só com os testes de unidade porque você precisa de um aplicativo real em execução para que os canais da plataforma realmente se comuniquem com a plataforma.
O Flutter é fornecido com a biblioteca de teste de integração flutter_driver, que permite testar aplicativos Flutter em execução em dispositivos reais e emuladores. Mas, no momento, a biblioteca flutter_driver não é integrada a outras bibliotecas para permitir testes de componentes do Flutter e da plataforma. Tenho certeza de que essa é a área em que o Flutter vai evoluir no futuro.
Em algumas situações, você pode usar a forma atual da biblioteca flutter_driver para testar o uso dos canais de plataforma. Para tanto, é necessário que a sua interface do usuário do Flutter possa ser usada para acionar qualquer interação com a plataforma e que ela possa ser atualizada com detalhes suficientes para permitir que seu teste determine o resultado da interação.
Se esse não é o seu caso, ou se você está empacotando o uso de canais de plataforma na forma de um plug-in do Flutter no qual você quer fazer um teste de módulo, crie um aplicativo do Flutter simples para testes. Esse aplicativo deve ter as características acima e ser testado com a biblioteca flutter_driver. Há um exemplo no repositório do Flutter no GitHub .
Prepare a plataforma para receber chamadas síncronas
Os canais de plataforma sempre são assíncronos. Mas existem algumas APIs de plataforma que fazem chamadas síncronas aos componentes do aplicativo host, solicitando informações ou ajuda ou oferecendo uma janela de oportunidade. Um exemplo é Activity.onSaveInstanceState, do Android. Sincronismo significa que tudo tem que ser feito antes de a chamada recebida retornar. Talvez você queira incluir informações do Dart nesse processamento, mas não é possível começar a enviar mensagens assíncronas quando já existe uma chamada síncrona ativa no thread de IU principal.
A abordagem usada pelo Flutter, principalmente para informações de semântica ou acessibilidade, é enviar proativamente informações atualizadas (ou atualizações dessas informações) à plataforma sempre que as informações forem alteradas no Dart. Quando a chamada síncrona chegar, as informações do Dart já estarão presentes e disponíveis no código da plataforma.
Materiais
Documentação da Flutter API :
Guias:
O site flutter.io tem documentos sobre como usar canais de método e as conversões de valor do Dart/Android/iOS envolvidas no uso do codec de métodos padrão.
O episódio 6 do The Boring Flutter Development Show : Packages e plugins é um vídeo do YouTube que mostra a implementação de um plug-in do Flutter, em tempo real, usando canais de plataforma.
Exemplos de código:
4 comentários :
Situs Judi Terpercaya dewapoker online pasti memberikan fasilitas sistem permainan poker online yang canggih, proses transaksi yang sangat cepat, dan pelayanan terbaik demi kenyamanan semua member. Bagi anda yang ingin bermain judi online seperti permainan poker online,domino qiu qiu, ceme,blackjack, ataupun capsa susun bisa mendaftarkan diri anda di dewa poker . dewa poker online menjadi Trendsetter Game Judi Poker Online Paling Terpercaya dan Recommended dari berbagai Situs, Blog, Media Sosial dan Beberapa Grup Pecinta Game Online Uang Asli.
Pengen main game poker online terpercaya di Wargapoker ? Berikut kami sajikan website game online wargakartu terpercaya yang memfokuskan pelayanan utamanya terhadap permainan poker online.Disini UBC Poker kamu juga bisa menikmati permainan seru lainnya.Cukup dengan deposit 50 ribu kamu bisa langsung mulai bermain game seru ini di . ditambah lagi biaya pendaftaran yang gratis!! Website Game online
SobatPoker kami adalah sebagai tempat bermain poker di ratucapsa yang aman. Dengan situs game poker online uang asli Indonesia Terpercaya, keramahan staff kami serta dapatkan berbagai bonus menarik untuk semua member kami.
Tunggu apalagi ? segeralah daftar poker online bersama situs terpercaya dibawah ini!!
jasapoker yang dilengkapi sistem teknologi terbaru dengan server berkecepatan tinggi yang akan memberikan permainan Judi bola online jasa qq dan sbobet. Caranya sangat mudah, hanya dengan mendaftarkan diri anda di link alternatif juarapoker 2019
sekarang juga dan mulailah mereferensikan link referral anda kepada semua teman dan kenalan anda untuk ikut bermain bersama agen judi online remipoker yang terpercaya ini. Daftarkan diri anda sekarang juga di dominobet 2019 dan ajak teman anda untuk bermain bersama di satu meja!
jangan lupa lagi Akan semakin seru jika Mainbolajalan bisa dimainkan oleh anda semua,permainan yang FAIR PLAY yang akan diberikan dan akan anda rasakan di Mainbolajalan dengan BONUS ROLLINGAN SPORTBOOK 0.10% , BONUS ROLLINGAN CASINO 0.5% serta BONUS REFERRAL 1% dari WL Teman anda, Aman dan Terpercaya hanya ada di Bandar agen sbobet terpercaya di asia seperti pokerace99
papua4d
ojktoto
rubah4d
Togel online
Postar um comentário