В Практикуме появились курсы по мобильной разработке для iOS и Android, а мы рассказываем, что это такое и как всё там устроено. Вот что уже было:
- Какую платформу выбрать — iOS или Android.
 - Кроссплатформенная разработка.
 - Пробный кроссплатформенный проект. ← Вы здесь.
 - Настройка среды для разработки собственного приложения.
 - Нативное приложение для iOS и Android (когда-нибудь с божьей помощью).
 
Сегодня сделаем сразу два кроссплатформенных приложения — простое и сложное. В простом будет текст и кнопка, а в сложном — заполняемая форма, анимация и всякая красота.
Писать код будем на Dart — он используется в кроссплатформенном фреймворке Flutter. Этот язык сложнее, чем JavaScript и Python, но если вы знакомы хотя бы с одним из них, то поймёте, что написано в коде.
Сразу предупреждаем, что мобильная разработка сложнее, чем скрипты на JavaScript. Если код проекта покажется сложным — это не вы глупый, это реально сложновато.
Что понадобится
В идеале нам нужно установить на компьютер много софта, который называют рабочим окружением. Это пакет программ, который нужен для написания кода и его исполнения на компьютере в режиме эмулятора. Шаги такие:
- Скачать и установить Flutter.
 - Добавить путь к Flutter в настройки командной строки.
 - Установить flutter doctor — софт, который выдаст нам компилятор языка Dart и заодно проверит, чего ещё не хватает на компьютере для работы.
 - Добавить поддержку Dart в свою среду разработки, например VS Code.
 - Установить эмулятор iOS и Android.
 - Убедиться, что всё это хозяйство дружит друг с другом и работает как нужно.
 
