company_banner

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

    Про Flutter написано уже много статей. С каждым месяцем он становится всё популярнее. Поэтому я решил интерпретировать официальную документацию Flutter в лаконичный формат «вопрос — ответ». Думаю, многие, как и я, не имеют достаточно свободного времени для подробного изучения документации фреймворка, с которым они ещё не работают.

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



    Содержание:


    1. Views

    2. Intents

    3. Async UI

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

    5. Activities & Fragments

    6. Layouts

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

    8. ListViews & Adapters

    9. Работа с текстом

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

    11. Плагины Flutter

    12. Themes

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

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


    Views


    Вопрос:


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

    Ответ:


    Widget

    Отличия:


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

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

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


    При запуске на самом Android под капотом Widget находится View. Flutter включает в себя библиотеку Material Components. В ней собраны виджеты, которые реализуют гайдлайны Material 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),
          ),
        );
      }
    }

    Вопрос:


    Как верстать экран с виджетами? Где файл XML layout?

    Ответ:


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

    Пример:


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

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

    Вопрос:


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

    Ответ:


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

    Отличия:


    В Android можно сделать addView() или removeView() во ViewGroup. Во Flutter так нельзя, т.к. виджеты неизменны. Может изменяться только их состояние.

    Пример:


    Как поменять Text на Button по нажатию на FloatingActionButton.

    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> {
      // Дефолтное значение для флага
      bool toggle = true;
      void _toggle() {
        setState(() {
          toggle = !toggle;
        });
      }
    
      _getToggleChild() {
        if (toggle) {
          return Text('Toggle One');
        } else {
          return MaterialButton(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, который сообщает о перерисовке экрана.

    Отличия:


    В Android можно создавать анимации в XML или анимировать View с помощью animate(). Во Flutter анимацию нужно писать в коде с помощью AnimationController.

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


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

    Пример:


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

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(FadeAppTest());
    }
    
    class FadeAppTest 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() {
        super.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();
            },
          ),
        );
      }
    }

    Вопрос:


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

    Ответ:


    У Android и Flutter одинаковый API для Canvas, т.к. они используют одинаковый низкоуровневый движок Skia.

    Отличия:


    Нет.

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


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

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

    Пример:


    import 'package:flutter/material.dart';
    
    void main() => runApp(MaterialApp(home: DemoApp()));
    
    class DemoApp extends StatelessWidget {
      Widget build(BuildContext context) => Scaffold(body: Signature());
    }
    
    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),
        );
      }
    }
    
    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;
    }

    Вопрос:


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

    Ответ:


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

    Отличия:


    В Android мы можем наследоваться от интересующей нас View и дописать свою логику. Во Flutter это похоже на ViewGroup, только виджет всегда наследуется от 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"),
      );
    }

    Intents


    Вопрос:


    Какой аналог Intent во Flutter?

    Ответ:


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

    Для взаимодействия с внешними компонентами (например, камерой или файл-пикером) можно использовать плагины или нативную интеграцию на каждой платформе. Подробнее о нативной интеграции: Developing Packages and Plugins.

    Отличия:


    Во Flutter нет таких понятий, как Activity и Fragment. Есть Navigator (навигатор) и Routes (маршруты). Приложение на Flutter напоминает single-activity приложение, где разные экраны представляют собой разные фрагменты, а управляет ими FragmentManager. Navigator похож на FragmentManager по принципу работы. Он может сделать push() или pop() указанному вами маршруту. Route — это своего рода Fragment, но во Flutter его принято сравнивать с экраном или страницей.

    В Android мы описываем все Activities, между которыми можем навигировать в AndroidManifest.xml.

    Во Flutter есть два способа:

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

    Пример:


    void main() {
      runApp(MaterialApp(
        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');

    Вопрос:


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

    Ответ:


    Взаимодействуя с Android-слоем приложения через MethodChannel.

    Пример:


    Прописываем intent-filter в AndroidManifest.xml:

    <activity
      android:name=".MainActivity"
      android:launchMode="singleTop"
      android:theme="@style/LaunchTheme"
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
      android:hardwareAccelerated="true"
      android:windowSoftInputMode="adjustResize">
      <!-- ... -->
      <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
      </intent-filter>
    </activity>

    Обрабатываем Intent в MainActivity и из Flutter вызываем код через MethodChannel:

    package com.example.shared;
    
    import android.content.Intent;
    import android.os.Bundle;
    
    import java.nio.ByteBuffer;
    
    import io.flutter.app.FlutterActivity;
    import io.flutter.plugin.common.ActivityLifecycleListener;
    import io.flutter.plugin.common.MethodCall;
    import io.flutter.plugin.common.MethodChannel;
    import io.flutter.plugins.GeneratedPluginRegistrant;
    
    public class MainActivity extends FlutterActivity {
    
      private String sharedText;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
        Intent intent = getIntent();
        String action = intent.getAction();
        String type = intent.getType();
    
        if (Intent.ACTION_SEND.equals(action) && type != null) {
          if ("text/plain".equals(type)) {
            handleSendText(intent); // Handle text being sent
          }
        }
    
        new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
          new MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
              if (call.method.contentEquals("getSharedText")) {
                result.success(sharedText);
                sharedText = null;
              }
            }
          });
      }
    
      void handleSendText(Intent intent) {
        sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
      }
    }

    Запрашиваем данные, когда виджет начнёт отрисовываться:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.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 Shared App Handler',
          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> {
      static const platform = const MethodChannel('app.channel.shared.data');
      String dataShared = "No data";
    
      @override
      void initState() {
        super.initState();
        getSharedText();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(body: Center(child: Text(dataShared)));
      }
    
      getSharedText() async {
        var sharedData = await platform.invokeMethod("getSharedText");
        if (sharedData != null) {
          setState(() {
            dataShared = sharedData;
          });
        }
      }
    }

    Вопрос:


    Какой аналог у startActivityForResult()?

    Ответ:


    Ключевое слово await и результат Future-класса.

    Отличия:


    После вызова startActivityForResult() в Android нам нужно реализовывать обработку в onActivityResult(). Во Flutter ничего реализовывать не нужно, т.к. метод навигатора push() возвращает объект Future.

    Пример:


    Map coordinates = await Navigator.of(context).pushNamed('/location');

    И когда на экране '/location' получили координаты, делаем pop():

    Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

    Async UI


    Вопрос:


    Какой аналог у runOnUiThread() во 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).

    Отличия:


    «Из коробки» в Android можно использовать AsyncTask. В нём нужно реализовать onPreExecute(), doInBackground(), onPostExecute(). Во 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;
      }
    }

    Вопрос:


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

    Ответ:


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

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


    Пока в HTTP Package реализованы не все фичи из OkHttp, поэтому многие недостающие из них вынесены в абстракции и вы можете реализовать их самостоятельно по мере необходимости.

    Пример:


    Чтобы использовать 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.

    Отличия:


    В Android у ресурсов есть папка res и есть assets. Во Flutter есть только assets. Папка assets может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.

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


    Сопоставление размеров графических ресурсов в Android и Flutter.
    Android density qualifier Flutter pixel ratio
    ldpi 0.75x
    mdpi 1.0x
    hdpi 1.5x
    xhdpi 2.0x
    xxhdpi 3.0x
    xxxhdpi 4.0x
    Во Flutter для использования ресурсов в коде используется AssetManager или специализированные классы, начинающиеся с Asset.

    Пример:


    AssetManager:

    val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

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

    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.jpeg

    Использование 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)

    Вопрос:


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

    Ответ:


    pubspec.yaml.

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


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

    Activities & Fragments


    Вопрос:


    Какой аналог у Activity и Fragment во Flutter?

    Ответ:


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

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


    Flutter For Android Developers: How to design an Activity UI in Flutter.

    Вопрос:


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

    Ответ:


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

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


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

    • inactive — этот метод есть только в iOS, в Android нет аналога;
    • paused — аналогичен onPause() в Android;
    • resumed — аналогичен onPostResume() в Android;
    • suspending — аналогичен onStop в 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


    Вопрос:


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

    Ответ:


    Row — для горизонтального расположения, Column — для вертикального.

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


    Flutter For Android Developers: How to design LinearLayout in Flutter?

    Пример:


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

    Вопрос:


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

    Ответ:


    Виджет Stack.

    Подробнее:

    StackOverflow

    Вопрос:


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

    Ответ:


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

    Пример:


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

    Вопрос:


    Как обрабатывать переходы между portrait и landscape?

    Ответ:


    FlutterView обрабатывает перевороты, если AndroidManifest.xml содержит
    android:configChanges=«orientation|screenSize»

    Жесты и обработка 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();
                  }
                },
            ),
        ));
      }
    }

    ListViews & Adapters


    Вопрос:


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

    Ответ:


    ListView.

    Отличия:


    Во Flutter не нужно думать об очистке и повторном использовании элементов (чем занимается ListView/RecyclerView в Android, используя паттерн ViewHolder).

    Пример:


    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;
      }
    }

    Вопрос:


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

    Ответ:


    Оборачивая элемент в GestureDetector.

    Пример:


    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(GestureDetector(
            child: Padding(
                padding: EdgeInsets.all(10.0),
                child: Text("Row $i")),
            onTap: () {
              print('row tapped');
            },
          ));
        }
        return widgets;
      }
    }

    Вопрос:


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

    Ответ:


    Если у вас небольшой набор данных, то это можно сделать через setState(). Если набор данных большой, то через ListView.Builder, который является аналогом RecyclerView.

    Пример:


    Используя 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 = <Widget>[];
    
      @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 = <Widget>[];
    
      @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');
            });
          },
        );
      }
    }

    Работа с текстом


    Вопрос:


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

    Ответ:


    Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в 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.

    Форма ввода


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

    Вопрос:


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

    Ответ:


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

    Пример:


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

    Вопрос:


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

    Ответ:


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

    Пример:


    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> {
      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 em) {
        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(em);
      }
    }

    Плагины Flutter


    Вопрос:


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

    Ответ:


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

    Вопрос:


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

    Ответ:


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

    Вопрос:


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

    Ответ:


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

    Вопрос:


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

    Ответ:


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


    Вопрос:


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

    Ответ:


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

    Вопрос:


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

    Ответ:


    Написать свой плагин для взаимодействия вашего NDK-кода с Flutter. Пока Flutter не поддерживает прямое взаимодействие.

    Themes


    Вопрос:


    Как использовать тему (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(),
        );
      }
    }

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


    Вопрос:


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

    Ответ:


    С помощью Shared_Preferences plugin (для NSUserDefaults в iOS тоже).

    Пример:


    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);
    }

    Вопрос:


    Как получить доступ к SQLite во Flutter?

    Ответ:


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

    Уведомления


    Вопрос:


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

    Ответ:


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

    Заключение


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

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

      +4
      Наверное лучшая статья среди подобных, просто и ясно объяснят многие вещи по полочкам и ставит все на свои месте. Благодарю автора за проделанный труд!
        +2
        Автор, большое тебе человеческое спасибо, как раз такого справочника и не хватало, судя по заголовку будут и другие части? Было бы здорово все это в pdf-ку оформить. Кстати у меня тут вопрос — как получить данные из виджета, допустим текстовое поле (аналог EditText)
        TextFormField(
        decoration: InputDecoration(labelText: 'Enter Your username:'),
        )
        В Андроиде получали идентификатор объекта, далее String value = mEditText.getText().toString();
        вот во Flutter'e как это будет выглядеть?
          +1
          Спасибо за поддержку! В планах ещё части для iOS, React Native, Web, Xamarin.Forms разработчиков.
          По поводу получения текста — это делается через TextEditingController. Подробный пример можно посмотреть тут: flutter.dev/docs/cookbook/forms/retrieve-input
            0
            TextFormField Два способа… или через контролер
            У него есть такое поле controller а controller имеет поле text или же если оно обернуто в Form тогда срабатывает onSaved: (val) => print(val)
            +2
            А в дополненную реальность Flutter умеет?
              0
                0
                Хм, но Flutter же — это просто кроссплатформенный фреймворк, зачем в него тащить дополненную реальность? Или вас больше интересовало, есть ли какие-то обвязки вокруг существующих AR решений?
                  0
                  На возможное будущее, если вдруг Гугл решит ввести паралельно Фуксию Андроиду году в 23-25-м
                0
                Спасибо за статью!
                Определенно в закладки.
                  0
                  Автор, спасибо за статью

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

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