Это заключительная часть лаконичной интерпретации документации по Flutter, которая будет полезна Xamarin.Forms-разработчикам. Учитывая текущую ситуацию, сейчас самое время изучать что-то новое! Под катом вы сможете найти для себя информацию, достаточную, чтобы оценить, стоит ли переходить с одного кросплатформенного фреймворка на другой и сколько времени это займёт.
Если этой информации будет недостаточно или у вас есть опыт в нативной разработке под конкретную платформу, то рекомендую заглянуть в другие части:
Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для веб-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Где точка входа?
Функция
В Xamarin.Forms это
Как создать Page или Element?
Во Flutter нет понятия Page и Element как таковых. Все компоненты — это виджеты. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.
StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.
StatefulWidget имеет класс State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.
StatelessWidget:
StatefulWidget:
Как располагать виджеты? Какой эквивалент у
Во Flutter вёрстка происходит прямо в коде с помощью дерева виджетов.
В Xamarin.Forms чаще всего вёрстка делается в
Список виджетов можно посмотреть тут.
Как добавить или удалить виджет через код?
С помощью обновления состояния родительского виджета и последующем перестроении дерева виджетов.
В Xamarin.Forms это можно сделать с помощью свойства
Как анимировать виджет?
С помощью классов Animation и AnimationController.
В Xamarin.Forms используются ViewExtensions и методы, например
Подробнее про анимацию можно почитать здесь.
Как рисовать на экране?
С помощью классов CustomPaint и CustomPainter.
В Xamarin.Forms используется сторонний SkiaSharp. Во Flutter Skia Canvas используется напрямую из коробки.
Как менять прозрачность?
С помощью виджета Opacity.
В Xamarin.Forms используется Opacity у VisualElement. Во Flutter нужно просто обернуть нужный виджет в Opacity.
Как создавать кастомные виджеты?
Компоновать их внутри одного (вместо наследования).
В Xamarin.Forms можно наследоваться от интересующего нас VisualElement и дописать свою логику. Во Flutter виджет всегда наследуется от StatelessWidget и StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных виджетов в качестве параметров или полей.
Как навигировать между экранами?
Для навигации между экранами используются классы Navigator и Route.
В Xamarin.Forms используется NavigationPage.
Во Flutter есть два способа навигации, которые схожи с NavigationPage:
Navigator может сделать push() или pop() указанному вами маршруту.
Как навигировать в стороннее приложение?
С помощью нативных реализаций через MethodChannel либо плагина url_launcher.
Какой эквивалент Device.BeginInvokeOnMainThread()? Как выполнять код асинхронно?
В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы знакомы из C#.
Выполнение запроса и возврат результата для обновления UI:
Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.
Пример загрузки и обновления данных в ListView:
Как делать запросы к сети?
С помощью плагина http.
Подключение зависимости через
Запрос:
Как отображать прогресс долгих операций?
С помощью ProgressIndicator.
В Xamarin.Forms это можно сделать напрямую, разместив индикатор прогресса в
Где хранить ресурсы разного разрешения?
В
В Xamarin.Forms нет унифицированного хранилища платформенных ресурсов, поэтому приходится хранить их в платформенных папках по отдельности. Во Flutter есть
Сопоставление размеров графических ресурсов в Android и Flutter.
Пример расположения ресурсов:
Пример пути в
Пример использования AssetImage:
Пример использования
Где хранить строки? Как их локализовывать?
Во Flutter нет определённого места для хранения строк в данный момент. Их предлагается хранить как статические поля в отдельном классе. Для локализации используются плагины, например flutter_localizations или l10n.
В Xamarin.Forms используется файл
Хранение:
Использование:
Локализация:
Где файл проекта?
Во Flutter нет файла проекта, который бы открывал этот проект в среде разработки. Ближайший похожий файл —
Как обрабатывать события жизненного цикла?
С помощью WidgetsBinding и метода didChangeAppLifecycleState().
Во Flutter используется FlutterActivity в Android-коде и FlutterAppDelegate в iOS, за счёт этого движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
Более подробно в AppLifecycleStatus documentation.
Какой аналог StackLayout?
Аналогом StackLayout с вертикальной ориентацией является Column, а с горизонтальной — Row.
Column:
Row:
Какой аналог у Grid?
GridView.
Какой аналог у ScrollView?
Ближайший аналог — SingleChildScrollView. Но для построения скролящегося контента во Flutter чаще всего используется ListView.
SingleChildScrollView:
ListView:
Как обрабатывать клики?
Если виджет поддерживает метод
onPressed:
GestureDetector:
Как обрабатывать жесты?
Используя GestureDetector. Им можно обрабатывать следующие действия:
Какой аналог у ListView?
ListView.
В Xamarin.Forms нужно создать ViewCell и (чаще всего) DataTemplateSelector и передать их в ListView. Во Flutter нужно просто передать список виджетов для отображения.
Как определить, на каком элементе был клик?
Во Flutter виджет элемента должен обработать свой клик сам.
В Xamarin.Forms чаще всего используется ItemTapped.
Как динамически обновить ListView?
Обновить список данных и вызвать
В Xamarin.Forms для этого используется ItemsSource. Во Flutter после
Для формирования списка рекомендуется использовать ListView.Builder.
Как использовать кастомные шрифты?
Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в
Как стилизовать текстовые виджеты?
С помощью параметров:
Какой аналог у Placeholder?
Свойство hintText у InputDecoration.
Как показать ошибки валидации?
Всё так же — с помощью InputDecoration и его состояния.
Как получить доступ к GPS?
С помощью плагина geolocator.
Как получить доступ к камере?
С помощью плагина image_picker.
Как авторизоваться через Facebook?
С помощью плагина flutter_facebook_login.
Как использовать Firebase?
Firebase поддерживает Flutter first party plugins:
Как определить, на какой платформе выполняется код?
С помощью класса поля
Поле
Класс Platform:
Как вызвать нативный платформенный код?
Через MethodChannel.
Подробнее тут.
Какие есть инструменты для отладки приложения?
DevTools.
Как сделать
Если приложение запускалось из IntelliJ IDE, Android Studio или VSCode, то сочетанием
Как получить доступ к меню разработчика в приложении?
Если запуск был из IDE, то с помощью инструментов IDE. Если из консоли, то с помощью ввода h.
Полный список команд:
Как хранить
С помощью плагина shared_preferences.
В Xamarin.Forms используется
Подключение зависимости:
Использование:
Как хранить сложные данные?
С помощью плагинов БД, например sqflite или hive.
Вот, пожалуй, ответы на основные вопросы. На этом серия интерпретаций заканчивается. Надеюсь, они были полезны всем интересующимся этим фреймворком разработчикам. Возможно, даже сподвигли начать писать на Flutter и завербовалм вас в дружное комьюнити Flutter-разработчиков. А я пойду думать над новыми статьями, чтобы развивать сообщество и делать мир приложений лучше. Да не сломает Xamarin ваш Forms!
Если этой информации будет недостаточно или у вас есть опыт в нативной разработке под конкретную платформу, то рекомендую заглянуть в другие части:
Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для веб-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Содержание:
- Проект
- Где точка входа?
- Как создать Page или Element?
- Views
- Как располагать виджеты? Какой эквивалент у XAML-файла?
- Как добавить или удалить виджет через код?
- Как анимировать виджет?
- Как рисовать на экране?
- Как менять прозрачность?
- Как создавать кастомные виджеты?
- Навигация
- Как навигировать между экранами?
- Как навигировать в стороннее приложение?
- Async UI
- Какой эквивалент Device.BeginInvokeOnMainThread()? Как выполнять код асинхронно?
- Как делать запросы к сети?
- Как отображать прогресс долгих операций?
- Структура проекта и ресурсы
- Где хранить ресурсы разного разрешения?
- Где хранить строки? Как их локализовывать?
- Где файл проекта?
- Жизненный цикл приложения
- Как обрабатывать события жизненного цикла?
- Layouts
- Какой аналог StackLayout?
- Какой аналог Grid?
- Какой аналог ScrollView?
- Обработка жестов
- Как обрабатывать клики?
- Как обрабатывать жесты?
- ListView и адаптеры
- Какой аналог у ListView?
- Как определить, на каком элементе был клик?
- Как динамически обновить ListView?
- Текст
- Как использовать кастомные шрифты?
- Как стилизовать текстовые виджеты?
- Какой аналог у Placeholder?
- Как показать ошибки валидации?
- Плагины Flutter
- Как получить доступ к GPS?
- Как получить доступ к камере?
- Как авторизоваться через Facebook?
- Как использовать Firebase?
- Platform-specific code
- Как определить, на какой платформе выполняется код?
- Как вызвать нативный платформенный код?
- Отладка
- Какие есть инструменты для отладки приложения?
- Как сделать hot reload?
- Как получить доступ к меню разработчика в приложении?
- Локальное хранилище
- Как хранить key-value-данные в приложении?
- Как хранить сложные данные?
Проект
Вопрос:
Где точка входа?
Ответ:
Функция
main()
.Отличия:
В Xamarin.Forms это
LoadApplication(new App());
.Пример:
void main() {
runApp(new MyApp());
}
Вопрос:
Как создать Page или Element?
Ответ:
Во Flutter нет понятия Page и Element как таковых. Все компоненты — это виджеты. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.
Отличия:
StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.
StatefulWidget имеет класс State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.
Пример:
StatelessWidget:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
StatefulWidget:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
// Take the value from the MyHomePage object that was created by
// the App.build method, and use it to set the appbar title.
title: new Text(widget.title),
),
body: new Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
Views
Вопрос:
Как располагать виджеты? Какой эквивалент у
XAML
-файла?Ответ:
Во Flutter вёрстка происходит прямо в коде с помощью дерева виджетов.
Отличие:
В Xamarin.Forms чаще всего вёрстка делается в
XAML
-файле. Во Flutter нет его аналога.Дополнительная информация:
Список виджетов можно посмотреть тут.
Пример:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new Center(
child: new MaterialButton(
onPressed: () {},
child: new Text('Hello'),
padding: new EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}
Вопрос:
Как добавить или удалить виджет через код?
Ответ:
С помощью обновления состояния родительского виджета и последующем перестроении дерева виджетов.
Отличия:
В Xamarin.Forms это можно сделать с помощью свойства
Content
у элемента или методов Add()
и Remove()
.Пример:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return new Text('Toggle One');
} else {
return new CupertinoButton(
onPressed: () {},
child: new Text('Toggle Two'),
);
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new Center(
child: _getToggleChild(),
),
floatingActionButton: new FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: new Icon(Icons.update),
),
);
}
}
Вопрос:
Как анимировать виджет?
Ответ:
С помощью классов Animation и AnimationController.
Отличия:
В Xamarin.Forms используются ViewExtensions и методы, например
FadeTo
или TranslateTo
.Дополнительная информация:
Подробнее про анимацию можно почитать здесь.
Пример:
import 'package:flutter/material.dart';
void main() {
runApp(new FadeAppTest());
}
class FadeAppTest extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Fade Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
MyFadeTest({Key key, this.title}) : super(key: key);
final String title;
@override
_MyFadeTest createState() => new _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Container(
child: new FadeTransition(
opacity: curve,
child: new FlutterLogo(
size: 100.0,
)))),
floatingActionButton: new FloatingActionButton(
tooltip: 'Fade',
child: new Icon(Icons.brush),
onPressed: () {
controller.forward();
},
),
);
}
}
Вопрос:
Как рисовать на экране?
Ответ:
С помощью классов CustomPaint и CustomPainter.
Отличия:
В Xamarin.Forms используется сторонний SkiaSharp. Во Flutter Skia Canvas используется напрямую из коробки.
Пример:
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(home: new DemoApp()));
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => new Scaffold(body: new Signature());
}
class Signature extends StatefulWidget {
SignatureState createState() => new SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
Widget build(BuildContext context) {
return new GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(painter: new SignaturePainter(_points), size: Size.infinite),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
var paint = new 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;
}
Вопрос:
Как менять прозрачность?
Ответ:
С помощью виджета Opacity.
Отличия:
В Xamarin.Forms используется Opacity у VisualElement. Во Flutter нужно просто обернуть нужный виджет в Opacity.
Вопрос:
Как создавать кастомные виджеты?
Ответ:
Компоновать их внутри одного (вместо наследования).
Отличия:
В Xamarin.Forms можно наследоваться от интересующего нас VisualElement и дописать свою логику. Во Flutter виджет всегда наследуется от StatelessWidget и StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных виджетов в качестве параметров или полей.
Пример:
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return new RaisedButton(onPressed: () {}, child: new Text(label));
}
}
Навигация
Вопрос:
Как навигировать между экранами?
Ответ:
Для навигации между экранами используются классы Navigator и Route.
Отличия:
В Xamarin.Forms используется NavigationPage.
Во Flutter есть два способа навигации, которые схожи с NavigationPage:
- описать Map с именами Route;
- напрямую навигировать к Route.
Navigator может сделать push() или pop() указанному вами маршруту.
Пример:
void main() {
runApp(CupertinoApp(
home: MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => MyPage(title: 'page A'),
'/b': (BuildContext context) => MyPage(title: 'page B'),
'/c': (BuildContext context) => MyPage(title: 'page C'),
},
));
}
Navigator.of(context).pushNamed('/b');
Вопрос:
Как навигировать в стороннее приложение?
Ответ:
С помощью нативных реализаций через MethodChannel либо плагина url_launcher.
Async UI
Вопрос:
Какой эквивалент Device.BeginInvokeOnMainThread()? Как выполнять код асинхронно?
Ответ:
В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы знакомы из C#.
Пример:
Выполнение запроса и возврат результата для обновления UI:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.
Пример загрузки и обновления данных в ListView:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}")
);
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
Вопрос:
Как делать запросы к сети?
Ответ:
С помощью плагина http.
Пример:
Подключение зависимости через
pubspec.yaml
:dependencies:
...
http: ^0.11.3+16
Запрос:
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.
Отличия:
В Xamarin.Forms это можно сделать напрямую, разместив индикатор прогресса в
XAML
.Пример:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _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 new Center(child: new CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: getBody());
}
ListView getListView() => new ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return new Padding(padding: new EdgeInsets.all(10.0), child: new 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
.Отличия:
В Xamarin.Forms нет унифицированного хранилища платформенных ресурсов, поэтому приходится хранить их в платформенных папках по отдельности. Во 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 |
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
Пример пути в
pubspec.yaml
файле:assets:
- images/my_icon.jpeg
Пример использования AssetImage:
return AssetImage("images/a_dot_burr.jpeg");
Пример использования
asset
напрямую:@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
Вопрос:
Где хранить строки? Как их локализовывать?
Ответ:
Во Flutter нет определённого места для хранения строк в данный момент. Их предлагается хранить как статические поля в отдельном классе. Для локализации используются плагины, например flutter_localizations или l10n.
Отличия:
В Xamarin.Forms используется файл
resx
.Пример:
Хранение:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
Использование:
new Text(Strings.welcomeMessage)
Локализация:
dependencies:
# ...
flutter_localizations:
sdk: flutter
intl: "^0.15.6"
import 'package:flutter_localizations/flutter_localizations.dart';
new MaterialApp(
localizationsDelegates: [
// Add app-specific localization delegate[s] here.
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
// ...
)
Вопрос:
Где файл проекта?
Ответ:
Во Flutter нет файла проекта, который бы открывал этот проект в среде разработки. Ближайший похожий файл —
pubspec.yaml
— содержит в себе зависимости на плагины и детали проекта.Жизненный цикл приложения
Вопрос:
Как обрабатывать события жизненного цикла?
Ответ:
С помощью WidgetsBinding и метода didChangeAppLifecycleState().
Дополнительная информация:
Во Flutter используется FlutterActivity в Android-коде и FlutterAppDelegate в iOS, за счёт этого движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
- inactive — этот метод есть только в iOS, в Android нет аналога;
- paused — аналогичен onPause() в Android;
- resumed — аналогичен onPostResume() в Android;
- suspending — аналогичен onStop в Android, в iOS нет аналога.
Более подробно в AppLifecycleStatus documentation.
Пример:
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null)
return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);
return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr);
}
}
void main() {
runApp(Center(child: LifecycleWatcher()));
}
Layouts
Вопрос:
Какой аналог StackLayout?
Ответ:
Аналогом StackLayout с вертикальной ориентацией является Column, а с горизонтальной — Row.
Пример:
Column:
@override
Widget build(BuildContext context) {
return new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('Column One'),
new Text('Column Two'),
new Text('Column Three'),
new Text('Column Four'),
],
);
}
Row:
@override
Widget build(BuildContext context) {
return new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('Row One'),
new Text('Row Two'),
new Text('Row Three'),
new Text('Row Four'),
],
);
}
Вопрос:
Какой аналог у Grid?
Ответ:
GridView.
Пример:
GridView.count(
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this would produce 2 rows.
crossAxisCount: 2,
// Generate 100 widgets that display their index in the List
children: List.generate(100, (index) {
return Center(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headline,
),
);
}),
);
Вопрос:
Какой аналог у ScrollView?
Ответ:
Ближайший аналог — SingleChildScrollView. Но для построения скролящегося контента во Flutter чаще всего используется ListView.
Пример:
SingleChildScrollView:
@override
Widget build(BuildContext context) {
return new SingleChildScrollView(
child: new Text('Long Content'),
);
}
ListView:
@override
Widget build(BuildContext context) {
return new ListView(
children: <Widget>[
new Text('Row One'),
new Text('Row Two'),
new Text('Row Three'),
new Text('Row Four'),
],
);
}
Обработка жестов
Вопрос:
Как обрабатывать клики?
Ответ:
Если виджет поддерживает метод
onPressed
, то обработать клик можно с его помощью. В противном случае это можно сделать через GestureDetector.Пример:
onPressed:
@override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: () {
print("click");
},
child: new Text("Button"));
}
GestureDetector:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new GestureDetector(
child: new FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}
Вопрос:
Как обрабатывать жесты?
Ответ:
Используя GestureDetector. Им можно обрабатывать следующие действия:
Tap
- onTapDown
- onTapUp
- onTap
- onTapCancel
Double tap
- onDoubleTap
Long press
- onLongPress
Vertical drag
- onVerticalDragStart
- onVerticalDragUpdate
- onVerticalDragEnd
Horizontal drag
- onHorizontalDragStart
- onHorizontalDragUpdate
- onHorizontalDragEnd
Пример:
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new GestureDetector(
child: new RotationTransition(
turns: curve,
child: new FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
));
}
}
ListView и адаптеры
Вопрос:
Какой аналог у ListView?
Ответ:
ListView.
Отличия:
В Xamarin.Forms нужно создать ViewCell и (чаще всего) DataTemplateSelector и передать их в ListView. Во Flutter нужно просто передать список виджетов для отображения.
Пример:
import 'package:flutter/material.dart';
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row $i")));
}
return widgets;
}
}
Вопрос:
Как определить, на каком элементе был клик?
Ответ:
Во Flutter виджет элемента должен обработать свой клик сам.
Отличия:
В Xamarin.Forms чаще всего используется ItemTapped.
Пример:
import 'package:flutter/material.dart';
void main() {
runApp(new SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Sample App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => new _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Sample App"),
),
body: new ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(new GestureDetector(
child: new Padding(
padding: new EdgeInsets.all(10.0),
child: new Text("Row $i")),
onTap: () {
print('row tapped');
},
));
}
return widgets;
}
}
Вопрос:
Как динамически обновить ListView?
Ответ:
Обновить список данных и вызвать
setState()
.Отличия:
В Xamarin.Forms для этого используется ItemsSource. Во Flutter после
setState()
виджет будет перерисован заново.Пример:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
Дополнительная информация:
Для формирования списка рекомендуется использовать ListView.Builder.
Пример:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
Текст
Вопрос:
Как использовать кастомные шрифты?
Ответ:
Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в
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.
Вопрос:
Какой аналог у Placeholder?
Ответ:
Свойство hintText у InputDecoration.
Пример:
body: new Center(
child: new TextField(
decoration: new InputDecoration(hintText: "This is a hint"),
)
)
Вопрос:
Как показать ошибки валидации?
Ответ:
Всё так же — с помощью InputDecoration и его состояния.
Пример:
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String _errorText;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: TextField(
onSubmitted: (String text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
),
),
);
}
_getErrorText() {
return _errorText;
}
bool isEmail(String emailString) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(emailString);
}
}
Плагины Flutter
Вопрос:
Как получить доступ к GPS?
Ответ:
С помощью плагина geolocator.
Вопрос:
Как получить доступ к камере?
Ответ:
С помощью плагина image_picker.
Вопрос:
Как авторизоваться через Facebook?
Ответ:
С помощью плагина flutter_facebook_login.
Вопрос:
Как использовать Firebase?
Ответ:
Firebase поддерживает Flutter first party plugins:
- firebase_admob для Firebase AdMob;
- firebase_analytics для Firebase Analytics;
- firebase_auth для Firebase Auth;
- firebase_database для Firebase RTDB;
- firebase_storage для Firebase Cloud Storage;
- firebase_messaging для Firebase Messaging (FCM);
- flutter_firebase_ui для быстрой интеграции Firebase Auth (Facebook, Google, Twitter and e-mail);
- cloud_firestore для Firebase Cloud Firestore.
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 или VSCode, то сочетанием
⌘s/ctrl-s
или нажатием на иконку hot reload
. Если запускалось из терминала, то вводом буквы r
.Вопрос:
Как получить доступ к меню разработчика в приложении?
Ответ:
Если запуск был из IDE, то с помощью инструментов IDE. Если из консоли, то с помощью ввода h.
Дополнительная информация:
Полный список команд:
Действие | Команда в терминале | Функции и поля |
Иерархия виджетов | w | debugDumpApp() |
Дерево рендеринга | t | debugDumpRenderTree() |
Слои | L | debugDumpLayerTree() |
Accessibility | S (traversal order) or U (inverse hit test order) | debugDumpSemantics() |
Инспектор виджетов | i | WidgetsApp.showWidgetInspectorOverride |
Отображение линий построения | p | debugPaintSizeEnabled |
Симуляция разных ОС | o | defaultTargetPlatform |
Перфоманс | P | WidgetsApp. showPerformanceOverlay |
Скриншот flutter.png | s | |
Закрытие приложения | q |
Локальное хранилище
Вопрос:
Как хранить
key-value
-данные в приложении?Ответ:
С помощью плагина shared_preferences.
Отличия:
В Xamarin.Forms используется
Xam.Plugins.Settings
.Пример:
Подключение зависимости:
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.
Вот, пожалуй, ответы на основные вопросы. На этом серия интерпретаций заканчивается. Надеюсь, они были полезны всем интересующимся этим фреймворком разработчикам. Возможно, даже сподвигли начать писать на Flutter и завербовалм вас в дружное комьюнити Flutter-разработчиков. А я пойду думать над новыми статьями, чтобы развивать сообщество и делать мир приложений лучше. Да не сломает Xamarin ваш Forms!