flutter / website

Flutter documentation web site
https://docs.flutter.dev
Other
2.79k stars 3.2k forks source link

Cookbook recipes: POST/PUT/DELETE data to the internet. #3679

Closed petermichaux closed 4 years ago

petermichaux commented 4 years ago

The cookbook has the recipe Fetch data from the internet. Recipes also showing how to POST/PUT/DELETE data to the internet would be good to make a complete set of recipes for all four of the fundamental CRUD operations using FutureBuilder.

For example, the main point in the POST and PUT recipes would be to show that the future would be null to begin with and there would be no initState. When the future is null, the widget will show the save button. When a user presses the save button, then setState to change the future to non-null. When the future is non-null then the UI will show spinner, error message, or something indicating success. Anyway, it would be a very helpful companion to the "Fetch data from the internet" recipe.

With DELETE, there is an issue with what the future will return. It can't return null for success as then snapshot.hasData becomes complicated. It could return boolean value true.

Making this all easy for someone new would help get them up to speed with Flutter with less rethinking about things others have already had to think through.

Perhaps POST could be covered in one recipe called "Create data on the internet". PUT in a recipe called "Update data on the internet". DELETE in a recipe called "Delete data from the internet".

sfshaza2 commented 4 years ago

Good idea.

petermichaux commented 4 years ago

@sfshaza2 are pull requests welcome?

sfshaza2 commented 4 years ago

You betcha!!! We don't always suggest it, but they are always welcome.

petermichaux commented 4 years ago

Verify that the mock API can do what's necessary for the full set of GET/POST/PUT/DELETE recipes without complicating them with unrelated details. It looks like it can.

