Это заключительная часть лаконичной интерпретации документации по Flutter, которая будет полезна Xamarin.Forms-разработчикам. Учитывая текущую ситуацию, сейчас самое время изучать что-то новое! Под катом вы сможете найти для себя информацию, достаточную, чтобы оценить, стоит ли переходить с одного кросплатформенного фреймворка на другой и сколько времени это займёт.



Если этой информации будет недостаточно или у вас есть опыт в нативной разработке под конкретную платформу, то рекомендую заглянуть в другие части:

Flutter. Часть 1. Для Android-разработчиков
Flutter. Часть 2. Для iOS-разработчиков
Flutter. Часть 3. Для разработчиков React Native
Flutter. Часть 4. Для веб-разработчиков
Flutter. Часть 5. Для Xamarin.Forms-разработчиков

Содержание:


  1. Проект

  2. Views

  3. Навигация

  4. Async UI

  5. Структура проекта и ресурсы

  6. Жизненный цикл приложения

  7. Layouts

  8. Обработка жестов

  9. ListView и адаптеры

  10. Текст

  11. Плагины Flutter

  12. Platform-specific code

  13. Отладка

  14. Локальное хранилище



Проект


Вопрос:


Где точка входа?

Ответ:


Функция main().

Отличия:


В Xamarin.Forms это LoadApplication(new App());.

Пример:


void main() {
  runApp(new MyApp());
}

Вопрос:


Как создать Page или Element?

Ответ:


Во Flutter нет понятия Page и Element как таковых. Все компоненты — это виджеты. Во Flutter есть 2 вида виджетов: StatelessWidget и StatefulWidget. Они работают одинаково, отличие только в состоянии при рендеринге.

Отличия:


StatelessWidget имеет неизменное состояние. Подойдёт для отображения текста, логотипа и т.д. Т.е. если элемент на экране не должен изменяться за всё время отображения, значит, он вам подходит. Его можно использовать и как контейнер для виджетов с изменяемым состоянием.

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

Пример:


StatelessWidget:
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

StatefulWidget:
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        // Take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set the appbar title.
        title: new Text(widget.title),
      ),
      body: new Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}

Views


Вопрос:


Как располагать виджеты? Какой эквивалент у XAML-файла?

Ответ:


Во Flutter вёрстка происходит прямо в коде с помощью дерева виджетов.

Отличие:


В Xamarin.Forms чаще всего вёрстка делается в XAML-файле. Во Flutter нет его аналога.

Дополнительная информация:


Список виджетов можно посмотреть тут.

Пример:


@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text("Sample App"),
    ),
    body: new Center(
      child: new MaterialButton(
        onPressed: () {},
        child: new Text('Hello'),
        padding: new EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

Вопрос:


Как добавить или удалить виджет через код?

Ответ:


С помощью обновления состояния родительского виджета и последующем перестроении дерева виджетов.

Отличия:


В Xamarin.Forms это можно сделать с помощью свойства Content у элемента или методов Add() и Remove().

Пример:


class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return new Text('Toggle One');
    } else {
      return new CupertinoButton(
        onPressed: () {},
        child: new Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: new Icon(Icons.update),
      ),
    );
  }
}

Вопрос:


Как анимировать виджет?

Ответ:


С помощью классов Animation и AnimationController.

Отличия:


В Xamarin.Forms используются ViewExtensions и методы, например FadeTo или TranslateTo.

Дополнительная информация:


Подробнее про анимацию можно почитать здесь.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(new FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Fade Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => new _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
          child: new Container(
              child: new FadeTransition(
                  opacity: curve,
                  child: new FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'Fade',
        child: new Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}

Вопрос:


Как рисовать на экране?

Ответ:


С помощью классов CustomPaint и CustomPainter.

Отличия:


В Xamarin.Forms используется сторонний SkiaSharp. Во Flutter Skia Canvas используется напрямую из коробки.

Пример:


import 'package:flutter/material.dart';

void main() => runApp(new MaterialApp(home: new DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => new Scaffold(body: new Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => new SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return new GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = new List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: new CustomPaint(painter: new SignaturePainter(_points), size: Size.infinite),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

Вопрос:


Как менять прозрачность?

Ответ:


С помощью виджета Opacity.

Отличия:


В Xamarin.Forms используется Opacity у VisualElement. Во Flutter нужно просто обернуть нужный виджет в Opacity.

Вопрос:


Как создавать кастомные виджеты?

Ответ:


Компоновать их внутри одного (вместо наследования).

Отличия:


В Xamarin.Forms можно наследоваться от интересующего нас VisualElement и дописать свою логику. Во Flutter виджет всегда наследуется от StatelessWidget и StatefulWidget. Т.е. нужно создать новый виджет и использовать в нём набор нужных виджетов в качестве параметров или полей.

Пример:


class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return new RaisedButton(onPressed: () {}, child: new Text(label));
  }
}

Навигация


Вопрос:


Как навигировать между экранами?

Ответ:


Для навигации между экранами используются классы Navigator и Route.

Отличия:


В Xamarin.Forms используется NavigationPage.

Во Flutter есть два способа навигации, которые схожи с NavigationPage:

  1. описать Map с именами Route;
  2. напрямую навигировать к Route.

Navigator может сделать push() или pop() указанному вами маршруту.

Пример:


void main() {
  runApp(CupertinoApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

Navigator.of(context).pushNamed('/b');

Вопрос:


Как навигировать в стороннее приложение?

Ответ:


С помощью нативных реализаций через MethodChannel либо плагина url_launcher.

Async UI


Вопрос:


Какой эквивалент Device.BeginInvokeOnMainThread()? Как выполнять код асинхронно?

Ответ:


В Dart реализована однопоточная модель исполнения, которая работает на изоляциях (Isolates). Для асинхронного выполнения используется async/await, с которым вы знакомы из C#.

Пример:


Выполнение запроса и возврат результата для обновления UI:
loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

Когда ответ на запрос получен, нужно вызвать метод setState() для перерисовки дерева виджетов с новыми данными.

Пример загрузки и обновления данных в ListView:
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();

    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Вопрос:


Как делать запросы к сети?

Ответ:


С помощью плагина http.

Пример:


Подключение зависимости через pubspec.yaml:
dependencies:
  ...
  http: ^0.11.3+16

Запрос:
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Вопрос:


Как отображать прогресс долгих операций?

Ответ:


С помощью ProgressIndicator.

Отличия:


В Xamarin.Forms это можно сделать напрямую, разместив индикатор прогресса в XAML.

Пример:


import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return new Center(child: new CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => new ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Структура проекта и ресурсы


Вопрос:


Где хранить ресурсы разного разрешения?

Ответ:


В assets.

Отличия:


В Xamarin.Forms нет унифицированного хранилища платформенных ресурсов, поэтому приходится хранить их в платформенных папках по отдельности. Во Flutter есть assets. Папка assets может располагаться в любом месте проекта, главное, прописать путь к ней в файле pubspec.yaml.

Дополнительная информация:


Сопоставление размеров графических ресурсов в Android и Flutter.
Android density qualifier Flutter pixel ratio
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x
Пример расположения ресурсов:
images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

Пример пути в pubspec.yaml файле:
assets:
 - images/my_icon.jpeg

Пример использования AssetImage:
return AssetImage("images/a_dot_burr.jpeg");

Пример использования asset напрямую:
@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

Вопрос:


Где хранить строки? Как их локализовывать?

Ответ:


Во Flutter нет определённого места для хранения строк в данный момент. Их предлагается хранить как статические поля в отдельном классе. Для локализации используются плагины, например flutter_localizations или l10n.

Отличия:


В Xamarin.Forms используется файл resx.

Пример:


Хранение:
class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

Использование:
new Text(Strings.welcomeMessage)

Локализация:
dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // Add app-specific localization delegate[s] here.
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

Вопрос:


Где файл проекта?

Ответ:


Во Flutter нет файла проекта, который бы открывал этот проект в среде разработки. Ближайший похожий файл — pubspec.yaml — содержит в себе зависимости на плагины и детали проекта.

Жизненный цикл приложения


Вопрос:


Как обрабатывать события жизненного цикла?

Ответ:


С помощью WidgetsBinding и метода didChangeAppLifecycleState().

Дополнительная информация:


Во Flutter используется FlutterActivity в Android-коде и FlutterAppDelegate в iOS, за счёт этого движок Flutter делает обработку изменений состояния максимально незаметной. Но если вам всё же необходимо выполнить какую-либо работу в зависимости от состояния, то жизненный цикл немного отличается:
  • inactive — этот метод есть только в iOS, в Android нет аналога;
  • paused — аналогичен onPause() в Android;
  • resumed — аналогичен onPostResume() в Android;
  • suspending — аналогичен onStop в Android, в iOS нет аналога.

Более подробно в AppLifecycleStatus documentation.

Пример:


import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null)
      return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);

    return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
        textDirection: TextDirection.ltr);
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

Layouts


Вопрос:


Какой аналог StackLayout?

Ответ:


Аналогом StackLayout с вертикальной ориентацией является Column, а с горизонтальной — Row.

Пример:


Column:
@override
Widget build(BuildContext context) {
  return new Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      new Text('Column One'),
      new Text('Column Two'),
      new Text('Column Three'),
      new Text('Column Four'),
    ],
  );
}

Row:
@override
Widget build(BuildContext context) {
  return new Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      new Text('Row One'),
      new Text('Row Two'),
      new Text('Row Three'),
      new Text('Row Four'),
    ],
  );
}

