[По докам] Flutter. Часть 3. Для разработчиков React Native
Продолжаем лаконичную интерпретацию официальной документации Flutter в формате «вопрос-ответ». Вот уже 3-я часть, и она в большей степени будет полезна React Native-разработчикам. В данной интерпретации вы можете найти ответы на частые вопросы, а также определить для себя, сколько усилий нужно приложить, чтобы перейти с одного кроссплатформенного фреймворка на другой.
Если этой информации будет недостаточно или у вас есть опыт в нативной разработке под конкретную платформу, то рекомендую заглянуть в другие части:
Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для Web-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Содержание:
- Немного про Dart
- Основы
- Структура проекта и ресурсы
- Views
- Layouts
- Стилизация
- State management
- Локальное хранилище
- Навигация
- Жесты и обработка touch event
- HTTP-запросы
- Форма ввода
- Platform-specific code
- Отладка
- Анимация
- Эквиваленты компонентов
- Плагины Flutter
Немного про Dart
Вопрос:
Где точка входа?
Ответ:
Функция main()
.
Отличия:
В JavaScript нет предопределённой точки входа, ею может быть любая функция, определённая разработчиком. В Dart это только main()
.
Вопрос:
Как делать вывод в консоль?
Ответ:
С помощью функции print()
.
Отличия:
В JavaScript вывод в консоль осуществляется с помощью console.log()
.
Пример:
print('Hello world!');
Вопрос:
Как создавать и присваивать переменные/поля?
Ответ:
Dart поддерживает и динамическую, и строгую типизацию. Поэтому переменные/поля создавать можно в любом удобном вам виде типизации. Присваивание происходит через одинарное =.
Отличия:
В JavaScript строгая типизация не поддерживается.
Дополнительная информация:
Подробнее тут.
Пример:
String name = 'dart'; // Explicitly typed as a string.
var otherName = 'Dart'; // Inferred string.
// Both are acceptable in Dart.
Вопрос:
Какое значение у переменной/поля по умолчанию?
Ответ:
null
Отличия:
В JavaScript по умолчанию undefined
.
Дополнительная информация:
Подробнее тут.
Вопрос:
Как проверять значение на null
или на 0?
Ответ:
С помощью явной проверки ==
.
Отличия:
В JavaScript в рамках проверки if
число 1 или любой non-null
объект будет эквивалентен true
. В Dart только булевое значение true
эквивалентно true
.
Пример:
var myNull = null;
if (myNull == null) {
print('use "== null" to check null');
}
var zero = 0;
if (zero == 0) {
print('use "== 0" to check zero');
}
Вопрос:
Как декларировать функции?
Ответ:
В Dart функции, как и поля, могут быть с динамической или строгой типизацией возвращаемого значения. Динамически типизируемые декларируются просто именем функции и опционально параметрами, а строго типизируемые в начале ещё имеют возвращаемый тип.
Отличия:
В JavaScript функция декларируется ключевым словом function
, далее имя и опционально — параметры.
Дополнительная информация:
Подробнее тут.
Пример:
fn() {
return true;
}
// can also be written as
bool fn() {
return true;
}
Вопрос:
Какой аналог у Promise?
Ответ:
Future
Дополнительная информация:
Dart, как и JavaScript, поддерживает однопоточное выполнение. Future в Dart по смыслу выполняет те же функции, что и Promise в React Native.
Подробнее тут…
Пример:
import 'dart:convert';
import 'package:http/http.dart' as http;
class Example {
Future _getIPAddress() {
final url = 'https://httpbin.org/ip';
return http.get(url).then((response) {
String ip = jsonDecode(response.body)['origin'];
return ip;
});
}
}
main() {
final example = new Example();
example
._getIPAddress()
.then((ip) => print(ip))
.catchError((error) => print(error));
}
Вопрос:
Какой аналог async
и await
?
Ответ:
async
и await
Отличия:
В JavaScript async-функции возвращают Promise, в Dart — Future. await синхронно ожидает результата вызова async-функции.
Дополнительная информация:
Подробнее тут.
Пример:
import 'dart:convert';
import 'package:http/http.dart' as http;
class Example {
Future _getIPAddress() async {
final url = 'https://httpbin.org/ip';
final response = await http.get(url);
String ip = jsonDecode(response.body)['origin'];
return ip;
}
}
main() async {
final example = new Example();
try {
final ip = await example._getIPAddress();
print(ip);
} catch (error) {
print(error);
}
}
Основы
Вопрос:
Как создать проект приложения на Flutter?
Ответ:
- С помощью IDE с установленными плагинами Flutter и Dart.
- С помощью команды
flutter create {projectname}
.
Отличия:
Для создания проекта в React Native используется команда create-react-native-app {projectname}
.
Дополнительная информация:
Подробнее тут.
Вопрос:
Как запустить приложение?
Ответ:
- С помощью функции run в IDE с установленными плагинами Flutter и Dart.
- С помощью команды
flutter run
.
Отличия:
Для запуска приложения в React Native используются команды npm run
или yarn run
.
Вопрос:
Как импортировать виджеты?
Ответ:
Во Flutter все виджеты разбиты по пакетам, поэтому достаточно импортировать пакет, чтобы использовать его виджеты.
Отличия:
В React Native необходимо импортировать каждый виджет.
Пример:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/my_widgets.dart';
Вопрос:
Как использовать виджеты и вложенность для построения дерева виджетов?
Ответ:
Во Flutter почти всё — виджеты. Даже объект приложения — виджет. Каждый виджет может быть вложенным в родительский. Комбинируя виджеты, вы строите иерархию, которая называется «деревом виджетов». Именно она в дальнейшем превращается в отображение на экране.
Пример:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Text('Hello world'),
),
),
);
}
}
Вопрос:
Как создавать переиспользуемые компоненты?
Ответ:
Во Flutter можно создать класс виджета и переиспользовать его. Используемые параметры являются полями класса.
Отличия:
В React Native для изменения параметров переиспользуемого элемента необходимо их пометить как props.
Дополнительная информация:
Параметры в конструкторе могут декларироваться двумя способами: обычным перечислением и опциональным, с помощью фигурных скобок {}
. Отличия проявляются при использовании параметров для вызова конструктора.
При обычном перечислении все параметры являются обязательными, и при вызове конструктора нельзя указать, какой конкретно параметр передаётся, т.к. они обусловлены порядком следования.
При опциональном можно использовать любой порядок передачи параметров, но необходимо указать, какой конкретно параметр передаётся или не передавать их вовсе. Чтобы обозначить опциональный параметр как обязательный, нужно использовать аннотацию @required
.
Пример:
class CustomCard extends StatelessWidget {
CustomCard({@required this.index, @required
this.onPress});
final index;
final Function onPress;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text('Card $index'),
FlatButton(
child: const Text('Press'),
onPressed: this.onPress,
),
],
)
);
}
}
...
// Usage
CustomCard(
index: index,
onPress: () {
print('Card $index');
},
)
...
Структура проекта и ресурсы
Вопрос:
Где начинать писать код?
Ответ:
В {projectname}/lib/main.dart
.
Вопрос:
Какая структура файлов в проекте Flutter?
Ответ:
┬
└ projectname
┬
├ android - файлы для Android сборки.
├ build - iOS и Android результаты сборки.
├ ios - файлы для iOS сборки.
├ lib - Dart файлы.
┬
└ src - Contains additional source files.
└ main.dart - точка входа в приложение.
├ test - Файлы тестов.
└ pubspec.yaml - Описание зависимостей и ресурсов приложения.
Аналог package.json файла в React Native.
Вопрос:
Где хранить ресурсы и ассеты и как их использовать?
Ответ:
Ссылки на ресурсы и зависимости проекта хранятся в файле pubspec.yaml
. Фактически располагать их можно в любой папке внутри lib
, главное, указать к ним путь в pubspec.yaml
. Использовать их в проекте можно либо с помощью специализированных виджетов, например AssetImage, либо напрямую, с помощью AssetBundle.
Дополнительная информация:
Подробнее тут.
Пример:
Декларирование ассетов в pubspec.yaml
flutter:
assets:
- assets/my_icon.png
- assets/background.png
Использование в коде
image: AssetImage('assets/background.png'),
Вопрос:
Как загружать изображения из сети?
Ответ:
С помощью Image.network.
Пример:
body: Image.network(
'https://flutter.io/images/owl.jpg',
Вопрос:
Как подключать сторонние пакеты и плагины?
Ответ:
С помощью dependencies
в pubspec.yaml
.
Отличия:
В React Native для добавления зависимостей используются команды yarn add {package-name}
или npm install --save {package-name}
.
Дополнительная информация:
Использование пакетов
Разработка пакетов и плагинов
Популярные плагины для Flutter
Пример:
dependencies:
flutter:
sdk: flutter
google_sign_in: ^3.0.3
Views
Вопрос:
Какой аналог у View контейнера?
Ответ:
Все базовые виджеты для вёрcтки, например Container, Column, Row и Center.
Дополнительная информация:
Подробнее тут.
Вопрос:
Какой аналог FlatList или SelectionList?
Ответ:
ListView
Пример:
var data = [ ... ];
ListView.builder(
itemCount: data.length,
itemBuilder: (context, int index) {
return Text(
data[index],
);
},
)
Вопрос:
Как использовать Canvas?
Ответ:
С помощью классов CustomPaint и CustomPainter.
Отличия:
В React Native нет возможности рисовать с помощью Canvas из коробки. Есть сторонние плагины, например react-native-canvas
.
Пример:
class MyCanvasPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();
paint.color = Colors.amber;
canvas.drawCircle(Offset(100.0, 200.0), 40.0, paint);
Paint paintRect = Paint();
paintRect.color = Colors.lightBlue;
Rect rect = Rect.fromPoints(Offset(150.0, 300.0), Offset(300.0, 400.0));
canvas.drawRect(rect, paintRect);
}
bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
bool shouldRebuildSemantics(MyCanvasPainter oldDelegate) => false;
}
class _MyCanvasState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: MyCanvasPainter(),
),
);
}
}
Layouts
Вопрос:
Как использовать виджеты для описания свойств вёрстки?
Ответ:
Во Flutter для свойств вёрстки используются специализированные виджеты, например:
Column, Row, Padding, Align, Stack.
Отличия:
В React Native чаще всего свойства вёрстки можно задать с помощью props
.
Дополнительная информация:
Полный список виджетов
Вопрос:
Как позиционировать виджеты при вёрстке?
Ответ:
Для абсолютного позиционирования используется виджет Stack. Для относительного — различные виджеты и их комбинации, которые можно найти здесь.
Пример:
Stack(
alignment: const Alignment(0.6, 0.6),
children: [
CircleAvatar(
backgroundImage: NetworkImage(
'https://avatars3.githubusercontent.com/u/14101776?v=4'),
),
Container(
decoration: BoxDecoration(
color: Colors.black45,
),
child: Text('Flutter'),
),
],
)
Стилизация
Вопрос:
Как стилизовать компоненты?
Ответ:
Чаще всего у виджетов есть свойство style
.
Отличия:
В React Native для стилизации используется stylesheets.create
.
Пример:
var textStyle = TextStyle(fontSize: 32.0, color: Colors.cyan, fontWeight:
FontWeight.w600);
...
Center(
child: Column(
children: [
Text(
'Sample text',
style: textStyle,
),
Padding(
padding: EdgeInsets.all(20.0),
child: Icon(Icons.lightbulb_outline,
size: 48.0, color: Colors.redAccent)
),
],
),
)
Вопрос:
Как использовать иконки и цвета?
Ответ:
С помощью классов Icons и Colors.
Отличия:
В React Native нет поддержки иконок из коробки.
Пример:
Icons
Icon(Icons.lightbulb_outline, color: Colors.redAccent)
Colors
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
Вопрос:
Как настраивать стилистичекую тему приложения?
Ответ:
С помощью класса ThemeData.
Отличия:
В React Native общая тема настривается через stylesheets
.
Пример:
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
primaryColor: Colors.cyan,
brightness: brightness,
),
child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
...
...
),
);
}
State management
Вопрос:
Как обновлять отображение виджетов?
Ответ:
Используя StatefulWidget и его State. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.
Отличия:
StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.
StatefulWidget имеет состояние State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.
Пример:
1) StatelessWidget — Text
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
2) StatefulWidget — при нажатии на кнопку (FloatingActionButton) текст в виджете Text меняется с I Like Flutter на Flutter is Awesome!
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// Этот виджет корневой в приложении.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State {
// дефолтный текст
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),
),
);
}
}
Локальное хранилище
Вопрос:
Как хранить key-value
данные в приложении?
Ответ:
С помощью плагина shared_preferences.
Отличия:
В React Native используется AsyncStorage.
Пример:
Подключение зависимости
dependencies:
flutter:
sdk: flutter
shared_preferences: ^0.4.3
Использование
SharedPreferences prefs = await SharedPreferences.getInstance();
_counter = prefs.getInt('counter');
prefs.setInt('counter', ++_counter);
setState(() {
_counter = _counter;
});
Вопрос:
Как хранить сложные данные?
Ответ:
С помощью плагинов БД, например, sqflite или hive.
Навигация
Вопрос:
Как навигировать между экранами?
Ответ:
Для навигации между экранами используются классы Navigator и Route.
Отличия:
В React Native используются StackNavigator, TabNavigator и DrawerNavigator.
Во Flutter есть два способа навигации:
- описать Map с именами Route;
- напрямую навигировать к Route.
Navigator может сделать push () или pop () указанному вами маршруту.
Пример:
void main() {
runApp(CupertinoApp(
home: MyAppHome(), // becomes the route named '/'
routes: {
'/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');
Вопрос:
Как использовать tab navigation?
Ответ:
С помощью классов: TabController, TabBar, Tab и TabBarView.
Отличия:
В React Native используются createBottomTabNavigator и TabNavigation.
Дополнительная информация:
- TabController координирует TabBar и TabBarView;
- TabBar отображает горизонтальный список табов;
- Tab отображает таб;
- TabBarView отображает контент выбранного таба.
Подробнее тут…
Пример:
class _NavigationHomePageState extends State with SingleTickerProviderStateMixin {
TabController controller=TabController(length: 2, vsync: this);
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Material (
child: TabBar(
tabs: [
Tab(icon: Icon(Icons.person),)
Tab(icon: Icon(Icons.email),),
],
controller: controller,
),
color: Colors.blue,
),
body: TabBarView(
children: [
home.homeScreen(),
tabScreen.tabScreen()
],
controller: controller,
)
);
}
}
Вопрос:
Как использовать drawer navigation?
Ответ:
С помощью класса Drawer.
Отличия:
В React Native используются createDrawerNavigator и DrawerNavigation.
Пример:
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListTile(
leading: Icon(Icons.change_history),
title: Text('Screen2'),
onTap: () {
Navigator.of(context).pushNamed('/b');
},
),
elevation: 20.0,
),
appBar: AppBar(
title: Text('Home'),
),
body: Container(),
);
}
Жесты и обработка touch event
Вопрос:
Как обрабатывать клик?
Ответ:
Если виджет поддерживает клики, то в onPressed()
. Если нет, то с помощью виджета GestureDetector.
Отличия:
В React Native для этого используются PanResponder или Touchable.
Пример:
GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text('Gestures'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Tap, Long Press, Swipe Horizontally or Vertically '),
],
)
),
),
onTap: () {
print('Tapped');
},
onLongPress: () {
print('Long Pressed');
},
onVerticalDragEnd: (DragEndDetails value) {
print('Swiped Vertically');
},
onHorizontalDragEnd: (DragEndDetails value) {
print('Swiped Horizontally');
},
);
HTTP-запросы
Вопрос:
Как получать данные из API запросов?
Ответ:
С помощью http-плагина.
Отличия:
В React Native используется fetch API
.
Пример:
Подключение плагина
dependencies:
flutter:
sdk: flutter
http:
Получение данных
final url = Uri.https('httpbin.org', 'ip');
final httpClient = HttpClient();
_getIPAddress() async {
var request = await httpClient.getUrl(url);
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
String ip = jsonDecode(responseBody)['origin'];
setState(() {
_ipAddress = ip;
});
}
Форма ввода
Вопрос:
Какие виджеты использовать для ввода?
Ответ:
TextField в связке с TextEditingController или TextFormField.
Отличия:
В React Native используется TextInput.
TextFormField отличается от TextField встроенной валидацией и логикой сохранения значений в поля.
Пример:
TextField
final TextEditingController _controller = TextEditingController();
...
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type something', labelText: 'Text Field '
),
),
RaisedButton(
child: Text('Submit'),
onPressed: () {
showDialog(
context: context,
child: AlertDialog(
title: Text('Alert'),
content: Text('You typed ${_controller.text}'),
),
);
},
),
)
TextFormField
final formKey = GlobalKey();
...
Form(
key:formKey,
child: Column(
children: [
TextFormField(
validator: (value) => !value.contains('@') ? 'Not a valid email.' : null,
onSaved: (val) => _email = val,
decoration: const InputDecoration(
hintText: 'Enter your email',
labelText: 'Email',
),
),
RaisedButton(
onPressed: _submit,
child: Text('Login'),
),
],
),
)
void _submit() {
final form = formKey.currentState;
if (form.validate()) {
form.save();
showDialog(
context: context,
child: AlertDialog(
title: Text('Alert'),
content: Text('Email: $_email, password: $_password'),
)
);
}
}
Platform-specific code
Вопрос:
Как определить, на какой платформе выполняется код?
Ответ:
С помощью класса поля platform
в Theme или класса Platform.
Пример:
Поле platform
if (Theme.of(context).platform == TargetPlatform.iOS) {
return 'iOS';
} else if (Theme.of(context).platform == TargetPlatform.android) {
return 'android';
} else if (Theme.of(context).platform == TargetPlatform.fuchsia) {
return 'fuchsia';
} else {
return 'not recognised ';
}
Класс Platform
if (Platform.isIOS) {
return 'iOS';
} else if (Platform.isAndroid) {
return 'android';
} else if (Platform.isFuchsia) {
return 'fuchsia';
} else {
return 'not recognised ';
}
Вопрос:
Как вызвать нативный платформенный код?
Ответ:
Через MethodChannel.
Дополнительная информация:
Подробнее тут…
Отладка
Вопрос:
Какие есть инструменты для отладки приложения?
Ответ:
DevTools.
Вопрос:
Как сделать hot reload
?
Ответ:
Если приложение запускалось из IntelliJ IDE или Android Studio, то сочетанием ⌘s/ctrl-s
или нажатием на иконку hot reload
. Если запускалось из терминала, то вводом буквы r
.
Отличия:
В React Native для iOS-эмуляторов используется сочетание ⌘R/Ctrl+R
, для Android — двойное R
.
Вопрос:
Как получить доступ к меню разработчика в приложении?
Ответ:
Если запуск был из IDE, то с помощью инструментов IDE. Если из консоли, то с помощью ввода h.
Отличия:
В React Native используются ⌘D/Ctrl+D
для iOS эмуляторов и ⌘M/Ctrl+M
для Android.
Дополнительная информация:
Полный список команд
Анимация
Вопрос:
Что используется для анимации?
Ответ:
Animation и AnimationController.
Отличия:
В React Native используется Animation API
.
Дополнительная информация:
Подробнее тут.
Вопрос:
Как добавить простую fade-in
анимацию?
Ответ:
С помощью FadeTransition.
Пример:
import 'package:flutter/material.dart';
void main() {
runApp(Center(child: LogoFade()));
}
class LogoFade extends StatefulWidget {
_LogoFadeState createState() => _LogoFadeState();
}
class _LogoFadeState extends State with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 3000), vsync: this);
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation = Tween(begin: 0.0, end: 1.0).animate(curve);
controller.forward();
}
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
height: 300.0,
width: 300.0,
child: FlutterLogo(),
),
);
}
dispose() {
controller.dispose();
super.dispose();
}
}
Вопрос:
Как добавить анимацию смахивания элементам списка?
Ответ:
С помощью Dismissible.
Пример:
child: Dismissible(
key: key,
onDismissed: (DismissDirection dir) {
cards.removeLast();
},
child: Container(
...
),
),
Эквиваленты компонентов
Вопрос:
Какие эквиваленты есть во Flutter по сравнению с React Native?
Ответ:
Плагины Flutter
Вопрос:
Как получить доступ к GPS?
Ответ:
С помощью плагина geolocator.
Вопрос:
Как получить доступ к камере?
Ответ:
С помощью плагина image_picker.
Вопрос:
Как авторизоваться через Facebook?
Ответ:
С помощью плагина flutter_facebook_login.
Вопрос:
Как использовать Firebase?
Ответ:
Firebase поддерживает Flutter first party plugins:
Вот, пожалуй, ответы на основные вопросы. Надеюсь, моя интерпретация была для вас полезна, и если вы ещё не пишите на Flutter, то хотя бы задумались об этом. А это значит, что в полку Flutter разработчиков, возможно, будет прибавление, и мы вместе будем делать мир лучше через призму разработанных удобных и быстрых приложений! Да не сломает React ваш Native!