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

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

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

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

Художник:

Даня Берковский

Корректор:

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

Вёрстка:

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

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
hard
[anycomment]
Exit mobile version