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 aoNSTemporaryDirectory()
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 aNSDocumentDirectory
. No Android, este éAppData
o 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áUnsupportedError
uma exceção, enquanto no Android, o resultado égetExternalStorageDirectory
o 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:io
a 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:
- Importe
PathProvider
o plug-in;pubspec.yaml
adicione a seguinte instrução ao arquivo:
path_provider: ^2.0.15
Depois de adicionar, execute flutter packages get
Get 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 HttpClient
para iniciar solicitações. Há cinco etapas para HttpClient
iniciar uma solicitação:
- Crie um
HttpClient
:
HttpClient httpClient = HttpClient();
- Abra
Http
a 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 Query
forem incluídos parâmetros, eles podem uri
ser adicionados no momento da compilação, como:
Uri uri = Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
"xx":"xx",
"yy":"dd"
});
Definindo HttpClientRequest
o request header
, como:
request.headers.add("user-agent", "test");
Se for post
ou put
puder carregar o método do corpo da solicitação, você pode HttpClientRequest
enviar o corpo da solicitação por meio do objeto, como:
String payload="...";
request.add(utf8.encode(payload));
//request.addStream(_inputStream); //可以直接添加输入流
- 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 HttpClientResponse
objeto é 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.
- 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
.
- Fim da solicitação, fechar
HttpClient
:
httpClient.close();
Quando fechado , todas as solicitações iniciadas client
por 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
HttpClient
Existem 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, SocketException uma 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-Encoding a 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 HttpClientRequest
defini-los diretamente header
. A diferença é que HttpClient
os definidos httpClient
são eficazes para o todo e HttpClientRequest
os 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 Basic
recurso com autenticação habilitada no navegador, uma caixa de login irá aparecer durante a navegação, conforme a figura:
Vejamos primeiro Basic
o processo básico de autenticação:
-
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 Unauthozied
para o cliente eheader
adicionar um“WWW-Authenticate”
campo na resposta, por exemplo:WWW-Authenticate: Basic realm="admin"
Entre eles, "Básico" é o método de autenticação
realm
e é o agrupamento de funções de usuário, que podem ser adicionadas em segundo plano. -
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çãoAuthorization
e continue visitando:Authorization: Basic YXXFISDJFISJFGIJIJG
O servidor verifica as credenciais do usuário e retorna o conteúdo do recurso, se aprovado.
Observe que, Http
além da autenticação, existem outros métodos Basic
: Digest
autenticação, Client
autenticação, Form Based
autenticação, etc. Atualmente, o Flutter HttpClient
oferece suporte apenas Basic
a Digest
dois métodos de autenticação. é diferente da primeira É apenas Base64
codificaçã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 Basic
ou Digest
autenticação, deve estar sob Https
o protocolo , o que pode impedir a captura de pacotes e ataques man-in-the-middle.
HttpClient
Sobre Http
métodos e propriedades de autenticação:
-
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 Digest
autenticação, você pode criar Digest
credenciais de autenticação:
HttpClientDigestCredentials("username","password")
-
authenticate(Future<bool> f(Uri url, String scheme, String realm))
Este é um
setter
e 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,httpClient
esse retorno de chamada será chamado. Nesse retorno de chamada, geralmente é chamadoaddCredential()
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 HttpClient
chamada no momento da inicialização addCredentials()
para adicionar credenciais globais em vez de adicioná-las dinamicamente.
atuando
A política de proxy pode findProxy
ser 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";
};
findProxy
O 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, DIRECT
basta 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 HttpClient
fornece Proxy
mé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 addCredentials
mesmo apresentado em "Autenticação de solicitação HTTP" acima authenticate
, portanto, não os repetirei aqui.
Verificação de certificado
Https
Para evitar ataques man-in-the-middle falsificando certificados, o cliente deve verificar os certificados autoassinados ou emitidos por não-CA. HttpClient
A lógica da verificação do certificado é a seguinte:
- Se o
Https
certificado solicitado for emitido por uma CA confiável, e o acessohost
estiver incluído na lista de certificadosdomain
(ou estiver em conformidade com as regras curinga) e o certificado não tiver expirado, a verificação será aprovada. - Se a primeira etapa da verificação falhar, mas o certificado
HttpClient
tiver sidoSecurityContext
adicionado à cadeia confiável de certificados quando foi criado, se o certificado retornado pelo servidor estiver na cadeia confiável, a verificação será aprovada. - Se ambas as verificações 1 e 2 falharem, se o usuário fornecer
badCertificateCallback
um callback, ele será chamado, se o callback retornartrue
, a conexão poderá continuar, se for retornadofalse
, a conexão será encerrada.
Resumindo, nossa verificação de certificado é, na verdade, para fornecer um badCertificateCallback
retorno 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 é PEM
format 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 badCertificateCallback
retorno 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 PEM
ou 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 PKCS12
o certificado no formato da verificação do certificado do cliente.
biblioteca de solicitação Http-dio
HttpClient
Descobrimos que é complicado usar diretamente para iniciar solicitações de rede. Muitas coisas precisam ser tratadas manualmente. Se envolver upload/download de arquivos, Cookie
gerenciamento etc., será muito complicado. Felizmente, existem algumas bibliotecas de solicitação de terceiros http
na comunidade Dart, e usá-las para iniciar http
solicitaçõ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 dio
iniciar uma solicitação de rede por meio da instância. Observe que uma dio
instância pode iniciar várias http
solicitações. De um modo geral, http
quando o APP possui apenas uma fonte de dados, dio
o modo singleton deve ser usado.
Iniciar uma solicitação por meio de dio
Inicie GET
uma solicitação:
Response response;
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
Para GET
a requisição podemos query
passar 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 POST
pedido:
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 dio
será header
definido contentType
como " multipart/form-data
".
Ao FormData
fazer 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 dio
ainda é 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:HttpClient
HttpClient
onHttpClientCreate
(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 onHttpClientCreate
ele será chamado quando a dio
instância atual precisar ser criada HttpClient
, portanto, essa configuração de retorno de chamada HttpClient
terá dio
efeito em toda a instância. Se o aplicativo exigir várias estratégias de verificação de proxy ou certificado, diferentes dio
instâncias podem ser criadas para implementá-las separadamente.
Além desses usos básicos, dio
ele 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
- carregamento de pop-up durante a fase de solicitação
- 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
Http
O protocolo define o header
campo de resposta para transmissão em partes, mas se ele oferece suporte depende da Server
implementação. Podemos especificar o range
campo " " do cabeçalho da solicitação para verificar se o servidor oferece suporte à transmissão em partes. Por exemplo, podemos usar curl
o 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 10
o total 11
de 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-Range
campo " ". 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-10
Indica o bloco retornado desta vez, 233295878
representando o comprimento total do arquivo, e a unidade é byte
, ou seja, o arquivo provavelmente é 233M
um pouco maior.
Com base nisso, podemos projetar um simples downloader de bloco de arquivo multiencadeado. A ideia de implementação é:
- 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.
- 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.
- 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 dio
a download
implementaçã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
Http
O 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. WebSocket
O 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:
Http
Embora o protocolo possa usarkeep-alive
um mecanismo para manter o servidor conectado por um período de tempo após o término da resposta, ele eventualmente será desconectado. Okeep-alive
mecanismo é 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.
WebSocket
O protocolo é essencialmente um TCP
protocolo baseado no protocolo. Ele primeiro HTTP
inicia uma http
solicitação especial por meio do protocolo para handshake. Se o servidor suportar WebSocket
o protocolo, ele atualizará o protocolo. WebSocket
Será utilizado o link http
criado 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.TCP
http
WebSocket
TCP
TCP
Vamos nos concentrar em como usá-lo no Flutter WebSocket
.
etapas de comunicação
O uso WebSocket
da comunicação é dividido em 4 etapas:
- Conecte-se a um servidor WebSocket.
- Ouça as mensagens do servidor.
- Enviar dados para o servidor.
- 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 WebSocketChannel
método que nos permite ouvir mensagens do servidor e enviar mensagens para o servidor.
No Flutter, podemos criar uma WebSocketChannel
conexã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 StreamBuilder
para ouvir novas mensagens e a Text
para exibi-las.
StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${
snapshot.data}' : '');
},
);
WebSocketChannel
Forneceu uma mensagem do servidor Stream
. Esta classeStream
é dart:async
uma classe base no pacote. Ele fornece uma maneira de escutar eventos assíncronos de fontes de dados. Em vez de Future
retornar uma única resposta assíncrona, Stream
uma classe pode entregar muitos eventos ao longo do tempo. O StreamBuilder
componente se conectará a um Stream
e 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 add
uma mensagem ao WebSocketChannel
fornecido sink
.
channel.sink.add('Hello!');
WebSocketChannel
Um é fornecido StreamSink
, que envia a mensagem para o servidor.
StreamSink
A 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();
}
}
WebSocket
Vamos pensar em uma pergunta agora, o que devemos fazer se quisermos transmitir dados binários (por exemplo, para receber uma imagem do servidor)? Constatamos StreamBuilder
que Stream
não existe um parâmetro especificando o tipo de recebimento, e WebSocket
nã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 WebSocket
todos 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 Opcode
especificado 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
Socket
API é 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 Socket
as APIs em linguagens de programação de alto nível são, na verdade, Socket
um encapsulamento das APIs do sistema operacional.
Http
Os protocolos e protocolos que apresentamos antes WebSocket
pertencem 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 é HttpClient
fácil de usar e queremos reimplementá-lo, precisamos usá-lo Socket
. A API do Flutter Socket
está no dart:io
pacote, vamos ver um exemplo de uso Socket
para implementar uma http
requisiçã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 Socket
precisamos 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
, XML
etc. Por exemplo, no exemplo anterior, onde solicitamos a API do Github, os dados retornados são JSON
uma string no formato. Para facilitar nossas operações em o código JSON
, primeiro JSON
formatamos o Converter uma string em Dart
um objeto, que pode ser obtido por meio do decodificador dart:convert
integrado , 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:JSON
json.decode()
JSON
List
Map
//一个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 JSON
de 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.decode
decodificá-lo chamando um método JSON
com JSON
uma 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 name
ou email
, digitamos muito rapidamente, fazendo com que o nome do campo seja digitado errado. Mas como isso JSON
está map
na 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 Json
correspondentes à estrutura Model
e, em seguida, dinamicamente de acordo com os dados após solicitar o dados. Crie Model
uma instância da classe. Desta forma, na fase de desenvolvimento, utilizamos Model
uma instância da classe ao invés de uma instância da classe, Map/List
para que não haja erros de digitação ao acessar as propriedades internas.
1. Escreva manualmente a classe Dart Model
User
Por exemplo, podemos resolver o problema mencionado anteriormente introduzindo uma classe de modelo simples . Dentro User
da classe temos:
- Um
User.fromJson
construtor nomeado que constróimap
umaUser
instância de um structmap
. - Um
toJson
método que converteUser
uma instância em umamap
.
user.dart
O 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 ( name
e email
) e exceções de tempo de compilação. Se, em vez disso, tratássemos campos com erros ortográficos como int
tipos 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 User
passar o objeto para o json.encode
método. Não precisamos chamar toJson
este método manualmente, pois JSON.encode
ele será chamado automaticamente internamente.
String json = json.encode(user);
Dessa forma, o código de chamada não precisa se preocupar com JSON
a serialização, mas Model
a classe ainda é necessária. Na prática, User.fromJson
ambos os User.toJson
mé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, JSON
objetos aninhados não são incomuns, e seria muito bom se houvesse algo que lide automaticamente com JSON
a 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_serializable
em 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.yaml
add:
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 User
classe 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 name
e campos.email
Também é fácil personalizar a estratégia de nomenclatura, se desejar. Por exemplo, se a API que estamos usando retorna snake_case
um objeto com , mas queremos usá-lo em nosso modelo lowerCamelCase
, podemos usar @JsonKey
a anotação:
//显式关联JSON字段名与Model属性的对应关系
(name: 'registration_date_millis')
final int registrationDateMillis;
3) Execute o gerador de código
json_serializable
Ao criar uma classe pela primeira vez, você verá um erro semelhante à imagem abaixo.
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
Model
Isso 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 Model
os arquivos de origem que precisam gerar classes (contendo @JsonSerializable
as anotações) para gerar os .g.dart
arquivos correspondentes. Uma boa sugestão é Model
colocar 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 watcher
pode 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 watch
iniciar 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 Json
o arquivo Dart
.
usar
jsons
Crie um diretório chamado " " no diretório raiz do projeto ;- Crie ou copie o arquivo Json para o
jsons
diretório " "; - Execute
pub run json_mode
o comando l (Dart VM project) ouflutter packages pub run json_model
(no Flutter) para gerar a classe de modelo Dart e os arquivos gerados estarão nolib/models
diretó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:
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 shaking
A reflexão do tempo de execução interfere no uso do Dart detree 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.yaml
arquivo: Um arquivo de metadados que declara o nome, versão, autor, etc. do Pacote. - Uma
lib
pasta: incluindo o código público no pacote, deve haver pelo menos um<package-name>.dart
arquivo
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 defaultTargetPlatform
para obter as informações da plataforma da aplicação atual, defaultTargetPlatform
definida 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 MaterialPageRoute
aplicará 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 debugDefaultTargetPlatformOverride
especificar 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á defaultTargetPlatform
alterado 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:flutter
para 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
- 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());
- 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, MaterialApp
usamos 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
MaterialApp
em 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.WidgetsApp
MaterialApp
WidgetsApp
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_localizations
dependência de pacote chamada " ", e então precisa MaterialApp
fazer algumas configurações nela. Para usar flutter_localizations
um pacote, primeiro você precisa adicionar dependências ao pubspec.yaml
arquivo:
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 MaterialApp
a localizationsDelegates
soma 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
MaterialApp
dos aplicativos baseados em classe,WidgetsApp
não é necessário ao internacionalizar aplicativos baseados em classeGlobalMaterialLocalizations.delegate
.
-
localizationsDelegates
Os elementos na lista são classes de fábrica que geram coleções de valores localizados. -
GlobalMaterialLocalizations.delegate
Material
Strings localizadas e outros valores fornecidos para a biblioteca de componentes, o que pode fazer comMaterial
que o componente suporte vários idiomas. -
GlobalWidgetsLocalizations.delegate
Defina 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. -
supportedLocales
Ele também recebe umLocale
array, 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
Locale
A 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); //当系统切换语言环境时这个值会更新
Localizations
O 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 locale
for null
, significa que o Flutter falhou em obter Locale
as informações do dispositivo, portanto locale
devemos 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 Localizations
serão reconstruídos e Localizations.localeOf(context)
os obtidos Locale
serão atualizados e, finalmente, a interface obterá build
o 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 locale
o evento de mudança.
Podemos ouvir o evento alterado por meio localeResolutionCallback
de ou localeListResolutionCallback
callback , vamos primeiro ver a assinatura da função de callback:locale
localeResolutionCallback
Locale Function(Locale locale, Iterable<Locale> supportedLocales)
- O valor do parâmetro
locale
é a configuração atual do idioma do sistema, quelocale
élocale
. Quando o desenvolvedor especifica manualmenteMaterialApp
,locale
então estelocale
parâmetro representa aquele especificado pelo desenvolvedorlocale
, e o sistema será ignorado neste momentolocale
. supportedLocales
A lista suportada pelo aplicativo atual é cadastrada pelolocale
desenvolvedorMaterialApp
através de atributos.supportedLocales
- O valor de retorno é um
Locale
, que éLocale
finalmente usado pelo Flutter APPLocale
. Geralmente retorna um padrão no caso de uma localidade não suportadaLocale
.
localeListResolutionCallback
A única localeResolutionCallback
diferença está no primeiro tipo de parâmetro, o primeiro recebe uma Locale
lista, 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:
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 Locale
será conter apenas um item.
Componente de localização
Localizations
Os 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 Locale
o locale do dispositivo mude, Localizations
o componente carregará automaticamente o valor do novo locale Locale
, e então reutilizará build
(dependerá de) seus componentes, o motivo disso é por causa Localizations
do uso interno InheritedWidget
, dissemos ao apresentar este componente: Quando a build
função de um subcomponente é referenciada , uma dependência implícita InheritedWidget
dela é criada . InheritedWidget
Portanto, quando InheritedWidget
ocorre uma alteração, ou seja, Localizations
as Locale
configurações do arquivo .
Os valores localizados são carregados da lista Localizations
de 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 Localizations
gerenciar tabelas de objetos. Para usar um objeto produzido LocalizationsDelegate
por load
um dos métodos, especifique um BuildContext
e o tipo de objeto para localizá-lo. Por exemplo, Material
as strings localizadas da biblioteca de componentes MaterialLocalizations
são definidas pela classe e as instâncias dessa classe são criadas pela MaterialApp
classe LocalizationDelegate
e 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, MaterialLocalizations
a 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 MaterialLocalizations
e WidgetsLocalizations
implementações de interface são fornecidos no pacote flutter. Essas classes de implementação são chamadas DefaultMaterialLocalizations
e DefaultWidgetsLocalizations
. flutter_localizations
O pacote contém implementações GlobalMaterialLocalizations
multilíngues GlobalWidgetsLocalizations
das 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 GlobalMaterialLocalizations
e acima GlobalWidgetsLocalizations
são apenas Material
a 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 Localizations
a 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";
}
//... 其他的值
}
DemoLocalizations
retornará textos diferentes de acordo com o idioma atual, por exemplo title
, podemos definir todos os textos que precisam suportar vários idiomas nesta classe. DemoLocalizations
Uma instância de will será criada em um método Delegate
da classe load
.
Implemente a classe Delegado
Delegate
A responsabilidade da classe é Locale
carregar novos Locale
recursos quando ela muda, então ela tem um load
método. Delegate
A classe precisa herdar da LocalizationsDelegate
classe 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;
}
shouldReload
O valor de retorno de determina se o método deve ser chamado para recarregar recursos quando Localizations
o 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 .build
load
Locale
Locale
Locale
Localizations
build
false
false
load
Locale
Locale
load
Locale
shouldReload
true
false
Adicionar suporte multilíngue
Agora precisamos registrar DemoLocalizationsDelegate
a classe primeiro e, em seguida, DemoLocalizations.of(context)
obter dinamicamente o Locale
texto atual por meio dela.
Basta adicionar nossa instância à lista de MaterialApp
ou para completar o cadastro:WidgetsApp
localizationsDelegates
Delegate
MaterialApp(
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
// 注册我们的Delegate
DemoLocalizationsDelegate()
],
)
Em seguida, podemos Widget
usar Locale
o 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 DemoLocalizations
obtê-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 .title
Locale
Locale
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 Intl
o 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 arb
arquivo separado e arb
gerar o código da linguagem correspondente de acordo com o arquivo dart
. intl
O pacote principalmente referencia e carrega intl_generator
o código gerado dart
. A seguir explicaremos passo a passo como utilizá-lo:
Passo 1: Crie os diretórios necessários
l10n-arb
Primeiro, crie um diretório sob o diretório raiz do projeto , que irá conter os arquivos que intl_generator
iremos gerar através do próximo comando arb
. Um arquivo simples arb
com 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 @@locale
ver que isso arb
corresponde à tradução chinesa simplificada, e title
os campos dentro correspondem à tradução chinesa simplificada do título do nosso aplicativo. @title
O campo contém title
algumas informações descritivas sobre o par.
Em seguida, lib
criamos um l10n
diretório sob o diretório, que é usado para salvar os arquivos de código arb
gerados a partir dos arquivos dart
.
Etapa 2: implementar as classes Localizações e Delegar
Semelhante aos passos da seção anterior, ainda precisamos implementar Localizations
e classificar, a diferença é que agora precisamos usar alguns métodos do pacote (alguns são gerados dinamicamente) Delegate
ao implementar .intl
Em seguida, lib/l10n
criamos um novo localization_intl.dart
arquivo " " 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:
1
O "messages_all.dart
" arquivo comentado é o código geradointl_generator
a partir do arquivo pela ferramenta , portanto este arquivo não existe até a primeira execução do comando build. O método noarb
comentário é o mesmo do arquivo " ", que é gerado ao mesmo tempo.2
initializeMessages()
messages_all.dart
- O comentário
3
é diferente do código de amostra da seção anterior, aqui podemosDemoLocalizations.load()
chamá-lo diretamente.
Etapa 3: adicionar atributos que exigem internacionalização
Agora podemos DemoLocalizations
adicionar propriedades ou métodos que precisam ser internacionalizados na classe, como title
as propriedades do exemplo de código acima. Neste momento, usaremos Intl
alguns 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:
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.plural
o método pode howMany
gerar diferentes informações de prompt quando os valores são diferentes. Intl
O pacote tem alguns outros métodos, veja sua documentação você mesmo.
Passo 4: Gerar arquivo arb
Agora podemos intl_generator
extrair as strings do código para um arb
arquivo 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, Intl
os atributos e strings que identificamos através da API serão extraídos para o l10n-arb/intl_messages.arb
arquivo " " 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 Locale
arquivo de recurso padrão. Se quisermos oferecer suporte ao chinês simplificado agora, precisamos apenas criar um intl_zh_CN.arb
arquivo " " no mesmo diretório do arquivo, intl_messages.arb
copiar o conteúdo de " " para o intl_zh_CN.arb
arquivo " " e depois traduzir para o inglês para chinês. , o conteúdo do intl_zh_CN.arb
arquivo " " 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 title
e remainingEmailsMessage
campo, description
que é a descrição do campo, que normalmente é mostrado ao tradutor e não será utilizado no código.
Há dois pontos a observar:
- Se um determinado atributo estiver faltando em um arquivo específico , o aplicativo carregará o atributo correspondente no arquivo
arb
padrão ( ), que é a estratégia ascendente.arb
intl_messages.arb
Intl
- Toda vez que você executar o comando de extração,
intl_messages.arb
ele será regenerado de acordo com o código, mas outrosarb
arquivos não, portanto, quando você quiser adicionar novos campos ou métodos, outrosarb
arquivos são incrementais, não se preocupe em sobrescrever.
arb
O 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 arb
baseado nos arquivos através dos seguintes passos .arb
dart
Etapa 5: Gerar código de dardo
O último passo é arb
gerar dart
o 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/l10n
vá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
windows
que 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 nogit bash
terminal
Se você encontrar conflitos
intl_translation
comintl
as versões dependentes dessas duas bibliotecas, você pode usar temporariamente a biblioteca intl_utils Como usar:
1. Coloquearb
o arquivointl_<LOCALE_ISO_CODE>.arb
no diretóriolib/l10n
no formato
2. Execute-o no terminal e o arquivo correspondenteflutter pub run intl_utils:generate
será ser geradolib/generate
sobdart
Resumir
Até agora, introduzimos o processo de usar Intl
o 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 arb
script separadamente quando concluímos a terceira etapa ou concluímos a tradução do arquivo. Criamos um script no diretório raiz intl.sh
com 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_generator
pacote 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 Intl
aprender 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 intl
o pacote, mas não tão problemático.
Depois de importar flutter_localizations
o pacote, siga as instruções abaixo para adicionar texto localizado ao seu aplicativo.
- Adicione
intl
o pacote como uma dependência, usandoany
comoflutter_localizations
valor da versão:
flutter pub add intl:any
- Além disso, no
pubspec.yaml
arquivo, ative o sinalizador de geração. Este item de configuração é adicionado napubspec
seção Flutter de , geralmente nopubspec
final do arquivo.
# The following section is specific to Flutter.
flutter:
generate: true # Add this line
- Adicione um novo
yaml
arquivo no diretório raiz do projeto Flutter, nomeado comol10n.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/l10n
arquivo app_en.arb
que fornece os modelos e os arquivos de localização gerados estão no app_localizations.dart
arquivo.
- Em
${FLUTTER_PROJECT}/lib/l10n
, adicioneapp_en.arb
o arquivo de modelo. do seguinte modo:
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
}
}
- Em seguida, adicione um arquivo no mesmo diretório
app_es.arb
para fazer uma tradução internacional da mesma mensagem, como o espanhol:
{
"helloWorld": "¡Hola Mundo!"
}
-
Agora, execute
flutter gen-l10n
o comando e você${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n
verá os arquivos gerados em formato . Você verá as classes geradas automaticamente
em formato .app_localizations.dart
AppLocalizations
Da mesma forma, você pode executar esse comando para gerar arquivos de localização quando o aplicativo não estiver em execução.
-
Depois de chamar
MaterialApp
o construtor de , importapp_localizations.dart
e useAppLocalizations.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(),
);
AppLocalizations
localizationsDelegates
As classes também podem gerar e listar automaticamente supportedLocales
sem precisar fornecê-las manualmente:
const MaterialApp(
title: 'Localizations Sample App',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
- 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, Text
o 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 arb
arquivo, o valor da chave de cada entrada é usado como getter
o 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 arb
o arquivo, copie-o para o diretório especificado e execute um comando simples, dart
o código será automaticamente gerado. Em seguida, apenas MaterialApp
pode 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
MaterialApp
Existe um title
atributo 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 title
ser internacionalizado. Mas o problema é que muitas configurações de internacionalização são MaterialApp
definidas acima e não podemos obter recursos localizados MaterialApp
por 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).title
ele irá relatar um erro, porque ele irá procurar da árvore atual para o topo , Localizations.of
mas 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:context
widget
DemoLocalizations
MaterialApp
DemoLocalizationsDelegate
DemoLocalizations
context
DemoLocalizations.of(context)
null
onGenerateTitle
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 localeListResolutionCallback
fazer a compatibilidade conforme descrito acima:
localeListResolutionCallback:
(List<Locale> locales, Iterable<Locale> supportedLocales) {
// 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')
}
referência: