- PVSM.RU - https://www.pvsm.ru -

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

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

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

Содержание:

  1. Views [1]

  2. Intents [9]

  3. Async UI [13]

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

  5. Activities & Fragments [21]

  6. Layouts [24]

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

  8. ListViews & Adapters [32]

  9. Работа с текстом [36]

  10. Форма ввода [39]

  11. Плагины Flutter [42]

  12. Themes [49]

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

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

Views

Вопрос:

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

Ответ:

Widget [57].

Отличия:

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

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

При запуске на самом Android под капотом Widget находится View. Flutter включает в себя библиотеку Material Components [58]. В ней собраны виджеты, которые реализуют гайдлайны Material Design [59].

Вопрос:

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

Ответ:

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

Отличия:

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 [63].

Вопрос:

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

Ответ:

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

Отличия:

В 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 [64], который является наследником абстрактного класса Animation<T> [65]. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker [66], который сообщает о перерисовке экрана.

Отличия:

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

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

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

Пример:

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 [70]?

Ответ:

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

Отличия:

Нет.

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

У Flatter есть два класса для рисования на Canvas — CustomPaint [72] и CustomPainter [73]. Второй реализует ваш алгоритм отрисовки.
Подробнее тут: StackOverflow [74].

Пример:

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 [75] во Flutter?

Ответ:

Его нет. Для навигации между экранами используются классы Navigator [76] и Route [77].
Для взаимодействия с внешними компонентами (например, камерой или файл-пикером) можно использовать плагины [78] или нативную интеграцию на каждой платформе. Подробнее о нативной интеграции: Developing Packages and Plugins [79].

Отличия:

Во Flutter нет таких понятий, как Activity и Fragment. Есть Navigator (навигатор) и Routes (маршруты). Приложение на Flutter напоминает single-activity приложение, где разные экраны представляют собой разные фрагменты, а управляет ими FragmentManager. Navigator похож на FragmentManager по принципу работы. Он может сделать push() [80] или pop() [81] указанному вами маршруту. 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 [82].

Пример:

Прописываем 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() [83]?

Ответ:

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

Отличия:

После вызова 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() [85] во Flutter?

Ответ:

В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates [86]). Для асинхронного выполнения используется 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() [87] для перерисовки дерева виджетов с новыми данными.

Пример:

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

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() [89], doInBackground() [90], onPostExecute() [91]. Во 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 [92] во Flutter?

Ответ:

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

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

Пока в 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 [94].

Пример:

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 [95]:

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

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

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

Вопрос:

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

Ответ:

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

Пример:

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

Text(Strings.welcomeMessage)

Вопрос:

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

Ответ:

pubspec.yaml.

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

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

Activities & Fragments

Вопрос:

Какой аналог у Activity [98] и Fragment [99] во Flutter?

Ответ:

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

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

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

Вопрос:

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

Ответ:

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

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

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

  • inactive — этот метод есть только в iOS, в Android нет аналога;
  • paused — аналогичен onPause() в Android;
  • resumed — аналогичен onPostResume() в Android;
  • suspending — аналогичен onStop в Android, в iOS нет аналога.

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

Пример:

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 [104]?

Ответ:

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

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

Flutter For Android Developers: How to design LinearLayout in Flutter? [107]

Пример:

@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 [108]?

Ответ:

Виджет Stack [109].

Подробнее:

StackOverflow [110].

Вопрос:

Какой аналог у ScrollView [111]?

Ответ:

ListView [88] с виджетами.

Пример:

@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 [112]. Им можно обрабатывать следующие действия:

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 [125] во Flutter?

Ответ:

ListView [88].

Отличия:

Во 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 [112].

Пример:

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 [88]?

Ответ:

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

Пример:

Используя setState() [87]:

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 [126]:

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 [128].

Вопрос:

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

Ответ:

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

Пример:

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

Вопрос:

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

Ответ:

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

Пример:

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 [131].

Вопрос:

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

Ответ:

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

Вопрос:

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

Ответ:

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

Вопрос:

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

Ответ:

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

Вопрос:

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

Ответ:

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

Вопрос:

Как использовать 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 [143] (для 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 [144].

Уведомления

Вопрос:

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

Ответ:

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

Заключение

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

Автор: smartdev

Источник [147]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/framework/310614

Ссылки в тексте:

[1] Views: #views

[2] Какой аналог у View во Flutter?: #view_analog

[3] Как обновлять отображение виджетов?: #widget_update

[4] Как верстать экран с виджетами? Где файл XML layout?: #widget_layout

[5] Как добавить или удалить компонент в вёрстку во время работы приложения?: #widget_remove_add

[6] Как анимировать виджеты?: #widget_animation

[7] Как использовать Canvas?: #canvas

[8] Как создавать кастомные виджеты?: #widget_custom

[9] Intents: #intents

[10] Какой аналог Intent во Flutter?: #intent_analog

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

[12] Какой аналог у startActivityForResult()?: #for_result_analog

[13] Async UI: #async_ui

[14] Какой аналог у runOnUiThread() во Flutter?: #run_on_ui_analog

[15] Как выполнить код в фоновом потоке?: #background_thread

[16] Какой аналог у OkHttp во Flutter?: #okhttp_analog

[17] Как показывать прогресс выполнения?: #progress

[18] Структура проекта и ресурсы: #project

[19] Где хранить ресурсы разного разрешения?: #resources

[20] Где хранить строки? Как их локализовать?: #strings

[21] Activities & Fragments: #activities_fragments

[22] Какой аналог у Activity и Fragment во Flutter?: #activity_fragment_analog

[23] Как обрабатывать события жизненного цикла?: #lifecycle

[24] Layouts: #layouts

[25] Какой аналог у LinearLayout?: #linear_layout

[26] Какой аналог у RelativeLayout?: #relative_layout

[27] Какой аналог у ScrollView?: #scrollview

[28] Как обрабатывать переходы между portrait и landscape?: #orientation

[29] Жесты и обработка touch event.: #gestures

[30] Как добавить слушатель onClick для виджета во Flutter?: #on_click

[31] Как обрабатывать другие жесты на виджетах?: #widget_gestures

[32] ListViews & Adapters: #listview_adapters

[33] Какой аналог у ListView во Flutter?: #listview_analog

[34] Как узнать на каком элементе было нажатие?: #item_click

[35] Как динамически обновить ListView?: #listview_update

[36] Работа с текстом: #work_with_text

[37] Как использовать кастомные шрифты?: #custom_fonts

[38] Как стилизовать текстовые виджеты?: #text_widgets_style

[39] Форма ввода: #input

[40] Какой аналог у hint в Input?: #hint

[41] Как показать ошибки валидации?: #validation_error

[42] Плагины Flutter: #plugins

[43] Как получить доступ к GPS?: #gps

[44] Как получить доступ к камере?: #camera

[45] Как авторизоваться через Facebook?: #facebook

[46] Как использовать Firebase?: #firebase

[47] Как делать нативные (платформенные) вставки кода?: #native_code

[48] Как использовать NDK?: #ndk

[49] Themes: #themes

[50] Как использовать тему (Theme) в приложении?: #theme

[51] Базы данных и локальное хранилище: #database

[52] Как получить доступ к Shared Preferences?: #shared_preferences

[53] Как получить доступ к SQLite во Flutter?: #sqlite

[54] Уведомления: #notifications

[55] Как показать push-уведомление?: #push

[56] View: https://developer.android.com/reference/android/view/View

[57] Widget: https://flutter.dev/docs/development/ui/widgets-intro

[58] Material Components: https://material.io/develop/flutter/

[59] гайдлайны Material Design: https://material.io/design/

[60] StatefulWidget: https://docs.flutter.io/flutter/widgets/StatefulWidget-class.html

[61] State: https://docs.flutter.io/flutter/widgets/State-class.html

[62] StatelessWidget: https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html

[63] widget catalog: https://flutter.io/docs/development/ui/widgets/layout

[64] AnimationController: https://docs.flutter.io/flutter/animation/AnimationController-class.html

[65] Animation<T>: https://docs.flutter.io/flutter/animation/Animation-class.html

[66] Ticker: https://docs.flutter.io/flutter/scheduler/Ticker-class.html

[67] Animation & Motion widgets: https://flutter.io/docs/development/ui/widgets/animation

[68] Animations tutorial: https://flutter.io/docs/development/ui/animations/tutorial

[69] Animations overview: https://flutter.io/docs/development/ui/animations

[70] Canvas: https://docs.flutter.io/flutter/dart-ui/Canvas-class.html

[71] Skia: https://skia.org/

[72] CustomPaint: https://docs.flutter.io/flutter/widgets/CustomPaint-class.html

[73] CustomPainter: https://docs.flutter.io/flutter/rendering/CustomPainter-class.html

[74] StackOverflow: https://stackoverflow.com/questions/46241071/create-signature-area-for-mobile-app-in-dart-flutter

[75] Intent: https://developer.android.com/reference/android/content/Intent

[76] Navigator: https://docs.flutter.io/flutter/widgets/Navigator-class.html

[77] Route: https://docs.flutter.io/flutter/widgets/Route-class.html

[78] плагины: https://pub.dartlang.org/flutter/

[79] Developing Packages and Plugins: https://flutter.io/docs/development/packages-and-plugins/developing-packages

[80] push(): https://docs.flutter.io/flutter/widgets/Navigator/push.html

[81] pop(): https://docs.flutter.io/flutter/widgets/Navigator/pop.html

[82] MethodChannel: https://docs.flutter.io/flutter/services/MethodChannel-class.html

[83] startActivityForResult(): https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent,%20int)

[84] Future-класса: https://docs.flutter.io/flutter/dart-async/Future-class.html

[85] runOnUiThread(): https://developer.android.com/reference/android/app/Activity.html#runOnUiThread(java.lang.Runnable)

[86] Isolates: https://docs.flutter.io/flutter/dart-isolate/Isolate-class.html

[87] setState(): https://docs.flutter.io/flutter/widgets/State/setState.html

[88] ListView: https://docs.flutter.io/flutter/widgets/ListView-class.html

[89] onPreExecute(): https://developer.android.com/reference/android/os/AsyncTask.html#onPreExecute()

[90] doInBackground(): https://developer.android.com/reference/android/os/AsyncTask.html#doInBackground(Params...)

[91] onPostExecute(): https://developer.android.com/reference/android/os/AsyncTask.html#onPostExecute(Result)

[92] OkHttp: https://square.github.io/okhttp/

[93] HTTP package: https://pub.dartlang.org/packages/http

[94] ProgressIndicator: https://docs.flutter.io/flutter/material/ProgressIndicator-class.html

[95] AssetImage: https://docs.flutter.io/flutter/painting/AssetImage-class.html

[96] intl package: https://pub.dartlang.org/packages/intl

[97] Pub: https://pub.dartlang.org/flutter/packages/

[98] Activity: https://developer.android.com/reference/android/app/Activity

[99] Fragment: https://developer.android.com/reference/android/app/Fragment

[100] Flutter For Android Developers: How to design an Activity UI in Flutter: https://medium.com/@burhanrashid52/flutter-for-android-developers-how-to-design-activity-ui-in-flutter-4bf7b0de1e48

[101] WidgetsBinding: https://docs.flutter.io/flutter/widgets/WidgetsBinding-mixin.html

[102] didChangeAppLifecycleState(): https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver/didChangeAppLifecycleState.html

[103] AppLifecycleStatus documentation: https://docs.flutter.io/flutter/dart-ui/AppLifecycleState-class.html

[104] LinearLayout: https://developer.android.com/reference/android/widget/LinearLayout

[105] Row: https://docs.flutter.io/flutter/widgets/Row-class.html

[106] Column: https://docs.flutter.io/flutter/widgets/Column-class.html

[107] Flutter For Android Developers: How to design LinearLayout in Flutter?: https://medium.com/@burhanrashid52/flutter-for-android-developers-how-to-design-linearlayout-in-flutter-5d819c0ddf1a

[108] RelativeLayout: https://developer.android.com/reference/android/widget/RelativeLayout

[109] Stack: https://docs.flutter.io/flutter/widgets/Stack-class.html

[110] StackOverflow: https://stackoverflow.com/questions/44396075/equivalent-of-relativelayout-in-flutter

[111] ScrollView: https://developer.android.com/reference/android/widget/ScrollView

[112] GestureDetector: https://docs.flutter.io/flutter/widgets/GestureDetector-class.html

[113] onTapDown: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapDown.html

[114] onTapUp: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapUp.html

[115] onTap: https://docs.flutter.io/flutter/widgets/GestureDetector/onTap.html

[116] onTapCancel: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapCancel.html

[117] onDoubleTap: https://docs.flutter.io/flutter/widgets/GestureDetector/onDoubleTap.html

[118] onLongPress: https://docs.flutter.io/flutter/widgets/GestureDetector/onLongPress.html

[119] onVerticalDragStart: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragStart.html

[120] onVerticalDragUpdate: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragUpdate.html

[121] onVerticalDragEnd: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragEnd.html

[122] onHorizontalDragStart: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragStart.html

[123] onHorizontalDragUpdate: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragUpdate.html

[124] onHorizontalDragEnd: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragEnd.html

[125] ListView: https://developer.android.com/reference/android/widget/ListView

[126] ListView.Builder: https://docs.flutter.io/flutter/widgets/ListView/ListView.builder.html

[127] RecyclerView: https://developer.android.com/reference/android/support/v7/widget/RecyclerView

[128] Retrieve the value of a text field: https://flutter.io/docs/cookbook/forms/retrieve-input

[129] TextInput: https://docs.flutter.io/flutter/services/TextInput-class.html

[130] InputDecoration: https://docs.flutter.io/flutter/material/InputDecoration-class.html

[131] geolocator: https://pub.dartlang.org/packages/geolocator

[132] image_picker: https://pub.dartlang.org/packages/image_picker

[133] flutter_facebook_login: https://pub.dartlang.org/packages/flutter_facebook_login

[134] Flutter first party plugins: https://pub.dartlang.org/flutter/packages?q=firebase

[135] firebase_admob: https://pub.dartlang.org/packages/firebase_admob

[136] firebase_analytics: https://pub.dartlang.org/packages/firebase_analytics

[137] firebase_auth: https://pub.dartlang.org/packages/firebase_auth

[138] firebase_database: https://pub.dartlang.org/packages/firebase_database

[139] firebase_storage: https://pub.dartlang.org/packages/firebase_storage

[140] firebase_messaging: https://pub.dartlang.org/packages/firebase_messaging

[141] flutter_firebase_ui: https://pub.dartlang.org/packages/flutter_firebase_ui

[142] cloud_firestore: https://pub.dartlang.org/packages/cloud_firestore

[143] Shared_Preferences plugin: https://pub.dartlang.org/packages/shared_preferences

[144] SQFlite: https://pub.dartlang.org/packages/sqflite

[145] Firebase_Messaging: https://github.com/flutter/plugins/tree/master/packages/firebase_messaging

[146] мозга: http://www.braintools.ru

[147] Источник: https://habr.com/ru/post/442432/?utm_campaign=442432