- PVSM.RU - https://www.pvsm.ru -
Про Flutter написано уже много статей. С каждым месяцем он становится всё популярнее. Поэтому я решил интерпретировать официальную документацию Flutter в лаконичный формат «вопрос — ответ». Думаю, многие, как и я, не имеют достаточно свободного времени для подробного изучения документации фреймворка, с которым они ещё не работают.
Если вы хотите понять, чем хорош этот фреймворк, и оценить, сколько усилий придётся приложить, чтобы его использовать — добро пожаловать под кат.
Какой аналог у 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"),
);
}
Какой аналог 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 есть два способа:
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});
Какой аналог у 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].
Какой аналог у 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 делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
Более подробно это описано в 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()));
}
Какой аналог у 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»
Как добавить слушатель 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]. Им можно обрабатывать следующие действия:
Обработка 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();
}
},
),
));
}
}
Какой аналог у 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'),
),
),
);
}
Как стилизовать текстовые виджеты?
С помощью параметров:
Более подробно написано здесь: 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);
}
}
Как получить доступ к 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 не поддерживает прямое взаимодействие.
Как использовать тему (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 не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для
Автор: 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
Нажмите здесь для печати.