Введение
Появилось немного свободного времени и я решил сделать простенькое приложение для управления интернет-магазином на Flutter, так как давно хотел расширить свои познания в мобильной разработе, а flutter, на первый взгял, показался интересным фреймворком.
Немного теории
Flutter — SDK с открытым исходным кодом для создания мобильных приложений от компании Google. (Wikipedia). Основным языком разработки является Dart
Общая архитектура выглядит так:

Верхний уровень реализует наборы виджетов(Widget), такие как виджеты темы оформления, виджеты элементов и т.д.
Любое приложение на flutter это набор виджетов, выстренных в определенную иерархию:

Главным в иерархии идет виджет оформления, затем слой представления (например Scaffold), а на нем располагаются виджеты элементов и оформления, например формы, поля и т.д.
Существует 2 типа виджетов:
- StatelessWidget - используется когда не нужно управление какой-либо формой или состоянием. Например Text, Icon и тд. Состояние устанавливается при инициализации.
- StatefulWidget - виджеты которые могут изменять свое содержимое с течением времени и изменять состояние полученное при инициализации. Например Form, Image и тд. Для работы с состоянием надо переопределить метод createState.
Подробнее можно почитать здесь.
Получаем список товаров
После ознакомления с основными понятиями можно перейти к написанию кода нашего приложения.
Первый этап будет получение списка товаров с сервера.
Для начала напишем главный виджет приложения:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Товары',
theme: ThemeData(primarySwatch: Colors.blue),
home: new ListDisplay(),
);
}
}
В данном случае мы задаем окна, тему офрмления, и виджет ListDisplay
который отвечает за главный экран.
Код ListDisplay
следующий:
class ListDisplay extends StatefulWidget {
@override
_ListDisplayState createState() => new _ListDisplayState();
}
class _ListDisplayState extends State<ListDisplay> {
@override
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder(
future: fetchProducts(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return new Text('loading...');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return createListView(snapshot.data.products);
}
},
);
return new Scaffold(
appBar: new AppBar(title: new Text("Товары")),
body: futureBuilder,
);
}
}
Как видно из листинга, данный виджет имеет состояние, которое зависит от выполения отложенного вызова функции (future), данный вызов строится с помощью класса FutureBuilder. В нашем случае функция будет fetchProducts()
(описание в следующей главе).
Из кода видно что свойтво builder имеет замыкание, которое проверяет статус выполения отложенного вызова и в случае, когда он выполнится оно вренет виджет createListView
(описан ниже), на вход которому идут данные из функции fetchProducts.
После создания виджета отложенного вызова, оборачиваем его в слой отображения Scaffold.
Теперь посмотрим, что делает createListView
:
// класс для отображения списка товаров
Widget createListView(List items) {
return new ListView(
shrinkWrap: true,
primary: false,
children: items.map((productInfo) {
return ListTile(
leading: Tab(icon: Image.network('http://127.0.0.1:8080${productInfo.img}')),
title: Text(productInfo.name),
subtitle: Text('Цена: ${productInfo.price}'),
);
}).toList());
}
Данный виджет создает представление ListView
, на вход которому идет список полученных товаров (items), и каждый из них оборачивается в виджет заголовка ListTile
. Каждый товар должен соответствовать классу Product
описанному ниже.
Также надо отметить то, что для отображения полученной картинки в виде иконки, надо обернуть ее в виджет Tab
:
Tab(icon: Image.network('http://127.0.0.1:8080${productInfo.img}'))
Взаимодействие с API
Вернемся к функции fetchProducts
, ее код следующий:
Future<ProductList> fetchProducts() async {
final response =
await http.get('http://127.0.0.1:8080/api/products', headers: {
HttpHeaders.authorizationHeader:
"Bearer 11111111111111111111111"
});
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return ProductList.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
Данная функция делает GET запрос к API, и в случае удачного выполнения возвращает класс ProductList
:
class ProductList {
final List<Product> products;
ProductList({this.products});
factory ProductList.fromJson(List<dynamic> parsedJson) {
List<Product> products = new List<Product>();
products = parsedJson.map((i) => Product.fromJson(i)).toList();
return new ProductList(
products: products,
);
}
}
У указанного выше класса есть свойство products, которое является списком продуктов (класс Product
), и есть метод котороый на вход получает json список объектов, а затем у каждого из них вызывает свойство fromJson, которое преобразует полученные данные в класс Product
, в котором описаны нужные поля:
class Product {
final int id;
final String name;
final String img;
final String price;
Product({this.id, this.name, this.img, this.price});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
name: json['name'],
img: json['img'],
price: json['price'].toString(),
);
}
}
Если теперь запустить файл приложения, то можно увидеть следующее:

Добавление товара на сервер
Итак список товаров отображается. Осталось добавить кнопку добавления нового товара и форму для заполнения его данных
Форма нового товара будет выглядеть следующим образом:
import 'package:flutter/material.dart';
class NewProductForm extends StatefulWidget {
@override
_NewProductFormState createState() {
return _NewProductFormState();
}
}
class _NewProductFormState extends State<NewProductForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey we created above
return Scaffold(
appBar: AppBar(title: Text("Новый продукт")),
body: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new TextFormField(
decoration: const InputDecoration(
labelText: 'Название продукта',
),
validator: (value) {
if (value.isEmpty) {
return 'Введите название продукта';
}
},
),
new TextFormField(
decoration: const InputDecoration(
labelText: 'Цена',
),
validator: (value) {
if (value.isEmpty) {
return 'Укажите цену';
}
},
),
new Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
print("send data to server");
Navigator.pop(context);
}
},
child: Text('Сохранить'),
),
),
],
),
),
);
}
}
Из листинга видно что это виджет с состоянием, возвращает слой Scaffold, который содержащий форму с 2-мя обязательными полями:
- Название продукта
- Цена
Также у формы есть кнопка, которая при нажатии возвращается на предыдущий экран с помощью Navigator.pop(context)
. Сама функция отправки на сервер заглушена, так как кому интересно допишет ее сам без особого труда.
Теперь когда форма есть необходимо добавить переход на нее с главного экрана. Для этого в класс _ListDisplayState
в Scaffold необходимо добавить следующее (после body):
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewProductForm()),
);
},
child: Icon(Icons.add),
)
Класс floatingActionButton отвечает за кнопку в правом нижнем углу, а класс Navigator
отвечает за переход между слоями.
В итоге получилось следующее:

Заключение
В итоге после создания простого приложения Flutter мне более чем понравился, и, как мне кажется, его возможностей вполне достаточно чтобы быстро делать бизнес-приложения под обе платформы.
Полный код примера находится на Github.
comments powered by Disqus