Flutter notes | E/S de arquivo Flutter, solicitação de rede, JSON, data e internacionalização

operação de e/s de arquivo

A biblioteca IO do Dart contém classes relacionadas para leitura e gravação de arquivos. Faz parte do padrão de sintaxe Dart. Portanto, através da biblioteca Dart IO, seja um script sob o Dart VM ou Flutter, os arquivos são manipulados através da biblioteca Dart IO. No entanto, em comparação com o Dart VM, uma diferença importante do Flutter é que o caminho do sistema de arquivos é diferente. Isso ocorre porque o Dart VM é executado em um PC ou sistema operacional de servidor, enquanto o Flutter é executado em um sistema operacional móvel e seus sistemas de arquivos serão têm algumas diferenças.

diretório APP

Android e iOS têm diferentes diretórios de armazenamento de aplicativos, e o plug-in PathProvider fornece uma maneira transparente de plataforma para acessar locais comumente usados ​​no sistema de arquivos do dispositivo. Esta classe atualmente suporta acesso a dois locais de sistema de arquivos:

  • Diretório temporário : você pode usar getTemporaryDirectory()para obter o diretório temporário; o sistema pode limpar os arquivos no diretório temporário a qualquer momento. No iOS, isso corresponde ao NSTemporaryDirectory()valor retornado por . No Android, esse é getCacheDir()o valor retornado por .
  • Diretório de documentos : pode ser usado getApplicationDocumentsDirectory()para obter o diretório de documentos do aplicativo, que é usado para armazenar arquivos que somente você pode acessar. O sistema limpa esse diretório somente quando o aplicativo é desinstalado. No iOS, isso corresponde a NSDocumentDirectory. No Android, este é AppDatao diretório.
  • Diretório de armazenamento externo : pode ser usado getExternalStorageDirectory()para obter diretório de armazenamento externo, como cartão SD; como o iOS não oferece suporte a diretório externo, chamar esse método no iOS gerará UnsupportedErroruma exceção, enquanto no Android, o resultado é getExternalStorageDirectoryo valor de retorno no SDK do Android.

Depois que seu aplicativo Flutter tiver uma referência a um local de arquivo, você poderá usar dart:ioa API para executar operações de leitura/gravação no sistema de arquivos. Por exemplo:

import 'dart:async';
import 'dart:io';
import 'dart:convert';

// Reading a file as text
// When reading a text file encoded using UTF-8,
// you can read the entire file contents with readAsString().
// When the individual lines are important, you can use readAsLines().
// In both cases, a Future object is returned that provides the contents of the
// file as one or more strings.
Future<void> readFileAsText() async {
    
    
  var config = File('config.txt');

// Put the whole file in a single string.
  var stringContents = await config.readAsString();
  print('The file is ${
      
      stringContents.length} characters long.');

// Put each line of the file into its own string.
  var lines = await config.readAsLines();
  print('The file is ${
      
      lines.length} lines long.');
}

// Reading a file as binary
// The following code reads an entire file as bytes into a list of ints.
// The call to readAsBytes() returns a Future, which provides the result when it’s available.
Future<void> readFileAsBinary() async {
    
    
  var config = File('config.txt');
  var contents = await config.readAsBytes();
  print('The file is ${
      
      contents.length} bytes long.');
}

// Handling errors
// To capture errors so they don’t result in uncaught exceptions, you can register
// a catchError handler on the Future, or (in an async function) use try-catch:
Future<void> handlingErrors() async {
    
    
  var config = File('config.txt');
  try {
    
    
    var contents = await config.readAsString();
    print(contents);
  } catch (e) {
    
    
    print(e);
  }
}

// Streaming file contents
// Use a Stream to read a file, a little at a time. You can use either the Stream
// API or await for, part of Dart’s asynchrony support.
Future<void> streamingFileContents() async {
    
    
  var config = File('config.txt');
  Stream<List<int>> inputStream = config.openRead();

  var lines = utf8.decoder
      .bind(inputStream)
      .transform(const LineSplitter());
  try {
    
    
    await for (final line in lines) {
    
    
      print('Got ${
      
      line.length} characters from stream');
    }
    print('file is now closed');
  } catch (e) {
    
    
    print(e);
  }
}

// Writing file contents
// You can use an IOSinklaunch to write data to a file. Use the File openWrite()
// method to get an IOSink that you can write to. The default mode, FileMode.write,
// completely overwrites existing data in the file.
Future<void> writingFileContent() async {
    
    
  var logFile = File('log.txt');
  var sink = logFile.openWrite();
  sink.write('FILE ACCESSED ${
      
      DateTime.now()}\n');
  // To add to the end of the file, use the optional mode parameter to specify FileMode.append:
  // var sink = logFile.openWrite(mode: FileMode.append);
  await sink.flush();
  await sink.close();
}

// To write binary data, use add(List<int> data).Listing files in a directory
// Finding all files and subdirectories for a directory is an asynchronous operation.
// The list() method returns a Stream that emits an object when a file or directory is encountered.
Future<void> writeBinaryData() async {
    
    
  var dir = Directory('tmp');
  try {
    
    
    var dirList = dir.list();
    await for (final FileSystemEntity f in dirList) {
    
    
      if (f is File) {
    
    
        print('Found file ${
      
      f.path}');
      } else if (f is Directory) {
    
    
        print('Found dir ${
      
      f.path}');
      }
    }
  } catch (e) {
    
    
    print(e.toString());
  }
}

Para obter mais detalhes sobre o processamento de arquivos do Dart, consulte a documentação da linguagem Dart Vejamos um exemplo simples abaixo.

exemplo

Ainda tomamos o contador como exemplo para perceber que o número de cliques pode ser restaurado após a saída e reinicialização do aplicativo. Aqui, usamos um arquivo para salvar os dados:

  1. Importe PathProvidero plug-in; pubspec.yamladicione a seguinte instrução ao arquivo:
path_provider: ^2.0.15

Depois de adicionar, execute flutter packages getGet it, o número da versão pode mudar com o tempo, você pode usar a versão mais recente.

concluir:

import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

class FileOperationRoute extends StatefulWidget {
    
    
  FileOperationRoute({
    
    Key? key}) : super(key: key);

  
  _FileOperationRouteState createState() => _FileOperationRouteState();
}

class _FileOperationRouteState extends State<FileOperationRoute> {
    
    
  int _counter = 0;

  
  void initState() {
    
    
    super.initState();
    // 从文件读取点击次数
    _readCounter().then((int value) {
    
    
      setState(() {
    
    
        _counter = value;
      });
    });
  }

  Future<File> _getLocalFile() async {
    
    
    // 获取应用目录
    String dir = (await getApplicationDocumentsDirectory()).path;
    return File('$dir/counter.txt');
  }

  Future<int> _readCounter() async {
    
    
    try {
    
    
      File file = await _getLocalFile();
      // 读取点击次数(以字符串)
      String contents = await file.readAsString();
      return int.parse(contents);
    } on FileSystemException {
    
    
      return 0;
    }
  }

  _incrementCounter() async {
    
    
    setState(() {
    
    
      _counter++;
    });
    // 将点击次数以字符串类型写到文件中
    await (await _getLocalFile()).writeAsString('$_counter');
  }


  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: Text('文件操作')),
      body: Center(
        child: Text('点击了 $_counter 次'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

O código acima é relativamente simples, então não vou entrar em detalhes aqui. O que precisa ser explicado é que este exemplo é apenas para demonstrar a leitura e escrita do arquivo. No desenvolvimento real, se você quiser armazenar alguns dados simples, ser mais fácil usar o plugin shared_preferences .

solicitação de rede

Iniciar solicitação HTTP por meio de HttpClient

A biblioteca Dart IO fornece algumas classes para iniciar solicitações Http, que podemos usar diretamente HttpClientpara iniciar solicitações. Há cinco etapas para HttpClientiniciar uma solicitação:

  1. Crie um HttpClient:
 HttpClient httpClient = HttpClient();
  1. Abra Httpa conexão e defina o cabeçalho da solicitação:
HttpClientRequest request = await httpClient.getUrl(uri);

Esta etapa pode usar qualquer Http Method, como httpClient.post(...), httpClient.delete(...)etc. Se Queryforem incluídos parâmetros, eles podem uriser adicionados no momento da compilação, como:

Uri uri = Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
    
    
    "xx":"xx",
    "yy":"dd"
  });

Definindo HttpClientRequesto request header, como:

request.headers.add("user-agent", "test");

Se for postou putpuder carregar o método do corpo da solicitação, você pode HttpClientRequestenviar o corpo da solicitação por meio do objeto, como:

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); //可以直接添加输入流
  1. Aguardando conexão com o servidor:
HttpClientResponse response = await request.close();

Após a conclusão desta etapa, as informações da solicitação foram enviadas ao servidor e um HttpClientResponseobjeto é retornado, que contém o cabeçalho da resposta ( header) e o fluxo da resposta (do corpo da resposta Stream), e então o conteúdo da resposta pode ser obtido lendo o fluxo de resposta.

  1. Leia o conteúdo da resposta:
String responseBody = await response.transform(utf8.decoder).join();

Obtemos os dados retornados pelo servidor lendo o fluxo de resposta e podemos definir o formato de codificação ao ler, aqui está utf8.

  1. Fim da solicitação, fechar HttpClient:
httpClient.close();

Quando fechado , todas as solicitações iniciadas clientpor meio dele serão encerradas.client

exemplo

O código a seguir percebe que, após clicar no botão, a página inicial do Baidu é solicitada. Após a solicitação ser bem-sucedida, o conteúdo retornado é exibido e o cabeçalho de resposta é impresso no console. O código é o seguinte:

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';

class HttpTestRoute extends StatefulWidget {
    
    
  
  _HttpTestRouteState createState() => _HttpTestRouteState();
}

class _HttpTestRouteState extends State<HttpTestRoute> {
    
    
  bool _loading = false;
  String _text = "";

  
  Widget build(BuildContext context) {
    
    
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          ElevatedButton(
            child: Text("获取百度首页"),
            onPressed: _loading ? null : request,
          ),
          Container(
            width: MediaQuery.of(context).size.width - 50.0,
            child: Text(_text.replaceAll(RegExp(r"\s"), "")),
          )
        ],
      ),
    );
  }

  request() async {
    
    
    setState(() {
    
    
      _loading = true;
      _text = "正在请求...";
    });
    try {
    
    
      //创建一个HttpClient
      HttpClient httpClient = HttpClient();
      //打开Http连接
      HttpClientRequest request =
          await httpClient.getUrl(Uri.parse("https://www.baidu.com"));
      //使用iPhone的UA
      request.headers.add(
        "user-agent",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1",
      );
      //等待连接服务器(会将请求信息发送给服务器)
      HttpClientResponse response = await request.close();
      //读取响应内容
      _text = await response.transform(utf8.decoder).join();
      //输出响应头
      print(response.headers);

      //关闭client后,通过该client发起的所有请求都会终止。
      httpClient.close();
    } catch (e) {
    
    
      _text = "请求失败:$e";
    } finally {
    
    
      setState(() {
    
    
        _loading = false;
      });
    }
  }
}

Saída do console:

I/flutter (18545): connection: Keep-Alive
I/flutter (18545): cache-control: no-cache
I/flutter (18545): set-cookie: ....  //有多个,省略...
I/flutter (18545): transfer-encoding: chunked
I/flutter (18545): date: Tue, 30 Oct 2018 10:00:52 GMT
I/flutter (18545): content-encoding: gzip
I/flutter (18545): vary: Accept-Encoding
I/flutter (18545): strict-transport-security: max-age=172800
I/flutter (18545): content-type: text/html;charset=utf-8
I/flutter (18545): tracecode: 00525262401065761290103018, 00522983

Configuração HttpClient

HttpClientExistem muitos atributos que podem ser configurados, e a lista de atributos comumente usados ​​é a seguinte:

Atributos significado
idleTimeout Correspondente ao valor do campo no cabeçalho da solicitação keep-alive, para evitar o estabelecimento frequente de conexões, o httpClient manterá a conexão por um período de tempo após o término da solicitação e fechará a conexão após ultrapassar esse limite.
connectionTimeout O tempo limite para estabelecer uma conexão com o servidor. Se esse valor for excedido, SocketExceptionuma exceção será lançada.
maxConnectionsPerHost O mesmo host, o número máximo de conexões permitidas ao mesmo tempo.
autoUncompress Correspondente a no cabeçalho da solicitação Content-Encoding, se definido como true, o valor no cabeçalho da solicitação é Content-Encodinga lista de algoritmos de compactação suportados pelo HttpClient atual, atualmente apenas " gzip"
userAgent Corresponde aos campos do cabeçalho da solicitação User-Agent.

Pode-se descobrir que alguns atributos são apenas para uma configuração mais conveniente do cabeçalho da solicitação. Para esses atributos, você pode HttpClientRequestdefini-los diretamente header. A diferença é que HttpClientos definidos httpClientsão eficazes para o todo e HttpClientRequestos definidos são eficazes apenas para o solicitação atual.

Autenticação de solicitação HTTP

O mecanismo de autenticação (autenticação) do protocolo Http pode ser usado para proteger recursos não públicos. Se o servidor Http tiver autenticação habilitada, o usuário precisa portar credenciais de usuário ao fazer uma requisição. Se você acessar um Basicrecurso com autenticação habilitada no navegador, uma caixa de login irá aparecer durante a navegação, conforme a figura:

insira a descrição da imagem aqui
Vejamos primeiro Basico processo básico de autenticação:

  1. O cliente envia uma requisição http para o servidor, e o servidor verifica se o usuário está logado e autenticado. Caso contrário, o servidor irá retornar uma requisição 401 Unauthoziedpara o cliente e headeradicionar um “WWW-Authenticate”campo na resposta, por exemplo:

    WWW-Authenticate: Basic realm="admin"

    Entre eles, "Básico" é o método de autenticação realme é o agrupamento de funções de usuário, que podem ser adicionadas em segundo plano.

  2. Depois que o cliente obtiver o código de resposta, codifique o nome de usuário e a senha base64(o formato é nome de usuário:senha), defina o cabeçalho da solicitação Authorizatione continue visitando:

    Authorization: Basic YXXFISDJFISJFGIJIJG

    O servidor verifica as credenciais do usuário e retorna o conteúdo do recurso, se aprovado.

Observe que, Httpalém da autenticação, existem outros métodos Basic: Digestautenticação, Clientautenticação, Form Basedautenticação, etc. Atualmente, o Flutter HttpClientoferece suporte apenas Basica Digestdois métodos de autenticação. é diferente da primeira É apenas Base64codificação simples (reversível), e a segunda vai realizar operações de hash, o que é relativamente seguro, mas por questões de segurança, seja autenticação Basicou Digestautenticação, deve estar sob Httpso protocolo , o que pode impedir a captura de pacotes e ataques man-in-the-middle.

HttpClientSobre Httpmétodos e propriedades de autenticação:

  1. addCredentials(Uri url, String realm, HttpClientCredentials credentials)

    Este método é usado para adicionar credenciais de usuário como:

httpClient.addCredentials(_uri,
 "admin", 
  HttpClientBasicCredentials("username","password"), //Basic认证凭据
);

Para Digestautenticação, você pode criar Digestcredenciais de autenticação:

HttpClientDigestCredentials("username","password")
  1. authenticate(Future<bool> f(Uri url, String scheme, String realm))

    Este é um settere o tipo é um retorno de chamada. Quando o servidor precisa de credenciais de usuário e as credenciais do usuário não foram adicionadas, httpClientesse retorno de chamada será chamado. Nesse retorno de chamada, geralmente é chamado addCredential()para adicionar credenciais de usuário dinamicamente, por exemplo:

httpClient.authenticate=(Uri url, String scheme, String realm) async{
    
    
  if(url.host=="xx.com" && realm=="admin"){
    
    
    httpClient.addCredentials(url,
      "admin",
      HttpClientBasicCredentials("username","pwd"), 
    );
    return true;
  }
  return false;
};

Uma sugestão é que, se todas as solicitações exigirem autenticação, ela deve ser HttpClientchamada no momento da inicialização addCredentials()para adicionar credenciais globais em vez de adicioná-las dinamicamente.

atuando

A política de proxy pode findProxyser definida por, por exemplo, queremos enviar todas as solicitações por meio do servidor proxy (192.168.1.2:8888):

  client.findProxy = (uri) {
    
    
    // 如果需要过滤uri,可以手动判断
    return "PROXY 192.168.1.2:8888";
 };

findProxyO valor de retorno do retorno de chamada é uma string que segue o formato de script PAC do navegador. Para obter detalhes, consulte a documentação da API. Se você não precisar de um proxy, DIRECTbasta retornar " ".

No desenvolvimento de APP, muitas vezes precisamos capturar pacotes para depuração, e o software de captura de pacotes (como charles) é um proxy. Neste momento, podemos enviar a solicitação para nosso software de captura de pacotes e podemos vê-lo no pacote software de captura. Os dados solicitados chegaram.

Às vezes, o servidor proxy também habilita a autenticação, que httpé semelhante à autenticação de protocolo e HttpClientfornece Proxymétodos e atributos de autenticação correspondentes:

set authenticateProxy(
    Future<bool> f(String host, int port, String scheme, String realm));
void addProxyCredentials(
    String host, int port, String realm, HttpClientCredentials credentials);

Seu uso é o addCredentialsmesmo apresentado em "Autenticação de solicitação HTTP" acima authenticate, portanto, não os repetirei aqui.

Verificação de certificado

HttpsPara evitar ataques man-in-the-middle falsificando certificados, o cliente deve verificar os certificados autoassinados ou emitidos por não-CA. HttpClientA lógica da verificação do certificado é a seguinte:

  1. Se o Httpscertificado solicitado for emitido por uma CA confiável, e o acesso hostestiver incluído na lista de certificados domain(ou estiver em conformidade com as regras curinga) e o certificado não tiver expirado, a verificação será aprovada.
  2. Se a primeira etapa da verificação falhar, mas o certificado HttpClienttiver sido SecurityContextadicionado à cadeia confiável de certificados quando foi criado, se o certificado retornado pelo servidor estiver na cadeia confiável, a verificação será aprovada.
  3. Se ambas as verificações 1 e 2 falharem, se o usuário fornecer badCertificateCallbackum callback, ele será chamado, se o callback retornar true, a conexão poderá continuar, se for retornado false, a conexão será encerrada.

Resumindo, nossa verificação de certificado é, na verdade, para fornecer um badCertificateCallbackretorno de chamada, que será ilustrado por um exemplo abaixo.

exemplo

Assumindo que nosso serviço em segundo plano usa um certificado autoassinado, o formato do certificado é PEMformat e salvamos o conteúdo do certificado em uma string local, então nossa lógica de verificação é a seguinte:

String PEM = "XXXXX";//可以从文件读取
...
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
    
    
  if(cert.pem==PEM){
    
    
    return true; //证书一致,则允许发送数据
  }
  return false;
};

X509CertificateÉ o formato padrão do certificado, que contém todas as informações do certificado, exceto a chave privada, e você mesmo pode verificar o documento. Além disso, o exemplo acima não possui verificação host, pois desde que o conteúdo do certificado devolvido pelo servidor seja consistente com o armazenamento local, ele já pode comprovar que é nosso servidor (não um intermediário) host. evitar a incompatibilidade entre o certificado e o nome de domínio.

Para um certificado autoassinado, também podemos adicioná-lo à cadeia de confiança do certificado local, para que o certificado passe automaticamente pela verificação, em vez de ir para o badCertificateCallbackretorno de chamada:

SecurityContext sc = SecurityContext();
// file为证书路径
sc.setTrustedCertificates(file);
// 创建一个HttpClient
HttpClient httpClient = HttpClient(context: sc);

Observe que setTrustedCertificates()o formato do certificado definido deve ser PEMou PKCS12, se o formato do certificado for PKCS12, a senha do certificado precisa ser passada, o que exporá a senha do certificado no código, portanto, não é recomendável usar PKCS12o certificado no formato da verificação do certificado do cliente.

biblioteca de solicitação Http-dio

HttpClientDescobrimos que é complicado usar diretamente para iniciar solicitações de rede. Muitas coisas precisam ser tratadas manualmente. Se envolver upload/download de arquivos, Cookiegerenciamento etc., será muito complicado. Felizmente, existem algumas bibliotecas de solicitação de terceiros httpna comunidade Dart, e usá-las para iniciar httpsolicitações será muito mais simples. Deixe-me apresentar a atualmente popular biblioteca dio .

Endereço do documento chinês Dio : clique aqui

Apresente o Dio:

dependencies:
  dio: ^5.1.2 #请使用pub上的最新版本

Importe e crie uma instância de dio:

import 'package:dio/dio.dart';
Dio dio =  Dio();

Em seguida, você pode dioiniciar uma solicitação de rede por meio da instância. Observe que uma dioinstância pode iniciar várias httpsolicitações. De um modo geral, httpquando o APP possui apenas uma fonte de dados, dioo modo singleton deve ser usado.

Iniciar uma solicitação por meio de dio

Inicie GETuma solicitação:

Response response;
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());

Para GETa requisição podemos querypassar os parâmetros através do objeto, o código acima é equivalente a:

response = await dio.get("/test", queryParameters: {
    
    "id":12,"name":"wendu"})
print(response);

Faça um POSTpedido:

response = await dio.post("/test",data:{
    
    "id":12,"name":"wendu"})

Inicie várias solicitações simultâneas:

response = await Future.wait([dio.post("/info"), dio.get("/token")]);

⇬ Fazer download do arquivo:

response = await dio.download("https://www.google.com/",_savePath);

enviar FormData:

FormData formData = FormData.from({
    
    
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)

Se os dados enviados forem FormData, o solicitado dioserá headerdefinido contentTypecomo " multipart/form-data".

Ao FormDatafazer upload de vários arquivos:

FormData formData = FormData.from({
    
    
   "name": "wendux",
   "age": 25,
   "file1": UploadFileInfo(File("./upload.txt"), "upload1.txt"),
   "file2": UploadFileInfo(File("./upload.txt"), "upload2.txt"),
     // 支持文件数组上传
   "files": [
      UploadFileInfo(File("./example/upload.txt"), "upload.txt"),
      UploadFileInfo(File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData)

Vale ressaltar que a requisição iniciada dioainda é utilizada internamente , então o proxy, autenticação de requisição, verificação de certificado, etc. são os mesmos, e podemos defini-los no callback, por exemplo:HttpClientHttpClientonHttpClientCreate

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    
    
    //设置代理 
    client.findProxy = (uri) {
    
    
      return "PROXY 192.168.1.2:8888";
    };
    //校验证书
    httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
    
    
      if(cert.pem==PEM){
    
    
      return true; //证书一致,则允许发送数据
     }
     return false;
    };   
  };

Observe que onHttpClientCreateele será chamado quando a dioinstância atual precisar ser criada HttpClient, portanto, essa configuração de retorno de chamada HttpClientterá dioefeito em toda a instância. Se o aplicativo exigir várias estratégias de verificação de proxy ou certificado, diferentes dioinstâncias podem ser criadas para implementá-las separadamente.

Além desses usos básicos, dioele também suporta configuração de solicitação, interceptadores, etc., e suas funções são muito ricas. Para obter detalhes, consulte a documentação oficial do dio.

Exemplo : solicitamos todos os projetos públicos de código aberto da organização flutterchina por meio da API aberta do Github

  1. carregamento de pop-up durante a fase de solicitação
  2. Após o término da solicitação, se a solicitação falhar, uma mensagem de erro será exibida; se for bem-sucedida, uma lista de nomes de projetos será exibida.

código mostra como abaixo:

class _FutureBuilderRouteState extends State<FutureBuilderRoute> {
    
    
  Dio _dio = Dio();

  
  Widget build(BuildContext context) {
    
    

    return Container(
      alignment: Alignment.center,
      child: FutureBuilder(
          future: _dio.get("https://api.github.com/orgs/flutterchina/repos"),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
    
    
            //请求完成
            if (snapshot.connectionState == ConnectionState.done) {
    
    
              Response response = snapshot.data;
              //发生错误
              if (snapshot.hasError) {
    
    
                return Text(snapshot.error.toString());
              }
              //请求成功,通过项目信息构建用于显示项目名称的ListView
              return ListView(
                children: response.data.map<Widget>((e) =>
                    ListTile(title: Text(e["full_name"]))
                ).toList(),
              );
            }
            //请求未完成时弹出loading
            return CircularProgressIndicator();
          }
      ),
    );
  }
}

Exemplo: download em partes Http

Princípio de download do bloco HTTP

HttpO protocolo define o headercampo de resposta para transmissão em partes, mas se ele oferece suporte depende da Serverimplementação. Podemos especificar o rangecampo " " do cabeçalho da solicitação para verificar se o servidor oferece suporte à transmissão em partes. Por exemplo, podemos usar curlo comando para verificar:

bogon:~ duwen$ curl -H "Range: bytes=0-10" http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg -v
# 请求头
> GET /HBuilder.9.0.2.macosx_64.dmg HTTP/1.1
> Host: download.dcloud.net.cn
> User-Agent: curl/7.54.0
> Accept: */*
> Range: bytes=0-10
# 响应头
< HTTP/1.1 206 Partial Content
< Content-Type: application/octet-stream
< Content-Length: 11
< Connection: keep-alive
< Date: Thu, 21 Feb 2019 06:25:15 GMT
< Content-Range: bytes 0-10/233295878

A função de adicionar " " no cabeçalho da requisição Range: bytes=0-10é informar ao servidor que queremos apenas obter o conteúdo do arquivo 0-10(incluindo 10o total 11de bytes) nesta requisição. Se o servidor suporta transmissão em partes, o código de status da resposta é 206, que significa "conteúdo parcial", e ao mesmo tempo, o cabeçalho da resposta contém o Content-Rangecampo " ". Se não for compatível, não será incluído. Vejamos o conteúdo de " " acima Content-Range:

Content-Range: bytes 0-10/233295878

0-10Indica o bloco retornado desta vez, 233295878representando o comprimento total do arquivo, e a unidade é byte, ou seja, o arquivo provavelmente é 233Mum pouco maior.

Com base nisso, podemos projetar um simples downloader de bloco de arquivo multiencadeado. A ideia de implementação é:

  1. Primeiro, verifique se a transmissão em partes é suportada, caso contrário, faça o download diretamente; se for compatível, baixe o conteúdo restante em partes.
  2. Cada parte é salva em seu próprio arquivo temporário durante o download, e o arquivo temporário é mesclado após o download de todas as partes.
  3. Excluir arquivos temporários.

O seguinte é o processo geral:

// 通过第一个分块请求检测服务器是否支持分块传输  
Response response = await downloadChunk(url, 0, firstChunkSize, 0);
if (response.statusCode == 206) {
    
        //如果支持
    //解析文件总长度,进而算出剩余长度
    total = int.parse(
        response.headers.value(HttpHeaders.contentRangeHeader).split("/").last);
    int reserved = total -
        int.parse(response.headers.value(HttpHeaders.contentLengthHeader));
    //文件的总块数(包括第一块)
    int chunk = (reserved / firstChunkSize).ceil() + 1;
    if (chunk > 1) {
    
    
        int chunkSize = firstChunkSize;
        if (chunk > maxChunk + 1) {
    
    
            chunk = maxChunk + 1;
            chunkSize = (reserved / maxChunk).ceil();
        }
        var futures = <Future>[];
        for (int i = 0; i < maxChunk; ++i) {
    
    
            int start = firstChunkSize + i * chunkSize;
            //分块下载剩余文件  
            futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
        }
        //等待所有分块全部下载完成
        await Future.wait(futures);
    }
    //合并文件文件  
    await mergeTempFiles(chunk);
}

Abaixo usamos dioa downloadimplementação da API downloadChunk:

//start 代表当前块的起始位置,end代表结束位置
//no 代表当前是第几块
Future<Response> downloadChunk(url, start, end, no) async {
    
    
  progress.add(0); //progress记录每一块已接收数据的长度
  --end;
  return dio.download(
    url,
    savePath + "temp$no", //临时文件按照块的序号命名,方便最后合并
    onReceiveProgress: createCallback(no), // 创建进度回调,后面实现
    options: Options(
      headers: {
    
    "range": "bytes=$start-$end"}, //指定请求的内容区间
    ),
  );
}

Próximo implemento mergeTempFiles:

Future mergeTempFiles(chunk) async {
    
    
  File f = File(savePath + "temp0");
  IOSink ioSink= f.openWrite(mode: FileMode.writeOnlyAppend);
  //合并临时文件  
  for (int i = 1; i < chunk; ++i) {
    
    
    File _f = File(savePath + "temp$i");
    await ioSink.addStream(_f.openRead());
    await _f.delete(); //删除临时文件
  }
  await ioSink.close();
  await f.rename(savePath); //合并后的文件重命名为真正的名称
}

Vamos dar uma olhada na implementação completa:

Future downloadWithChunks(
  url,
  savePath, {
    
    
  ProgressCallback onReceiveProgress,
}) async {
    
    
  const firstChunkSize = 102;
  const maxChunk = 3;

  int total = 0;
  var dio = Dio();
  var progress = <int>[];

  createCallback(no) {
    
    
    return (int received, _) {
    
    
      progress[no] = received;
      if (onReceiveProgress != null && total != 0) {
    
    
        onReceiveProgress(progress.reduce((a, b) => a + b), total);
      }
    };
  }

  Future<Response> downloadChunk(url, start, end, no) async {
    
    
    progress.add(0);
    --end;
    return dio.download(
      url,
      savePath + "temp$no",
      onReceiveProgress: createCallback(no),
      options: Options(
        headers: {
    
    "range": "bytes=$start-$end"},
      ),
    );
  }

  Future mergeTempFiles(chunk) async {
    
    
    File f = File(savePath + "temp0");
    IOSink ioSink= f.openWrite(mode: FileMode.writeOnlyAppend);
    for (int i = 1; i < chunk; ++i) {
    
    
      File _f = File(savePath + "temp$i");
      await ioSink.addStream(_f.openRead());
      await _f.delete();
    }
    await ioSink.close();
    await f.rename(savePath);
  }

  Response response = await downloadChunk(url, 0, firstChunkSize, 0);
  if (response.statusCode == 206) {
    
    
    total = int.parse(
        response.headers.value(HttpHeaders.contentRangeHeader).split("/").last);
    int reserved = total -
        int.parse(response.headers.value(HttpHeaders.contentLengthHeader));
    int chunk = (reserved / firstChunkSize).ceil() + 1;
    if (chunk > 1) {
    
    
      int chunkSize = firstChunkSize;
      if (chunk > maxChunk + 1) {
    
    
        chunk = maxChunk + 1;
        chunkSize = (reserved / maxChunk).ceil();
      }
      var futures = <Future>[];
      for (int i = 0; i < maxChunk; ++i) {
    
    
        int start = firstChunkSize + i * chunkSize;
        futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
      }
      await Future.wait(futures);
    }
    await mergeTempFiles(chunk);
  }
}

O download em partes está pronto:

main() async {
    
    
  var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg";
  var savePath = "./example/HBuilder.9.0.2.macosx_64.dmg";
  await downloadWithChunks(url, savePath, onReceiveProgress: (received, total) {
    
    
    if (total != -1) {
    
    
      print("${
      
      (received / total * 100).floor()}%");
    }
  });
}

pensar

1. O download em partes pode realmente melhorar a velocidade de download?

  • Na verdade, o principal gargalo da velocidade de download depende da velocidade da rede e da velocidade de exportação do servidor. Se for a mesma fonte de dados, o significado do download em blocos não é grande, porque o servidor é o mesmo e o a velocidade de exportação é determinada, depende principalmente da velocidade da rede. O exemplo acima é o download do bloco da mesma fonte, você pode comparar a velocidade de download do bloco e não do bloco. Se houver várias fontes de download e a largura de banda de exportação de cada fonte de download for limitada, o download em partes pode ser mais rápido neste momento. A razão para dizer "possível" é que isso não é certo, por exemplo, há três fontes , a largura de banda de exportação das três fontes é de 1 Gb/s e o valor de pico da rede conectada ao nosso equipamento é considerado de apenas 800 Mb/s, então o gargalo está em nossa rede. Mesmo que a largura de banda do nosso dispositivo seja maior do que qualquer fonte, a velocidade de download ainda não é necessariamente mais rápida do que um download de linha única de fonte única. Imagine, suponha que haja duas fontes A e B e a velocidade da fonte A é 3 vezes o da fonte B. Se você usar download em bloco , se baixar metade das duas fontes, poderá calcular o tempo de download necessário e, em seguida, calcular o tempo necessário para baixar apenas da fonte A para ver qual é mais rápido.

  • A velocidade final do download em blocos é afetada por muitos fatores, como a largura de banda da rede do dispositivo, a velocidade da fonte e da saída, o tamanho de cada bloco e o número de blocos. É difícil garantir a velocidade ideal em o processo real. No desenvolvimento real, você pode testar e comparar antes de decidir se deve usá-lo.

2. Existe algum uso prático para baixar em pedaços?

  • Outro cenário útil para o download do bloco é a retomada do ponto de interrupção, que pode dividir o arquivo em vários blocos e, em seguida, manter um arquivo de status do download para registrar o status de cada bloco, para que, mesmo após a interrupção da rede, ele possa ser restaurado. a interrupção, a implementação específica pode ser tentada por você mesmo, ou há alguns detalhes que precisam de atenção especial, como qual é o tamanho de bloco apropriado? E os blocos baixados pela metade? Deseja manter uma fila de tarefas?

Usar WebSockets

HttpO protocolo é sem estado e só pode ser iniciado pelo cliente, e o servidor responde passivamente. O servidor não pode enviar conteúdo ativamente para o cliente e, uma vez que o servidor responda, o link será desconectado, portanto a comunicação em tempo real não poderá ser realizada. WebSocketO protocolo é uma tecnologia criada para resolver a comunicação em tempo real entre o cliente e o servidor. Agora é suportado pelos navegadores convencionais, por isso deve ser familiar aos desenvolvedores da Web. O Flutter também fornece um pacote especial para suportar o protocolo WebSocket.

Nota: HttpEmbora o protocolo possa usar keep-aliveum mecanismo para manter o servidor conectado por um período de tempo após o término da resposta, ele eventualmente será desconectado. O keep-alivemecanismo é usado principalmente para evitar a criação frequente de conexões quando o mesmo servidor solicita vários recursos É essencialmente uma tecnologia que suporta multiplexação de link, não para comunicação em tempo real, observe a diferença entre os dois.

WebSocketO protocolo é essencialmente um TCPprotocolo baseado no protocolo. Ele primeiro HTTPinicia uma httpsolicitação especial por meio do protocolo para handshake. Se o servidor suportar WebSocketo protocolo, ele atualizará o protocolo. WebSocketSerá utilizado o link httpcriado após o handshake do protocolo , ao contrário do protocolo, o link é um link longo (não será quebrado), portanto servidor e cliente podem se comunicar em tempo real através desta conexão.TCPhttpWebSocketTCPTCP

Vamos nos concentrar em como usá-lo no Flutter WebSocket.

etapas de comunicação

O uso WebSocketda comunicação é dividido em 4 etapas:

  1. Conecte-se a um servidor WebSocket.
  2. Ouça as mensagens do servidor.
  3. Enviar dados para o servidor.
  4. Feche a conexão WebSocket.

1. Conecte-se ao servidor WebSocket

O pacote web_socket_channel fornece as ferramentas que precisamos para nos conectar a um servidor WebSocket. Este pacote fornece um WebSocketChannelmétodo que nos permite ouvir mensagens do servidor e enviar mensagens para o servidor.

No Flutter, podemos criar uma WebSocketChannelconexão com um servidor:

final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');

2. Ouça as mensagens do servidor

Agora que temos uma conexão, podemos ouvir as mensagens do servidor, que retornarão as mensagens depois de enviá-las ao servidor de teste. Como recebemos mensagens e as exibimos? Neste exemplo, usaremos a StreamBuilderpara ouvir novas mensagens e a Textpara exibi-las.

StreamBuilder(
  stream: widget.channel.stream,
  builder: (context, snapshot) {
    
    
    return Text(snapshot.hasData ? '${
      
      snapshot.data}' : '');
  },
);

WebSocketChannelForneceu uma mensagem do servidor Stream. Esta classeStream é dart:asyncuma classe base no pacote. Ele fornece uma maneira de escutar eventos assíncronos de fontes de dados. Em vez de Futureretornar uma única resposta assíncrona, Streamuma classe pode entregar muitos eventos ao longo do tempo. O StreamBuildercomponente se conectará a um Streame dirá ao Flutter para reconstruir a interface toda vez que uma mensagem for recebida.

3. Envie dados para o servidor

Para enviar dados ao servidor, enviamos adduma mensagem ao WebSocketChannelfornecido sink.

channel.sink.add('Hello!');

WebSocketChannelUm é fornecido StreamSink, que envia a mensagem para o servidor.

StreamSinkA classe fornece métodos gerais para adicionar eventos a fontes de dados de forma síncrona ou assíncrona.

4. Feche a conexão WebSocket

Depois de usarmos o WebSocket, para fechar a conexão:

channel.sink.close();

A seguir, um exemplo completo que demonstra o processo de comunicação do WebSocket.

import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';

class WebSocketRoute extends StatefulWidget {
    
    
  
  _WebSocketRouteState createState() => _WebSocketRouteState();
}

class _WebSocketRouteState extends State<WebSocketRoute> {
    
    
  TextEditingController _controller = TextEditingController();
  IOWebSocketChannel channel;
  String _text = "";


  
  void initState() {
    
    
    //创建websocket连接
    channel = IOWebSocketChannel.connect('ws://echo.websocket.org');
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: Text("WebSocket(内容回显)"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: InputDecoration(labelText: 'Send a message'),
              ),
            ),
            StreamBuilder(
              stream: channel.stream,
              builder: (context, snapshot) {
    
    
                //网络不通会走到这
                if (snapshot.hasError) {
    
    
                  _text = "网络不通...";
                } else if (snapshot.hasData) {
    
    
                  _text = "echo: "+snapshot.data;
                }
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 24.0),
                  child: Text(_text),
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: Icon(Icons.send),
      ),
    );
  }

  void _sendMessage() {
    
    
    if (_controller.text.isNotEmpty) {
    
    
      channel.sink.add(_controller.text);
    }
  }

  
  void dispose() {
    
    
    channel.sink.close();
    super.dispose();
  }
}

WebSocketVamos pensar em uma pergunta agora, o que devemos fazer se quisermos transmitir dados binários (por exemplo, para receber uma imagem do servidor)? Constatamos StreamBuilderque Streamnão existe um parâmetro especificando o tipo de recebimento, e WebSocketnão existe uma configuração correspondente na hora de criar o link. Parece que não tem jeito... Na verdade é bem simples. Ainda é usado para receber dados binários StreamBuilder, porque WebSockettodos os dados enviados são enviados na forma de quadros. , e o quadro tem um formato fixo, e o tipo de dados de cada quadro pode ser Opcodeespecificado pelo campo, que pode especificar se o quadro atual é um tipo de texto ou um tipo binário (e outros tipos), então o cliente já sabe quando recebe o quadro Seu tipo de dados, então flutter pode analisar completamente o tipo correto depois de receber os dados, então não há necessidade de desenvolvedores se preocuparem com isso. os dados transmitidos pelo servidor são especificados como binários, o tipo é, e quando é text StreamBuilder, snapshot.dataé List<int>.String

Use a API de soquete

SocketAPI é um conjunto de API básico e padrão fornecido pelo sistema operacional para implementar o protocolo de rede da camada de aplicação , que é um encapsulamento do protocolo de rede da camada de transporte (principalmente TCP/UDP). A API Socket implementa a API básica para estabelecer conexões ponta a ponta e enviar/receber dados, enquanto Socketas APIs em linguagens de programação de alto nível são, na verdade, Socketum encapsulamento das APIs do sistema operacional.

HttpOs protocolos e protocolos que apresentamos antes WebSocketpertencem ao protocolo da camada de aplicação.AlémSocket deles , existem muitos outros protocolos da camada de aplicação, como: SMTP, FTP, etc.

Resumindo, se precisamos personalizar o protocolo ou queremos controlar e gerenciar diretamente os links de rede, ou se sentimos que o integrado não é HttpClientfácil de usar e queremos reimplementá-lo, precisamos usá-lo Socket. A API do Flutter Socketestá no dart:iopacote, vamos ver um exemplo de uso Socketpara implementar uma httprequisição simples.

Use o Socket para implementar a solicitação Http Get

Considere a solicitação para a página inicial do Baidu como exemplo:

class SocketRoute extends StatelessWidget {
    
    
  const SocketRoute({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return FutureBuilder(
      future: _request(),
      builder: (context, snapShot) {
    
    
        return Text(snapShot.data.toString());
      },
    );
  }

  _request() async {
    
    
    //建立连接
    var socket = await Socket.connect("baidu.com", 80);
    //根据http协议,发起 Get请求头
    socket.writeln("GET / HTTP/1.1");
    socket.writeln("Host:baidu.com");
    socket.writeln("Connection:close");
    socket.writeln();
    await socket.flush(); //发送
    //读取返回内容,按照utf8解码为字符串
    String _response = await utf8.decoder.bind(socket).join();
    await socket.close();
    return _response;
  }
}

Pode-se ver que Socketprecisamos implementar o protocolo nós mesmos Http(precisamos implementar o processo de comunicação com o servidor por nós mesmos). Este exemplo é apenas um exemplo simples, sem redirecionamento de processamento, cookies, etc., e alguns cabeçalhos de solicitação podem ser adicionados de acordo com as necessidades reais.

JSON para a classe Dart Model

No combate real, a interface em segundo plano geralmente retorna alguns dados estruturados, como JSON, XMLetc. Por exemplo, no exemplo anterior, onde solicitamos a API do Github, os dados retornados são JSONuma string no formato. Para facilitar nossas operações em o código JSON, primeiro JSONformatamos o Converter uma string em Dartum objeto, que pode ser obtido por meio do decodificador dart:convertintegrado , que pode convertê-lo em ou de acordo com o conteúdo específico da string , para que possamos usá-los para encontrar o necessário valor, como:JSONjson.decode()JSONListMap

//一个JSON格式的用户列表字符串
String jsonStr ='[{"name":"Jack"},{"name":"Rose"}]';`
//将JSON字符串转为Dart对象(此处是List)
List items = json.decode(jsonStr);
//输出第一个用户的姓名
print(items[0]["name"]);

O método de json.decode()conversão JSONde uma string List/Mapé relativamente simples, não possui dependências externas ou outras configurações e é muito conveniente para pequenos projetos. Mas quando o projeto se torna maior, essa lógica de serialização manual pode se tornar incontrolável e propensa a erros, por exemplo, da seguinte forma JSON:

{
    
    
  "name": "John Smith",
  "email": "[email protected]"
}

Podemos json.decodedecodificá-lo chamando um método JSONcom JSONuma string como argumento:

Map<String, dynamic> user = json.decode(json);

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

Como json.decode()apenas um é retornado Map<String, dynamic>, isso significa que não sabemos o tipo do valor até o tempo de execução. Com essa abordagem, perdemos a maioria dos recursos de linguagem tipada estaticamente: segurança de tipo, preenchimento automático e, o mais importante, exceções de tempo de compilação. Como resultado, nosso código pode se tornar muito propenso a erros. Por exemplo, quando acessamos o campo nameou email, digitamos muito rapidamente, fazendo com que o nome do campo seja digitado errado. Mas como isso JSONestá mapna estrutura, o compilador não sabe o nome do campo errado, então nenhum erro será reportado ao compilar.

Na verdade, esse problema será encontrado em muitas plataformas e há muito tempo existe uma boa solução, a saber, " Json Model化". O método específico é pré-definir algumas classes Jsoncorrespondentes à estrutura Modele, em seguida, dinamicamente de acordo com os dados após solicitar o dados. Crie Modeluma instância da classe. Desta forma, na fase de desenvolvimento, utilizamos Modeluma instância da classe ao invés de uma instância da classe, Map/Listpara que não haja erros de digitação ao acessar as propriedades internas.

1. Escreva manualmente a classe Dart Model

UserPor exemplo, podemos resolver o problema mencionado anteriormente introduzindo uma classe de modelo simples . Dentro Userda classe temos:

  • Um User.fromJsonconstrutor nomeado que constrói mapuma Userinstância de um struct map.
  • Um toJsonmétodo que converte Useruma instância em uma map.

user.dartO conteúdo é o seguinte:

class User {
    
    
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() =>
    <String, dynamic>{
    
    
      'name': name,
      'email': email,
    };
}

Dessa forma, o código de chamada agora pode ter segurança de tipo, campos de preenchimento automático ( namee email) e exceções de tempo de compilação. Se, em vez disso, tratássemos campos com erros ortográficos como inttipos String, nosso código falharia ao compilar em vez de travar no tempo de execução.

Agora a lógica de serialização é movida para dentro do próprio modelo. Com essa nova abordagem, podemos desserializar com muita facilidade user:

Map userMap = json.decode(json);
var user = User.fromJson(userMap);

print('Howdy, ${
      
      user.name}!');
print('We sent the verification link to ${
      
      user.email}.');

Se quisermos serializar um user, basta Userpassar o objeto para o json.encodemétodo. Não precisamos chamar toJsoneste método manualmente, pois JSON.encodeele será chamado automaticamente internamente.

String json = json.encode(user);

Dessa forma, o código de chamada não precisa se preocupar com JSONa serialização, mas Modela classe ainda é necessária. Na prática, User.fromJsonambos os User.toJsonmétodos e precisam de testes de unidade para verificar o comportamento correto.

Além disso, no mundo real JSON, objetos raramente são tão simples, JSONobjetos aninhados não são incomuns, e seria muito bom se houvesse algo que lide automaticamente com JSONa serialização para nós. Felizmente, existe!

2. Gere automaticamente classes de modelo Dart

Vamos apresentar o pacote json_serializable oficialmente recomendado . É um gerador de código-fonte automatizado que pode gerar modelos de serialização JSON para nós durante a fase de desenvolvimento. Dessa forma, como o código de serialização não é mais escrito à mão e mantido por nós, reduzimos ao mínimo o risco de exceções de serialização JSON em tempo de execução. .

1) Defina json_serializable no projeto

Para incluir json_serializableem nosso projeto, precisamos de uma dependência regular e duas de desenvolvimento . Resumindo, as dependências de desenvolvimento são dependências que não estão incluídas no código-fonte da nossa aplicação, são algumas ferramentas e scripts auxiliares no processo de desenvolvimento, semelhantes às dependências de desenvolvimento em node.

No pubspec.yamladd:

dependencies:
  json_annotation: <最新版本>

dev_dependencies:
  build_runner: <最新版本>
  json_serializable: <最新版本>

Execute flutter packages get (ou clique em "Packages Get" no editor) na pasta raiz do projeto para usar essas novas dependências no projeto.

2) Crie uma classe de modelo na forma de json_serializable

Vamos ver como converter nossa Userclasse em uma json_serializable. Para simplificar, usamos o modelo JSON simplificado do exemplo anterior.

usuário.dart

import 'package:json_annotation/json_annotation.dart';

// user.g.dart 将在我们运行生成命令后自动生成
part 'user.g.dart';

///这个标注是告诉生成器,这个类是需要生成Model类的
()

class User{
    
    
  User(this.name, this.email);

  String name;
  String email;
  
  ///在Terminal下面执行:flutter packages pub run build_runner build (只构建一次)
  ///             或者:flutter packages pub run build_runner watch (持续在后台监听文件变化并构建生成必要文件)
  ///通过上面命令之后就会生成user.g.dart文件,文件内容就是下面两个反序列化和序列化方法的实现代码
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);  
}

Com a configuração acima, o gerador de código-fonte gerará código JSON para serialização namee campos.email

Também é fácil personalizar a estratégia de nomenclatura, se desejar. Por exemplo, se a API que estamos usando retorna snake_caseum objeto com , mas queremos usá-lo em nosso modelo lowerCamelCase, podemos usar @JsonKeya anotação:

//显式关联JSON字段名与Model属性的对应关系 
(name: 'registration_date_millis')
final int registrationDateMillis;

3) Execute o gerador de código

json_serializableAo criar uma classe pela primeira vez, você verá um erro semelhante à imagem abaixo.

insira a descrição da imagem aqui
Esses erros são completamente normais e isso ocorre porque o código gerado para a classe Model ainda não existe. Para corrigir isso, temos que executar o gerador de código para gerar o modelo de serialização para nós.

Existem duas maneiras de executar o gerador de código:

  • geração única

Executando no diretório raiz do nosso projeto:

flutter packages pub run build_runner build

ModelIsso aciona uma compilação única e podemos gerar código de serialização para nosso quando necessário json, que percorre nossos arquivos de origem e encontra Modelos arquivos de origem que precisam gerar classes (contendo @JsonSerializableas anotações) para gerar os .g.dartarquivos correspondentes. Uma boa sugestão é Modelcolocar todas as classes em um diretório separado e executar os comandos desse diretório.

Embora isso seja muito conveniente, seria bom se não tivéssemos que executar manualmente o comando build toda vez que fizermos uma alteração na classe Model.

  • geração contínua

O uso watcherpode tornar nosso processo de geração de código-fonte mais conveniente. Ele observará as alterações nos arquivos em nosso projeto e criará automaticamente os arquivos necessários quando necessário.Podemos flutter packages pub run build_runner watchiniciar o _watcher_ executando no diretório raiz do projeto. Basta iniciar o observador uma vez e ele será executado em segundo plano, com segurança.

3. Converta JSON em classe dart por meio de comandos

Um dos maiores problemas com o método acima é escrever um modelo para cada json, o que é bastante chato. Se houver uma ferramenta que possa gerar modelos diretamente do texto JSON, podemos liberar completamente nossas mãos.

Felizmente, já existe um pacote Json_model completo com função , que possui funções flexíveis de configuração e personalização. Depois que os desenvolvedores adicionam o pacote às dependências de desenvolvimento, eles podem usar um comando para gerar classes de acordo com Jsono arquivo Dart.

usar

  1. jsonsCrie um diretório chamado " " no diretório raiz do projeto ;
  2. Crie ou copie o arquivo Json para o jsonsdiretório " ";
  3. Execute pub run json_modeo comando l (Dart VM project) ou flutter packages pub run json_model(no Flutter) para gerar a classe de modelo Dart e os arquivos gerados estarão no lib/modelsdiretório " " por padrão

Por exemplo, o arquivo JSON é o seguinte:

{
    
    
  "@meta": {
    
     // @meta 可以定制单个 json 的生成规则,默认使用全局配置
    "import": [
      "test_dir/profile.dart" // 导入其他文件
    ],
    "comments": {
    
    
      "name": "名字" // 给 "name" 字段添加注释
    },
    "nullable": false, // 字段默认非可空,会生成 late 
    "ignore": false // 是否跳过当前 JSON 的 model 类生成
  },
  "@JsonKey(ignore: true) Profile?": "profile",
  "@JsonKey(name: '+1') int?": "loved",
  "name": "wendux",
  "father": "$user",
  "friends": "$[]user",
  "keywords": "$[]String",
  "age?": 20 // 指定 age 字段可空
}

Então a classe Model gerada é a seguinte:

import 'package:json_annotation/json_annotation.dart';
import 'test_dir/profile.dart';
part 'user.g.dart';

()
class User {
    
    
  User();

  (ignore: true) Profile? profile;
  (name: '+1') int? loved;
  //名字
  late String name;
  late User father;
  late List<User> friends;
  late List<String> keywords;
  num? age;
  
  factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Nota: Para mais instruções de uso, você pode verificar sua documentação oficial . No entanto, o autor da biblioteca não a atualiza há muito tempo. Pode haver alguns problemas ou compatibilidade. Se você tiver algum problema, verifique sua lista de problemas . Muitos internautas irão compartilhar alguns baseados nesta biblioteca. Versões otimizadas como json_to_model , json5_model , json2model, etc.

4. Use plug-ins IDE para gerar classes de modelo

Se você acha que os métodos acima são incômodos, a versão mais preguiçosa é instalar um plug-in IDE para gerá-lo automaticamente para você. Por exemplo, podemos pesquisar a palavra-chave "Json to Dart" em Configuração->Plugins de Android Studio para encontrá-lo. Vários plugins semelhantes:

insira a descrição da imagem aqui

Basta escolher um plugin que você goste de instalar, pois as funções são parecidas. O mesmo vale para usuários que utilizam o Visual Studio Code, basta pesquisar no mercado de plug-ins.

Geralmente, as teclas de atalho serão fornecidas após a instalação ou uma caixa de entrada de texto pode ser aberta na barra de ferramentas, menu do botão direito, etc., e então o texto JSON pode ser colado para gerar automaticamente a classe Dart Model. Semelhante ao plug-in GsonFormat comumente usado no Android antes.

Embora seja muito simples, ainda precisamos entender as vantagens e desvantagens dos plug-ins IDE e da geração de linha de comando Json_model:

  • Json_model precisa manter uma pasta separada para armazenar arquivos Json. Se houver alguma alteração, você só precisa modificar o arquivo Json para gerar novamente a classe Model; e os plug-ins IDE geralmente exigem que os usuários copiem e copiem manualmente o conteúdo Json para um caixa de entrada, para que depois de gerar Json O arquivo não seja arquivado e precise ser alterado manualmente posteriormente.
  • Json_model pode especificar manualmente outras classes de modelo referenciadas por um campo, o que pode evitar a geração de classes duplicadas; e os plug-ins IDE geralmente geram uma classe de modelo para todos os objetos aninhados em cada arquivo Json, mesmo que esses objetos aninhados possam estar em outra classe de modelo que foi gerada .
  • Json_model fornece um método de conversão de linha de comando, que pode ser facilmente integrado em cenários de ambiente não UI, como CI.

5. Gere classes modelo online

Se você estiver com preguiça de instalar o plug-in, use o on-line. Aqui estão dois sites que podem converter Json em Dart Model on-line:

FAQ : Muitas pessoas podem perguntar se existe uma biblioteca de serialização Json como Gson/Jackson no desenvolvimento Java no Flutter?

  • A resposta é não! Porque tais bibliotecas requerem o uso de reflexão em tempo de execução, que está desabilitada no Flutter . tree shakingA reflexão do tempo de execução interfere no uso do Dart de tree shaking, que pode "retirar" o código não utilizadorelease em versões , o que pode otimizar significativamente o tamanho do seu aplicativo. Como a reflexão será aplicada a todo o código por padrão, será difícil trabalhar, porque é difícil saber qual código não é usado quando a reflexão está habilitada, então o código redundante é difícil de remover, então a função de reflexão do Dart está desabilitada no Flutter , e porque Dessa forma, a função de transformar dinamicamente o Modelo não pode ser realizada.tree shaking

Pacotes e Plugins

Pacotes em Flutter

Um pacote mínimo inclui:

  • Um pubspec.yamlarquivo: Um arquivo de metadados que declara o nome, versão, autor, etc. do Pacote.
  • Uma libpasta: incluindo o código público no pacote, deve haver pelo menos um <package-name>.dartarquivo

Os pacotes Flutter se enquadram em duas categorias:

  • Pacotes Dart: alguns deles podem conter funcionalidades específicas do Flutter e, portanto, ter uma dependência da estrutura do Flutter; esses pacotes são apenas para o Flutter, como o pacote fluro .
  • Pacote de plug-in: um pacote Dart dedicado que contém uma API escrita em código Dart, bem como implementações específicas da plataforma para Android (usando Java ou Kotlin) e para iOS (usando OC ou Swift), o que significa que os plug-ins incluem código nativo, por exemplo, o pacote de plugins battery_plus .

Desenvolvimento de plug-in do Flutter

O Flutter é essencialmente apenas uma estrutura de interface do usuário executada na plataforma host. O próprio Flutter não pode fornecer alguns recursos do sistema, como o uso de Bluetooth, câmera, GPS etc. plataforma. Para chamar uma API de plataforma específica, você precisa escrever um plug-in. O plug-in é um pacote especial e a principal diferença do pacote dart puro é que, além do código dart, o plug-in também inclui código específico da plataforma. Por exemplo, o plug-in image_picker pode acessar a foto álbum e câmera em dispositivos iOS e Android.

Para o processo específico de desenvolvimento de plug-in Flutter e o mecanismo de comunicação entre Flutter e Native , consulte minha nota anterior Flutter Native plug-in development (Android) .

Como obter informações da plataforma

Muito simples, o Flutter disponibiliza uma variável global defaultTargetPlatformpara obter as informações da plataforma da aplicação atual, defaultTargetPlatformdefinida em " platform.dart", seu tipo é TargetPlatform, trata-se de uma classe de enumeração, definida da seguinte forma:

enum TargetPlatform {
    
    
  android,
  fuchsia,
  iOS,
  linux, 
  macOS, 
  windows,
}

Às vezes queremos adicionar algumas funções diferenciadas de acordo com a plataforma host, podemos julgar a plataforma através do seguinte código:

if (defaultTargetPlatform == TargetPlatform.android) {
    
    
    // 是安卓系统,do something
    ...
} else {
    
    
	...
}

Como diferentes plataformas possuem suas próprias especificações de interação, alguns componentes da biblioteca Flutter Material fizeram algumas adaptações para as plataformas correspondentes, como o componente de roteamento, que MaterialPageRouteaplicará as animações de comutação das respectivas especificações de plataforma no android e ios. Portanto, se quisermos que nosso aplicativo se comporte de maneira consistente em todas as plataformas, por exemplo, o que devemos fazer se quisermos que a animação de alternância de roteamento em todas as plataformas siga o estilo consistente de alternância deslizante esquerda-direita da plataforma ios? O Flutter fornece um mecanismo para substituir a plataforma padrão. Podemos debugDefaultTargetPlatformOverrideespecificar a plataforma do aplicativo especificando explicitamente o valor da variável global. por exemplo:

debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform); // 会输出TargetPlatform.iOS

Depois que o código acima for executado no Android, o Flutter APP considerará o sistema atual como iOS e todos os métodos de interação de componentes na biblioteca de componentes materiais serão alinhados com a plataforma iOS, e o valor do valor também será defaultTargetPlatformalterado TargetPlatform.iOS.

Plug-ins comumente usados

O Flutter fornece oficialmente uma série de plug-ins comumente usados, como acesso a câmera/álbum, armazenamento local, reprodução de vídeo etc. Para obter uma lista completa, consulte: https://github.com/flutter/packages ou visite https://pub.dev Digite palavras-chave em sdk:flutterpara obter uma lista de plugins mantidos oficialmente.

Além dos plug-ins mantidos oficialmente, a comunidade Flutter também possui muitos plug-ins muito úteis, que podem ser pesquisados ​​inserindo palavras-chave em https://pub.dev/ de acordo com as necessidades.

data e hora

carimbo de data e hora

Obtenha a data e hora atual:

DateTime d = DateTime.now();
print(d); // 2023-05-29 12:05:56.010143
print("${
      
      d.year}-${
      
      d.month}-${
      
      d.day} ${
      
      d.hour}:${
      
      d.minute}:${
      
      d.second}");

Obtenha o carimbo de data/hora atual:

DateTime d = DateTime.now();
print(d.millisecondsSinceEpoch);

Converter data em carimbo de data/hora:

DateTime d = DateTime(2022, 08, 1);
print(d.millisecondsSinceEpoch);

Converter string de data em hora:

print(DateTime.parse("2021-08-01"));
print(DateTime.parse("2021-08-01 08:01:30"));
DateTime d = DateTime.parse("2021-08-01 08:01:30");
print(d.millisecondsSinceEpoch);

Timestamps convertidos em datas:

DateTime d = DateTime(2023, 08, 1);
int unixtime = d.millisecondsSinceEpoch;
print(DateTime.fromMillisecondsSinceEpoch(unixtime));

Adição e subtração de tempo e comparação

Adição e subtração de datas:

DateTime time = DateTime.now();
print(time);
print(time.add(const Duration(minutes: 30)));
print(time.add(const Duration(minutes: -30)));

Comparação de datas:

print(date1.isAfter(date2));  
print(date1.isBefore(date2)); 
print(date1.isAtSameMomentAs(date2)); 
final difference = dateTime1.difference(dateTime2); 
print(difference.inDays);  
print(difference.inHours);
print(difference.inMinutes);
print(difference.inSeconds);
print(difference.inMilliseconds);
print(difference > Duration(seconds: 10));
final date = DateTime.utc(1989, DateTime.november, 9);
print(DateTime.november); // 11
assert(date.month == DateTime.november);
assert(date.weekday == DateTime.thursday);

formatação de data e hora

  1. Use a biblioteca oficial da comunidade Dart intl para formatação de data

Por exemplo:

DateFormat("HH:mm:ss").format(DateTime.now());
DateFormat("yy:MM:dd HH:mm:ss").format(DateTime.now());
DateFormat.yMd().format(DateTime.now());
  1. Formatação de data usando a biblioteca da comunidade date_format

Por exemplo:

print(formatDate(DateTime.now(), [yyyy, '年', mm, '月', dd, '日']));
print(formatDate(DateTime(1989, 2, 21), [yyyy, '-', mm, '-', dd]));
print(formatDate(DateTime(1989, 2, 21), [yy, '-', m, '-', dd]));
print(formatDate(DateTime(1989, 2, 1), [yy, '-', m, '-', d]));

globalização

Deixe o aplicativo oferecer suporte a vários idiomas

Se nosso aplicativo for compatível com vários idiomas, precisamos "internacionalizá-lo". Isso significa que precisamos definir alguns valores "localizados", como texto e layout, para cada localidade suportada pelo aplicativo durante o desenvolvimento. O Flutter SDK forneceu alguns componentes e classes para nos ajudar a alcançar a internacionalização. Vamos apresentar as etapas para alcançar a internacionalização no Flutter.

Em seguida, MaterialAppusamos a classe como ponto de entrada para ilustrar como oferecer suporte à internacionalização.

A maioria dos aplicativos é importada por meio da API, mas os aplicativos escritos MaterialAppem termos de classes de nível inferior também podem ser internacionalizados usando as mesmas classes e lógica. Na verdade, também é um pacote.WidgetsAppMaterialAppWidgetsApp

Observe que "valores e recursos localizados" referem-se aos diferentes recursos que preparamos para diferentes idiomas. Esses recursos geralmente se referem a direitos autorais (strings). Claro, também existem alguns outros recursos que variam de acordo com diferentes regiões de idioma. Para Por exemplo, é necessário exibir uma imagem da bandeira nacional no aplicativo, portanto, precisamos fornecer diferentes imagens da bandeira nacional para diferentes regiões locais.

Apoiar a internacionalização

Por padrão, os componentes no Flutter SDK fornecem apenas recursos localizados em inglês dos EUA (principalmente texto). Para adicionar suporte a outras linguagens, a aplicação deve adicionar uma flutter_localizationsdependência de pacote chamada " ", e então precisa MaterialAppfazer algumas configurações nela. Para usar flutter_localizationsum pacote, primeiro você precisa adicionar dependências ao pubspec.yamlarquivo:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

Em seguida, o Pub get baixa a biblioteca flutter_localizations de forma síncrona e, em seguida, especifica MaterialAppa localizationsDelegatessoma supportedLocales:

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
  // 本地化的代理类 
 localizationsDelegates: [ 
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 //应用支持的语言类型 
 supportedLocales: [
    const Locale('en', 'US'), // 美国英语
    const Locale('zh', 'CN'), // 中文简体
    //其他Locales
  ],
  // ...
)

Ao contrário MaterialAppdos aplicativos baseados em classe, WidgetsAppnão é necessário ao internacionalizar aplicativos baseados em classe GlobalMaterialLocalizations.delegate.

  • localizationsDelegatesOs elementos na lista são classes de fábrica que geram coleções de valores localizados.

  • GlobalMaterialLocalizations.delegateMaterialStrings localizadas e outros valores fornecidos para a biblioteca de componentes, o que pode fazer com Materialque o componente suporte vários idiomas.

  • GlobalWidgetsLocalizations.delegateDefina a direção do texto padrão do componente, da esquerda para a direita ou da direita para a esquerda, pois o hábito de leitura de alguns idiomas não é da esquerda para a direita, como o árabe é da direita para a esquerda.

  • supportedLocalesEle também recebe um Localearray, indicando a lista de idiomas suportados pelo nosso aplicativo. Neste exemplo, nosso aplicativo suporta apenas dois idiomas: inglês americano e chinês simplificado.

Obter a localidade atual Localidade

LocaleA classe é usada para identificar a localidade do usuário, que inclui dois sinais de idioma e país, como:

MaterialApp(
	...
	locale: const Locale('zh', 'CN'), // 手动指定locale为中文简体, 这将忽略系统设置的locale
}

Sempre podemos obter a localidade atual do aplicativo com Locale:

Locale myLocale = Localizations.localeOf(context); //当系统切换语言环境时这个值会更新

LocalizationsO componente geralmente está localizado no topo de outros componentes de negócios na árvore de widgets e sua função é definir a localidade da área e definir os recursos localizados dos quais a subárvore depende. Se a localidade do sistema mudar, será usado o recurso localizado do idioma correspondente.

Se localefor null, significa que o Flutter falhou em obter Localeas informações do dispositivo, portanto localedevemos julgá-lo vazio antes de usá-lo.

Monitorar a troca de idioma do sistema

Quando alteramos a configuração do idioma do sistema, os componentes do APP Localizationsserão reconstruídos e Localizations.localeOf(context)os obtidos Localeserão atualizados e, finalmente, a interface obterá buildo efeito de alternar os idiomas novamente. Mas esse processo é feito implicitamente, não tomamos a iniciativa de monitorar a troca de idioma do sistema, mas às vezes precisamos fazer algo quando o idioma do sistema muda, por exemplo, quando o idioma do sistema é trocado para um idioma que nosso APP não suporte, precisamos definir um idioma padrão, então precisamos ouvir localeo evento de mudança.

Podemos ouvir o evento alterado por meio localeResolutionCallbackde ou localeListResolutionCallbackcallback , vamos primeiro ver a assinatura da função de callback:localelocaleResolutionCallback

Locale Function(Locale locale, Iterable<Locale> supportedLocales)
  • O valor do parâmetro localeé a configuração atual do idioma do sistema, que localeé locale. Quando o desenvolvedor especifica manualmente MaterialApp, localeentão este localeparâmetro representa aquele especificado pelo desenvolvedor locale, e o sistema será ignorado neste momento locale.
  • supportedLocalesA lista suportada pelo aplicativo atual é cadastrada pelo localedesenvolvedor MaterialAppatravés de atributos.supportedLocales
  • O valor de retorno é um Locale, que é Localefinalmente usado pelo Flutter APP Locale. Geralmente retorna um padrão no caso de uma localidade não suportada Locale.

localeListResolutionCallbackA única localeResolutionCallbackdiferença está no primeiro tipo de parâmetro, o primeiro recebe uma Localelista, enquanto o último recebe um único arquivo Locale.

Locale Function(List<Locale> locales, Iterable<Locale> supportedLocales)

No sistema Android mais recente, o usuário pode definir uma lista de idiomas. Dessa forma, o aplicativo que suporta vários idiomas obterá essa lista. O método de processamento usual do aplicativo é tentar carregar os idiomas correspondentes no ordem da lista Locale, para quando o idioma é carregado com sucesso. A seguir, uma captura de tela da lista de idiomas no sistema Android:

insira a descrição da imagem aqui

No Flutter, ele deve ser usado primeiro localeListResolutionCallback. Claro, você não precisa se preocupar com as diferenças do sistema Android. Se estiver em uma versão inferior do sistema Android, o Flutter tratará automaticamente dessa situação e a lista Localeserá conter apenas um item.

Componente de localização

LocalizationsOs componentes são usados ​​para carregar e encontrar valores ou recursos localizados no idioma atual do aplicativo. Os aplicativos Localizations.of(context, type)se referem a esses objetos por meio de . Caso Localeo locale do dispositivo mude, Localizationso componente carregará automaticamente o valor do novo locale Locale, e então reutilizará build(dependerá de) seus componentes, o motivo disso é por causa Localizationsdo uso interno InheritedWidget, dissemos ao apresentar este componente: Quando a buildfunção de um subcomponente é referenciada , uma dependência implícita InheritedWidgetdela é criada . InheritedWidgetPortanto, quando InheritedWidgetocorre uma alteração, ou seja, Localizationsas Localeconfigurações do arquivo .

Os valores localizados são carregados da lista Localizationsde arquivos LocalizationsDelegates. Cada delegado deve definir um método assíncrono load()para gerar um objeto que encapsula uma lista de valores localizados. Geralmente esses objetos definem um método para cada valor localizado.

Em grandes aplicações, diferentes módulos ou pacotes podem ser agrupados com seus próprios valores localizados. É por isso que você precisa Localizationsgerenciar tabelas de objetos. Para usar um objeto produzido LocalizationsDelegatepor loadum dos métodos, especifique um BuildContexte o tipo de objeto para localizá-lo. Por exemplo, Materialas strings localizadas da biblioteca de componentes MaterialLocalizationssão definidas pela classe e as instâncias dessa classe são criadas pela MaterialAppclasse LocalizationDelegatee podem ser obtidas da seguinte maneira:

Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);

Essa Localizations.of()expressão específica será usada com bastante frequência, portanto, MaterialLocalizationsa classe fornece um método conveniente:

static MaterialLocalizations of(BuildContext context) {
    
    
  return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}

// 可以直接调用便捷方法
tooltip: MaterialLocalizations.of(context).backButtonTooltip,

Use o pacote LocalizationsDelegates

Para ser o mais pequeno e simples possível, apenas valores em inglês dos EUA MaterialLocalizationse WidgetsLocalizationsimplementações de interface são fornecidos no pacote flutter. Essas classes de implementação são chamadas DefaultMaterialLocalizationse DefaultWidgetsLocalizations. flutter_localizationsO pacote contém implementações GlobalMaterialLocalizationsmultilíngues GlobalWidgetsLocalizationsdas interfaces localizadas para e os aplicativos internacionalizados devem especificar classes de proxy localizadas para essas classes, conforme explicado no início desta seção.

O acima GlobalMaterialLocalizationse acima GlobalWidgetsLocalizationssão apenas Materiala implementação localizada da biblioteca de componentes.Se quisermos que nosso layout suporte vários idiomas, precisamos implementar nosso próprio Localizations.

Implementar localizações

Já sabemos que Localizationsa implementação principal da classe fornece valores localizados, como text:

//Locale资源类
class DemoLocalizations {
    
    
  DemoLocalizations(this.isZh);
  //是否为中文
  bool isZh = false;
  //为了使用方便,我们定义一个静态方法
  static DemoLocalizations of(BuildContext context) {
    
    
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
  }
  //Locale相关值,title为应用标题
  String get title {
    
    
    return isZh ? "Flutter应用" : "Flutter APP";
  }
  //... 其他的值  
}

DemoLocalizationsretornará textos diferentes de acordo com o idioma atual, por exemplo title, podemos definir todos os textos que precisam suportar vários idiomas nesta classe. DemoLocalizationsUma instância de will será criada em um método Delegateda classe load.

Implemente a classe Delegado

DelegateA responsabilidade da classe é Localecarregar novos Localerecursos quando ela muda, então ela tem um loadmétodo. DelegateA classe precisa herdar da LocalizationsDelegateclasse e implementar a interface correspondente. O exemplo é o seguinte:

//Locale代理类
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
    
    
  const DemoLocalizationsDelegate();

  //是否支持某个Local
  
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  // Flutter会调用此类加载相应的Locale资源类
  
  Future<DemoLocalizations> load(Locale locale) {
    
    
    print("$locale");
    return SynchronousFuture<DemoLocalizations>(
        DemoLocalizations(locale.languageCode == "zh")
    );
  }

  
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}

shouldReloadO valor de retorno de determina se o método deve ser chamado para recarregar recursos quando Localizationso componente for reiniciado . Em circunstâncias normais, os recursos devem ser carregados apenas uma vez ao alternar e não precisam ser carregados toda vez que são recarregados , portanto, basta retornar . Algumas pessoas podem se preocupar que, se você retornar, o método não será chamado quando o usuário alterar o idioma do sistema após o início do APP , portanto , os recursos não serão carregados. Na verdade, o Flutter vai chamar o método para carregar um novo toda vez que ele mudar , quer ele retorne ou não .buildloadLocaleLocaleLocaleLocalizationsbuildfalsefalseloadLocaleLocaleloadLocaleshouldReloadtruefalse

Adicionar suporte multilíngue

Agora precisamos registrar DemoLocalizationsDelegatea classe primeiro e, em seguida, DemoLocalizations.of(context)obter dinamicamente o Localetexto atual por meio dela.

Basta adicionar nossa instância à lista de MaterialAppou para completar o cadastro:WidgetsApplocalizationsDelegatesDelegate

MaterialApp(
	localizationsDelegates: [
		 // 本地化的代理类
		 GlobalMaterialLocalizations.delegate,
		 GlobalWidgetsLocalizations.delegate,
		 // 注册我们的Delegate
		 DemoLocalizationsDelegate()
	],
)

Em seguida, podemos Widgetusar Localeo valor em:

return Scaffold(
  appBar: AppBar( 
    title: Text(DemoLocalizations.of(context).title), // 使用Locale title  
  ),
  ... //省略无关代码

Dessa forma, ao alternar o idioma do sistema entre inglês americano e chinês simplificado, os títulos do APP serão “Flutter APP” e “Flutter Application” respectivamente.

Use o pacote Internacional

Uma falha grave do exemplo acima é que precisamos julgar manualmente o idioma atual ao DemoLocalizationsobtê-lo na classe e, em seguida, retornar o texto apropriado. Imagine só, quando os idiomas que queremos suportar não são dois, mas 8 ou até mais de 20, será muito complicado julgar qual é para cada atributo de texto e obter o texto no idioma correspondente. Além disso, geralmente o tradutor não é um desenvolvedor, a tradução pode ser salva como um arquivo arb separadamente como o padrão i18n ou l10n e entregue ao tradutor para tradução, após a conclusão da tradução, o desenvolvedor usará a ferramenta para converter o arb para codificar. A resposta é sim! Mostraremos como fazer isso com o pacote Dart intl abaixo .titleLocaleLocale

Use intl_generator para gerar configuração multilíngue

Usando o pacote Intl , podemos não apenas alcançar a internacionalização com muita facilidade, mas também separar o texto da string em arquivos separados, o que é conveniente para desenvolvedores e tradutores trabalharem juntos. Para usar Intlo pacote, precisamos adicionar duas dependências:

dependencies:
  #...省略无关项
  intl: ^0.17.0 
dev_dependencies:
   #...省略无关项
  intl_generator:  0.2.1 

O pacote intl_generator inclui principalmente algumas ferramentas. Sua principal função na fase de desenvolvimento é extrair as strings a serem internacionalizadas do código para um arbarquivo separado e arbgerar o código da linguagem correspondente de acordo com o arquivo dart. intlO pacote principalmente referencia e carrega intl_generatoro código gerado dart. A seguir explicaremos passo a passo como utilizá-lo:

Passo 1: Crie os diretórios necessários

l10n-arbPrimeiro, crie um diretório sob o diretório raiz do projeto , que irá conter os arquivos que intl_generatoriremos gerar através do próximo comando arb. Um arquivo simples arbcom o seguinte conteúdo:

{
    
    
  "@@last_modified": "2018-12-10T15:46:20.897228",
  "@@locale":"zh_CH",
  "title": "Flutter应用",
  "@title": {
    
    
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {
    
    }
  }
}

De acordo com o campo " ", podemos @@localever que isso arbcorresponde à tradução chinesa simplificada, e titleos campos dentro correspondem à tradução chinesa simplificada do título do nosso aplicativo. @titleO campo contém titlealgumas informações descritivas sobre o par.

Em seguida, libcriamos um l10ndiretório sob o diretório, que é usado para salvar os arquivos de código arbgerados a partir dos arquivos dart.

Etapa 2: implementar as classes Localizações e Delegar

Semelhante aos passos da seção anterior, ainda precisamos implementar Localizationse classificar, a diferença é que agora precisamos usar alguns métodos do pacote (alguns são gerados dinamicamente) Delegateao implementar .intl

Em seguida, lib/l10ncriamos um novo localization_intl.dartarquivo " " no diretório, o conteúdo do arquivo é o seguinte:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'messages_all.dart'; // 1

class DemoLocalizations {
    
    
  static Future<DemoLocalizations> load(Locale locale) {
    
    
    final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    // 2
    return initializeMessages(localeName).then((b) {
    
    
      Intl.defaultLocale = localeName;
      return DemoLocalizations();
    });
  }

  static DemoLocalizations of(BuildContext context) {
    
    
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
  }

  String get title {
    
    
    return Intl.message(
      'Flutter APP',
      name: 'title',
      desc: 'Title for the Demo application',
    );
  }
}

//Locale代理类
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
    
    
  const DemoLocalizationsDelegate();

  //是否支持某个Local
  
  bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);

  // Flutter会调用此类加载相应的Locale资源类
  
  Future<DemoLocalizations> load(Locale locale) {
    
    
    // 3
    return  DemoLocalizations.load(locale);
  }

  // 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
  
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}

Perceber:

  • 1O " messages_all.dart" arquivo comentado é o código gerado intl_generatora partir do arquivo pela ferramenta , portanto este arquivo não existe até a primeira execução do comando build. O método no arbcomentário é o mesmo do arquivo " ", que é gerado ao mesmo tempo.2initializeMessages()messages_all.dart
  • O comentário 3é diferente do código de amostra da seção anterior, aqui podemos DemoLocalizations.load()chamá-lo diretamente.

Etapa 3: adicionar atributos que exigem internacionalização

Agora podemos DemoLocalizationsadicionar propriedades ou métodos que precisam ser internacionalizados na classe, como titleas propriedades do exemplo de código acima. Neste momento, usaremos Intlalguns métodos fornecidos pela biblioteca. Esses métodos podem nos ajudar a implementar facilmente alguns recursos gramaticais de idiomas diferentes. Por exemplo, se tivermos uma página de lista de e-mails, precisamos exibir o número de e-mails não lidos na parte superior. O texto que exibimos pode ser diferente dependendo do número de e-mails não lidos:

insira a descrição da imagem aqui

Podemos Intl.plural(...)conseguir isso por:

remainingEmailsMessage(int howMany) => Intl.plural(howMany,
    zero: 'There are no emails left',
    one: 'There is $howMany email left',
    other: 'There are $howMany emails left',
    name: "remainingEmailsMessage",
    args: [howMany],
    desc: "How many emails remain after archiving.",
    examples: const {
    
    'howMany': 42, 'userName': 'Fred'});

Você pode ver que Intl.pluralo método pode howManygerar diferentes informações de prompt quando os valores são diferentes. IntlO pacote tem alguns outros métodos, veja sua documentação você mesmo.

Passo 4: Gerar arquivo arb

Agora podemos intl_generatorextrair as strings do código para um arbarquivo através das ferramentas do pacote, execute o seguinte nomeado:

flutter pub pub run intl_generator:extract_to_arb --output-dir=l10n-arb \ lib/l10n/localization_intl.dart

Após executar este comando, Intlos atributos e strings que identificamos através da API serão extraídos para o l10n-arb/intl_messages.arbarquivo " " no diretório raiz, vamos dar uma olhada em seu conteúdo:

{
    
    
  "@@last_modified": "2018-12-10T17:37:28.505088",
  "title": "Flutter APP",
  "@title": {
    
    
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {
    
    }
  },
  "remainingEmailsMessage": "{howMany,plural, =0{There are no emails left}=1{There is {howMany} email left}other{There are {howMany} emails left}}",
  "@remainingEmailsMessage": {
    
    
    "description": "How many emails remain after archiving.",
    "type": "text",
    "placeholders": {
    
    
      "howMany": {
    
    
        "example": 42
      }
    }
  }
}

Este é o Localearquivo de recurso padrão. Se quisermos oferecer suporte ao chinês simplificado agora, precisamos apenas criar um intl_zh_CN.arbarquivo " " no mesmo diretório do arquivo, intl_messages.arbcopiar o conteúdo de " " para o intl_zh_CN.arbarquivo " " e depois traduzir para o inglês para chinês. , o conteúdo do intl_zh_CN.arbarquivo " " traduzido é o seguinte:

{
    
    
  "@@last_modified": "2018-12-10T15:46:20.897228",
  "@@locale":"zh_CN",
  "title": "Flutter应用",
  "@title": {
    
    
    "description": "Title for the Demo application",
    "type": "text",
    "placeholders": {
    
    }
  },
  "remainingEmailsMessage": "{howMany,plural, =0{没有未读邮件}=1{有{howMany}封未读邮件}other{有{howMany}封未读邮件}}",
  "@remainingEmailsMessage": {
    
    
    "description": "How many emails remain after archiving.",
    "type": "text",
    "placeholders": {
    
    
      "howMany": {
    
    
        "example": 42
      }
    }
  }
}

Temos que traduzir titlee remainingEmailsMessagecampo, descriptionque é a descrição do campo, que normalmente é mostrado ao tradutor e não será utilizado no código.

Há dois pontos a observar:

  1. Se um determinado atributo estiver faltando em um arquivo específico , o aplicativo carregará o atributo correspondente no arquivo arbpadrão ( ), que é a estratégia ascendente.arbintl_messages.arbIntl
  2. Toda vez que você executar o comando de extração, intl_messages.arbele será regenerado de acordo com o código, mas outros arbarquivos não, portanto, quando você quiser adicionar novos campos ou métodos, outros arbarquivos são incrementais, não se preocupe em sobrescrever.

arbO arquivo é padrão e sua especificação de formato pode ser entendida por si só. Normalmente os arquivos são entregues aos tradutores, e quando eles terminam a tradução, geramos o código final arbbaseado nos arquivos através dos seguintes passos .arbdart

Etapa 5: Gerar código de dardo

O último passo é arbgerar darto arquivo de acordo com:

flutter pub pub run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb

Quando este comando for executado pela primeira vez, lib/l10nvários arquivos serão gerados no diretório " ", correspondentes a vários tipos Locale, e esses códigos são os códigos a serem usados ​​no final dart.

Observe windowsque se *o curinga do número de identificação não puder ser executado no seguinte comando, um erro será relatado e poderá ser executado no git bashterminal

Se você encontrar conflitos intl_translationcom intlas versões dependentes dessas duas bibliotecas, você pode usar temporariamente a biblioteca intl_utils Como usar:
1. Coloque arbo arquivo intl_<LOCALE_ISO_CODE>.arbno diretório lib/l10nno formato
2. Execute-o no terminal e o arquivo correspondente flutter pub run intl_utils:generateserá ser gerado lib/generatesobdart

Resumir

Até agora, introduzimos o processo de usar Intlo pacote para internacionalizar o APP. Podemos descobrir que a primeira e a segunda etapas são necessárias apenas pela primeira vez, e o principal trabalho de nosso desenvolvimento está na terceira etapa. Como as duas últimas etapas são necessárias sempre após a conclusão da terceira etapa, podemos colocar as duas últimas etapas em um script de shell e só precisamos executar o arbscript separadamente quando concluímos a terceira etapa ou concluímos a tradução do arquivo. Criamos um script no diretório raiz intl.shcom o conteúdo:

flutter pub run intl_generator:extract_to_arb --output-dir=l10n-arb lib/l10n/localization_intl.dart
flutter pub run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb

Em seguida, conceda permissão de execução:

chmod +x intl.sh

implementointl.sh

./intl.sh

Use o esquema oficial de geração automatizada

Embora o intl_generatorpacote acima possa gerar automaticamente classes dart relacionadas para configuração multilíngue, ele é um pouco pesado e complicado de usar. Precisamos escrever manualmente parte do código (localization_intl.dart e adicionar manualmente atributos de internacionalização neste arquivo ainda precisa Intlaprender API), e muitos comandos precisam ser executados (é fácil relatar erros). Na prática, esta solução também é fácil de gerar diferentes conflitos de dependência de pacote de pacote, o que é difícil de resolver.

Felizmente, o funcionário nos forneceu uma solução de geração mais automatizada. Ainda uso intlo pacote, mas não tão problemático.

Depois de importar flutter_localizationso pacote, siga as instruções abaixo para adicionar texto localizado ao seu aplicativo.

  1. Adicione intlo pacote como uma dependência, usando anycomo flutter_localizationsvalor da versão:
 flutter pub add intl:any
  1. Além disso, no pubspec.yamlarquivo, ative o sinalizador de geração. Este item de configuração é adicionado na pubspecseção Flutter de , geralmente no pubspecfinal do arquivo.
# The following section is specific to Flutter.
flutter:
  generate: true # Add this line
  1. Adicione um novo yamlarquivo no diretório raiz do projeto Flutter, nomeado como l10n.yaml, com o seguinte conteúdo:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

Este arquivo é usado para configurar a ferramenta de localização; no exemplo acima, o arquivo de entrada especificado está no ${FLUTTER_PROJECT}/lib/l10narquivo app_en.arbque fornece os modelos e os arquivos de localização gerados estão no app_localizations.dartarquivo.

  1. Em ${FLUTTER_PROJECT}/lib/l10n, adicione app_en.arbo arquivo de modelo. do seguinte modo:
{
    
    
  "helloWorld": "Hello World!",
  "@helloWorld": {
    
    
    "description": "The conventional newborn programmer greeting"
  }
}
  1. Em seguida, adicione um arquivo no mesmo diretório app_es.arbpara fazer uma tradução internacional da mesma mensagem, como o espanhol:
{
    
    
    "helloWorld": "¡Hola Mundo!"
}
  1. Agora, execute flutter gen-l10no comando e você ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10nverá os arquivos gerados em formato . Você verá as classes geradas automaticamente
    insira a descrição da imagem aqui
    em formato .app_localizations.dartAppLocalizations

    Da mesma forma, você pode executar esse comando para gerar arquivos de localização quando o aplicativo não estiver em execução.

  2. Depois de chamar MaterialAppo construtor de , import app_localizations.darte use AppLocalizations.delegate.

import 'package:flutter_gen/gen_l10n/app_localizations.dart';
return const MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: [
    AppLocalizations.delegate, // Add this line
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en'), // English
    Locale('es'), // Spanish
  ],
  home: MyHomePage(),
);

AppLocalizationslocalizationsDelegatesAs classes também podem gerar e listar automaticamente supportedLocalessem precisar fornecê-las manualmente:

const MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
);
  1. Agora, você pode usar em qualquer lugar em seu aplicativo AppLocalizations:
appBar: AppBar(
  // title 会根据选择的语言locale自动显示helloWorld对应的语言文本 
  title: Text(AppLocalizations.of(context)!.helloWorld),
),

Se a localidade do dispositivo de destino estiver definida como inglês, Texto widget gerado por esse código exibirá " Hello World!". Se a localidade do dispositivo de destino for definida como espanhol, Hola Mundo!será exibido " ". No arbarquivo, o valor da chave de cada entrada é usado como gettero nome do método do , e o valor da entrada representa informações localizadas.

Para ver um aplicativo Flutter de exemplo completo com as etapas acima, consulte gen_l10n_example

Se você deseja definir o título internacionalizado, use o seguinte método:

return MaterialApp(
      ...
      onGenerateTitle: (context) => AppLocalizations.of(context).title,
 )

Resumir

Pode-se ver que, em comparação com o uso de intl_generator, esse método não precisa escrever nenhum código. Desde que o tradutor nos forneça arbo arquivo, copie-o para o diretório especificado e execute um comando simples, darto código será automaticamente gerado. Em seguida, apenas MaterialApppode ser usado após a configuração em

Perguntas frequentes sobre internacionalização

A localidade padrão está errada

Para alguns dispositivos Android e iOS adquiridos de canais licenciados fora do continente, a localidade padrão pode não ser chinês simplificado. Este é um fenômeno normal, mas para evitar que a localidade obtida pelo dispositivo seja inconsistente com a região real, todos os aplicativos que suportam vários idiomas devem fornecer uma entrada para seleção manual de idiomas .

Como internacionalizar títulos de aplicativos

MaterialAppExiste um titleatributo para especificar o título do APP. No sistema Android, o título do APP aparecerá no gerenciador de tarefas. Por isso, também precisa titleser internacionalizado. Mas o problema é que muitas configurações de internacionalização são MaterialAppdefinidas acima e não podemos obter recursos localizados MaterialApppor meio do build time , como:Localizations.of

MaterialApp(
  title: DemoLocalizations.of(context).title, //不能正常工作!
  localizationsDelegates: [
    // 本地化的代理类
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    DemoLocalizationsDelegate() // 设置Delegate
  ],
);

Depois que o código acima for executado, DemoLocalizations.of(context).titleele irá relatar um erro, porque ele irá procurar da árvore atual para o topo , Localizations.ofmas depois de defini-lo em , ele está na subárvore atual, então ele retornará e relatará um erro. Então, como lidamos com essa situação? Na verdade é muito simples, só precisamos definir um callback:contextwidgetDemoLocalizationsMaterialAppDemoLocalizationsDelegateDemoLocalizationscontextDemoLocalizations.of(context)nullonGenerateTitle

MaterialApp(
  onGenerateTitle: (context){
    
    
    // 此时context在Localizations的子树中
    return DemoLocalizations.of(context).title;
  },
  localizationsDelegates: [
    DemoLocalizationsDelegate(),
    ...
  ],
);

Como especificar a mesma localidade para países de língua inglesa

Existem muitos países de língua inglesa, como Estados Unidos, Reino Unido, Austrália, etc. Embora todos esses países de língua inglesa falem inglês, existem algumas diferenças. Se nosso APP quiser fornecer apenas um tipo de inglês (como o inglês americano) para uso em todos os países de língua inglesa, podemos localeListResolutionCallbackfazer a compatibilidade conforme descrito acima:

localeListResolutionCallback:
    (List<Locale> locales, Iterable<Locale> supportedLocales) {
    
    
  // 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')     
}

referência:

Acho que você gosta

Origin blog.csdn.net/lyabc123456/article/details/130919727
Recomendado
Clasificación