Вопрос:


Какой аналог у Grid?

Ответ:


GridView.

Пример:


GridView.count(
  // Create a grid with 2 columns. If you change the scrollDirection to
  // horizontal, this would produce 2 rows.
  crossAxisCount: 2,
  // Generate 100 widgets that display their index in the List
  children: List.generate(100, (index) {
    return Center(
      child: Text(
        'Item $index',
        style: Theme.of(context).textTheme.headline,
      ),
    );
  }),
);

Вопрос:


Какой аналог у ScrollView?

Ответ:


Ближайший аналог — SingleChildScrollView. Но для построения скролящегося контента во Flutter чаще всего используется ListView.

Пример:


SingleChildScrollView:
@override
Widget build(BuildContext context) {
  return new SingleChildScrollView(
    child: new Text('Long Content'),
  );
}

ListView:
@override
Widget build(BuildContext context) {
  return new ListView(
    children: <Widget>[
      new Text('Row One'),
      new Text('Row Two'),
      new Text('Row Three'),
      new Text('Row Four'),
    ],
  );
}

Обработка жестов


Вопрос:


Как обрабатывать клики?

Ответ:


Если виджет поддерживает метод onPressed, то обработать клик можно с его помощью. В противном случае это можно сделать через GestureDetector.

Пример:


onPressed:
@override
Widget build(BuildContext context) {
  return new RaisedButton(
      onPressed: () {
        print("click");
      },
      child: new Text("Button"));
}

GestureDetector:
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        body: new Center(
      child: new GestureDetector(
        child: new FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}

Вопрос:


Как обрабатывать жесты?

Ответ:


Используя GestureDetector. Им можно обрабатывать следующие действия:

Tap



Double tap



Long press



Vertical drag



Horizontal drag



Пример:


AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        body: new Center(
          child: new GestureDetector(
            child: new RotationTransition(
                turns: curve,
                child: new FlutterLogo(
                  size: 200.0,
                )),
            onDoubleTap: () {
              if (controller.isCompleted) {
                controller.reverse();
              } else {
                controller.forward();
              }
            },
        ),
    ));
  }
}

ListView и адаптеры


Вопрос:


Какой аналог у ListView?

Ответ:


ListView.

Отличия:


В Xamarin.Forms нужно создать ViewCell и (чаще всего) DataTemplateSelector и передать их в ListView. Во Flutter нужно просто передать список виджетов для отображения.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(new Padding(padding: new EdgeInsets.all(10.0), child: new Text("Row $i")));
    }
    return widgets;
  }
}

Вопрос:


Как определить, на каком элементе был клик?

Ответ:


Во Flutter виджет элемента должен обработать свой клик сам.

Отличия:


