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