Основы Flutter для начинающих (Часть V)

от автора

Наконец-то мы добрались до одной из самых важных тем, без которой идти дальше нет смысла.

План довольно простой: нам предстоит познакомиться с клиент-серверной архитектурой и реализовать получение списка постов.

В конце мы правильно организуем файлы наших страниц и вынесем элемент списка в отдельный файл.

Полетели!

Наш план
  • Часть 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';  // мы ещё не раз будем использовать  // константу SERVER const 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(); }  // не забываем расширяться от StateMVC class _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(); }  // не забываем расширяться от StateMVC class _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]);           },         ),       );     }   }     }

Заключение

В последующих частях мы ещё не раз будем сталкиваться с созданием сетевых запросов.

Я постарался кратко рассказать и показать на наглядном примере работу с сетью.

Надеюсь моя статья принесла вам пользу)

Ссылка на Github

Всем хорошего кода!

ссылка на оригинал статьи https://habr.com/ru/post/560964/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *