Learn about Flutter interaction, gesture and animation UI layout and controls familiar with Dart language writing first application development environment setup
In this article we will learn the basics of Flutter IO and then continue to develop an Echo client based on the Flutter Study Guide: Interaction, Gestures, and Animation. Since HTTP is more common than socket in daily development, our Echo client will use THE HTTP protocol to communicate with the server. The Echo server will also use Dart for implementation.
file
To perform file operations, we can use Dart’s IO package:
import 'dart:io';
Copy the code
Create a file
In Dart, we use the File class to perform File operations:
void foo() async {
const filepath = "path to your file";
var file = File(filepath);
try {
bool exists = await file.exists();
if(! exists) {
await file.create();
}
} catch (e) {
print(e);
}
}
Copy the code
IO is always slow compared to CPU, so most file operations return a Future and throw an exception if something goes wrong. You can also use the synchronized version if you want, all with the suffix Sync:
void foo() {
const filepath = "path to your file";
var file = File(filepath);
try {
bool exists = file.existsSync();
if(! exists) {
file.createSync();
}
} catch (e) {
print(e);
}
}
Copy the code
Async methods allow us to write asynchronous code in the same way that synchronous methods do, and the synchronous version of IO methods is no longer necessary. (Dart 1 does not support async functions, so synchronous methods are necessary.)
Write files
To write a String, we can use the writeAsString and writeAsBytes methods:
const filepath = "path to your file";
var file = File(filepath);
await file.writeAsString('Hello, Dart IO');
List<int> toBeWritten = [1.2.3];
await file.writeAsBytes(toBeWritten);
Copy the code
If only for writing files, you can also open an IOSink using openWrite:
void foo() async {
const filepath = "path to your file";
var file = File(filepath);
IOSink sink;
try {
sink = file.openWrite();
// The default write operation overwrites the original content; If you want to track content, use append mode
// sink = file.openWrite(mode: FileMode.append);
// Write () takes an Object and will execute obj.tostring () to convert it
// Writes the String to the file
sink.write('Hello, Dart');
// Call flush before the data is actually written out
await sink.flush();
} catch (e) {
print(e);
} finally {
sink? .close();
}
}
Copy the code
Read the file
Reading and writing raw bytes is also fairly simple:
var msg = await file.readAsString();
List<int> content = await file.readAsBytes();
Copy the code
Like writing a file, it also has an openRead method:
// Stream is a class in the async package
import 'dart:async';
// utf8 and LineSplitter belong to the convert package
import 'dart:convert';
import 'dart:io';
void foo() async {
const filepath = "path to your file";
var file = File(filepath);
try {
Stream<List<int>> stream = file.openRead();
var lines = stream
// Decode the content in UTF-8
.transform(utf8.decoder)
// Return one line at a time
.transform(LineSplitter());
await for (var line in lines) {
print(line);
}
} catch (e) {
print(e);
}
}
Copy the code
Finally, note that we read and write bytes using List<int>, and an int has 64 bits in Dart. Dart was originally designed for use on the Web, which is not as efficient.
JSON
Json-related apis are included in the convert package:
import 'dart:convert';
Copy the code
Convert the object to JSON
Suppose we have an object like this:
class Point {
int x;
int y;
String description;
Point(this.x, this.y, this.description);
}
Copy the code
To convert it toJson, we define a toJson method for it (note that we cannot change its method signature) :
class Point {
// ...
// Notice that our method has only one statement, which defines a map.
Dart automatically treats this map as the method return value when using this syntax
Map<String.dynamic> toJson() => {
'x': x,
'y': y,
'desc': description
};
}
Copy the code
Next we call the json.encode method to convert the object to JSON:
void main() {
var point = Point(2.12.'Some point');
var pointJson = json.encode(point);
print('pointJson = $pointJson');
// List and Map are supported
var points = [point, point];
var pointsJson = json.encode(points);
print('pointsJson = $pointsJson');
}
// Print out:
// pointJson = {"x":2,"y":12,"desc":"Some point"}
// pointsJson = [{"x":2,"y":12,"desc":"Some point"},{"x":2,"y":12,"desc":"Some point"}]
Copy the code
Convert JSON to objects
First, we add one more constructor to the Point class:
class Point {
// ...
Point.fromJson(Map<String.dynamic> map)
: x = map['x'], y = map['y'], description = map['desc'];
// Add a toString for later demonstrations
@override
String toString() {
return "Point{x=$x, y=$y, desc=$description}";
}
}
Copy the code
To parse JSON strings, we can use the json.decode method:
dynamic obj = json.decode(jsonString);
Copy the code
The reason for returning a Dynamic is that Dart doesn’t know what JSON is being passed in. If it is a JSON object, the return value will be a Map; If it is a JSON array, List<dynamic> is returned:
void main() {
var point = Point(2.12.'Some point');
var pointJson = json.encode(point);
print('pointJson = $pointJson');
var points = [point, point];
var pointsJson = json.encode(points);
print('pointsJson = $pointsJson');
print(' ');
var decoded = json.decode(pointJson);
print('decoded.runtimeType = ${decoded.runtimeType}');
var point2 = Point.fromJson(decoded);
print('point2 = $point2');
decoded = json.decode(pointsJson);
print('decoded.runtimeType = ${decoded.runtimeType}');
var points2 = <Point>[];
for (var map in decoded) {
points2.add(Point.fromJson(map));
}
print('points2 = $points2');
}
Copy the code
The running results are as follows:
pointJson = {"x": 2."y": 12."desc":"Some point"}
pointsJson = [{"x": 2."y": 12."desc":"Some point"}, {"x": 2."y": 12."desc":"Some point"}]
decoded.runtimeType = _InternalLinkedHashMap<String, dynamic>
point2 = Point{x=2, y=12, desc=Some point}
decoded.runtimeType = List<dynamic>
points2 = [Point{x=2, y=12, desc=Some point}, Point{x=2, y=12, desc=Some point}]
Copy the code
It is important to note that we define a constructor when we convert a Map to an object, but this is arbitrary, using static methods, Dart factory methods, and so on is possible. The toJson method prototype is qualified because json.encode only supports Map, List, String, int, and other built-in types. When it encounters a type it doesn’t recognize, it calls the object’s toJson method if it is not toEncodable (so the method prototype cannot be changed).
HTTP
To send HTTP requests to the server, we can use HttpClient in the IO package. But it didn’t really work that well, so someone came up with an HTTP package. To use HTTP packages, you need to modify pubspec.yaml:
# pubspec.yaml
dependencies:
HTTP: ^ 0.11.3 + 17
Copy the code
The use of HTTP packages is fairly straightforward. To issue a GET, use the http.get method; Corresponding, there are post, put, etc.
import 'package:http/http.dart' as http;
Future<String> getMessage() async {
try {
final response = await http.get('http://www.xxx.com/yyy/zzz');
if (response.statusCode == 200) {
return response.body;
}
} catch (e) {
print('getMessage: $e');
}
return null;
}
Copy the code
We’ll look at the HTTP POST example below when we implement the Echo client.
Use the SQLite database
The package sqflite allows us to use SQLite:
dependencies:
sqflite: any
Copy the code
Sqflite’s apis are very similar to those on Android, so let’s use an example to illustrate:
import 'package:sqflite/sqflite.dart';
class Todo {
static const columnId = 'id';
static const columnTitle = 'title';
static const columnContent = 'content';
int id;
String title;
String content;
Todo(this.title, this.content, [this.id]);
Todo.fromMap(Map<String.dynamic> map)
: id = map[columnId], title = map[columnTitle], content = map[columnContent];
Map<String.dynamic> toMap() => {
columnTitle: title,
columnContent: content,
};
@override
String toString() {
return 'Todo{id=$id, title=$title, content=$content}';
}
}
void foo() async {
const table = 'Todo';
// Sqflite provided by getDatabasesPath()
var path = await getDatabasesPath() + '/demo.db';
// Open the database with openDatabase
var database = await openDatabase(
path,
version: 1.
onCreate: (db, version) async {
var sql =' ' '
CREATE TABLE $table ('
${Todo.columnId} INTEGER PRIMARY KEY,'
${Todo.columnTitle} TEXT,'
${Todo.columnContent} TEXT'
)
' ' ';
The execute method can execute arbitrary SQL
await db.execute(sql);
}
);
// To make the result of each run the same, clear the data first
await database.delete(table);
var todo1 = Todo('Flutter'.'Learn Flutter widgets.');
var todo2 = Todo('Flutter'.'Learn how to to IO in Flutter.');
// Insert data
await database.insert(table, todo1.toMap());
await database.insert(table, todo2.toMap());
List<Map> list = await database.query(table);
// reassign todo.id so that it is not 0
todo1 = Todo.fromMap(list[0]);
todo2 = Todo.fromMap(list[1]);
print('query: todo1 = $todo1');
print('query: todo2 = $todo2');
todo1.content += ' Come on! ';
todo2.content += ' I\'m tired';
// Use transactions
await database.transaction((txn) async {
// Only TXN can be used. Using database directly causes a deadlock
await txn.update(table, todo1.toMap(),
// Where we can use? As placeholders, the corresponding values are placed in whereArgs in order
Todo1.id.tostring (); todo1.id.tostring ();
// Otherwise, we will use String and int to compare, which will not match the row to be updated
where: '${Todo.columnId} = ?', whereArgs: [todo1.id]);
await txn.update(table, todo2.toMap(),
where: '${Todo.columnId} = ?', whereArgs: [todo2.id]);
});
list = await database.query(table);
for (var map in list) {
var todo = Todo.fromMap(map);
print('updated: todo = $todo');
}
// Finally, don't forget to close the database
await database.close();
}
Copy the code
The running results are as follows:
query: todo1 = Todo{id=1, title=Flutter, content=Learn Flutter widgets}
query: todo2 = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter}
updated: todo = Todo{id=1, title=Flutter, content=Learn Flutter widgets. Come on! }
updated: todo = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter. I'm tired}
Copy the code
Readers with Android experience will find Dart much more comfortable writing database-related code. If the reader is not familiar with the database, you can refer to “SQL must Know must know”. This is the end of this article, as an exercise, let’s implement the echo client back end.
Echo client
The HTTP server
Before we get started, you can find the code from the previous article on GitHub, and we’ll build on it.
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout ux-basic
Copy the code
Server architecture
First let’s take a look at the architecture of the server side:
import 'dart:async';
import 'dart:io';
class HttpEchoServer {
final int port;
HttpServer httpServer;
// In Dart, the function is also a first class object
// Put the function inside the Map
Map<String.void Function(HttpRequest)> routes;
HttpEchoServer(this.port) {
_initRoutes();
}
void _initRoutes() {
routes = {
// We only support requests with path '/history' and '/echo'.
// history is used to obtain historical records;
// echo provides the echo service.
'/history': _history,
'/echo': _echo,
};
}
// Return a Future so that the client can do something after the start is complete
Future start() async {
// 1. Create an HttpServer
httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
// 2. Start listening for customer requests
return httpServer.listen((request) {
final path = request.uri.path;
final handler = routes[path];
if(handler ! =null) {
handler(request);
} else {
// Return a 404 to the client
request.response.statusCode = HttpStatus.notFound;
request.response.close();
}
});
}
void _history(HttpRequest request) {
// ...
}
void _echo(HttpRequest request) async {
// ...
}
void close() async {
var server = httpServer;
httpServer = null;
awaitserver? .close();
}
}
Copy the code
In the server framework, we add all the supported paths to routes. When we receive the customer request, we just need to take the corresponding handler function from Routes and distribute the request to the routes. If you are not interested in or familiar with server-side programming, you can skip this section.
Serialize the object to JSON
To serialize the Message object to JSON, here we make a few minor changes to Message:
class Message {
final String msg;
final int timestamp;
Message(this.msg, this.timestamp);
Message.create(String msg)
: msg = msg, timestamp = DateTime.now().millisecondsSinceEpoch;
Map<String.dynamic> toJson() => {
"msg": "$msg".
"timestamp": timestamp
};
@override
String toString() {
return 'Message{msg: $msg, timestamp: $timestamp}';
}
}
Copy the code
Here we add a toJson method. Here is the _echo method on the server side:
class HttpEchoServer {
static const GET = 'GET';
static const POST = 'POST';
const List<Message> messages = [];
// ...
_unsupportedMethod(HttpRequest request) {
request.response.statusCode = HttpStatus.methodNotAllowed;
request.response.close();
}
void _echo(HttpRequest request) async {
if(request.method ! = POST) {
_unsupportedMethod(request);
return;
}
// Get the body from the client POST request for more knowledge, reference
// https://www.dartlang.org/tutorials/dart-vm/httpserver
String body = await request.transform(utf8.decoder).join();
if(body ! =null) {
var message = Message.create(body);
messages.add(message);
request.response.statusCode = HttpStatus.ok;
Json is the object in the convert package. The encode method has a second parameter toEncodable. When the encountered object is not
// Dart's built-in object, if provided, is called to serialize the object; We didn't provide it here,
// So the encode method calls the object's toJson method, which we defined earlier
var data = json.encode(message);
// Write the response back to the client
request.response.write(data);
} else {
request.response.statusCode = HttpStatus.badRequest;
}
request.response.close();
}
}
Copy the code
The HTTP client
Our Echo server is developed using HttpServer from the DART: IO package. Alternatively, we could use HttpRequest in this package to perform HTTP requests, but we’re not going to do that here. The third-party library HTTP provides a much simpler interface.
First add the dependency to pubSpec:
# pubspec.yaml
dependencies:
#...
HTTP: ^ 0.11.3 + 17
Copy the code
The client implementation is as follows:
import 'package:http/http.dart' as http;
class HttpEchoClient {
final int port;
final String host;
HttpEchoClient(this.port): host = 'http://localhost:$port';
Future<Message> send(String msg) async {
// http.post is used to execute an HTTP POST request.
// The body argument is dynamic, which can support different types of body
// Just send the message directly to the server. Since MSG is a String,
// The post method automatically sets the HTTP content-type to text/plain
final response = await http.post(host + '/echo', body: msg);
if (response.statusCode == 200) {
Map<String.dynamic> msgJson = json.decode(response.body);
// Dart doesn't know what our Message looks like
// Map
to construct the object
,>
var message = Message.fromJson(msgJson);
return message;
} else {
return null;
}
}
}
class Message {
final String msg;
final int timestamp;
Message.fromJson(Map<String.dynamic> json)
: msg = json['msg'], timestamp = json['timestamp'];
// ...
}
Copy the code
Now, let’s combine them with the UI from the previous section. Start the server first, then create the client:
HttpEchoServer _server;
HttpEchoClient _client;
class _MessageListState extends State<MessageList> {
final List<Message> messages = [];
@override
void initState() {
super.initState();
const port = 6060;
_server = HttpEchoServer(port);
// initState is not an async function, here we cannot directly await _server.start(),
// future.then(...) "Await" is equivalent to await
_server.start().then((_) {
// Wait for the server to start before creating the client
_client = HttpEchoClient(port);
});
}
// ...
}
Copy the code
class MessageListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddMessageScreen())
);
// Here are the changes
if (_client == null) return;
// Now, instead of constructing a Message directly, we pass the Message through _client
// Send to the server
var msg = await _client.send(result);
if(msg ! =null) {
messageListKey.currentState.addMessage(msg);
} else {
debugPrint('fail to send $result');
}
},
// ...
)
);
}
}
Copy the code
We’re done, and after all that work, our app is now a real Echo client, although it looks the same. Now, we’re going to do something different — we’re going to preserve the historical record.
Historical record storage and recovery
Obtain the application storage path
To get the application’s file storage path, we introduce one more library:
# pubspec.yaml
dependencies:
#...
Path_provider: ^ 0.4.1
Copy the code
We can obtain the file, cache, and external storage paths of the application:
import 'package:path_provider/path_provider.dart' as path_provider;
class HttpEchoServer {
String historyFilepath;
Future start() async {
historyFilepath = await _historyPath();
// ...
}
Future<String> _historyPath() async {
// Get the application's private file directory
final directory = await path_provider.getApplicationDocumentsDirectory();
return directory.path + '/messages.json';
}
}
Copy the code
Keep historical records
class HttpEchoServer {
void _echo(HttpRequest request) async {
// ...
// Forgive me, let's save more times for simplicity
_storeMessages();
}
Future<bool> _storeMessages() async {
try {
// json. Encode supports List and Map
final data = json.encode(messages);
// File is the class in dart: IO
final file = File(historyFilepath);
final exists = await file.exists();
if(! exists) {
await file.create();
}
file.writeAsString(data);
return true;
// Even though file operations are asynchronous, we can still catch them in this way
// The exception they throw
} catch (e) {
print('_storeMessages: $e');
return false;
}
}
}
Copy the code
Loading history
class HttpEchoServer {
// ...
Future start() async {
historyFilepath = await _historyPath();
Load the history before starting the server
await _loadMessages();
httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
// ...
}
Future _loadMessages() async {
try {
var file = File(historyFilepath);
var exists = await file.exists();
if(! exists)return;
var content = await file.readAsString();
var list = json.decode(content);
for (var msg in list) {
var message = Message.fromJson(msg);
messages.add(message);
}
} catch (e) {
print('_loadMessages: $e');
}
}
}
Copy the code
Now, let’s implement the _history function:
class HttpEchoServer {
// ...
void _history(HttpRequest request) {
if(request.method ! = GET) {
_unsupportedMethod(request);
return;
}
String historyData = json.encode(messages);
request.response.write(historyData);
request.response.close();
}
}
Copy the code
The implementation of _history is straightforward, we just return messages all to the client.
Next comes the client part:
class HttpEchoClient {
// ...
Future<List<Message>> getHistory() async {
try {
// The HTTP package's get method is used to perform HTTP GET requests
final response = await http.get(host + '/history');
if (response.statusCode == 200) {
return _decodeHistory(response.body);
}
} catch (e) {
print('getHistory: $e');
}
return null;
}
List<Message> _decodeHistory(String response) {
// JSON array decode is a
var messages = json.decode(response);
var list = <Message>[];
for (var msgJson in messages) {
list.add(Message.fromJson(msgJson));
}
return list;
}
}
class _MessageListState extends State<MessageList> {
final List<Message> messages = [];
@override
void initState() {
super.initState();
const port = 6060;
_server = HttpEchoServer(port);
_server.start().then((_) {
// We wait for the server to start before creating the client
_client = HttpEchoClient(port);
// Pull the history immediately after the client is created
_client.getHistory().then((list) {
setState(() {
messages.addAll(list);
});
});
});
}
// ...
}
Copy the code
The life cycle
The last thing you need to do is shut down the server after your APP exits. This requires us to be notified of changes in the application lifecycle. For this purpose, Flutter provides a WidgetsBinding class (although not as useful as Android’s Lifecycle).
// To use WidgetsBinding, we inherit WidgetsBindingObserver and override the corresponding method
class _MessageListState extends State<MessageList> with WidgetsBindingObserver {
// ...
@override
void initState() {
// ...
_server.start().then((_) {
// ...
// Register the lifecycle callback
WidgetsBinding.instance.addObserver(this);
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
var server = _server;
_server = null;
server? .close();
}
}
}
Copy the code
Now, our application looks like this:
All the code can be found on GitHub:
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout io-basic
Copy the code
Use the SQLite database
In the previous implementation, we stored the echo server data in a file. Let’s change that in this section and store the data in SQLite.
Don’t forget to add dependencies:
dependencies:
sqflite: any
Copy the code
Initializing the database
import 'package:sqflite/sqflite.dart';
class HttpEchoServer {
// ...
static const tableName = 'History';
// This part of the constant is best placed in the definition of Message. I'll put it there for the sake of reading
static const columnId = 'id';
static const columnMsg = 'msg';
static const columnTimestamp = 'timestamp';
Database database;
Future start() async {
await _initDatabase();
// ...
}
Future _initDatabase() async {
var path = await getDatabasesPath() + '/history.db';
database = await openDatabase(
path,
version: 1.
onCreate: (db, version) async {
var sql = ' ' '
CREATE TABLE $tableName (
$columnId INTEGER PRIMARY KEY,
$columnMsg TEXT,
$columnTimestamp INTEGER
)
' ' ';
await db.execute(sql);
}
);
}
}
Copy the code
Loading history
The code for loading history is in the _loadMessages method, where we modify our implementation to load data from the database:
class HttpEchoServer {
// ...
Future _loadMessages() async {
var list = await database.query(
tableName,
columns: [columnMsg, columnTimestamp],
orderBy: columnId,
);
for (var item in list) {
// fromJson also works in database scenarios
var message = Message.fromJson(item);
messages.add(message);
}
}
}
Copy the code
In fact, by switching to database storage, we don’t need to store all messages in memory (i.e., _loadMessage is not necessary here). When the customer requests the history, we then read the data from the database on demand. To avoid modification to the program’s logic, a copy of the data is kept in memory. Interested readers can modify the program accordingly.
Save the record
Saving records is simple and can be done with one line of code:
void _echo(HttpRequest request) async {
// ...
_storeMessage(message);
}
void _storeMessage(Message msg) {
database.insert(tableName, msg.toJson());
}
Copy the code
With the JSON version, we need to save all the data each time. For the database, you just need to save the information you receive. Readers should also be able to sense that the version using SQLite is simpler and more efficient to implement for our needs.
Closing the database
The close method should also be modified accordingly:
void close() async {
// ...
var db = database;
database = null;
db? .close();
}
Copy the code
This section of the code can be seen in the tag echo-db:
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout echo-db
Copy the code
Welcome to scan code attention