В Xamarin.Forms чаще всего используется ItemTapped.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(new GestureDetector(
        child: new Padding(
            padding: new EdgeInsets.all(10.0),
            child: new Text("Row $i")),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

Вопрос:


Как динамически обновить ListView?

Ответ:


Обновить список данных и вызвать setState().

Отличия:


В Xamarin.Forms для этого используется ItemsSource. Во Flutter после setState() виджет будет перерисован заново.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

Дополнительная информация:


Для формирования списка рекомендуется использовать ListView.Builder.

Пример:


import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

Текст


Вопрос:


Как использовать кастомные шрифты?

Ответ:


Файл шрифтов нужно просто положить в папку (название придумайте сами) и указать к ней путь в pubspec.yaml.

Пример:


fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

Вопрос:


Как стилизовать текстовые виджеты?

Ответ:


С помощью параметров:

  • color;
  • decoration;
  • decorationColor;
  • decorationStyle;
  • fontFamily;
  • fontSize;
  • fontStyle;
  • fontWeight;
  • hashCode;
  • height;
  • inherit;
  • letterSpacing;
  • textBaseline;
  • wordSpacing.

Вопрос:


Какой аналог у Placeholder?

Ответ:


Свойство hintText у InputDecoration.

Пример:


body: new Center(
  child: new TextField(
    decoration: new InputDecoration(hintText: "This is a hint"),
  )
)

Вопрос:


Как показать ошибки валидации?

Ответ:


Всё так же — с помощью InputDecoration и его состояния.

Пример:


class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String emailString) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(emailString);
  }
}

Плагины Flutter


Вопрос:


Как получить доступ к GPS?

Ответ:


С помощью плагина geolocator.

Вопрос:


Как получить доступ к камере?

Ответ:


С помощью плагина image_picker.

Вопрос:


Как авторизоваться через Facebook?

Ответ:


С помощью плагина flutter_facebook_login.

Вопрос:


Как использовать Firebase?

Ответ:


Firebase поддерживает Flutter first party plugins:

Platform-specific code


Вопрос:


Как определить, на какой платформе выполняется код?

Ответ:


С помощью класса поля platform в Theme или класса Platform.

Пример:


Поле platform:
if (Theme.of(context).platform == TargetPlatform.iOS) {
  return 'iOS';
} else if (Theme.of(context).platform == TargetPlatform.android) {
  return 'android';
} else if (Theme.of(context).platform == TargetPlatform.fuchsia) {
  return 'fuchsia';
} else {
  return 'not recognised ';
}

Класс Platform:
if (Platform.isIOS) {
  return 'iOS';
} else if (Platform.isAndroid) {
  return 'android';
} else if (Platform.isFuchsia) {
  return 'fuchsia';
} else {
  return 'not recognised ';
}

Вопрос:


Как вызвать нативный платформенный код?

Ответ:


Через MethodChannel.

Дополнительная информация:


Подробнее тут.

Отладка


Вопрос:


Какие есть инструменты для отладки приложения?

Ответ:


DevTools.

Вопрос:


Как сделать hot reload?

Ответ:


Если приложение запускалось из IntelliJ IDE, Android Studio или VSCode, то сочетанием ?s/ctrl-s или нажатием на иконку hot reload. Если запускалось из терминала, то вводом буквы r.

Вопрос:


Как получить доступ к меню разработчика в приложении?

Ответ:


Если запуск был из IDE, то с помощью инструментов IDE. Если из консоли, то с помощью ввода h.

Дополнительная информация:


Полный список команд:

Действие Команда в терминале Функции и поля
Иерархия виджетов w debugDumpApp()
Дерево рендеринга t debugDumpRenderTree()
Слои L debugDumpLayerTree()
Accessibility S (traversal order) or U (inverse hit test order) debugDumpSemantics()
Инспектор виджетов i WidgetsApp.showWidgetInspectorOverride
Отображение линий построения p debugPaintSizeEnabled
Симуляция разных ОС o defaultTargetPlatform
Перфоманс P WidgetsApp. showPerformanceOverlay
Скриншот flutter.png s
Закрытие приложения q

Локальное хранилище


Вопрос:


Как хранить key-value-данные в приложении?

Ответ:


С помощью плагина shared_preferences.

Отличия:


В Xamarin.Forms используется Xam.Plugins.Settings.

Пример:


Подключение зависимости:
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^0.4.3

Использование:
SharedPreferences prefs = await SharedPreferences.getInstance();
_counter = prefs.getInt('counter');
prefs.setInt('counter', ++_counter);
setState(() {
  _counter = _counter;
});

Вопрос:


Как хранить сложные данные?

Ответ:


С помощью плагинов БД, например sqflite или hive.

Вот, пожалуй, ответы на основные вопросы. На этом серия интерпретаций заканчивается. Надеюсь, они были полезны всем интересующимся этим фреймворком разработчикам. Возможно, даже сподвигли начать писать на Flutter и завербовалм вас в дружное комьюнити Flutter-разработчиков. А я пойду думать над новыми статьями, чтобы развивать сообщество и делать мир приложений лучше. Да не сломает Xamarin ваш Forms!