Наконец-то мы добрались до одной из самых важных тем, без которой идти дальше нет смысла.
План довольно простой: нам предстоит познакомиться с клиент-серверной архитектурой и реализовать получение списка постов.
В конце мы правильно организуем файлы наших страниц и вынесем элемент списка в отдельный файл.
Полетели!
Наш план-
Часть 1- введение в разработку, первое приложение, понятие состояния;
-
Часть 2- файл pubspec.yaml и использование flutter в командной строке;
-
Часть 3- BottomNavigationBar и Navigator;
-
Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самых простых;
-
Часть 5 (текущая статья) - http пакет. Создание Repository класса, первые запросы, вывод списка постов;
-
Часть 6 - работа с формами, текстовые поля и создание поста.
-
Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;
-
Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;
-
Часть 9 - немного о тестировании;
Client и Server
Модель Client / Server лежит в основе всего Интернета и является наиболее распространенной.
В чем её суть?
Сначала разберемся что такое клиент и сервер:
-
Клиент - пользовательское устройство, которое отправляет запросы за сервер и получает ответы. Это может быть смартфон, компьютер или MacBook.
-
Сервер - специальный компьютер, который содержит данные, необходимые для пользователя.
Вся модель сводиться к примитивному принципу: клиент отправил запрос, сервер принял его, обработал и передал ответ клиенту.
Для организации взаимодействия сервера и клиента используются специальные протоколы. На текущий момент одним из самых распространенных протоколов в сети Интернет является http / https (s означает защищенный, secure).
http / https позволяет передавать почти все известные форматы данных: картинки, видео, текст.
Мы будем работать с JSON форматом.
JSON - простой и понятный формат данных, а главное легковесный, т.к. передается только текст.
Пример JSON:
[ { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }, { "userId": 1, "id": 2, "title": "qui est esse", "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" }, ...]
Здесь массив постов, который мы будем получать от сервера.
Обратите внимание: квадратные скобки указывает на массив данных, а фигурные на отдельный объект.
JSON позволяет создавать глубокую вложенность объектов и массивов:
{ "total_items" : 1 "result" : [ { "id" : 1, "name" : "Twillight Sparkle", "pony_type" : "alicorn", "friends" : [ "Starlight Glimmer", "Applejack", "Rarity", "Spike" ]} ]}
Понятие запроса
Для обмена данными клиент должен отправлять запросы на сервер.
Т.к. интернет в большинстве случаев использует http / https то запросы называются HTTP запросами.
Структура HTTP запроса:
-
URL - уникальный адрес в Интернете, который идентифицирует сервер и его конкретный ресурс, данные которого мы собираемся получить. В нашем случае URL выглядит следующим образом: https://jsonplaceholder.typicode.com/posts. (об структуре самого URL'а можно почитать в Википедии)
-
Метод, который определяет типа запроса. GET используется только для получения данных, POST позволяет клиенту добавить свои данные на сервер, DELETE - удалить их, PUT - изменить.
-
Данные запроса обычно называются телом запроса и используются совместно с POST, PUT и DELETE методами. Для GET метода в основном используются параметры самого URL'а. Выглядит это следующим образом: https://jsonplaceholder.typicode.com/posts/1 (здесь мы обращаемся к конкретному посту по его id = 1)
Запрос и вывод списка постов
Мы будем использовать довольно мощный и простой пакет
http
для отправки запросов на сервер.
Сначала убедимся, что мы указали его в pubspec.yaml
файле:
# блок зависимостейdependencies: flutter: sdk: flutter # подключение необходимых pub-пакетов # используется для произвольного размещения # компонентов в виде сетки flutter_staggered_grid_view: ^0.4.0 # мы будем использовать MVC паттерн mvc_pattern: ^7.0.0 # http предоставляет удобный интерфейс для создания# запросов и обработки ошибок http: ^0.13.3
Переходим к созданию классов модели.
Для этого создайте файл post.dart
в папке
models
:
// сначала создаем объект самого постаclass Post { // все поля являются private // это сделано для инкапсуляции данных final int _userId; final int _id; final String _title; final String _body; // создаем getters для наших полей // дабы только мы могли читать их int get userId => _userId; int get id => _id; String get title => _title; String get body => _body; // Dart позволяет создавать конструкторы с разными именами // В данном случае Post.fromJson(json) - это конструктор // здесь мы принимаем JSON объект поста и извлекаем его поля // обратите внимание, что dynamic переменная // может иметь разные типы: String, int, double и т.д. Post.fromJson(Map<String, dynamic> json) : this._userId = json["userId"], this._id = json["id"], this._title = json["title"], this._body = json["body"];}// PostList являются оберткой для массива постовclass PostList { final List<Post> posts = []; PostList.fromJson(List<dynamic> jsonItems) { for (var jsonItem in jsonItems) { posts.add(Post.fromJson(jsonItem)); } }}// наше представление будет получать объекты// этого класса и определять конкретный его// подтипabstract class PostResult {}// указывает на успешный запросclass PostResultSuccess extends PostResult { final PostList postList; PostResultSuccess(this.postList);}// произошла ошибкаclass PostResultFailure extends PostResult { final String error; PostResultFailure(this.error);}// загрузка данныхclass PostResultLoading extends PostResult { PostResultLoading();}
Одной из наиболее неприятных проблем является несоответствие типов.
Если взглянуть на JSON объект поста:
{ "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}
То можно заметить, что userId
и id
являются целыми числами, а title
и body
строками, поэтому в конструкторе Post.fromJson(json)
мы не замарачиваемся с привидением типов.
Пришло время создать Repository
класс.
Для этого создадим новую папку data
и в нем файл
repository.dart
:
import 'dart:convert';// импортируем http пакетimport 'package:http/http.dart' as http;import 'package:json_placeholder_app/models/post.dart';// мы ещё не раз будем использовать // константу SERVERconst String SERVER = "https://jsonplaceholder.typicode.com";class Repository { // обработку ошибок мы сделаем в контроллере // мы возвращаем Future объект, потому что // fetchPhotos асинхронная функция // асинхронные функции не блокируют UI Future<PostList> fetchPosts() async { // сначала создаем URL, по которому // мы будем делать запрос final url = Uri.parse("$SERVER/posts"); // делаем GET запрос final response = await http.get(url);// проверяем статус ответаif (response.statusCode == 200) { // если все ок то возвращаем посты // json.decode парсит ответ return PostList.fromJson(json.decode(response.body));} else { // в противном случае говорим об ошибке throw Exception("failed request");} }}
Вы скажите: мы могли все запихнуть в контроллер, зачем создавать ещё один класс?
Т.к. контроллеров может быть огромное количество и каждый из них будет обращаться к одному и тому же серверу, нам придеться дублировать логику.
К тому же это не очень гибко. Вдруг нам нужно будет поменять URL адрес сервера.
Реализуем PostController
:
import '../data/repository.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostController extends ControllerMVC { // создаем наш репозиторий final Repository repo = new Repository(); // конструктор нашего контроллера PostController(); // первоначальное состояние - загрузка данных PostResult currentState = PostResultLoading(); void init() async { try { // получаем данные из репозитория final postList = await repo.fetchPosts(); // если все ок то обновляем состояние на успешное setState(() => currentState = PostResultSuccess(postList)); } catch (error) { // в противном случае произошла ошибка setState(() => currentState = PostResultFailure("Нет интернета")); } }}
Заключительная часть: подключим наш контроллер к представлению и выведем посты:
import 'package:flutter/material.dart';import '../controllers/post_controller.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget { @override _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC { // ссылка на наш контроллер PostController _controller; // передаем наш контроллер StateMVC конструктору и // получаем на него ссылку _PostListPageState() : super(PostController()) { _controller = controller as PostController; } // после инициализации состояния // мы запрашивает данные у сервера @override void initState() { super.initState(); _controller.init(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post List Page"), ), body: _buildContent() ); } Widget _buildContent() { // первым делом получаем текущее состояние final state = _controller.currentState; if (state is PostResultLoading) { // загрузка return Center( child: CircularProgressIndicator(), ); } else if (state is PostResultFailure) { // ошибка return Center( child: Text( state.error, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red) ), ); } else { // отображаем список постов final posts = (state as PostResultSuccess).postList.posts; return Padding( padding: EdgeInsets.all(10), // ListView.builder создает элемент списка // только когда он видим на экране child: ListView.builder( itemCount: posts.length, itemBuilder: (context, index) { return _buildPostItem(posts[index]); }, ), ); } } // элемент списка Widget _buildPostItem(Post post) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(15)), border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3) ), margin: EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), color: Theme.of(context).primaryColor, ), padding: EdgeInsets.all(10), child: Text( post.title, textAlign: TextAlign.left, style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),), ), Container( child: Text( post.body, style: Theme.of(context).textTheme.bodyText2, ), padding: EdgeInsets.all(10), ), ], ) ); }}
Не пугайтесь если слишком много кода.
Все сразу освоить невозможно, поэтому не спешите)
Запуск
Попробуем запустить:

Вуаля! Теперь отключим интернет:

Все работает!
Небольшая заметка
Одним из важных принципов программирования является стремление к минимизации кода и его упрощению.
Файл post_list_page.dart
содержит всего 110 строк
кода, это не проблема. Но если бы он был в 10 или даже в 20 раз
больше!
Какой ужас был бы на глазах у того, кто взглянул бы на него.
Лучшей практикой считается выносить повторяющие фрагменты кода в отдельные файлы.
Давайте попробуем вынести функцию Widget
_buildItem(post)
в другой файл.
Для этого создадим для каждой группы страниц свою папку:

Затем в папке post создадим новый файл
post_list_item.dart:
import 'package:flutter/material.dart';import '../../models/post.dart';// элемент спискаclass PostListItem extends StatelessWidget { final Post post; // элемент списка отображает один пост PostListItem(this.post); Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(15)), border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3) ), margin: EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), color: Theme.of(context).primaryColor, ), padding: EdgeInsets.all(10), child: Text( post.title, textAlign: TextAlign.left, style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),), ), Container( child: Text( post.body, style: Theme.of(context).textTheme.bodyText2, ), padding: EdgeInsets.all(10), ), ], ) ); }}
Не забудьте удалить ненужный код из
post_list_page.dart
:
import 'package:flutter/material.dart';import '../../controllers/post_controller.dart';import '../../models/post.dart';import 'post_list_item.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget { @override _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC { // ссылка на наш контроллер PostController _controller; // передаем наш контроллер StateMVC конструктору и // получаем на него ссылку _PostListPageState() : super(PostController()) { _controller = controller as PostController; } // после инициализации состояние // мы запрашивает данные у сервера @override void initState() { super.initState(); _controller.init(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post List Page"), ), body: _buildContent() ); } Widget _buildContent() { // первым делом получаем текущее состояние final state = _controller.currentState; if (state is PostResultLoading) { // загрузка return Center( child: CircularProgressIndicator(), ); } else if (state is PostResultFailure) { // ошибка return Center( child: Text( state.error, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red) ), ); } else { // отображаем список постов final posts = (state as PostResultSuccess).postList.posts; return Padding( padding: EdgeInsets.all(10), // ListView.builder создает элемент списка // только когда он видим на экране child: ListView.builder( itemCount: posts.length, itemBuilder: (context, index) { // мы вынесли элемент списка в // отдельный виджет return PostListItem(posts[index]); }, ), ); } } }
Заключение
В последующих частях мы ещё не раз будем сталкиваться с созданием сетевых запросов.
Я постарался кратко рассказать и показать на наглядном примере работу с сетью.
Надеюсь моя статья принесла вам пользу)
Всем хорошего кода!