Sending data to a server is extremely important, as without it, you cannot search/lookup data, add new data, make changes, or even potentially remove data.
We’ll build off the idea of our last two examples of getting data from an Internet server with Flutter. And if we send data to a server, we’re almost always going to have to send a key to ensure we have the correct rights to be able to modify that data. To do that, we’ll need to build a form, which we learned how to do in Creating Form Fields in Flutter, and Getting Values on Demand.
In this example, we’re going to look at how we could create a new post. We’re going to assume we have the info needed to connect to the server from before already.
Creating the Post
We’re going to send data to https://jsonplaceholder.typicode.com/ using the http.post()
method. This is the same location we’ve been retrieving data from in the last two examples.
Future<http.Response> createPost(String title, String body) {
return http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
'body': body
}),
);
}
Notice how we are using jsonEncode to take information, and encode it into the JSON data format that got data from. While you might think that is obvious for a site called JSONPlaceHolder, many sites using REST APIs use the JSON data format to send and receive data.
Notice that we still return a Future object based upon the http.Response. This is because our desire to push data may fail, and/or there may be a success method. Your data provider should let you know what your expected responses are. In some cases, it will return the added/updated information to you, with any extra information that you didn’t provide.
Extra information not included by you, at least directly, might include things like dateAdded, id – since it is normally auto-generated, and author. The author might be set up based upon the authentication request you send with each piece of data. That might be tied to an account/user on the back end and then the authorId can be added from that.
Returned Data
We see that we’re going to get a response when we send data to the server. In this case, we are hoping for a status code of 201, which is a CREATED response. If we don’t get that, we’ll assume, and throw, an error. Your server may provide additional information you can test for and use, such as “data exists”, “improper rights”, etc.
Future<Post> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
'body': body,
}),
);
if (response.statusCode == 201) {
// If the server did return a 201 CREATED response, then parse the JSON.
return Post.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 201 CREATED response, then throw an exception.
throw Exception('Post creation failed for some reason.');
}
}
This returns a Post and you can modify it as necessary to display the single post like we did originally.
Taking User Input
Of course, the goal would be to take user input to upload to the server. While the goal in this case is to create a “fake” blog/news post, the same idea could be used to post on social media, add a review for a local company, place a job ad, or post for any other topic.
To do this, we will create a new screen (route) which will hold three Widgets as you see below. They would be for displaying FormTextField
s for the title and the body, as well as a button to know when to submit the post.
This widget we will call PostCreateScreen. This will be a Stateful Widget.
class PostCreateScreen extends StatefulWidget {
@override
_PostCreateScreen createState() => _PostCreateScreen();
static const routeName = '/post-create';
}
class _PostCreateScreen extends State<MyHomePage> {
final _titleController = TextEditingController();
final _bodyController = TextEditingController();
late Future<Post> _futurePost;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Single Post Display"),
),
body: Center(
child: Column()
);
}
}
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _titleController,
decoration: const InputDecoration(hintText: 'Enter Title'),
),
TextField(
controller: _bodyController,
decoration: const InputDecoration(hintText: 'Enter Post Body'),
),
ElevatedButton(
onPressed: () {
setState(() {
_futurePost = createPost(_titleController.text, _bodyController.text);
});
},
child: const Text('Submit Post!'),
),
],
)
As you can see, we use a similar process for creating this form as we did when we were Getting Values on Demand. You will have needed to created your controllers (two this time) and onPressed
, you will create your post.
We can create a FutureBuilder<Post> like we did in our previous example to display the results (maybe below the form). It would look something like:
FutureBuilder<Post>(
future: _futurePost,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Text(
snapshot.data!.title,
style: Theme.of(context).textTheme.headline3,
),
Text(snapshot.data!.body),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return Text('');
}
})
This means the whole widget will look like:
class _PostCreateScreen extends State<PostCreateScreen> {
final _titleController = TextEditingController();
final _bodyController = TextEditingController();
late Future<Post> _futurePost;
@override
void initState() {
super.initState();
_futurePost = fetchPost(-1);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Single Post Display"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _titleController,
decoration: const InputDecoration(hintText: 'Enter Title'),
),
TextField(
controller: _bodyController,
decoration: const InputDecoration(hintText: 'Enter Post Body'),
),
ElevatedButton(
onPressed: () {
setState(() {
_futurePost =
createPost(_titleController.text, _bodyController.text);
});
},
child: const Text('Submit Post!'),
),
FutureBuilder<Post>(
future: _futurePost,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Text(
snapshot.data!.title,
style: Theme.of(context).textTheme.headline3,
),
Text(snapshot.data!.body),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return Text('');
}
})
],
),
),
);
}
}
Creating a Link to the Create Post Route
Now we need to create a link to the PostCreateScreen. To do that, we’ll add an FloatingActionButton onto the main Homepage Route.
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Create a New Post',
child: Icon(Icons.add),
)
This is almost identical to the FloatingActionButton that is on the default app. We’ve changed the Tooltip, and removed the onPressed callback handler, but we’re going to add that next.
The onPressed function is going to navigate to a named route. So we’ll use the Navigator object to get us there.
onPressed: () {
Navigator.pushNamed(
context,
PostCreateScreen.routeName,
);
},
If you were to save and reload this however, you’d notice that it doesn’t work. That’s because we haven’t updated the routes yet. So now we’ll need to update the routes.
PostCreateScreen.routeName: (context) => PostCreateScreen(),
Since we put a variable called routeName inside of our PostCreateScreen Widget, we use that to simplify our process. Notice we cannot use const
as it is a stateful widget.
Sending Data to the Server in Flutter was originally found on Access 2 Learn