[По докам] Flutter. Часть 1. Для Android разработчиков
Про Flutter написано уже много статей. С каждым месяцем он становится всё популярнее. Поэтому я решил интерпретировать официальную документацию Flutter в лаконичный формат «вопрос — ответ». Думаю, многие, как и я, не имеют достаточно свободного времени для подробного изучения документации фреймворка, с которым они ещё не работают.
Если вы хотите понять, чем хорош этот фреймворк, и оценить, сколько усилий придётся приложить, чтобы его использовать — добро пожаловать под кат.
Содержание:
- Views
- Intents
- Async UI
- Структура проекта и ресурсы
- Activities & Fragments
- Layouts
- Жесты и обработка touch event.
- ListViews & Adapters
- Работа с текстом
- Форма ввода
- Плагины Flutter
- Themes
- Базы данных и локальное хранилище
- Уведомления
Views
Вопрос:
Какой аналог у View во Flutter?
Ответ:
Widget
Отличия:
View — фактически то, что будет на экране. Для отображения изменений вызывается invalidate().
Для изменения создаётся заново. Widget — описание того, что будет на экране.
Дополнительная информация:
При запуске на самом Android под капотом Widget находится View. Flutter включает в себя библиотеку Material Components. В ней собраны виджеты, которые реализуют гайдлайны Material 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
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { // дефолтный текст String textToShow = "Мне нравится Flutter"; void _updateText() { setState(() { // обновление текста textToShow = "Flutter крутой!"; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center(child: Text(textToShow)), floatingActionButton: FloatingActionButton( onPressed: _updateText, tooltip: 'Обновить текст', child: Icon(Icons.update), ), ); }
}
Вопрос:
Как верстать экран с виджетами? Где файл XML layout?
Ответ:
Во Flutter нет XML-вёрстки экранов. Всё верстается в дереве виджетов прямо в коде.
Пример:
@override
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: MaterialButton( onPressed: () {}, child: Text('Hello'), padding: EdgeInsets.only(left: 10.0, right: 10.0), ), ), );
}
Все дефолтные виджеты во Flutter можно посмотреть в widget catalog.
Вопрос:
Как добавить или удалить компонент в вёрстку во время работы приложения?
Ответ:
Через функцию, которая будет возвращать нужный виджет в зависимости от состояния.
Отличия:
В Android можно сделать addView() или removeView() во ViewGroup. Во Flutter так нельзя, т.к. виджеты неизменны. Может изменяться только их состояние.
Пример:
Как поменять Text на Button по нажатию на FloatingActionButton.
import 'package:flutter/material.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // Этот виджет корневой в приложении. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { // Дефолтное значение для флага bool toggle = true; void _toggle() { setState(() { toggle = !toggle; }); } _getToggleChild() { if (toggle) { return Text('Toggle One'); } else { return MaterialButton(onPressed: () {}, child: Text('Toggle Two')); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: _getToggleChild(), ), floatingActionButton: FloatingActionButton( onPressed: _toggle, tooltip: 'Update Text', child: Icon(Icons.update), ), ); }
}
Вопрос:
Как анимировать виджеты?
Ответ:
Используя класс AnimationController, который является наследником абстрактного класса Animation<T>. Кроме запуска анимации он может ставить её на паузу, перематывать, останавливать и проигрывать в обратную сторону. Работает с помощью Ticker, который сообщает о перерисовке экрана.
Отличия:
В Android можно создавать анимации в XML или анимировать View с помощью animate(). Во Flutter анимацию нужно писать в коде с помощью AnimationController.
Дополнительная информация:
Более подробно можно изучить в Animation & Motion widgets, Animations tutorial и Animations overview.
Пример:
Fade-анимация лого Flutter.
import 'package:flutter/material.dart'; void main() { runApp(FadeAppTest());
} class FadeAppTest extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Fade Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyFadeTest(title: 'Fade Demo'), ); }
} class MyFadeTest extends StatefulWidget { MyFadeTest({Key key, this.title}) : super(key: key); final String title; @override _MyFadeTest createState() => _MyFadeTest();
} class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin { AnimationController controller; CurvedAnimation curve; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this); curve = CurvedAnimation(parent: controller, curve: Curves.easeIn); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Container( child: FadeTransition( opacity: curve, child: FlutterLogo( size: 100.0, )))), floatingActionButton: FloatingActionButton( tooltip: 'Fade', child: Icon(Icons.brush), onPressed: () { controller.forward(); }, ), ); }
}
Вопрос:
Как использовать Canvas?
Ответ:
У Android и Flutter одинаковый API для Canvas, т.к. они используют одинаковый низкоуровневый движок Skia.
Отличия:
Нет.
Дополнительная информация:
У Flutter есть два класса для рисования на Canvas — CustomPaint и CustomPainter. Второй реализует ваш алгоритм отрисовки.
Подробнее тут: StackOverflow
Пример:
import 'package:flutter/material.dart'; void main() => runApp(MaterialApp(home: DemoApp())); class DemoApp extends StatelessWidget { Widget build(BuildContext context) => Scaffold(body: Signature());
} class Signature extends StatefulWidget { SignatureState createState() => SignatureState();
} class SignatureState extends State<Signature> { List<Offset> _points = <Offset>[]; Widget build(BuildContext context) { return GestureDetector( onPanUpdate: (DragUpdateDetails details) { setState(() { RenderBox referenceBox = context.findRenderObject(); Offset localPosition = referenceBox.globalToLocal(details.globalPosition); _points = List.from(_points)..add(localPosition); }); }, onPanEnd: (DragEndDetails details) => _points.add(null), child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite), ); }
} class SignaturePainter extends CustomPainter { SignaturePainter(this.points); final List<Offset> points; void paint(Canvas canvas, Size size) { var paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; for (int i = 0; i < points.length - 1; i++) { if (points[i] != null && points[i + 1] != null) canvas.drawLine(points[i], points[i + 1], paint); } } bool shouldRepaint(SignaturePainter other) => other.points != points;
}
Вопрос:
Как создавать кастомные виджеты?
Ответ:
Компоновать виджеты внутри одного (вместо наследования).
Отличия:
В Android мы можем наследоваться от интересующей нас View и дописать свою логику. Во Flutter это похоже на ViewGroup, только виджет всегда наследуется от StatelessWidget или StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных вам виджетов в качестве параметров или полей.
Пример:
class CustomButton extends StatelessWidget { final String label; CustomButton(this.label); @override Widget build(BuildContext context) { return RaisedButton(onPressed: () {}, child: Text(label)); }
} @override
Widget build(BuildContext context) { return Center( child: CustomButton("Hello"), );
}
Intents
Вопрос:
Какой аналог Intent во Flutter?
Ответ:
Его нет. Для навигации между экранами используются классы Navigator и Route.
Подробнее о нативной интеграции: Developing Packages and Plugins. Для взаимодействия с внешними компонентами (например, камерой или файл-пикером) можно использовать плагины или нативную интеграцию на каждой платформе.
Отличия:
Во Flutter нет таких понятий, как Activity и Fragment. Есть Navigator (навигатор) и Routes (маршруты). Приложение на Flutter напоминает single-activity приложение, где разные экраны представляют собой разные фрагменты, а управляет ими FragmentManager. Navigator похож на FragmentManager по принципу работы. Он может сделать push() или pop() указанному вами маршруту. Route — это своего рода Fragment, но во Flutter его принято сравнивать с экраном или страницей.
В Android мы описываем все Activities, между которыми можем навигировать в AndroidManifest.xml.
Во Flutter есть два способа:
- описать Map с именами Route (MaterialApp);
- напрямую навигировать к Route (WidgetApp).
Пример:
void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named '/' routes: <String, WidgetBuilder> { '/a': (BuildContext context) => MyPage(title: 'page A'), '/b': (BuildContext context) => MyPage(title: 'page B'), '/c': (BuildContext context) => MyPage(title: 'page C'), }, ));
} Navigator.of(context).pushNamed('/b');
Вопрос:
Как обрабатывать поступающие от других приложений интенты?
Ответ:
Взаимодействуя с Android-слоем приложения через MethodChannel.
Пример:
Прописываем intent-filter в AndroidManifest.xml:
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <!-- ... --> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter>
</activity>
Обрабатываем Intent в MainActivity и из Flutter вызываем код через MethodChannel:
package com.example.shared; import android.content.Intent;
import android.os.Bundle; import java.nio.ByteBuffer; import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant; public class MainActivity extends FlutterActivity { private String sharedText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); Intent intent = getIntent(); String action = intent.getAction(); String type = intent.getType(); if (Intent.ACTION_SEND.equals(action) && type != null) { if ("text/plain".equals(type)) { handleSendText(intent); // Handle text being sent } } new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { if (call.method.contentEquals("getSharedText")) { result.success(sharedText); sharedText = null; } } }); } void handleSendText(Intent intent) { sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); }
}
Запрашиваем данные, когда виджет начнёт отрисовываться:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample Shared App Handler', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { static const platform = const MethodChannel('app.channel.shared.data'); String dataShared = "No data"; @override void initState() { super.initState(); getSharedText(); } @override Widget build(BuildContext context) { return Scaffold(body: Center(child: Text(dataShared))); } getSharedText() async { var sharedData = await platform.invokeMethod("getSharedText"); if (sharedData != null) { setState(() { dataShared = sharedData; }); } }
}
Вопрос:
Какой аналог у startActivityForResult()?
Ответ:
Ключевое слово await и результат Future-класса.
Отличия:
После вызова startActivityForResult() в Android нам нужно реализовывать обработку в onActivityResult(). Во Flutter ничего реализовывать не нужно, т.к. метод навигатора push() возвращает объект Future.
Пример:
Map coordinates = await Navigator.of(context).pushNamed('/location');
И когда на экране '/location' получили координаты, делаем pop():
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
Async UI
Вопрос:
Какой аналог у runOnUiThread() во 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).
Отличия:
«Из коробки» в Android можно использовать AsyncTask. В нём нужно реализовать onPreExecute(), doInBackground(), onPostExecute(). Во Flutter «из коробки» вам просто нужно использовать async/await, об остальном позаботится Dart.
Пример:
Здесь метод dataLoader() изолирован. В изоляциях вы можете запускать тяжелые операции, такие как парсинг больших JSON-ов, шифрование, обработка изображений и т.д.
loadData() async { ReceivePort receivePort = ReceivePort(); await Isolate.spawn(dataLoader, receivePort.sendPort); // The 'echo' isolate sends its SendPort as the first message SendPort sendPort = await receivePort.first; List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts"); setState(() { widgets = msg; });
} // The entry point for the isolate
static dataLoader(SendPort sendPort) async { // Open the ReceivePort for incoming messages. ReceivePort port = ReceivePort(); // Notify any other isolates what port this isolate listens to. sendPort.send(port.sendPort); await for (var msg in port) { String data = msg[0]; SendPort replyTo = msg[1]; String dataURL = data; http.Response response = await http.get(dataURL); // Lots of JSON to parse replyTo.send(json.decode(response.body)); }
} Future sendReceive(SendPort port, msg) { ReceivePort response = ReceivePort(); port.send([msg, response.sendPort]); return response.first;
} Полноценный запускаемый пример:
import 'dart:convert'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { List widgets = []; @override void initState() { super.initState(); loadData(); } showLoadingDialog() { if (widgets.length == 0) { return true; } return false; } getBody() { if (showLoadingDialog()) { return getProgressDialog(); } else { return getListView(); } } getProgressDialog() { return Center(child: CircularProgressIndicator()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: getBody()); } ListView getListView() => ListView.builder( itemCount: widgets.length, itemBuilder: (BuildContext context, int position) { return getRow(position); }); Widget getRow(int i) { return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}")); } loadData() async { ReceivePort receivePort = ReceivePort(); await Isolate.spawn(dataLoader, receivePort.sendPort); // The 'echo' isolate sends its SendPort as the first message SendPort sendPort = await receivePort.first; List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts"); setState(() { widgets = msg; }); } // the entry point for the isolate static dataLoader(SendPort sendPort) async { // Open the ReceivePort for incoming messages. ReceivePort port = ReceivePort(); // Notify any other isolates what port this isolate listens to. sendPort.send(port.sendPort); await for (var msg in port) { String data = msg[0]; SendPort replyTo = msg[1]; String dataURL = data; http.Response response = await http.get(dataURL); // Lots of JSON to parse replyTo.send(json.decode(response.body)); } } Future sendReceive(SendPort port, msg) { ReceivePort response = ReceivePort(); port.send([msg, response.sendPort]); return response.first; }
}
Вопрос:
Какой аналог у OkHttp во Flutter?
Ответ:
Во Flutter есть свой HTTP package.
Дополнительная информация:
Пока в HTTP Package реализованы не все фичи из OkHttp, поэтому многие недостающие из них вынесены в абстракции и вы можете реализовать их самостоятельно по мере необходимости.
Пример:
Чтобы использовать HTTP package, добавьте его как зависимость в pubspec.yaml:
dependencies: ... http: ^0.11.3+16
Для выполнения запроса вызовите await в async функции http.get():
import 'dart:convert'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...] loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }
}
Вопрос:
Как показывать прогресс выполнения?
Ответ:
С помощью виджета ProgressIndicator.
Пример:
import 'dart:convert'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { List widgets = []; @override void initState() { super.initState(); loadData(); } showLoadingDialog() { return widgets.length == 0; } getBody() { if (showLoadingDialog()) { return getProgressDialog(); } else { return getListView(); } } getProgressDialog() { return Center(child: CircularProgressIndicator()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: getBody()); } ListView getListView() => ListView.builder( itemCount: widgets.length, itemBuilder: (BuildContext context, int position) { return getRow(position); }); Widget getRow(int i) { return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}")); } loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }
}
Структура проекта и ресурсы
Вопрос:
Где хранить ресурсы разного разрешения?
Ответ:
В assets.
Отличия:
В Android у ресурсов есть папка res и есть assets. Во Flutter есть только assets. Папка assets может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.
Дополнительная информация:
Сопоставление размеров графических ресурсов в Android и Flutter.
Android density qualifier
Flutter pixel ratio
ldpi
0.75x
mdpi
1.0x
hdpi
1.5x
xhdpi
2.0x
xxhdpi
3.0x
xxxhdpi
4.0x
Во Flutter для использования ресурсов в коде используется AssetManager или специализированные классы, начинающиеся с Asset.
Пример:
AssetManager:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
Расположение ресурсов:
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
Путь в pubspec.yaml файле:
assets: - images/my_icon.jpeg
Использование AssetImage:
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)
Вопрос:
Какой аналог Gradle-файла? Как добавлять зависимости?
Ответ:
pubspec.yaml.
Дополнительная информация:
Flutter делегирует сборку нативным Android и iOS сборщикам. Посмотреть список всех популярных библиотек для Flutter можно в Pub.
Activities & Fragments
Вопрос:
Какой аналог у Activity и Fragment во Flutter?
Ответ:
Во Flutter всё — виджеты. Роль активити и фрагментов для работы с UI выполняют виджеты. А роль навигации, как было сказано в пункте про навигацию — Navigator и Route.
Дополнительная информация:
Flutter For Android Developers: How to design an Activity UI in Flutter.
Вопрос:
Как обрабатывать события жизненного цикла?
Ответ:
С помощью WidgetsBinding и метода didChangeAppLifecycleState().
Дополнительная информация:
Во Flutter используется FlutterActivity в нативном коде, и движок 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
Вопрос:
Какой аналог у LinearLayout?
Ответ:
Row — для горизонтального расположения, Column — для вертикального.
Дополнительная информация:
Flutter For Android Developers: How to design LinearLayout in Flutter?
Пример:
@override
Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], );
}
@override
Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Column One'), Text('Column Two'), Text('Column Three'), Text('Column Four'), ], );
}
Вопрос:
Какой аналог у RelativeLayout?
Ответ:
Виджет Stack.
Подробнее:
StackOverflow
Вопрос:
Какой аналог у ScrollView?
Ответ:
ListView с виджетами.
Пример:
@override
Widget build(BuildContext context) { return ListView( children: <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], );
}
Вопрос:
Как обрабатывать переходы между portrait и landscape?
Ответ:
FlutterView обрабатывает перевороты, если AndroidManifest.xml содержит
android:configChanges=«orientation|screenSize»
Жесты и обработка 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(); } }, ), )); }
}
ListViews & Adapters
Вопрос:
Какой аналог у ListView во Flutter?
Ответ:
ListView.
Отличия:
Во Flutter не нужно думать об очистке и повторном использовании элементов (чем занимается ListView/RecyclerView в Android, используя паттерн ViewHolder).
Пример:
import 'package:flutter/material.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } _getListData() { List<Widget> widgets = []; for (int i = 0; i < 100; i++) { widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i"))); } return widgets; }
}
Вопрос:
Как узнать на каком элементе было нажатие?
Ответ:
Оборачивая элемент в GestureDetector.
Пример:
import 'package:flutter/material.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } _getListData() { List<Widget> widgets = []; for (int i = 0; i < 100; i++) { widgets.add(GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { print('row tapped'); }, )); } return widgets; }
}
Вопрос:
Как динамически обновить ListView?
Ответ:
Если у вас небольшой набор данных, то это можно сделать через setState(). Если набор данных большой, то через ListView.Builder, который является аналогом RecyclerView.
Пример:
Используя 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 = <Widget>[]; @override void initState() { super.initState(); for (int i = 0; i < 100; i++) { widgets.add(getRow(i)); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: widgets), ); } Widget getRow(int i) { return GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { setState(() { widgets = List.from(widgets); widgets.add(getRow(widgets.length + 1)); print('row $i'); }); }, ); }
}
Используя ListView.Builder:
import 'package:flutter/material.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { List widgets = <Widget>[]; @override void initState() { super.initState(); for (int i = 0; i < 100; i++) { widgets.add(getRow(i)); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView.builder( itemCount: widgets.length, itemBuilder: (BuildContext context, int position) { return getRow(position); })); } Widget getRow(int i) { return GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { setState(() { widgets.add(getRow(widgets.length + 1)); print('row $i'); }); }, ); }
}
Работа с текстом
Вопрос:
Как использовать кастомные шрифты?
Ответ:
Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в pubspec.yaml.
Пример:
fonts: - family: MyCustomFont fonts: - asset: fonts/MyCustomFont.ttf - style: italic
@override
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: Text( 'This is a custom font text', style: TextStyle(fontFamily: 'MyCustomFont'), ), ), );
}
Вопрос:
Как стилизовать текстовые виджеты?
Ответ:
С помощью параметров:
- color;
- decoration;
- decorationColor;
- decorationStyle;
- fontFamily;
- fontSize;
- fontStyle;
- fontWeight;
- hashCode;
- height;
- inherit;
- letterSpacing;
- textBaseline;
- wordSpacing.
Форма ввода
Более подробно написано здесь: Retrieve the value of a text field.
Вопрос:
Какой аналог у hint в TextInput?
Ответ:
Подсказку можно показать с помощью InputDecoration, передав его в качестве конструктора в виджет.
Пример:
body: Center( child: TextField( decoration: InputDecoration(hintText: "This is a hint"), )
)
Вопрос:
Как показать ошибки валидации?
Ответ:
Всё так же — с помощью InputDecoration и его состояния.
Пример:
import 'package:flutter/material.dart'; void main() { runApp(SampleApp());
} class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }
} class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState();
} class _SampleAppPageState extends State<SampleAppPage> { String _errorText; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: TextField( onSubmitted: (String text) { setState(() { if (!isEmail(text)) { _errorText = 'Error: This is not an email'; } else { _errorText = null; } }); }, decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()), ), ), ); } _getErrorText() { return _errorText; } bool isEmail(String em) { String emailRegexp = r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; RegExp regExp = RegExp(emailRegexp); return regExp.hasMatch(em); }
}
Плагины Flutter
Вопрос:
Как получить доступ к GPS?
Ответ:
С помощью плагина geolocator.
Вопрос:
Как получить доступ к камере?
Ответ:
С помощью плагина image_picker.
Вопрос:
Как авторизоваться через Facebook?
Ответ:
С помощью плагина flutter_facebook_login.
Вопрос:
Как использовать Firebase?
Ответ:
Firebase поддерживает Flutter first party plugins.
Вопрос:
Как делать нативные (платформенные) вставки кода?
Ответ:
Flutter использует EventBus для взаимодействия с платформенным кодом. Подробно тут: developing packages and plugins.
Вопрос:
Как использовать NDK?
Ответ:
Написать свой плагин для взаимодействия вашего NDK-кода с Flutter. Пока Flutter не поддерживает прямое взаимодействие.
Themes
Вопрос:
Как использовать тему (Theme) в приложении?
Ответ:
Используя виджет MaterialApp или WidgetApp как корневой в приложении.
Пример:
class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, textSelectionColor: Colors.red ), home: SampleAppPage(), ); }
}
Базы данных и локальное хранилище
Вопрос:
Как получить доступ к Shared Preferences?
Ответ:
С помощью Shared_Preferences plugin (для NSUserDefaults в iOS тоже).
Пример:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; void main() { runApp( MaterialApp( home: Scaffold( body: Center( child: RaisedButton( onPressed: _incrementCounter, child: Text('Increment Counter'), ), ), ), ), );
} _incrementCounter() async { SharedPreferences prefs = await SharedPreferences.getInstance(); int counter = (prefs.getInt('counter') ?? 0) + 1; print('Pressed $counter times.'); prefs.setInt('counter', counter);
}
Вопрос:
Как получить доступ к SQLite во Flutter?
Ответ:
С помощью плагина SQFlite.
Уведомления
Вопрос:
Как показать push-уведомление?
Ответ:
С помощью плагина Firebase_Messaging.
Заключение
Новые языки программирования и фреймворки появляются практически постоянно. И на старте трудно понять, что выстрелит и будет долго жить, а что забудут уже через год. Боб Мартин в своей книге «Идеальный программист» призывает нас изучать новые языки программирования и фреймворки. Чед Фаулер в книге «Программист-фанатик» советует всегда быть на острие технологий. Но как понять, что ты не ошибся с выбором? В 2016 году я обратил внимание на Kotlin, но из-за высокой загруженности не смог уделить ему достаточно времени до второй половины 2017. На старте многие относились к нему скептически, а сейчас это один из самых популярных языков программирования, и огромное количество разработчиков создают на нём свои продукты. Я чувствую, что за те полтора года мог бы получить более глубокое понимание тонкостей языка.
В том же 2016 году появился фреймворк Flutter на языке Dart. Но рост его популярности был не такой стремительный, и только в 2018 году о нём заговорили громко. Тогда мне тоже захотелось попробовать его в действии. И мне понравилось! Время покажет, какое будущее ждёт этот фреймворк, но кажется, он очень перспективный. (И если Google Fuchsia выстрелит, то, без сомнений, Flutter не останется позади). Изучать его или нет — решать вам! В любом случае, изучение нового — отличная разминка для мозга. На этом у меня всё. Да не сломает Google ваш Play!