Хабрахабр

[По докам] Flutter. Часть 1. Для Android разработчиков

Про Flutter написано уже много статей. С каждым месяцем он становится всё популярнее. Поэтому я решил интерпретировать официальную документацию Flutter в лаконичный формат «вопрос — ответ». Думаю, многие, как и я, не имеют достаточно свободного времени для подробного изучения документации фреймворка, с которым они ещё не работают.

Если вы хотите понять, чем хорош этот фреймворк, и оценить, сколько усилий придётся приложить, чтобы его использовать — добро пожаловать под кат.

Содержание:

  1. Views
  2. Intents
  3. Async UI
  4. Структура проекта и ресурсы
  5. Activities & Fragments
  6. Layouts
  7. Жесты и обработка touch event.
  8. ListViews & Adapters
  9. Работа с текстом
  10. Форма ввода
  11. Плагины Flutter
  12. Themes
  13. Базы данных и локальное хранилище
  14. Уведомления

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!

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»