- PVSM.RU - https://www.pvsm.ru -
После длительного перерыва я продолжу рассказывать о популярном фреймворке Flutter в формате «вопрос — ответ». Первую статью для Android-разработчиков вы можете найти здесь [1], а сегодня будет полезный материал для разработчиков под iOS.
Если у вас мало времени для самостоятельного и глубокого изучения документации, но вы хотите понять, чем хорош Flutter и как его использовать, загляните под кат.
Flutter. Часть 1. Для Android-разработчиков [1]
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для React-Native-разработчиков
Flutter. Часть 4. Для Web-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков
Какой аналог у UIView [53] во Flutter?
Widget [54]
UIView [53] — фактически то, что будет на экране. Для отображения изменений вызывается setNeedsDisplay().
Widget [54] — описание того, что будет на экране. Для изменения создаётся заново.
Flutter включает в себя библиотеку Cupertino Widgets [55]. В ней собраны виджеты, которые реализуют гайдлайны Apple Design [56].
Как обновлять отображение виджетов?
Используя StatefulWidget [57] и его State [58]. Во Flutter есть 2 вида виджетов: StatelessWidget [59] и StatefulWidget [57]. Они работают одинаково, отличие только в состоянии при рендеринге.
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 [60]?
Во Flutter нет Storyboard [60]. Всё верстается в дереве виджетов прямо в коде.
@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 [61].
Как добавить или удалить компонент в вёрстке во время работы приложения?
Через функцию, которая будет возвращать нужный виджет в зависимости от состояния.
В 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 [62], который является наследником абстрактного класса Animation<T> [63]. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker [64], который сообщает о перерисовке экрана.
В iOS можно анимировать view с помощью animate(withDuration:animations:). Во Flutter анимацию нужно писать в коде с помощью AnimationController.
Более подробно можно изучить в Animation & Motion widgets [65], Animations tutorial [66] и Animations overview [67].
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 [68]?
Flutter вместо CoreGraphics использует Canvas API на низкоуровневом движке Skia [69]. В Android используется аналогичный Canvas API.
У Flutter есть два класса для рисования на Canvas: CustomPaint [70] и CustomPainter [71]. Второй реализует ваш алгоритм отрисовки.
Подробнее тут: StackOverflow [72]
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 [73].
В 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"),
);
}
Как реализовывать навигацию между экранами во Flutter?
Для навигации между экранами используются классы Navigator [74] и Route [75].
Во Flutter нет таких понятий, как UIViewController и UINavigationController. Есть Navigator (навигатор) и Routes (маршруты). Navigator похож на UINavigationController по принципу работы. Он может сделать push() [76] или pop() [77] указанному вами маршруту. Route — это своего рода UIViewController, но во Flutter его принято сравнивать с экраном или страницей.
Во Flutter есть два способа навигации:
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 [78], либо используя плагин URL launcher [79].
Как сделать 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 [78].
Как писать асинхронный код во Flutter?
В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates [80]). Для асинхронного выполнения используется 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() [81] для перерисовки дерева виджетов с новыми данными.
Загрузка и обновление данных в ListView [82]:
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 [83].
Чтобы использовать 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 [84].
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 [85]:
return AssetImage("images/a_dot_burr.jpeg");
Использование asset напрямую:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
Где хранить строки? Как их локализовать?
Хранить в статичных полях. Локализовать с помощью intl package [86].
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
Text(Strings.welcomeMessage)
Какой аналог CocoaPods? Как добавлять зависимости?
pubspec.yaml.
Flutter делегирует сборку нативным Android и iOS-сборщикам. Посмотреть список всех популярных библиотек для Flutter можно в Pub [87].
Какой аналог у ViewController [88] во Flutter?
Во Flutter всё — виджеты. Роль ViewController для работы с UI выполняют виджеты. А роль навигации, как было сказано в пункте про навигацию, — Navigator и Route.
Как обрабатывать события жизненного цикла?
С помощью WidgetsBinding [89] и метода didChangeAppLifecycleState() [90].
Во Flutter используется FlutterAppDelegate в нативном коде, и движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
Более подробно это описано в AppLifecycleStatus documentation [91].
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()));
}
Какой аналог у UITableView [92] и UICollectionView [93]?
ListView [94].
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 [95].
Как динамически обновить ListView [94]?
Обновить список данных и вызвать 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 [96].
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 [97]?
ListView [82] с виджетами.
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
Более подробно тут [98].
Как добавить слушатель 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 [99]. Им можно обрабатывать следующие действия:
Обработка 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'),
),
),
);
}
Как стилизовать текстовые виджеты?
С помощью параметров:
Как получить результат пользовательского ввода?
С помощью TextEditingController [112].
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 [113].
Какой аналог у hint в TextInput [114]?
Подсказку можно показать с помощью InputDecoration [115], передав его в качестве параметра конструктора в виджет.
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
),
)
Как показать ошибки валидации?
Всё так же — с помощью InputDecoration [115] и его состояния.
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);
}
}
Как получить доступ к GPS?
С помощью плагина geolocator [116].
Как получить доступ к камере?
С помощью плагина image_picker [117].
Как авторизоваться через Facebook?
С помощью плагина flutter_facebook_login [118].
Как использовать Firebase?
Firebase поддерживает Flutter first party plugins [119]:
Как делать нативные (платформенные) вставки кода?
Flutter использует EventBus для взаимодействия с платформенным кодом. Подробно тут: developing packages and plugins [128].
Как получить доступ к UserDefault?
С помощью Shared_Preferences plugin [129] (для 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 [130]?
SQFlite [131].
Как показать push-уведомление?
С помощью плагина Firebase_Messaging [132].
Новые языки программирования и фреймворки появляются практически постоянно. И на старте трудно понять, что выстрелит и будет долго жить, а что забудут уже через год. Боб Мартин в своей книге «Идеальный программист» призывает нас изучать новые языки программирования и фреймворки. Чед Фаулер в книге «Программист-фанатик» советует всегда быть на острие технологий. Но как понять, что ты не ошибся с выбором? В 2016 году я обратил внимание на Kotlin, но из-за высокой загруженности не смог уделить ему достаточно времени до второй половины 2017. На старте многие относились к нему скептически, а сейчас это один из самых популярных языков программирования, и огромное количество разработчиков создают на нём свои продукты. Я чувствую, что за те полтора года мог бы получить более глубокое понимание тонкостей языка.
В том же 2016 году появился фреймворк Flutter на языке Dart. Но рост его популярности был не такой стремительный, и только в 2018 году о нём заговорили громко. Тогда мне тоже захотелось попробовать его в действии. И мне понравилось! Время покажет, какое будущее ждёт этот фреймворк, но кажется, он очень перспективный. (И если Google Fuchsia выстрелит, то, без сомнений, Flutter не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для
Автор: Дмитрий Васильев
Источник [134]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/framework/338547
Ссылки в тексте:
[1] здесь: https://habr.com/ru/company/funcorp/blog/442432/
[2] Views: #views
[3] Какой аналог у UIView во Flutter?: #view_analog
[4] Как обновлять отображение виджетов?: #widget_update
[5] Как верстать экран с виджетами? Где Storyboard?: #widget_layout
[6] Как добавить или удалить компонент в вёрстке во время работы приложения?: #widget_remove_add
[7] Как анимировать виджеты?: #widget_animation
[8] Как использовать CoreGraphics?: #core_graphics
[9] Как изменять прозрачность виджетов?: #opacity
[10] Как создавать кастомные виджеты?: #widget_custom
[11] Navigation: #navigation
[12] Как реализовывать навигацию между экранами во Flutter?: #navigation_impl
[13] Как навигировать в стороннее приложение?: #outer_apps
[14] Как сделать pop back в iOS ViewController?: #native_pop_back
[15] Threading & asynchronicity: #async_ui
[16] Как писать асинхронный код во Flutter?: #async_code
[17] Как выполнить код в фоновом потоке?: #background_thread
[18] Как делать запросы к сети во Flutter?: #network
[19] Как показывать прогресс выполнения?: #progress
[20] Структура проекта и ресурсы: #project
[21] Где хранить ресурсы разного разрешения?: #resources
[22] Где хранить строки? Как их локализовать?: #strings
[23] ViewControllers: #view_controllers
[24] Какой аналог у ViewController во Flutter?: #viewcontrollert_analog
[25] Как обрабатывать события жизненного цикла?: #lifecycle
[26] Layouts: #layouts
[27] Какой аналог у UITableView и UICollectionView?: #table_and_collection_view
[28] Как узнать, на каком элементе списка был клик?: #list_item_click
[29] Как динамически обновить ListView?: #dynamic_listview_update
[30] Какой аналог у UIScrollView?: #scrollview
[31] Жесты и обработка touch event: #gestures
[32] Как добавить слушатель onClick для виджета во Flutter?: #on_click
[33] Как обрабатывать другие жесты на виджетах?: #widget_gestures
[34] Стилизация приложения: #themes
[35] Как использовать тему (Theme) в приложении?: #theme
[36] Как использовать кастомные шрифты?: #custom_fonts
[37] Как стилизовать текстовые виджеты?: #text_widgets_style
[38] Форма ввода: #input
[39] Как получить результат пользовательского ввода?: #text_retrieve
[40] Какой аналог у hint в Input?: #hint
[41] Как показать ошибки валидации?: #validation_error
[42] Плагины Flutter: #plugins
[43] Как получить доступ к GPS?: #gps
[44] Как получить доступ к камере?: #camera
[45] Как авторизоваться через Facebook?: #facebook
[46] Как использовать Firebase?: #firebase
[47] Как делать нативные (платформенные) вставки кода?: #native_code
[48] Базы данных и локальное хранилище: #database
[49] Как получить доступ к UserDefault?: #user_default
[50] Как аналог у Core Data?: #coredata
[51] Уведомления: #notifications
[52] Как показать push-уведомление?: #push
[53] UIView: https://developer.apple.com/documentation/uikit/uiview
[54] Widget: https://flutter.dev/docs/development/ui/widgets-intro
[55] Cupertino Widgets: https://flutter.dev/docs/development/ui/widgets/cupertino
[56] гайдлайны Apple Design: https://developer.apple.com/design/resources/
[57] StatefulWidget: https://docs.flutter.io/flutter/widgets/StatefulWidget-class.html
[58] State: https://docs.flutter.io/flutter/widgets/State-class.html
[59] StatelessWidget: https://docs.flutter.io/flutter/widgets/StatelessWidget-class.html
[60] Storyboard: https://developer.apple.com/library/archive/documentation/General/Conceptual/Devpedia-CocoaApp/Storyboard.html
[61] widget catalog: https://flutter.io/docs/development/ui/widgets/layout
[62] AnimationController: https://docs.flutter.io/flutter/animation/AnimationController-class.html
[63] Animation<T>: https://docs.flutter.io/flutter/animation/Animation-class.html
[64] Ticker: https://docs.flutter.io/flutter/scheduler/Ticker-class.html
[65] Animation & Motion widgets: https://flutter.io/docs/development/ui/widgets/animation
[66] Animations tutorial: https://flutter.io/docs/development/ui/animations/tutorial
[67] Animations overview: https://flutter.io/docs/development/ui/animations
[68] CoreGraphics: https://developer.apple.com/documentation/coregraphics
[69] Skia: https://skia.org/
[70] CustomPaint: https://docs.flutter.io/flutter/widgets/CustomPaint-class.html
[71] CustomPainter: https://docs.flutter.io/flutter/rendering/CustomPainter-class.html
[72] StackOverflow: https://stackoverflow.com/questions/46241071/create-signature-area-for-mobile-app-in-dart-flutter
[73] Opacity: https://api.flutter.dev/flutter/widgets/Opacity-class.html
[74] Navigator: https://docs.flutter.io/flutter/widgets/Navigator-class.html
[75] Route: https://docs.flutter.io/flutter/widgets/Route-class.html
[76] push(): https://docs.flutter.io/flutter/widgets/Navigator/push.html
[77] pop(): https://docs.flutter.io/flutter/widgets/Navigator/pop.html
[78] MethodChannel: https://docs.flutter.io/flutter/services/MethodChannel-class.html
[79] URL launcher: https://pub.dev/packages/url_launcher
[80] Isolates: https://docs.flutter.io/flutter/dart-isolate/Isolate-class.html
[81] setState(): https://docs.flutter.io/flutter/widgets/State/setState.html
[82] ListView: https://docs.flutter.io/flutter/widgets/ListView-class.html
[83] HTTP package: https://pub.dartlang.org/packages/http
[84] ProgressIndicator: https://docs.flutter.io/flutter/material/ProgressIndicator-class.html
[85] AssetImage: https://docs.flutter.io/flutter/painting/AssetImage-class.html
[86] intl package: https://pub.dartlang.org/packages/intl
[87] Pub: https://pub.dartlang.org/flutter/packages/
[88] ViewController: https://developer.apple.com/documentation/uikit/uiviewcontroller
[89] WidgetsBinding: https://docs.flutter.io/flutter/widgets/WidgetsBinding-mixin.html
[90] didChangeAppLifecycleState(): https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver/didChangeAppLifecycleState.html
[91] AppLifecycleStatus documentation: https://docs.flutter.io/flutter/dart-ui/AppLifecycleState-class.html
[92] UITableView: https://developer.apple.com/documentation/uikit/uitableview
[93] UICollectionView: https://developer.apple.com/documentation/uikit/uicollectionview
[94] ListView: https://api.flutter.dev/flutter/widgets/ListView-class.html
[95] GestureDetector: https://api.flutter.dev/flutter/widgets/GestureDetector-class.html
[96] ListView.Builder: https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html
[97] UIScrollView: https://developer.apple.com/documentation/uikit/uiscrollview
[98] тут: https://flutter.dev/docs/development/ui/widgets/layout
[99] GestureDetector: https://docs.flutter.io/flutter/widgets/GestureDetector-class.html
[100] onTapDown: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapDown.html
[101] onTapUp: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapUp.html
[102] onTap: https://docs.flutter.io/flutter/widgets/GestureDetector/onTap.html
[103] onTapCancel: https://docs.flutter.io/flutter/widgets/GestureDetector/onTapCancel.html
[104] onDoubleTap: https://docs.flutter.io/flutter/widgets/GestureDetector/onDoubleTap.html
[105] onLongPress: https://docs.flutter.io/flutter/widgets/GestureDetector/onLongPress.html
[106] onVerticalDragStart: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragStart.html
[107] onVerticalDragUpdate: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragUpdate.html
[108] onVerticalDragEnd: https://docs.flutter.io/flutter/widgets/GestureDetector/onVerticalDragEnd.html
[109] onHorizontalDragStart: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragStart.html
[110] onHorizontalDragUpdate: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragUpdate.html
[111] onHorizontalDragEnd: https://docs.flutter.io/flutter/widgets/GestureDetector/onHorizontalDragEnd.html
[112] TextEditingController: https://api.flutter.dev/flutter/widgets/TextEditingController-class.html
[113] Retrieve the value of a text field: https://flutter.io/docs/cookbook/forms/retrieve-input
[114] TextInput: https://docs.flutter.io/flutter/services/TextInput-class.html
[115] InputDecoration: https://docs.flutter.io/flutter/material/InputDecoration-class.html
[116] geolocator: https://pub.dartlang.org/packages/geolocator
[117] image_picker: https://pub.dartlang.org/packages/image_picker
[118] flutter_facebook_login: https://pub.dartlang.org/packages/flutter_facebook_login
[119] Flutter first party plugins: https://pub.dartlang.org/flutter/packages?q=firebase
[120] firebase_admob: https://pub.dartlang.org/packages/firebase_admob
[121] firebase_analytics: https://pub.dartlang.org/packages/firebase_analytics
[122] firebase_auth: https://pub.dartlang.org/packages/firebase_auth
[123] firebase_database: https://pub.dartlang.org/packages/firebase_database
[124] firebase_storage: https://pub.dartlang.org/packages/firebase_storage
[125] firebase_messaging: https://pub.dartlang.org/packages/firebase_messaging
[126] flutter_firebase_ui: https://pub.dartlang.org/packages/flutter_firebase_ui
[127] cloud_firestore: https://pub.dartlang.org/packages/cloud_firestore
[128] developing packages and plugins: https://flutter.io/docs/development/packages-and-plugins/developing-packages
[129] Shared_Preferences plugin: https://pub.dartlang.org/packages/shared_preferences
[130] Core Data: https://developer.apple.com/documentation/coredata
[131] SQFlite: https://pub.dartlang.org/packages/sqflite
[132] Firebase_Messaging: https://pub.dev/packages/firebase_messaging
[133] мозга: http://www.braintools.ru
[134] Источник: https://habr.com/ru/post/477182/?utm_source=habrahabr&utm_medium=rss&utm_campaign=477182
Нажмите здесь для печати.