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 == nullreturn;

          // 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<MessageListwith 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:

flutter-echo-demo

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








Programming · Thinking · workplace


Welcome to scan code attention