Простое приложение на Flutter

Введение

Появилось немного свободного времени и я решил сделать простенькое приложение для управления интернет-магазином на 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