内容简介:In today’s world, smartphones have become the primary source of entertainment, banking, photo/videography and shopping. To do many of the things their users request, from ordering food to booking movie tickets, apps on your smartphone needs Internet access
In today’s world, smartphones have become the primary source of entertainment, banking, photo/videography and shopping. To do many of the things their users request, from ordering food to booking movie tickets, apps on your smartphone needs Internet access.
If you plan to develop apps that fetch data from the Internet, you’ll need to know how to make network requests and how to handle the responses properly. Throughout this tutorial, you’ll learn how to do just that by building a Flutter app.
Getting Started
You’ll build an app named Favorite Books
that will download a list of popular books including details like title
, description
and author’s name
and display it in a ListView
. You can add your favorite book to the existing list and the details will upload to the server. You can even delete a book from the list.
While building this app, you’ll learn the following things:
- Locally deploying a RESTful API using the Aqueduct framework.
- Making GET , POST and DELETE requests.
- Using an HTTP client to make the network request.
-
Understanding
Future
,async
andawait
. - Handling different states like Loading , Success and Error .
Note : This tutorial assumes prior knowledge of Dart and the Flutter framework for developing cross-platform mobile apps. If you are unfamiliar with Flutter, please see Getting Started with Flutter .
To begin, download the starter project using the Download Materials button at the top or bottom of this tutorial. Open it in Visual Studio Code, Android Studio or your favorite editor and build the project. Run it to see the current state of the app:
Note
: If you have any issues building the app, enter the command line flutter packages get
in a terminal in the project root directory. This will fetch any missing dependencies.
Some Important Terminology
Before you start coding, take a moment to be sure that you understand the terminology that you’ll see throughout this tutorial.
What is a Network Request?
In simple terms, when you open an app like Whatsapp, Instagram or Twitter on your smartphone, the app tries to fetch the latest data from a remote location, usually called a Server . It then displays that information to the user. A server is a centralized place that stores most user data. The app that you’re using is called the Client .
So a network request is a request for data from a client to a server.
What is a RESTful API?
REST stands for REpresentational State Transfer. It’s an application program interface (API) that uses HTTP requests to get or send data between computers.
Communication between a client and a server mostly happens through RESTful APIs.
The most basic form of a REST API is a URL that the client uses to make a request to the server. Upon receiving a successful request, the server checks the endpoint of the URL, does some processing and sends the requested data or resource back to the client.
HTTP Methods
There are four main HTTP methods that you use in REST APIs. Here’s what each of them does:
- GET : Requests a representation of the specified resource. Requests using GET only retrieve data – they should have no other effect on the data.
- POST : Submits data to the specified resource. You use this method to send data to the server, such as customer information or file uploads.
- DELETE : Deletes the specified resource.
- PUT : Replaces all current representations of the target resource with the uploaded content.
Now that you have some theory under your belt, you can move on to exploring the starter project.
Exploring the Project
Once you’ve run the starter project, it’s time to take a look at the project structure. Start by expanding the lib folder and checking the folders within it. It should look like this:
Here’s what each package inside the lib directory does:
- model : This package consists of data model classes for each type of network response.
- network : This package holds the networking logic of the app.
- ui : Contains different UI screens for the app.
Open pubspec.yaml
in the project root, and notice that there is an http
package added under dependencies
. You will be using the http
package created by the official dart team to make HTTP requests:
dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 http: ^0.12.0+3
Deploying REST Apps
Inside the downloadable material’s folder, you’ll find a file named book_api_app.zip . That project serves as the backend/server for your Flutter app for this tutorial. It will handle all the requests coming from your app and provide you with the appropriate response.
The backend project uses the Aqueduct framework, which is an HTTP web server framework for building REST applications written in Dart. You won’t be getting into how to write server-side code using Dart; you will just deploy this project and focus on implementing the networking logic on the frontend, i.e the Flutter app, to communicate with the backend.
To start making HTTP requests to the book_api_app , you need to deploy the app locally on your machine.
Follow the steps to deploy an Aqueduct app locally:
- Unzip the book_api_app.zip file.
-
Type this command in your terminal from the root of the
book_api_app
project:flutter pub get
. -
Type this command in your terminal from the root of the
book_api_app
project:flutter pub global activate aqueduct
. -
The previous step will give you a warning about your path, which you can fix with a command like
export PATH="$PATH":"$HOME/bin/flutter/.pub-cache/bin"
. Run theexport
command you are given by the previous step. The specific path will be a subdirectory of where yourflutter
install is located. -
Now, execute the command:
aqueduct serve
in the project folder.
You should see something like the following in your terminal:
The base URL for this app after locally deploying the application is http://localhost:8888
.
Making GET Requests
The simplest and easiest way to make a GET
request is to load the URL with the endpoint as /books
on a browser.
Copy http://localhost:8888/books
and enter it into your favorite browser. You should see the following output in your browser’s page:
The above image shows the JSON body of a successful GET request. The response body is a list of favorite books. Depending on your browser settings, the JSON may not be formatted quite as nicely as the screenshot.
Now, go back to your terminal. You should see a GET request near the bottom:
Whenever a request is made to the book_api_app app, the app prints out the status code of the response in the terminal.
Next, you’ll make the same GET request, but this time you’ll add the request logic in the app and display it in a Flutter ListView .
Creating Data Model Classes
As you can see, the response from the server is in the form of JSON, which Dart cannot interpret directly. So you have to convert the JSON response to something that Dart can understand, i.e parse the JSON and store the details in Dart objects. Back in the starter project, navigate to libs/model/library.dart . You’ll create a data model class that will hold the logic to convert the JSON response to a custom Dart object.
Looking at the JSON response you can see it consists of an array of JSON elements that holds a number of JSON objects. Each JSON object consist of a book detail.
Inside libs/model/library.dart
, create a class Book
which will hold the details of a book:
class Book { final String name; final String author; final String description; Book({this.name, this.author, this.description}); factory Book.fromJson(Map<String, dynamic> json) => Book( name: json["name"], author: json["author"], description: json["description"]); Map<String, dynamic> toJson() => {"name": name, "author": author, "description": description}; }
A Book
holds the properties of a book and provides a custom Dart object. Note that factory
constructors are used to prepare calculated values to forward them as parameters to a normal constructor so that final fields can be initialized with them.
Now in the same file create a class Library
that will convert the JSON to a list of Book
‘s object:
class Library { final List<Book> books; // 1 Library({this.books}); factory Library.fromRawJson(String str) => Library.fromJson(json.decode(str)); // 2 factory Library.fromJson(Map<String, dynamic> json) => Library( books: List<Book>.from( json["bookList"].map((x) => Book.fromJson(x)))); Map<String, dynamic> toJson() => { "bookList": List<dynamic>.from(books.map((x) => x.toJson())), }; }
Add a necessary import to the top of the file:
import 'dart:convert';
In the Library
code:
-
Library
holds a list ofBook
objects. -
json.decode(jsonString)
converts the JSON string to aMap
object. The key in theMap
object will hold the key of the JSON object, and its value will hold the value of that particular key.
When you make a POST request to add the details of your favorite book, you’ll get the following response:
{ "message": "Book details have been added successfully" }
You need to parse the JSON and convert it to custom Dart object similar to what you did in the previous step.
Navigate to lib/model
and open network_reponse.dart
. Create a class NetworkResponse
which will parse the JSON and hold the message sent from the server:
import 'dart:convert'; class NetworkResponse { final String message; NetworkResponse({this.message}); factory NetworkResponse.fromRawJson(String str) => NetworkResponse.fromJson(json.decode(str)); factory NetworkResponse.fromJson(Map<String, dynamic> json) => NetworkResponse(message: json["message"]); Map<String, dynamic> toJson() => {"message": message}; }
Next, you need to understand what an HTTP client is and the importance of using one in the app.
Understanding the HTTP Client
To be able to send a request from your app and get a response from the server in HTTP format, you use an HTTP client. Navigate to lib/network
and open book_client.dart
. BookClient
is a custom client created specifically to make requests to your book_api
backend.
class BookClient { // 1 static const String _baseUrl = "http://127.0.0.1:8888"; // 2 final Client _client; BookClient(this._client); // 3 Future<Response> request({@required RequestType requestType, @required String path, dynamic parameter = Nothing}) async {_ // 4 switch (requestType) { case RequestType.GET: return _client.get("$_baseUrl/$path"); case RequestType.POST: return _client.post("$_baseUrl/$path", headers: {"Content-Type": "application/json"}, body: json.encode(parameter)); case RequestType.DELETE: return _client.delete("$_baseUrl/$path"); default: return throw RequestTypeNotFoundException("The HTTP request method is not found"); } } }
In the above implementation:
- All the requests made through this client will go to the baseUrl , i.e. the book_api’s url.
-
BookClient
uses the http.dartClient
internally. AClient
is an interface for HTTP clients that takes care of maintaining persistent connections across multiple requests to the same server. -
The
request()
method takes the following parameters:-
RequestType
is an enum class holding the different type of HTTP methods available. -
path
is the endpoint to which the request has to be made. -
parameter
holds the additional information to make a successful HTTP request. For example:body
for a POST request.
-
-
The
request()
method executes the proper HTTP request based on therequestType
specified.
You could customize the request()
method by adding more HTTP methods in the switch
statement, e.g. PUT
or PATCH
, based on your requirements.
Next, you’ll implement the network logic to make the GET
request and parse the JSON
response into Library
.
Implementing Network Logic
Navigate to lib/network
and open remote_data_source.dart
. This class will hold the logic to make calls to different endpoints like addBook
or deleteBook
and return the result to the upper layer, which can be a view or a repository layer. This type of segregation of data sources into separate classes is part of a layered architecture such asBLoC or Redux.
You have to create an HTTP
client that’s responsible for making network requests to a server. Replace the first TODO
with the following line of code:
BookClient client = BookClient(Client());
Note
: You’ll see a red line under the word Client
and BookClient
. Select each one of them and hit option + return
on macOS or Alt+Enter
on a PC. Select the Import Library
option from the dropdown menu.
Breaking down the above code:
-
BookClient
abstracts the implementation of HTTP requests from theRemoteDataSource
since it’s only responsibility is to hold the business logic. -
Client()
creates anIOClient
ifdart:io
is available and aBrowserClient
ifdart:html
is available, otherwise it will throw an unsupported error.
Replace the second TODO
with the following code:
//1 Future<Result> getBooks() async { try { //2 final response = await client.request(requestType: RequestType.GET, path: "books"); if (response.statusCode == 200) { //3 return Result<Library>.success(Library.fromRawJson(response.body)); } else { return Result.error("Book list not available"); } } catch (error) { return Result.error("Something went wrong!"); } }
Use the same hot-key as above for your platform to import any needed files.
Breaking down the code above:
-
The return type of the method is
Future
. A future represents the result of an asynchronous operation, and can have two states: completed or uncompleted. -
client.request(requestType: RequestType.GET, path: "books")
will make a GET request to the/books
endpoint with an asynchronous call using the keywordawait
. -
Result
is a generic class which has three subclasses:LoadingState
,SuccessState
andErrorState
.
Building the ListView
In the previous section, you implemented the network logic to make a GET
request and fetch the list of books. Now, you’ll display those books in a ListView
.
Navigate to lib/ui/favorite_book_screen and open favorite_book_screen.dart .
Now, to show the result of the network response, you need to call getBooks()
from within FavoriteBooksScreen
and wait for the result.
In order to access the getBooks()
method you need to create an instance of RemoteDataSource
inside _FavoriteBooksScreenState
. Replace the first TODO
with the following code:
RemoteDataSource _apiResponse = RemoteDataSource();
Next, you need to fetch the list of your favorite books from the backend and display them in a list. To perform this sort of operation Flutter provides a very handy widget named FutureBuilder
. You can use that widget to get the task done. Update the second TODO
by replaing the current child
with the following code:
child: FutureBuilder( future: _apiResponse.getBooks(), builder: (BuildContext context, AsyncSnapshot<Result> snapshot) { if (snapshot.data is SuccessState) { Library bookCollection = (snapshot.data as SuccessState).value; return ListView.builder( itemCount: bookCollection.books.length, itemBuilder: (context, index) { return bookListItem(index, bookCollection, context); }); } else if (snapshot.data is ErrorState) { String errorMessage = (snapshot.data as ErrorState).msg; return Text(errorMessage); } else { return CircularProgressIndicator(); } }), )
Looking over this code, you see that:
-
You’ve replaced
Text
withFutureBuilder
. As the name suggests, this widget builds itself based on the latest snapshot of interaction with aFuture
. -
FutureBuilder
takes an instance ofFuture
to which it is currently connected. -
AsyncSnapshot
holds the result of the HTTP response. Based on the snapshot’s data, it provides an appropriate widget. For example, aCircularProgressIndicator
during the fetching of data from the server. -
bookListItem()
, which you’ll add next, will return aListTile
widget for each book item from the collection. TheseListTile
widgets will be presented in a vertical stack in aListView
widget.
Now implement bookListItem()
to return a ListTile
widget containing the details of a book from the collection of favorite books. Add the following code at the bottom of the _FavoriteBooksScreenState
class:
ListTile bookListItem( int index, Library bookCollection, BuildContext context) { return ListTile( leading: Image.asset("images/book.png"), title: Text(bookCollection.books[index].name), subtitle: Text( bookCollection.books[index].description, maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.caption, ), isThreeLine: true, trailing: Text( bookCollection.books[index].author, style: Theme.of(context).textTheme.caption, ), ); }
Note
: You would normally never make network calls inside build
, because build
could get called every frame. Making 60 calls every second is a worst-case scenario. But for the simplicity of the tutorial, you’re going to break that rule and make the call in build
.
Breaking down the above implementation:
ListTile ListTile
Displaying the Favorite Books List
Note
: If you are following along with the tutorial using an Android emulator, you need to run this command in a Terminal window after you startup the emulator, in order to have correct port forwarding from the emulator: adb reverse tcp:8888 tcp:8888
. The adb
version you use must be the one inside your local Android SDK installation, which is often in ~/Android/Sdk/platform-tools
. The output from the command should be the port 8888.
Build and run the app and see what output you get. If the backend REST app is up and running, you should see the following output:
Making a POST Request
Next, you’ll add the network logic to upload the details of your favorite book by sending the data through a POST
request. Go back to lib/network/remote_data_source.dart
. In the third TODO
, you create a StreamController
object:
StreamController<Result> _addBookStream;
Here’s what you’ll be using the StreamController for:
StreamController
allows sending data, error and done events on its stream. You’ll use it to send the Result
to the UI and to update it accordingly. StreamController
has two important getters: sink
and stream
. The sink
is of type StreamSink
which has a method named add
that passes events to the sink. You use stream
to get the event that was added to the sink.
The fourth TODO
initializes the StreamController
. Add the following code inside init()
:
_addBookStream = StreamController();
Now, you’ll add the logic to make a POST
request. Replace the fifth TODO
with the following method:
//1 void addBook(Book book) async { _addBookStream.sink.add(Result<String>.loading("Loading")); try { //2 final response = await client.request( requestType: RequestType.POST, path: "addBook", parameter: book); if (response.statusCode == 200) { //3 _addBookStream.sink.add(Result<NetworkResponse>.success( NetworkResponse.fromRawJson(response.body))); } else { _addBookStream.sink.add(Result.error("Something went wrong")); } } catch (error) { _addBookStream.sink.add(Result.error("Something went wrong!")); } }
Breaking down this code:
-
addBook
takes aBook
as a parameter. -
client.request()
makes a POST request to the endpoint,/addBook
. You pass thebook
as an argument. -
_addStream.sink.add(...)
adds the event to theStreamSink
. Now,stream
can provide these events to the UI and update it accordingly.
Next, you’ll create a getter method in RemoteDataSource
that returns the stream
of the StreamController
so that the user can see it in the UI. To do this, replace the sixth TODO
with the following code:
Stream<Result> hasBookAdded() => _addBookStream.stream;
Since you opened a stream to add events, you must close the stream when you’re done observing the changes. Otherwise, you’ll get unwanted memory leaks.
In dispose()
, replace the seventh TODO
with the following code:
_addBookStream.close();
Updating the Add Book Screen
Navigate to lib/ui/addscreen
and open add_book_screen.dart
. The first TODO
is to create the RemoteDataSource
object. Replace the first TODO
with the following code:
RemoteDataSource _apiResponse = RemoteDataSource();
You need to initialize the remote data source in initState()
of _AddBookScreenState
. Update the second TODO
using the following code:
@override void initState() { super.initState(); _apiResponse.init(); }
In this code:
-
initState()
is a method which is called once when the stateful widget is inserted in the widget tree. -
You call
initState()
when you add AddBookScreen to the widget tree. You’ll call this method only once, when AddBookScreen is first created.
Next, you have to listen to the stream
, exposed by the RemoteDataSource
object, for the Result
that will be delivered through the sink
after the POST
request completes.
To do this, replace the third TODO
with the following code:
void hasBookAddedListener() { //1 _apiResponse.hasBookAdded().listen((Result result) { //2 if (result is LoadingState) { showProgressDialog(); //3 } else if (result is SuccessState) { Navigator.pop(context); Navigator.pop(context); //4 } else { SnackBar( content: Text("Unable to add book"), duration: Duration(seconds: 2), ); } }); }
Breaking down the code:
-
listen
adds a subscription to the stream. -
LoadingState
will show a progress dialog. -
In
SuccessState
, you’ll navigate back to the “Favorite Book” screen. -
ErrorState
will show aSnackBar
with the error message.
Update initState
to call the method you just added:
@override void initState() { super.initState(); _apiResponse.init(); hasBookAddedListener(); }
Finally, you’ll add the logic to submit the book’s detail and make a POST request to upload the details that the user enters.
Replace the fourth TODO
with the following code:
final book = Book( name: _name, author: _author, description: _description); _apiResponse.addBook(book);
That will collect the details of your book from the TextField
and make a POST request.
Congrats! Build and run the app, click the add button, and try adding your favorite book’s details:
If the POST request was successful, you’ll see your book’s details at the end of the list in Favorite Book screen.
Making a DELETE Request
Now, you’ll add a feature to delete a particular book detail from the favorite list.
Open RemoteDataSource
and replace the eighth TODO
with the following code snippet:
//1 Future<Result> deleteBook(int index) async { try { //2 final response = await client.request( requestType: RequestType.DELETE, path: "deleteBook/$index"); if (response.statusCode == 200) { return Result<NetworkResponse>.success( NetworkResponse.fromRawJson(response.body)); } else { return Result<NetworkResponse>.error( NetworkResponse.fromRawJson(response.body)); } } catch (error) { return Result.error("Something went wrong!"); } }
Here’s what this code does:
-
deleteBook()
takes the position of the book object in the list that the user wants to delete. The return type of this method isFuture<Result>
. -
client.request(...)
performs the DELETE request.
Next, add the swipe to delete feature in the Favorite Book screen.
Open favorite_book_screen.dart
and replace the previous bookListItem
code with the following:
//1 Dismissible bookListItem( int index, Library bookCollection, BuildContext context) { return Dismissible( //2 onDismissed: (direction) async { Result result = await _apiResponse.deleteBook(index); if (result is SuccessState) { //3 setState(() { bookCollection.books.removeAt(index); }); } }, background: Container( color: Colors.red, ), key: Key(bookCollection.books[index].name), child: ListTile( leading: Image.asset("images/book.png"), title: Text(bookCollection.books[index].name), subtitle: Text( bookCollection.books[index].description, maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.caption, ), isThreeLine: true, trailing: Text( bookCollection.books[index].author, style: Theme.of(context).textTheme.caption, ), ), ); }
Breaking down the new code:
-
You’ve wrapped the
ListTitle
with aDismissible
widget. -
In
onDismissed()
, you pass the index of the book item you want to delete from the backend todeleteBook()
. -
If the
DELETE
request is successful, callingsetState()
removes the item from the list of books.
Build and run the app and try swiping on one of the book items in the list. If the DELETE request is successful, you’ll see the item removed from the list.
Where to Go From Here?
Check out the final completed project by clicking the Download Materials button at the top or bottom of the tutorial.
To learn more about Flutter networking, take a look at the official documentation:
You can also explore more about the following topics:
We hope you enjoyed this tutorial on how to make network requests in the Flutter app! Feel free to share your feedback, findings or ask any questions in the comments below or in the forums.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。