После длительного перерыва я продолжу рассказывать о популярном фреймворке Flutter в формате «вопрос — ответ». Первую статью для Android-разработчиков вы можете найти здесь, а сегодня будет полезный материал для разработчиков под iOS.
Если у вас мало времени для самостоятельного и глубокого изучения документации, но вы хотите понять, чем хорош Flutter и как его использовать, загляните под кат.
Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для веб-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Какой аналог у UIView во Flutter?
Widget
UIView — фактически то, что будет на экране. Для отображения изменений вызывается setNeedsDisplay().
Widget — описание того, что будет на экране. Для изменения создаётся заново.
Flutter включает в себя библиотеку Cupertino Widgets. В ней собраны виджеты, которые реализуют гайдлайны Apple Design.
Как обновлять отображение виджетов?
Используя StatefulWidget и его State. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.
StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.
StatefulWidget имеет состояние State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.
1) StatelessWidget — Text
2) StatefulWidget — при нажатии на кнопку (FloatingActionButton) текст в виджете Text меняется с I Like Flutter на Flutter is Awesome!
Как верстать экран с виджетами? Где Storyboard?
Во Flutter нет Storyboard. Всё верстается в дереве виджетов прямо в коде.
Все дефолтные виджеты во Flutter можно посмотреть в widget catalog.
Как добавить или удалить компонент в вёрстке во время работы приложения?
Через функцию, которая будет возвращать нужный виджет в зависимости от состояния.
В iOS можно сделать addSubview() или removeFromSuperview(). Во Flutter так нельзя, т.к. виджеты неизменны. Может изменяться только их состояние.
Меняем Text на Button по нажатию на FloatingActionButton.
Как анимировать виджеты?
Используя класс AnimationController, который является наследником абстрактного класса Animation<T>. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker, который сообщает о перерисовке экрана.
В iOS можно анимировать view с помощью animate(withDuration:animations:). Во Flutter анимацию нужно писать в коде с помощью AnimationController.
Более подробно можно изучить в Animation & Motion widgets, Animations tutorial и Animations overview.
Fade-анимация лого Flutter.
Как использовать CoreGraphics?
Flutter вместо CoreGraphics использует Canvas API на низкоуровневом движке Skia. В Android используется аналогичный Canvas API.
У Flutter есть два класса для рисования на Canvas: CustomPaint и CustomPainter. Второй реализует ваш алгоритм отрисовки.
Подробнее тут: StackOverflow
Как изменять прозрачность виджетов?
Обернуть в виджет Opacity.
В iOS у всех view есть .opacity или .alpha. Во Flutter этот параметр заменяет виджет-обёртка.
Как создавать кастомные виджеты?
Компоновать виджеты внутри одного (вместо наследования).
В iOS можно наследоваться от интересующей нас view и дописать свою логику. Во Flutter виджет всегда наследуется от StatelessWidget или StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных вам виджетов в качестве параметров или полей.
Как реализовывать навигацию между экранами во Flutter?
Для навигации между экранами используются классы Navigator и Route.
Во Flutter нет таких понятий, как UIViewController и UINavigationController. Есть Navigator (навигатор) и Routes (маршруты). Navigator похож на UINavigationController по принципу работы. Он может сделать push() или pop() указанному вами маршруту. Route — это своего рода UIViewController, но во Flutter его принято сравнивать с экраном или страницей.
Во Flutter есть два способа навигации:
Как навигировать в стороннее приложение?
Либо взаимодействуя с iOS-слоем приложения через MethodChannel, либо используя плагин URL launcher.
Как сделать pop back в iOS ViewController?
Вызовом SystemNavigator.pop().
SystemNavigator.pop() из Dart-кода вызывает следующий код в iOS:
Если это не то, что вам нужно, то вы можете сделать свою реализацию через MethodChannel.
Как писать асинхронный код во Flutter?
В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы, возможно, знакомы из C#, JavaScript или Kotlin coroutines.
Выполнение запроса и возврата результата для обновления UI:
Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.
Загрузка и обновление данных в ListView:
Как выполнить код в фоновом потоке?
Как было сказано выше — с помощью async/await и изоляций (Isolate).
«Из коробки» в iOS можно использовать Operation с возможным переопределением методов. Во Flutter «из коробки» вам просто нужно использовать async/await, об остальном позаботится Dart.
Здесь метод dataLoader() изолирован. В изоляциях вы можете запускать тяжелые операции, такие как парсинг больших JSON-ов, шифрование, обработка изображений и т.д.
Как делать запросы к сети во Flutter?
Во Flutter есть свой HTTP package.
Чтобы использовать HTTP package, добавьте его как зависимость в pubspec.yaml:
Для выполнения запроса вызовите await в async функции http.get():
Как показывать прогресс выполнения?
С помощью виджета ProgressIndicator.
Где хранить ресурсы разного разрешения?
В assets.
В iOS у графических ресурсов есть Images.xcasset, которые находятся в папке assets. Во Flutter есть только assets. Папка ресурсов может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.
Размеры графических ресурсов в iOS и Flutter идентичны и следуют density-based формату.
Расположение ресурсов:
Путь в pubspec.yaml файле:
Использование AssetImage:
Использование asset напрямую:
Где хранить строки? Как их локализовать?
Хранить в статичных полях. Локализовать с помощью intl package.
Какой аналог CocoaPods? Как добавлять зависимости?
pubspec.yaml.
Flutter делегирует сборку нативным Android и iOS-сборщикам. Посмотреть список всех популярных библиотек для Flutter можно в Pub.
Какой аналог у ViewController во Flutter?
Во Flutter всё — виджеты. Роль ViewController для работы с UI выполняют виджеты. А роль навигации, как было сказано в пункте про навигацию, — Navigator и Route.
Как обрабатывать события жизненного цикла?
С помощью WidgetsBinding и метода didChangeAppLifecycleState().
Во Flutter используется FlutterAppDelegate в нативном коде, и движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
Более подробно это описано в AppLifecycleStatus documentation.
Какой аналог у UITableView и UICollectionView?
ListView.
Как узнать, на каком элементе списка был клик?
Виджет, который является элементом списка, должен сам обрабатывать нажатие на него.
В iOS за это отвечает отдельный метод tableView:didSelectRowAtIndexPath:. Во Flutter элемент списка должен быть обёрнут в виджет, обрабатывающий клики, например GestureDetector.
Как динамически обновить ListView?
Обновить список данных и вызвать setState().
В iOS для этого необходимо обновить данные и вызвать метод reloadData. Во Flutter после setState() виджет будет перерисован заново.
Для формирования списка рекомендуется использовать ListView.Builder.
Какой аналог у UIScrollView?
ListView с виджетами.
Более подробно тут.
Как добавить слушатель onClick для виджета во Flutter?
Если виджет поддерживает клики, то в onPressed(). Если нет, то в onTap().
В onPressed():
В onTap():
Как обрабатывать другие жесты на виджетах?
Используя GestureDetector. Им можно обрабатывать следующие действия:
Обработка onDoubleTap:
Как использовать тему (Theme) в приложении?
Используя виджет MaterialApp или WidgetApp как корневой в приложении.
Как использовать кастомные шрифты?
Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в pubspec.yaml.
Как стилизовать текстовые виджеты?
С помощью параметров:
Как получить результат пользовательского ввода?
С помощью TextEditingController.
Более подробно написано здесь: Retrieve the value of a text field.
Какой аналог у hint в TextInput?
Подсказку можно показать с помощью InputDecoration, передав его в качестве параметра конструктора в виджет.
Как показать ошибки валидации?
Всё так же — с помощью InputDecoration и его состояния.
Как получить доступ к GPS?
С помощью плагина geolocator.
Как получить доступ к камере?
С помощью плагина image_picker.
Как авторизоваться через Facebook?
С помощью плагина flutter_facebook_login.
Как использовать Firebase?
Firebase поддерживает Flutter first party plugins:
Как делать нативные (платформенные) вставки кода?
Flutter использует EventBus для взаимодействия с платформенным кодом. Подробно тут: developing packages and plugins.
Как получить доступ к UserDefault?
С помощью Shared_Preferences plugin (для Shared Preferences в Android тоже).
Какой аналог у Core Data?
SQFlite.
Как показать push-уведомление?
С помощью плагина Firebase_Messaging.
Новые языки программирования и фреймворки появляются практически постоянно. И на старте трудно понять, что выстрелит и будет долго жить, а что забудут уже через год. Боб Мартин в своей книге «Идеальный программист» призывает нас изучать новые языки программирования и фреймворки. Чед Фаулер в книге «Программист-фанатик» советует всегда быть на острие технологий. Но как понять, что ты не ошибся с выбором? В 2016 году я обратил внимание на Kotlin, но из-за высокой загруженности не смог уделить ему достаточно времени до второй половины 2017. На старте многие относились к нему скептически, а сейчас это один из самых популярных языков программирования, и огромное количество разработчиков создают на нём свои продукты. Я чувствую, что за те полтора года мог бы получить более глубокое понимание тонкостей языка.
В том же 2016 году появился фреймворк Flutter на языке Dart. Но рост его популярности был не такой стремительный, и только в 2018 году о нём заговорили громко. Тогда мне тоже захотелось попробовать его в действии. И мне понравилось! Время покажет, какое будущее ждёт этот фреймворк, но кажется, он очень перспективный. (И если Google Fuchsia выстрелит, то, без сомнений, Flutter не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для мозга. На этом у меня всё. Да не сломает Apple ваш Store!
Если у вас мало времени для самостоятельного и глубокого изучения документации, но вы хотите понять, чем хорош Flutter и как его использовать, загляните под кат.
Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для веб-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Содержание:
- Views
- Какой аналог у UIView во Flutter?
- Как обновлять отображение виджетов?
- Как верстать экран с виджетами? Где Storyboard?
- Как добавить или удалить компонент в вёрстке во время работы приложения?
- Как анимировать виджеты?
- Как использовать CoreGraphics?
- Как изменять прозрачность виджетов?
- Как создавать кастомные виджеты?
- Navigation
- Как реализовывать навигацию между экранами во Flutter?
- Как навигировать в стороннее приложение?
- Как сделать pop back в iOS ViewController?
- Threading & asynchronicity
- Как писать асинхронный код во Flutter?
- Как выполнить код в фоновом потоке?
- Как делать запросы к сети во Flutter?
- Как показывать прогресс выполнения?
- Структура проекта и ресурсы
- Где хранить ресурсы разного разрешения?
- Где хранить строки? Как их локализовать?
- Какой аналог CocoaPods? Как добавлять зависимости?
- ViewControllers
- Layouts
- Какой аналог у UITableView и UICollectionView?
- Как узнать, на каком элементе списка был клик?
- Как динамически обновить ListView?
- Какой аналог у UIScrollView?
- Жесты и обработка touch event
- Стилизация приложения
- Как использовать тему (Theme) в приложении?
- Как использовать кастомные шрифты?
- Как стилизовать текстовые виджеты?
- Форма ввода
- Как получить результат пользовательского ввода?
- Какой аналог у hint в Input?
- Как показать ошибки валидации?
- Плагины Flutter
- Как получить доступ к GPS?
- Как получить доступ к камере?
- Как авторизоваться через Facebook?
- Как использовать Firebase?
- Как делать нативные (платформенные) вставки кода?
- Базы данных и локальное хранилище
- Уведомления
Views
Вопрос:
Какой аналог у UIView во Flutter?
Ответ:
Widget
Отличия:
UIView — фактически то, что будет на экране. Для отображения изменений вызывается setNeedsDisplay().
Widget — описание того, что будет на экране. Для изменения создаётся заново.
Дополнительная информация:
Flutter включает в себя библиотеку Cupertino Widgets. В ней собраны виджеты, которые реализуют гайдлайны Apple Design.
Вопрос:
Как обновлять отображение виджетов?
Ответ:
Используя StatefulWidget и его State. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.
Отличия:
StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.
StatefulWidget имеет состояние State, в котором хранится информация о текущем состоянии. Если вы хотите изменить элемент на экране при выполнении какого-то действия (пришёл ответ с сервера, пользователь нажал на кнопку и т.д.) — это ваш вариант.
Пример:
1) StatelessWidget — Text
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
2) StatefulWidget — при нажатии на кнопку (FloatingActionButton) текст в виджете Text меняется с I Like Flutter на Flutter is Awesome!
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// Этот виджет корневой в приложении.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// дефолтный текст
String textToShow = "Мне нравится Flutter";
void _updateText() {
setState(() {
// обновление текста
textToShow = "Flutter крутой!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Обновить текст',
child: Icon(Icons.update),
),
);
}
}
Вопрос:
Как верстать экран с виджетами? Где Storyboard?
Ответ:
Во Flutter нет Storyboard. Всё верстается в дереве виджетов прямо в коде.
Пример:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: CupertinoButton(
onPressed: () {
setState(() { _pressedCount += 1; });
},
child: Text('Hello'),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}
Все дефолтные виджеты во Flutter можно посмотреть в widget catalog.
Вопрос:
Как добавить или удалить компонент в вёрстке во время работы приложения?
Ответ:
Через функцию, которая будет возвращать нужный виджет в зависимости от состояния.
Отличия:
В iOS можно сделать addSubview() или removeFromSuperview(). Во Flutter так нельзя, т.к. виджеты неизменны. Может изменяться только их состояние.
Пример:
Меняем Text на Button по нажатию на FloatingActionButton.
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> {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('Toggle One');
} else {
return CupertinoButton(
onPressed: () {},
child: Text('Toggle Two'),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: Icon(Icons.update),
),
);
}
}
Вопрос:
Как анимировать виджеты?
Ответ:
Используя класс AnimationController, который является наследником абстрактного класса Animation<T>. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker, который сообщает о перерисовке экрана.
Отличия:
В iOS можно анимировать view с помощью animate(withDuration:animations:). Во Flutter анимацию нужно писать в коде с помощью AnimationController.
Дополнительная информация:
Более подробно можно изучить в Animation & Motion widgets, Animations tutorial и Animations overview.
Пример:
Fade-анимация лого Flutter.
class SampleApp 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() {
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();
},
),
);
}
@override
dispose() {
controller.dispose();
super.dispose();
}
}
Вопрос:
Как использовать CoreGraphics?
Ответ:
Flutter вместо CoreGraphics использует Canvas API на низкоуровневом движке Skia. В Android используется аналогичный Canvas API.
Дополнительная информация:
У Flutter есть два класса для рисования на Canvas: CustomPaint и CustomPainter. Второй реализует ваш алгоритм отрисовки.
Подробнее тут: StackOverflow
Пример:
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;
}
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),
);
}
}
Вопрос:
Как изменять прозрачность виджетов?
Ответ:
Обернуть в виджет Opacity.
Отличия:
В iOS у всех view есть .opacity или .alpha. Во Flutter этот параметр заменяет виджет-обёртка.
Вопрос:
Как создавать кастомные виджеты?
Ответ:
Компоновать виджеты внутри одного (вместо наследования).
Отличия:
В iOS можно наследоваться от интересующей нас view и дописать свою логику. Во Flutter виджет всегда наследуется от 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"),
);
}
Navigation
Вопрос:
Как реализовывать навигацию между экранами во Flutter?
Ответ:
Для навигации между экранами используются классы Navigator и Route.
Отличия:
Во Flutter нет таких понятий, как UIViewController и UINavigationController. Есть Navigator (навигатор) и Routes (маршруты). Navigator похож на UINavigationController по принципу работы. Он может сделать push() или pop() указанному вами маршруту. Route — это своего рода UIViewController, но во Flutter его принято сравнивать с экраном или страницей.
Во Flutter есть два способа навигации:
- описать Map с именами Route;
- напрямую навигировать к Route.
Пример:
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');
Вопрос:
Как навигировать в стороннее приложение?
Ответ:
Либо взаимодействуя с iOS-слоем приложения через MethodChannel, либо используя плагин URL launcher.
Вопрос:
Как сделать pop back в iOS ViewController?
Ответ:
Вызовом SystemNavigator.pop().
Дополнительная информация:
SystemNavigator.pop() из Dart-кода вызывает следующий код в iOS:
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
[((UINavigationController*)viewController) popViewControllerAnimated:NO];
}
Если это не то, что вам нужно, то вы можете сделать свою реализацию через MethodChannel.
Threading & asynchronicity
Вопрос:
Как писать асинхронный код во Flutter?
Ответ:
В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы, возможно, знакомы из C#, JavaScript или Kotlin coroutines.
Пример:
Выполнение запроса и возврата результата для обновления UI:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.
Пример:
Загрузка и обновление данных в ListView:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}")
);
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
Вопрос:
Как выполнить код в фоновом потоке?
Ответ:
Как было сказано выше — с помощью async/await и изоляций (Isolate).
Отличия:
«Из коробки» в iOS можно использовать Operation с возможным переопределением методов. Во 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;
}
}
Вопрос:
Как делать запросы к сети во Flutter?
Ответ:
Во Flutter есть свой HTTP package.
Пример:
Чтобы использовать HTTP package, добавьте его как зависимость в pubspec.yaml:
dependencies:
...
http: ^0.11.3+16
Для выполнения запроса вызовите await в async функции http.get():
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
Вопрос:
Как показывать прогресс выполнения?
Ответ:
С помощью виджета ProgressIndicator.
Пример:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
return widgets.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
}
Структура проекта и ресурсы
Вопрос:
Где хранить ресурсы разного разрешения?
Ответ:
В assets.
Отличия:
В iOS у графических ресурсов есть Images.xcasset, которые находятся в папке assets. Во Flutter есть только assets. Папка ресурсов может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.
Дополнительная информация:
Размеры графических ресурсов в iOS и Flutter идентичны и следуют density-based формату.
Расположение ресурсов:
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.png
Использование AssetImage:
return AssetImage("images/a_dot_burr.jpeg");
Использование asset напрямую:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
Вопрос:
Где хранить строки? Как их локализовать?
Ответ:
Хранить в статичных полях. Локализовать с помощью intl package.
Пример:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
Text(Strings.welcomeMessage)
Вопрос:
Какой аналог CocoaPods? Как добавлять зависимости?
Ответ:
pubspec.yaml.
Дополнительная информация:
Flutter делегирует сборку нативным Android и iOS-сборщикам. Посмотреть список всех популярных библиотек для Flutter можно в Pub.
ViewControllers
Вопрос:
Какой аналог у ViewController во Flutter?
Ответ:
Во Flutter всё — виджеты. Роль ViewController для работы с UI выполняют виджеты. А роль навигации, как было сказано в пункте про навигацию, — Navigator и Route.
Вопрос:
Как обрабатывать события жизненного цикла?
Ответ:
С помощью WidgetsBinding и метода didChangeAppLifecycleState().
Дополнительная информация:
Во Flutter используется FlutterAppDelegate в нативном коде, и движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
- inactive — приложение находится в неактивном состоянии и не получает пользовательский ввод. Это состояние есть только в iOS, в Android нет аналога;
- paused — приложение в данный момент невидимо для пользователя, не отвечает на ввод пользователя, но работает в фоновом режиме;
- resumed — приложение видимо и отвечает на ввод пользователя;
- suspending — приложение в процессе остановки. Это состояние есть только в 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
Вопрос:
Какой аналог у UITableView и UICollectionView?
Ответ:
ListView.
Пример:
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;
}
}
Вопрос:
Как узнать, на каком элементе списка был клик?
Ответ:
Виджет, который является элементом списка, должен сам обрабатывать нажатие на него.
Отличия:
В iOS за это отвечает отдельный метод tableView:didSelectRowAtIndexPath:. Во Flutter элемент списка должен быть обёрнут в виджет, обрабатывающий клики, например GestureDetector.
Вопрос:
Как динамически обновить ListView?
Ответ:
Обновить список данных и вызвать setState().
Отличия:
В iOS для этого необходимо обновить данные и вызвать метод reloadData. Во 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');
});
},
);
}
}
Вопрос:
Какой аналог у UIScrollView?
Ответ:
ListView с виджетами.
Пример:
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
Дополнительная информация:
Более подробно тут.
Жесты и обработка touch event
Вопрос:
Как добавить слушатель onClick для виджета во Flutter?
Ответ:
Если виджет поддерживает клики, то в onPressed(). Если нет, то в onTap().
Пример:
В onPressed():
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"),
);
}
В onTap():
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
),
);
}
}
Вопрос:
Как обрабатывать другие жесты на виджетах?
Ответ:
Используя GestureDetector. Им можно обрабатывать следующие действия:
Tap
Double tap
Long press
Vertical drag
Horizontal drag
Пример:
Обработка onDoubleTap:
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: RotationTransition(
turns: curve,
child: FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
),
);
}
}
Стилизация приложения
Вопрос:
Как использовать тему (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(),
);
}
}
Вопрос:
Как использовать кастомные шрифты?
Ответ:
Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в 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.
Форма ввода
Вопрос:
Как получить результат пользовательского ввода?
Ответ:
С помощью TextEditingController.
Пример:
class _MyFormState extends State<MyForm> {
// Create a text controller and use it to retrieve the current value.
// of the TextField!
final myController = TextEditingController();
@override
void dispose() {
// Clean up the controller when disposing of the Widget.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Retrieve Text Input'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: myController,
),
),
floatingActionButton: FloatingActionButton(
// When the user presses the button, show an alert dialog with the
// text the user has typed into our text field.
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the user has typed in using our
// TextEditingController
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: Icon(Icons.text_fields),
),
);
}
}
Более подробно написано здесь: Retrieve the value of a text field.
Вопрос:
Какой аналог у hint в TextInput?
Ответ:
Подсказку можно показать с помощью InputDecoration, передав его в качестве параметра конструктора в виджет.
Пример:
body: Center(
child: TextField(
decoration: 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.
Вопрос:
Как делать нативные (платформенные) вставки кода?
Ответ:
Flutter использует EventBus для взаимодействия с платформенным кодом. Подробно тут: developing packages and plugins.
Базы данных и локальное хранилище
Вопрос:
Как получить доступ к UserDefault?
Ответ:
С помощью Shared_Preferences plugin (для Shared Preferences в Android тоже).
Пример:
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);
}
Вопрос:
Какой аналог у Core Data?
Ответ:
SQFlite.
Уведомления
Вопрос:
Как показать push-уведомление?
Ответ:
С помощью плагина Firebase_Messaging.
Заключение
Новые языки программирования и фреймворки появляются практически постоянно. И на старте трудно понять, что выстрелит и будет долго жить, а что забудут уже через год. Боб Мартин в своей книге «Идеальный программист» призывает нас изучать новые языки программирования и фреймворки. Чед Фаулер в книге «Программист-фанатик» советует всегда быть на острие технологий. Но как понять, что ты не ошибся с выбором? В 2016 году я обратил внимание на Kotlin, но из-за высокой загруженности не смог уделить ему достаточно времени до второй половины 2017. На старте многие относились к нему скептически, а сейчас это один из самых популярных языков программирования, и огромное количество разработчиков создают на нём свои продукты. Я чувствую, что за те полтора года мог бы получить более глубокое понимание тонкостей языка.
В том же 2016 году появился фреймворк Flutter на языке Dart. Но рост его популярности был не такой стремительный, и только в 2018 году о нём заговорили громко. Тогда мне тоже захотелось попробовать его в действии. И мне понравилось! Время покажет, какое будущее ждёт этот фреймворк, но кажется, он очень перспективный. (И если Google Fuchsia выстрелит, то, без сомнений, Flutter не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для мозга. На этом у меня всё. Да не сломает Apple ваш Store!