Но есть другой путь: использовать онлайн-компилятор Dart со встроенным эмулятором телефона. Этого хватит, чтобы попробовать себя в мобильной разработке и запустить наши проекты. Если всё понравится — поставите полный комплект на компьютер, а пока можно так.
Простое приложение: только приветствие
Начнём с простого: сделаем приложение, в котором есть приветствие.
Сначала подключим библиотеку со стандартным интерфейсом — так приложение поймёт, где брать детали и стили для отрисовки всего на экране. После этого создадим точку старта — функцию main(), в которой напишем, что нужно сделать. Внутри этой функции создадим новый экземпляр приложения и добавим текст на главный экран home.
Обратите внимание, что мы не говорим приложению, как оформить текст, какие нужны отступы и размер шрифта — за всё это отвечает интерфейс устройства.
Чтобы посмотреть программу в деле, вставьте код в онлайн-компилятор Dart:
// подключаем библиотеку со стандартными элементами интерфейса
import 'package:flutter/material.dart';
// основная функция — точка старта приложения
void main() {
  // говорим, что нужно запустить приложение
  runApp(
      // создаём новое приложение со стандартным дизайном
      const MaterialApp(
          // выводим сообщение на главный экран приложения
          home: Text('Привет, это журнал «Код»')
          )
  );
}
Посложнее: есть кнопка и меняется текст
Теперь сделаем что-то посложнее: разместим на экране кнопку и будем считать, столько раз на неё нажали.
Так как у приложения будет более сложная логика, код тоже будет непростой. В нём мы постепенно, шаг за шагом собираем главный экран приложения: сначала указываем точку старта, потом собираем основной модуль и начинаем его детализировать. Код получается громоздким — ему нужно будет работать в обеих операционных системах, поэтому нужно формально и тщательно описать все элементы и состояния. Мы добавили комментарии, чтобы было проще разобраться, что происходит на каждом шаге:
// подключаем библиотеку стандартных элементов
import 'package:flutter/material.dart';
// точка запуска программы
void main() {
// запускаем MyApp()
  runApp(const MyApp());
}
// основной модуль приложения
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  // команда означает, что мы можем переопределить стандартные функции на свои
  @override
  // собираем виджет
  Widget build(BuildContext context) {
  // возвращаем внутреннюю часть приложения
    return MaterialApp(
      // название
      title: 'Кроссплатформенное приложение',
      // настраиваем внешний вид
      theme: ThemeData(
      // основной цвет приложения — синий
        primarySwatch: Colors.blue,
      ),
      // говорим, где приложению взять главную страницу
      home: const MyHomePage(title: 'Привет, это журнал «Код»'),
    );
  }
}
// оформляем главную страницу
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
// настраиваем логику работы
class _MyHomePageState extends State<MyHomePage> {
  // переменная для счётчика нажатий на кнопку
  int _counter = 0;
  // функция, которая увеличивает значение счётчика на единицу
  void _incrementCounter() {
    // обращаемся к текущему состоянию главного экрана
    setState(() {
      // и увеличиваем на нём счётчик на единицу
      _counter++;
    });
  }
  @override
  // собираем компоненты главного экрана
  Widget build(BuildContext context) {
    // используем стандартный класс экрана
    return Scaffold(
      // настраиваем надпись на верхней строке
      appBar: AppBar(
        // берём текст из названия
        title: Text(widget.title)
      ));
      
  }
}
У нас появился главный экран с текстом на шапке, но пока непонятно, что будет дальше. Добавим подробностей и выведем текст про счётчик на экран. Для этого в раздел Widget build(BuildContext context){} добавим такой код:
// располагаем всё по центру
      body: Center(
        // начинаются дочерние элементы
        child: Column(
          // ось выравнивания — центр приложения
          mainAxisAlignment: MainAxisAlignment.center,
          // собираем виджет с текстом и счётчиком
          children: <Widget>[
            // переменная с текстом
            const Text(
              'Столько раз вы нажали на кнопку:',
            ),
            // выводим текст на экран
            Text(
              // добавляем к нему переменную-счётчик
              '$_counter',
              // применяем стандартный стиль форматирования
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
Единственное, чего нам не хватает, — кнопки, на которую можно нажать. Исправим это, добавив туда же код с кнопкой:
// настраиваем кнопку с плюсиком
floatingActionButton: FloatingActionButton(
  // что делаем при нажатии
  onPressed: _incrementCounter,
  // подсказка на кнопке
  tooltip: 'Нажми меня',
  // иконка кнопки
  child: const Icon(Icons.add),
),
Теперь всё работает как нужно: кнопка нажимается, счётчик увеличивается, а мы получили приложение.Чтобы понажимать на кнопку самостоятельно, запустите код в онлайн-компиляторе Dart:
// подключаем библиотеку стандартных элементов
import 'package:flutter/material.dart';
// точка запуска программы
void main() {
// запускаем MyApp()
  runApp(const MyApp());
}
// основной модуль приложения
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  // команда означает, что мы можем переопределить стандартные функции на свои
  @override
  // собираем виджет
  Widget build(BuildContext context) {
  // возвращаем внутреннюю часть приложения
    return MaterialApp(
      // название
      title: 'Кроссплатформенное приложение',
      // настраиваем внешний вид
      theme: ThemeData(
      // основной цвет приложения — синий
        primarySwatch: Colors.blue,
      ),
      // говорим, где приложению взять главную страницу
      home: const MyHomePage(title: 'Привет, это журнал «Код»'),
    );
  }
}
// оформляем главную страницу
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
// настраиваем логику работы
class _MyHomePageState extends State<MyHomePage> {
  // переменная для счётчика нажатий на кнопку
  int _counter = 0;
  // функция, которая увеличивает значение счётчика на единицу
  void _incrementCounter() {
    // обращаемся к текущему состоянию главного экрана
    setState(() {
      // и увеличиваем на нём счётчик на единицу
      _counter++;
    });
  }
  @override
  // собираем компоненты главного экрана
  Widget build(BuildContext context) {
    // используем стандартный класс экрана
    return Scaffold(
      // настраиваем надпись на верхней строке
      appBar: AppBar(
        // берём текст из названия
        title: Text(widget.title),
      ),
      // располагаем всё по центру
      body: Center(
        // начинаются дочерние элементы
        child: Column(
          // ось выравнивания — центр приложения
          mainAxisAlignment: MainAxisAlignment.center,
          // собираем виджет с текстом и счётчиком
          children: <Widget>[
            // переменная с текстом
            const Text(
              'Столько раз вы нажали на кнопку:',
            ),
            // выводим текст на экран
            Text(
              // добавляем к нему переменную-счётчик
              '$_counter',
              // применяем стандартный стиль форматирования
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      // настраиваем кнопку с плюсиком
      floatingActionButton: FloatingActionButton(
        // что делаем при нажатии
        onPressed: _incrementCounter,
        // подсказка на кнопке
        tooltip: 'Нажми меня',
        // иконка кнопки
        child: const Icon(Icons.add),
      ),
    );
  }
}
Сложное, но красивое: приложение с формой регистрации
Напоследок покажем ещё одно приложение — со сложной логикой, несколькими экранами, прогресс-баром и анимацией. В нём уже можно ввести свои данные, посмотреть, как заполняется форма и происходит процесс регистрации. На самом деле данные никуда пока не сохраняются, но выглядит красиво.
Чтобы посмотреть приложение в действии, вставьте код в онлайн-компилятор Dard.
import 'package:flutter/material.dart';
void main() => runApp(const SignUpApp());
// 
class SignUpApp extends StatelessWidget {
  const SignUpApp();
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => const SignUpScreen(),
        '/welcome': (context) => const WelcomeScreen(),
      },
    );
  }
}
class SignUpScreen extends StatelessWidget {
  const SignUpScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[200],
      body: Center(
        child: SizedBox(
          width: 400,
          child: Card(
            child: SignUpForm(),
          ),
        ),
      ),
    );
  }
}
class WelcomeScreen extends StatelessWidget {
  const WelcomeScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Добро пожаловать!', style: Theme.of(context).textTheme.headline2),
      ),
    );
  }
}
class SignUpForm extends StatefulWidget {
  @override
  _SignUpFormState createState() => _SignUpFormState();
}
class _SignUpFormState extends State<SignUpForm> {
  final _firstNameTextController = TextEditingController();
  final _lastNameTextController = TextEditingController();
  final _usernameTextController = TextEditingController();
  double _formProgress = 0;
  void _updateFormProgress() {
    var progress = 0.0;
    final controllers = [
      _firstNameTextController,
      _lastNameTextController,
      _usernameTextController
    ];
    for (final controller in controllers) {
      if (controller.value.text.isNotEmpty) {
        progress += 1 / controllers.length;
      }
    }
    setState(() {
      _formProgress = progress;
    });
  }
  void _showWelcomeScreen() {
    Navigator.of(context).pushNamed('/welcome');
  }
  @override
  Widget build(BuildContext context) {
    return Form(
      onChanged: _updateFormProgress,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedProgressIndicator(value: _formProgress),
          Text('Регистрация', style: Theme.of(context).textTheme.headline4),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _firstNameTextController,
              decoration: const InputDecoration(hintText: 'Имя'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _lastNameTextController,
              decoration: const InputDecoration(hintText: 'Фамилия'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _usernameTextController,
              decoration: const InputDecoration(hintText: 'Логин'),
            ),
          ),
          TextButton(
            style: ButtonStyle(
              foregroundColor: MaterialStateProperty.resolveWith(
                  (Set<MaterialState> states) {
                return states.contains(MaterialState.disabled)
                    ? null
                    : Colors.white;
              }),
              backgroundColor: MaterialStateProperty.resolveWith(
                  (Set<MaterialState> states) {
                return states.contains(MaterialState.disabled)
                    ? null
                    : Colors.blue;
              }),
            ),
            onPressed: _formProgress == 1 ? _showWelcomeScreen : null,
            child: const Text('Зарегистрироваться'),
          ),
        ],
      ),
    );
  }
}
class AnimatedProgressIndicator extends StatefulWidget {
  final double value;
  const AnimatedProgressIndicator({
    required this.value,
  });
  @override
  State<StatefulWidget> createState() {
    return _AnimatedProgressIndicatorState();
  }
}
class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _curveAnimation;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1200),
      vsync: this,
    );
    final colorTween = TweenSequence([
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.red, end: Colors.orange),
        weight: 1,
      ),
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
        weight: 1,
      ),
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.yellow, end: Colors.green),
        weight: 1,
      ),
    ]);
    _colorAnimation = _controller.drive(colorTween);
    _curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
  }
  @override
  void didUpdateWidget(oldWidget) {
    super.didUpdateWidget(oldWidget);
    _controller.animateTo(widget.value);
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) => LinearProgressIndicator(
        value: _curveAnimation.value,
        valueColor: _colorAnimation,
        backgroundColor: _colorAnimation.value?.withOpacity(0.4),
      ),
    );
  }
}
Выводы из этого
Мы для себя сделали такие выводы:
- Мобильная разработка — это сложно и многословно.
 - Поэтому специалисты в ней красавчики.
 - Мобильники никуда не уйдут, разрабатывать на них придётся.
 - Значит, будем разрабатывать.
 
Приходите в Практикум на курсы по мобильной разработке для iOS и Android. Выберите какую-то одну платформу, попробуйте себя в бесплатной части и, если зайдёт, становитесь разработчиком мобильных приложений. Будьте богаты и здоровы. Отдыхайте в выходные. Си ю некст вик.
