[По докам] Flutter. Часть 2. Для iOS разработчиков +28



После длительного перерыва я продолжу рассказывать о популярном фреймворке Flutter в формате «вопрос — ответ». Первую статью для Android-разработчиков вы можете найти здесь, а сегодня будет полезный материал для разработчиков под iOS.

Если у вас мало времени для самостоятельного и глубокого изучения документации, но вы хотите понять, чем хорош Flutter и как его использовать, загляните под кат.



Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для React-Native-разработчиков
Flutter. Часть 4. Для Web-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков

Содержание:


  1. Views

  2. Navigation

  3. Threading & asynchronicity

  4. Структура проекта и ресурсы

  5. ViewControllers

  6. Layouts

  7. Жесты и обработка touch event

  8. Стилизация приложения

  9. Форма ввода

  10. Плагины Flutter

  11. Базы данных и локальное хранилище

  12. Уведомления



Views


Вопрос:


Какой аналог у UIView во Flutter?

Ответ:


Widget

Отличия:


UIView — фактически то, что будет на экране. Для отображения изменений вызывается setNeedsDisplay().

Widget — описание того, что будет на экране. Для изменения создаётся заново.

Дополнительная информация:


Flutter включает в себя библиотеку Cupertino Widgets. В ней собраны виджеты, которые реализуют гайдлайны Apple Design.

Вопрос:


Как обновлять отображение виджетов?

Ответ:


Используя StatefulWidget и его State. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.

Отличия:


StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.

StatefulWidget имеет состояние State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.

Пример:


1) StatelessWidget — Text

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

2) StatefulWidget — при нажатии на кнопку (FloatingActionButton) текст в виджете Text меняется с I Like Flutter на Flutter is Awesome!

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // Этот виджет корневой в приложении.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // дефолтный текст
  String textToShow = "Мне нравится Flutter";

  void _updateText() {
    setState(() {
      // обновление текста
      textToShow = "Flutter крутой!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Обновить текст',
        child: Icon(Icons.update),
      ),
    );
  }
}

Вопрос:


Как верстать экран с виджетами? Где Storyboard?

Ответ:


Во Flutter нет Storyboard. Всё верстается в дереве виджетов прямо в коде.

Пример:


@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: CupertinoButton(
        onPressed: () {
          setState(() { _pressedCount += 1; });
        },
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

Все дефолтные виджеты во Flutter можно посмотреть в widget catalog.

Вопрос:


Как добавить или удалить компонент в вёрстке во время работы приложения?

Ответ:


Через функцию, которая будет возвращать нужный виджет в зависимости от состояния.

Отличия:


В iOS можно сделать addSubview() или removeFromSuperview(). Во Flutter так нельзя, т.к. виджеты неизменны. Может изменяться только их состояние.

Пример:


Меняем Text на Button по нажатию на FloatingActionButton.

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

Вопрос:


Как анимировать виджеты?

Ответ:


Используя класс AnimationController, который является наследником абстрактного класса Animation<T>. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker, который сообщает о перерисовке экрана.

Отличия:


В iOS можно анимировать view с помощью animate(withDuration:animations:). Во Flutter анимацию нужно писать в коде с помощью AnimationController.

Дополнительная информация:


Более подробно можно изучить в Animation & Motion widgets, Animations tutorial и Animations overview.

Пример:


Fade-анимация лого Flutter.

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          child: FadeTransition(
            opacity: curve,
            child: FlutterLogo(
              size: 100.0,
            )
          )
        )
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }

  @override
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

Вопрос:


Как использовать CoreGraphics?

Ответ:


Flutter вместо CoreGraphics использует Canvas API на низкоуровневом движке Skia. В Android используется аналогичный Canvas API.

Дополнительная информация:


У Flutter есть два класса для рисования на Canvas: CustomPaint и CustomPainter. Второй реализует ваш алгоритм отрисовки.

Подробнее тут: StackOverflow

Пример:


class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset> points;

  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {

  List<Offset> _points = <Offset>[];

  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

Вопрос:


Как изменять прозрачность виджетов?

Ответ:


Обернуть в виджет Opacity.

Отличия:


В iOS у всех view есть .opacity или .alpha. Во Flutter этот параметр заменяет виджет-обёртка.

Вопрос:


Как создавать кастомные виджеты?

Ответ:


Компоновать виджеты внутри одного (вместо наследования).

Отличия:


В iOS можно наследоваться от интересующей нас view и дописать свою логику. Во Flutter виджет всегда наследуется от StatelessWidget или StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных вам виджетов в качестве параметров или полей.

Пример:


class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Navigation


Вопрос:


Как реализовывать навигацию между экранами во Flutter?

Ответ:


Для навигации между экранами используются классы Navigator и Route.

Отличия:


Во Flutter нет таких понятий, как UIViewController и UINavigationController. Есть Navigator (навигатор) и Routes (маршруты). Navigator похож на UINavigationController по принципу работы. Он может сделать push() или pop() указанному вами маршруту. Route — это своего рода UIViewController, но во Flutter его принято сравнивать с экраном или страницей.

Во Flutter есть два способа навигации:

  • описать Map с именами Route;
  • напрямую навигировать к Route.

Пример:


void main() {
  runApp(CupertinoApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

Navigator.of(context).pushNamed('/b');

Вопрос:


Как навигировать в стороннее приложение?

Ответ:


Либо взаимодействуя с iOS-слоем приложения через MethodChannel, либо используя плагин URL launcher.

Вопрос:


Как сделать pop back в iOS ViewController?

Ответ:


Вызовом SystemNavigator.pop().

Дополнительная информация:


SystemNavigator.pop() из Dart-кода вызывает следующий код в iOS:
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  if ([viewController isKindOfClass:[UINavigationController class]]) {
    [((UINavigationController*)viewController) popViewControllerAnimated:NO];
  }

Если это не то, что вам нужно, то вы можете сделать свою реализацию через MethodChannel.

Threading & asynchronicity


Вопрос:


Как писать асинхронный код во Flutter?

Ответ:


В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы, возможно, знакомы из C#, JavaScript или Kotlin coroutines.

Пример:


Выполнение запроса и возврата результата для обновления UI:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.

Пример:


Загрузка и обновление данных в ListView:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();

    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Вопрос:


Как выполнить код в фоновом потоке?

Ответ:


Как было сказано выше — с помощью async/await и изоляций (Isolate).

Отличия:


«Из коробки» в iOS можно использовать Operation с возможным переопределением методов. Во Flutter «из коробки» вам просто нужно использовать async/await, об остальном позаботится Dart.

Пример:


Здесь метод dataLoader() изолирован. В изоляциях вы можете запускать тяжелые операции, такие как парсинг больших JSON-ов, шифрование, обработка изображений и т.д.

loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate
static dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(json.decode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

Полноценный запускаемый пример:
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

    setState(() {
      widgets = msg;
    });
  }

// the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(json.decode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

Вопрос:


Как делать запросы к сети во Flutter?

Ответ:


Во Flutter есть свой HTTP package.

Пример:


Чтобы использовать HTTP package, добавьте его как зависимость в pubspec.yaml:

dependencies:
  ...
  http: ^0.11.3+16

Для выполнения запроса вызовите await в async функции http.get():

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Вопрос:


Как показывать прогресс выполнения?

Ответ:


С помощью виджета ProgressIndicator.

Пример:


import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Структура проекта и ресурсы


Вопрос:


Где хранить ресурсы разного разрешения?

Ответ:


В assets.

Отличия:


В iOS у графических ресурсов есть Images.xcasset, которые находятся в папке assets. Во Flutter есть только assets. Папка ресурсов может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.

Дополнительная информация:


Размеры графических ресурсов в iOS и Flutter идентичны и следуют density-based формату.

Расположение ресурсов:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

Путь в pubspec.yaml файле:

assets:
 - images/my_icon.png

Использование AssetImage:

return AssetImage("images/a_dot_burr.jpeg");

Использование asset напрямую:

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

Вопрос:


Где хранить строки? Как их локализовать?

Ответ:


Хранить в статичных полях. Локализовать с помощью intl package.

Пример:


class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

Text(Strings.welcomeMessage)

Вопрос:


Какой аналог CocoaPods? Как добавлять зависимости?

Ответ:


pubspec.yaml.

Дополнительная информация:


Flutter делегирует сборку нативным Android и iOS-сборщикам. Посмотреть список всех популярных библиотек для Flutter можно в Pub.

ViewControllers


Вопрос:


Какой аналог у ViewController во Flutter?

Ответ:


Во Flutter всё — виджеты. Роль ViewController для работы с UI выполняют виджеты. А роль навигации, как было сказано в пункте про навигацию, — Navigator и Route.

Вопрос:


Как обрабатывать события жизненного цикла?

Ответ:


С помощью WidgetsBinding и метода didChangeAppLifecycleState().

Дополнительная информация:


Во Flutter используется FlutterAppDelegate в нативном коде, и движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:

  • inactive — приложение находится в неактивном состоянии и не получает пользовательский ввод. Это состояние есть только в iOS, в Android нет аналога;
  • paused — приложение в данный момент невидимо для пользователя, не отвечает на ввод пользователя, но работает в фоновом режиме;
  • resumed — приложение видимо и отвечает на ввод пользователя;
  • suspending — приложение в процессе остановки. Это состояние есть только в Android, в iOS нет аналога.

Более подробно это описано в AppLifecycleStatus documentation.

Пример:


import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null)
      return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);

    return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
        textDirection: TextDirection.ltr);
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

Layouts


Вопрос:


Какой аналог у UITableView и UICollectionView?

Ответ:


ListView.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

Вопрос:


Как узнать, на каком элементе списка был клик?

Ответ:


Виджет, который является элементом списка, должен сам обрабатывать нажатие на него.

Отличия:


В iOS за это отвечает отдельный метод tableView:didSelectRowAtIndexPath:. Во Flutter элемент списка должен быть обёрнут в виджет, обрабатывающий клики, например GestureDetector.

Вопрос:


Как динамически обновить ListView?

Ответ:


Обновить список данных и вызвать setState().

Отличия:


В iOS для этого необходимо обновить данные и вызвать метод reloadData. Во Flutter после setState() виджет будет перерисован заново.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}


Дополнительная информация:


Для формирования списка рекомендуется использовать ListView.Builder.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}


Вопрос:


Какой аналог у UIScrollView?

Ответ:


ListView с виджетами.

Пример:


@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}


Дополнительная информация:


Более подробно тут.

Жесты и обработка touch event


Вопрос:


Как добавить слушатель onClick для виджета во Flutter?

Ответ:


Если виджет поддерживает клики, то в onPressed(). Если нет, то в onTap().

Пример:


В onPressed():

@override
Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () {
      print("click");
    },
    child: Text("Button"),
  );
}

В onTap():

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: FlutterLogo(
            size: 200.0,
          ),
          onTap: () {
            print("tap");
          },
        ),
      ),
    );
  }
}

Вопрос:


Как обрабатывать другие жесты на виджетах?

Ответ:


Используя GestureDetector. Им можно обрабатывать следующие действия:

Tap



Double tap



Long press



Vertical drag



Horizontal drag



Пример:


Обработка onDoubleTap:

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: RotationTransition(
            turns: curve,
            child: FlutterLogo(
              size: 200.0,
            )),
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
        ),
      ),
    );
  }
}


Стилизация приложения


Вопрос:


Как использовать тему (Theme) в приложении?

Ответ:


Используя виджет MaterialApp или WidgetApp как корневой в приложении.

Пример:


class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}


Вопрос:


Как использовать кастомные шрифты?

Ответ:


Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в pubspec.yaml.

Пример:


fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

Вопрос:


Как стилизовать текстовые виджеты?

Ответ:


С помощью параметров:

  • color;
  • decoration;
  • decorationColor;
  • decorationStyle;
  • fontFamily;
  • fontSize;
  • fontStyle;
  • fontWeight;
  • hashCode;
  • height;
  • inherit;
  • letterSpacing;
  • textBaseline;
  • wordSpacing.


Форма ввода


Вопрос:


Как получить результат пользовательского ввода?

Ответ:


С помощью TextEditingController.

Пример:

class _MyFormState extends State<MyForm> {
  // Create a text controller and use it to retrieve the current value.
  // of the TextField!
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the Widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: myController,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text the user has typed into our text field.
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text the user has typed in using our
                // TextEditingController
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: Icon(Icons.text_fields),
      ),
    );
  }
}


Более подробно написано здесь: Retrieve the value of a text field.

Вопрос:


Какой аналог у hint в TextInput?

Ответ:


Подсказку можно показать с помощью InputDecoration, передав его в качестве параметра конструктора в виджет.

Пример:


body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  ),
)

Вопрос:


Как показать ошибки валидации?

Ответ:


Всё так же — с помощью InputDecoration и его состояния.

Пример:


class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String emailString) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(emailString);
  }
}

Плагины Flutter


Вопрос:


Как получить доступ к GPS?

Ответ:


С помощью плагина geolocator.

Вопрос:


Как получить доступ к камере?

Ответ:


С помощью плагина image_picker.

Вопрос:


Как авторизоваться через Facebook?

Ответ:


С помощью плагина flutter_facebook_login.

Вопрос:


Как использовать Firebase?

Ответ:


Firebase поддерживает Flutter first party plugins:


Вопрос:


Как делать нативные (платформенные) вставки кода?

Ответ:


Flutter использует EventBus для взаимодействия с платформенным кодом. Подробно тут: developing packages and plugins.

Базы данных и локальное хранилище


Вопрос:


Как получить доступ к UserDefault?

Ответ:


С помощью Shared_Preferences plugin (для Shared Preferences в Android тоже).

Пример:


import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: RaisedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  prefs.setInt('counter', counter);
}

Вопрос:


Какой аналог у Core Data?

Ответ:


SQFlite.

Уведомления


Вопрос:


Как показать push-уведомление?

Ответ:


С помощью плагина Firebase_Messaging.

Заключение


Новые языки программирования и фреймворки появляются практически постоянно. И на старте трудно понять, что выстрелит и будет долго жить, а что забудут уже через год. Боб Мартин в своей книге «Идеальный программист» призывает нас изучать новые языки программирования и фреймворки. Чед Фаулер в книге «Программист-фанатик» советует всегда быть на острие технологий. Но как понять, что ты не ошибся с выбором? В 2016 году я обратил внимание на Kotlin, но из-за высокой загруженности не смог уделить ему достаточно времени до второй половины 2017. На старте многие относились к нему скептически, а сейчас это один из самых популярных языков программирования, и огромное количество разработчиков создают на нём свои продукты. Я чувствую, что за те полтора года мог бы получить более глубокое понимание тонкостей языка.
В том же 2016 году появился фреймворк Flutter на языке Dart. Но рост его популярности был не такой стремительный, и только в 2018 году о нём заговорили громко. Тогда мне тоже захотелось попробовать его в действии. И мне понравилось! Время покажет, какое будущее ждёт этот фреймворк, но кажется, он очень перспективный. (И если Google Fuchsia выстрелит, то, без сомнений, Flutter не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для мозга. На этом у меня всё. Да не сломает Apple ваш Store!

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (7):

  1. webmascon
    /#20954590 / +1

    Картинка под заголовком имеет вес 1,8 мегабайт. 1,8 мегабайт карл. И эти 1,8 мегабайт скачают миллионы посетителей сайта. Только чтобы посмотреть первую страницу сайта. 1,8 мегабайт. Каждый.

    • smartdev
      /#20955066 / +6

      Я так понимаю речь идёт о пользователях PC, т.к. в мобильной версии сайта и мобильном приложении:
      А) Нет превью статей, а показываются только заголовки;
      Б) Эта же картинка весит ~300 KB.

      А раз речь о пользователях PC, то следовательно это либо Wi-Fi, либо оптоволокно, либо 4G (модем или раздача с телефона). В картинке размером 1.8 MB с моей точки зрения всего 2 аспекта для беспокойства: потраченный трафик и кеширование в памяти.

      Скорее всего у большинства пользователей PC безлимитный трафик, поэтому скачивание беспокойство вызывать не должно. А если трафик лимитированный — то возникает вопрос: почему у такого пользователя всё ещё включена предзагрузка картинок? Ведь прилететь может отовсюду, та же лента в любой соцсети отожрёт больше трафика, чем первая страница хабра. Картинки в статью заливаются через habrastorage, где: “Доступные расширения: jpg, gif, png; ширина до 5000px; максимальный размер до 8 Мбайт”.

      Если зайти с позиции памяти на жёстком диске, то чаще всего браузеры используют сложные стратегии кеширования, в том числе LRU. И новые картинки просто вытеснят старый кэш и изменения в памяти заметно не будет.

      Я, конечно, попрошу графического дизайнера переделать картинку, но, кажется, беспокойство немного гиперболизированно.

      UPD

      Картинку обновили, теперь весит 250 KB.

      • webmascon
        /#20955470 / +1

        > А раз речь о пользователях PC,
        а также о пользователях айпадов. которые смотрят сайт по мобильному интернету. и этот мобильный интернет не безлимитный. в метро. и когда ваша картинка наконец загружается она сдвигает весь текст. потому что вы забыли о том, чт овсе картинки олжны иметь прописаный размер. так во всяком случае считалось правилом хорошего тона делать в 1997 году. чтобы дизайн сайта не плясал всякий раз когда новая картинка загружается.

  2. RomanKerimov
    /#20966210

    Лучше, чем UIKit, но хуже, чем SwiftUI.
    Зачем эти запятые после скобок? Зачем точка с запятой?
    Но обидно, что если Apple не откроет SwiftUI, то Flutter может победить.