Делаем простое кроссплатформенное мобильное приложение

Делаем простое кроссплатформенное мобильное приложение

И сразу покажем, как сделать сложное

В Практикуме появились курсы по мобильной разработке для iOS и Android, а мы рассказываем, что это такое и как всё там устроено. Вот что уже было:

Сегодня сделаем сразу два кроссплатформенных приложения — простое и сложное. В простом будет текст и кнопка, а в сложном — заполняемая форма, анимация и всякая красота.

Писать код будем на Dart — он используется в кроссплатформенном фреймворке Flutter. Этот язык сложнее, чем JavaScript и Python, но если вы знакомы хотя бы с одним из них, то поймёте, что написано в коде.

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

Что понадобится

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

  1. Скачать и установить Flutter.
  2. Добавить путь к Flutter в настройки командной строки.
  3. Установить flutter doctor — софт, который выдаст нам компилятор языка Dart и заодно проверит, чего ещё не хватает на компьютере для работы.
  4. Добавить поддержку Dart в свою среду разработки, например VS Code.
  5. Установить эмулятор iOS и Android.
  6. Убедиться, что всё это хозяйство дружит друг с другом и работает как нужно.

Но есть другой путь: использовать онлайн-компилятор 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),
      ),
    );
  }
}

Делаем простое кроссплатформенное мобильное приложение

Выводы из этого

Мы для себя сделали такие выводы: 

  1. Мобильная разработка — это сложно и многословно.
  2. Поэтому специалисты в ней красавчики.
  3. Мобильники никуда не уйдут, разрабатывать на них придётся.
  4. Значит, будем разрабатывать.

Приходите в Практикум на курсы по мобильной разработке для iOS и Android. Выберите какую-то одну платформу, попробуйте себя в бесплатной части и, если зайдёт, становитесь разработчиком мобильных приложений. Будьте богаты и здоровы. Отдыхайте в выходные. Си ю некст вик. 

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Апскиллинг, как говорится
Апскиллинг — это, например, переход с уровня junior на уровень middle, а потом — senior. У «Яндекс Практикума» есть курсы ровно для этого: от алгоритмов и типов данных до модных фреймворков.
Изучить вопрос
Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
Что такое Пик Балмера
Что такое Пик Балмера

И почему это не про продуктивность.

easy
Как работает беспроводная зарядка
Как работает беспроводная зарядка

Всё дело в магнитном поле.

easy
Кто такой фронтенд

Руководство для входа в профессию.

easy
JavaScript для новичков: чем опасны нестрогие типы данных
JavaScript для новичков: чем опасны нестрогие типы данных

В JavaScript есть удобная штука, которая может сильно вам навредить.

medium
Как проверить, работает ли интернет
Как проверить, работает ли интернет

Рассказываем про две команды, которыми пользуются сотрудники техподдержки провайдера

easy
Как устроено сжатие с потерями
Как устроено сжатие с потерями

Благодаря этому у нас есть стримы и ютуб.

easy
Как работает быстрая зарядка в телефоне
Как работает быстрая зарядка в телефоне

Оказывается, там тоже не всё так просто

medium
Как работает пузырьковая сортировка
Как работает пузырьковая сортировка

Самый простой, но не самый эффективный алгоритм.

easy
Верстаем в сетке

Разбираем, как можно расположить текст и фото в CSS Grid.

medium
Как писали игры для приставок: чудеса оптимизации и жёсткий кодинг

Для всех, кто вырос, проходя восьмибитного Марио.

medium
hard