(I don't see anything for incremental paged loading of a list of resources. It would be nice for an infinite scroll example.)

Read/GET all resources

$ curl -i https://jsonplaceholder.typicode.com/albums
HTTP/2 200 
date: Mon, 10 Feb 2020 22:46:47 GMT
content-type: application/json; charset=utf-8
set-cookie: __cfduid=da95146d177867edae84c53042ca814901581374807; expires=Wed, 11-Mar-20 22:46:47 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: max-age=14400
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"2475-YUx9CiwTgoHXeL4210gtmqFIZNA"
via: 1.1 vegur
cf-cache-status: MISS
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 56319f00e9cef4a6-YVR

[
  {
    "userId": 1,
    "id": 1,
    "title": "quidem molestiae enim"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "sunt qui excepturi placeat culpa"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "omnis laborum odio"
  },
  {
    "userId": 1,
    "id": 4,
    "title": "non esse culpa molestiae omnis sed optio"
  },
  ...
]

Read/GET a resource

$ curl -i https://jsonplaceholder.typicode.com/albums/1
HTTP/2 200 
date: Mon, 10 Feb 2020 22:45:51 GMT
content-type: application/json; charset=utf-8
content-length: 64
set-cookie: __cfduid=d525317651095b8708feec80c00b5fc821581374751; expires=Wed, 11-Mar-20 22:45:51 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: max-age=14400
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"40-74G1+b66MteeTYAz6G+NybtDGFA"
via: 1.1 vegur
cf-cache-status: MISS
accept-ranges: bytes
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 56319da508c43af4-YVR

{
  "userId": 1,
  "id": 1,
  "title": "quidem molestiae enim"
}

Create/POST a resource. Verifying we can pretend that the userId parameter doesn't exist so that the example code doesn't involve concepts of authentication and a session user.

$ curl -i                                                       \
>      --header 'Content-Type: application/json; charset=UTF-8' \
>      --data '{"title":"Super Cool Mix Tape"}'                 \
>      https://jsonplaceholder.typicode.com/albums
HTTP/2 201 
date: Mon, 10 Feb 2020 22:58:23 GMT
content-type: application/json; charset=utf-8
content-length: 49
set-cookie: __cfduid=d8676a357807df96ad27eeba4b1c1ca8a1581375503; expires=Wed, 11-Mar-20 22:58:23 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
vary: Origin, X-HTTP-Method-Override, Accept-Encoding
access-control-allow-credentials: true
cache-control: no-cache
pragma: no-cache
expires: -1
access-control-expose-headers: Location
location: http://jsonplaceholder.typicode.com/albums/101
x-content-type-options: nosniff
etag: W/"31-stY/FnssNHUwY6wFqYq40cdZB4o"
via: 1.1 vegur
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 5631affedba6f4ba-YVR

{
  "title": "Super Cool Mix Tape",
  "id": 101
}

Update/PUT a resource. Again, verifying userId isn't required by the server API.

$ curl -i -X PUT                                                \
>      --header 'Content-Type: application/json; charset=UTF-8' \
>      --data '{"id": 1, "title":"Updated title"}'              \
>      https://jsonplaceholder.typicode.com/albums/1
HTTP/2 200 
date: Mon, 10 Feb 2020 22:55:38 GMT
content-type: application/json; charset=utf-8
content-length: 41
set-cookie: __cfduid=db769ca955f447cc90b485e86f39fc2981581375338; expires=Wed, 11-Mar-20 22:55:38 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: no-cache
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"29-ntvTOAUv21D/iEA6e5jBnEjOPXo"
via: 1.1 vegur
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 5631abf69ab13adc-YVR

{
  "id": 1,
  "title": "Updated title"
}

Delete/DELETE a resource.

$ curl -i -X DELETE https://jsonplaceholder.typicode.com/posts/1
HTTP/2 200 
date: Mon, 10 Feb 2020 22:59:55 GMT
content-type: application/json; charset=utf-8
content-length: 2
set-cookie: __cfduid=dea190c88efc87b2c7c8d2947b52ed8621581375595; expires=Wed, 11-Mar-20 22:59:55 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: no-cache
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8"
via: 1.1 vegur
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 5631b23f6ffbf4a6-YVR

{}
tedhenry100 commented 4 years ago

Here is a top Google search result that shows the confusion.

https://stackoverflow.com/questions/50014848/network-request-after-button-click-with-flutter

The original question is trying to find a way to relate POST with the cookbook's GET example.

The person who answered gives a poor solution that might call setState after dispose. This could happen if the back button is pressed before the POST request completes.

petermichaux commented 4 years ago

The following POST example app attempts to remain in the spirit of the GET recipe that is already in the cookbook. By that, I mean it uses FutureBuilder and avoids getting into more complex state management architecture like Provider, Bloc, etc. Another main goal was to keep the FutureBuilder builder's internal structure of if, else if, and default case as close as possible to the cookbook's existing "Fetch data from internet" recipe. Other goals were to avoid checking mounted, avoid calling setState after dispose, avoid adding post frame callbacks, avoid await in the UI code, avoid try and catch.

The use of _futureAlbum being set to null or not is the key feature in this example code that I wanted to propose for the cookbook article on POST.

UPDATE: I updated the code below to be simpler after some discussion of the pros and cons of several options.

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> createAlbum(final String title) async {
  final http.Response response = await http.post(
    'https://jsonplaceholder.typicode.com/albums',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  if (response.statusCode == 201) {
    return Album.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to create album.');
  }
}

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(final Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  Future<Album> _futureAlbum;

  @override
  Widget build(final BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Create Data Example'),
        ),
        body: Center(
          child: (this._futureAlbum == null)
              ? RaisedButton(
                  child: Text('Create Album'),
                  onPressed: () {
                    this.setState(() {
                      // The album name should come from a text input.
                      this._futureAlbum = createAlbum(
                        'Rubber Soul ${DateTime.now().millisecondsSinceEpoch}',
                      );
                    });
                  },
                )
              : FutureBuilder<Album>(
                  future: this._futureAlbum,
                  builder: (final BuildContext context, final AsyncSnapshot<Album> snapshot) {
                    if (snapshot.hasData) {
                      return Text(snapshot.data.title);
                    } else if (snapshot.hasError) {
                      return Text("${snapshot.error}");
                    }
                    // By default, show a loading spinner.
                    return CircularProgressIndicator();
                  },
                ),
        ),
      ),
    );
  }
}
petermichaux commented 4 years ago

@sfshaza2 Can you review and merge @hemanthrajv recipe for POST requests?

We expect that both PUT and DELETE recipes will be quite similar to the POST recipe so having the POST recipe merged is a good sign that continuing is worthwhile.

Thanks.