company_banner

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

    После длительного перерыва я продолжу рассказывать о популярном фреймворке 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!
    • +28
    • 2,5k
    • 7
    FunCorp
    310,78
    Разработка развлекательных сервисов
    Поделиться публикацией

    Комментарии 7

      0

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

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

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

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

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

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

        UPD

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

          webmascon, вы предложения в области QA рассматриваете?)

            0
            в чем?
              0

              Предложения работы)

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

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

          Самое читаемое