Перейти к основному содержимому

Flutter — готовые виджеты

Готовые экраны Flutter на Dart с разбором каждой важной строки: скопировали lib/main.dartflutter run → на эмуляторе или телефоне уже работает кнопка, форма или список. Подойдёт, если вы ищете «flutter counter example», «flutter textfield example», «flutter todo list», «как сделать кнопку во flutter», «statefulwidget setstate» или сдаёте лабораторную / курсовую по мобильной разработке.

Стиль галереи — как в Turtle на Python и Tkinter — окна и виджеты: код целиком, таблица «строка → смысл», типичные ошибки и блок «попробуйте сами».


Основы UI во Flutter

Flutter — фреймворк Google для приложений на Android, iOS, Windows, macOS, Linux и веб. Интерфейс собирают из виджетов — классов Dart, которые описывают, что рисовать на экране. Язык — Dart; без базового синтаксиса Dart начните с первой программы.

С чего начать

Теория фреймворка — Flutter. Мобильный контекст — мобильные приложения. Десктоп на Python — Tkinter, на Java — Swing, в браузере — React и Vue / Svelte. HTTP — Fetch / axios.

Частые запросы в Google — куда смотреть

Ищут в интернетеРаздел ниже
flutter hello world / первое приложениеHello Flutter
flutter counter example setstateСчётчик
flutter elevatedbutton onclickКнопка и SnackBar
flutter textfield controller exampleПоле ввода и приветствие
flutter temperature converterКонвертер °C → °F
flutter checkbox switch radiolisttileПереключатели
flutter todo list listview builderСписок задач
flutter slider exampleПолзунок
flutter login form textformfieldФорма входа
flutter drawer menu exampleБоковое меню
flutter navigator push popВторой экран
flutter tabbar tabbarviewВкладки
flutter alertdialog showdialogДиалог
flutter http get futurebuilderЗагрузка с API
flutter create project runОбязательный каркас
renderflex overflowed / setstate after disposeЧастые ошибки

Кому подойдёт эта страница

Школьникам — pet-проект «моё приложение», кружок робототехники с экраном на телефоне.
Студентам — лабораторная «UI на Flutter», сравнение с Tkinter или React в отчёте.
Самоучкам — скопировали main.dartflutter run → разобрали таблицу под кодом. Ищете «как сделать счётчик на flutter» — ниже код и объяснение, зачем каждая строка.


Как запустить пример за 2 минуты

  1. Установите Flutter SDK. В терминале: flutter doctor — исправьте красные пункты (Android Studio / Xcode для мобильных платформ).
  2. flutter create my_app && cd my_app
  3. Откройте lib/main.dart, удалите шаблонный код, вставьте пример целиком (от import до последней }).
  4. Запустите эмулятор или подключите телефон с USB-отладкой.
  5. flutter run — через минуту приложение на устройстве.
  6. Меняете код и жмёте Hot Reload (r в терминале) — экран обновится без полного перезапуска.
ГдеПлюсы
Эмулятор Android / симулятор iOSКак на реальном телефоне
flutter run -d chromeБыстрая проверка без эмулятора (веб)
VS Code / Android StudioКнопка Run, подсветка ошибок

В браузере на сайте, как симулятор Turtle, Flutter не запускается — нужен компьютер с SDK.


Базовые термины

ТерминПростыми словами
ВиджетЛюбой элемент UI — кнопка, текст, отступ, весь экран
StatelessWidgetЭкран или блок без своей памяти; картинка не меняется сама
StatefulWidgetБлок с state — число счётчика, текст в поле, список задач
setState()«Данные изменились — перерисуй экран»
build()Метод, который описывает, как выглядит UI прямо сейчас
ScaffoldКаркас экрана: AppBar, body, кнопка FAB, drawer
MaterialAppКорень приложения, тема, заголовок
Hot ReloadПравка кода → экран обновился за секунду, state часто сохраняется
pubspec.yamlФайл зависимостей (как package.json у Node)

Flutter, Tkinter и React — одна идея, разный синтаксис

ЗадачаTkinter (Python)React (браузер)Flutter (Dart)
Окно / кореньtk.Tk()<div id="root">runApp(MaterialApp(...))
НадписьLabel(text=...)<p>...</p>Text('...')
КнопкаButton(command=fn)<button onClick=&#123;fn&#125;>ElevatedButton(onPressed: fn)
Поле вводаEntry + .get()value + onChangeTextField + controller.text
Динамический текстStringVaruseStatesetState + поле в State
Цикл событийmainloop()React перерисовывает DOMFlutter пересобирает дерево виджетов

Смысл: вы описываете интерфейс как функцию от данных. Данные изменились → фреймворк сам обновляет экран. В Tkinter часть этого делаете вручную через StringVar; во Flutter — через setState.


Словарь виджетов за 30 секунд

ВиджетЗачемКак читать / менять
MaterialAppКорень, темаhome:, theme:
ScaffoldКаркас экранаappBar, body, drawer, floatingActionButton
AppBarВерхняя панельtitle: Text(...)
TextТекстText('строка')
CenterЦентрировать childchild: ...
Column / RowСтолбец / строкаchildren: [...]
PaddingОтступыpadding: EdgeInsets.all(16)
ElevatedButtonКнопкаonPressed: () { }
TextFieldВвод одной строкиcontroller.text
TextFormFieldПоле + валидацияvalidator:
ListView.builderДлинный списокitemCount, itemBuilder
NavigatorЭкраныpush, pop
SnackBarСтрока снизучерез ScaffoldMessenger

Компоновка: CSS во Flutter нет. Отступы — Padding, SizedBox; ширина на всю строку — crossAxisAlignment: CrossAxisAlignment.stretch в Column.


Как работает Flutter — цикл обновления

Пользователь нажал «+» → _count стал 5 → Flutter снова вызвал build → на экране обновилась только цифра в Text, кнопка не «мигала» целиком.


Обязательный каркас

Любой пример ниже — полный файл lib/main.dart. Запомните команды, как import tkinter и mainloop() в Tkinter.

Задача: создать проект и убедиться, что dev-сборка работает.

flutter create my_flutter_app
cd my_flutter_app
flutter run

Минимальный lib/main.dart — замените home: на код из примеров ниже:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Моё приложение',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const Placeholder(), // ← замените на HelloScreen, CounterScreen и т.д.
);
}
}

Разбор по строкам.

СтрокаСмысл
import 'package:flutter/material.dart'Material-виджеты: кнопки, поля, тема
void main()Точка входа — первая функция, которую вызывает Dart
runApp(...)«Включить» Flutter; без неё пустой экран
StatelessWidgetВиджет без своего изменяемого state
const MyApp(&#123;super.key&#125;)const + super.key — идиома Flutter 3 для оптимизации
MaterialAppОбёртка: тема, локализация, навигация верхнего уровня
debugShowCheckedModeBanner: falseУбирает ленточку «DEBUG» в углу (удобно для скриншотов в отчёт)
ThemeData(...)Цвета кнопок, шрифты — единый стиль
home:Стартовый экран — ваш Scaffold или кастомный виджет
build(BuildContext context)Flutter вызывает его, когда нужно нарисовать UI

Типичные ошибки.

  • flutter: command not found — Flutter не в PATH; добавьте flutter/bin из установки.
  • «No devices found» — запустите эмулятор в Android Studio или flutter run -d chrome.
  • Красный экран RenderFlex overflowed — содержимое Column не помещается; см. ошибки.
  • Правите код, а экран не меняется — сохраните файл; при изменении main() нужен Hot Restart (R), не Reload.

Попробуйте: замените Placeholder() на Scaffold(body: Center(child: Text('Hello'))).


Стартовые экраны

Простые целые main.dart — с них удобно начинать лабораторную.


Hello Flutter

Задача. Минимальный экран: заголовок в AppBar и текст по центру — проверка, что SDK и эмулятор работают.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hello',
home: const HelloScreen(),
);
}
}

class HelloScreen extends StatelessWidget {
const HelloScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Привет, Flutter')),
body: const Center(
child: Text(
'Окно работает!',
style: TextStyle(fontSize: 18),
),
),
);
}
}

Разбор.

Виджет / строкаЗачем
HelloScreen extends StatelessWidgetОтдельный экран без изменяемых данных
Scaffold«Скелет» Material-экрана
AppBarСистемная верхняя полоска с заголовком
body:Основная область под AppBar
CenterРазмещает child по центру по горизонтали и вертикали
Text(..., style: TextStyle(fontSize: 18))Надпись и размер шрифта
const перед виджетамиПодсказка компилятору: параметры не меняются → меньше лишней работы

Попробуйте сами. Добавьте backgroundColor: Colors.teal.shade50 в Scaffold. Поменяйте title в AppBar — изменится текст в верхней панели.


Счётчик

Задача. Число в памяти увеличивается по нажатию — классический flutter counter example (как шаблон flutter create).

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CounterScreen(),
);
}
}

class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});

@override
State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
int _count = 0;

void _increment() {
setState(() {
_count++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Счётчик')),
body: Center(
child: Text(
'Значение: $_count',
style: Theme.of(context).textTheme.headlineMedium,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
tooltip: 'Добавить',
child: const Icon(Icons.add),
),
);
}
}

Разбор — почему два класса CounterScreen и _CounterScreenState.

ЭлементСмысл
StatefulWidget«Оболочка» — сама не хранит _count, только создаёт State
createState()Flutter вызывает один раз и получает объект _CounterScreenState
_CounterScreenStateЗдесь живёт _count; подчёркивание = приватный класс в Dart
int _count = 0Начальное значение счётчика
setState(() &#123; _count++; &#125;)Обязательно при изменении данных UI; без этого цифра на экране не обновится
'Значение: $_count'Интерполяция строк — $ вставляет значение переменной
Theme.of(context).textTheme.headlineMediumСтиль из темы приложения — единообразные заголовки
FloatingActionButtonКруглая кнопка «+» в углу — паттерн Material Design
onPressed: _incrementПередаём имя функции, не _increment() — иначе вызовется сразу при сборке

Сравнение с React (см. React — счётчик):

ReactFlutter
const [count, setCount] = useState(0)int _count = 0 в State
setCount(count + 1)setState(() &#123; _count++; &#125;)
&#123;count&#125; в JSX'$_count' в Text

Типичные ошибки.

  • Пишут _count++ без setState — переменная меняется, экран стоит на месте.
  • onPressed: _increment() — функция вызывается при каждой пересборке, не по клику.

Попробуйте сами. Кнопка «−» в body: второй ElevatedButton с setState(() &#123; _count--; &#125;).


Кнопка и SnackBar

Задача. По нажатию показать короткое сообщение снизу — аналог messagebox.showinfo в Tkinter.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const ButtonDemo());
}
}

class ButtonDemo extends StatelessWidget {
const ButtonDemo({super.key});

void _showMessage(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Кнопка нажата!'),
duration: Duration(seconds: 2),
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Кнопка')),
body: Center(
child: ElevatedButton(
onPressed: () => _showMessage(context),
child: const Text('Нажми меня'),
),
),
);
}
}

Разбор.

СтрокаСмысл
ElevatedButtonОсновная «объёмная» кнопка Material
onPressed: () => _showMessage(context)Лямбда передаёт context в метод — он нужен для поиска Scaffold
ScaffoldMessenger.of(context)Сервис, который показывает SnackBar поверх текущего Scaffold
SnackBar(content: Text(...))Чёрная/тёмная полоска внизу экрана
duration: Duration(seconds: 2)Сколько секунд висит сообщение

Попробуйте сами. Замените на SnackBar(action: SnackBarAction(label: 'OK', onPressed: () {})) — кнопка на полоске.

Для модального окна «OK / Отмена» см. диалог.


Поле ввода и приветствие

Задача. Прочитать имя из поля и показать приветствие — типичная форма на лабораторной.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const GreetingScreen());
}
}

class GreetingScreen extends StatefulWidget {
const GreetingScreen({super.key});

@override
State<GreetingScreen> createState() => _GreetingScreenState();
}

class _GreetingScreenState extends State<GreetingScreen> {
final _controller = TextEditingController();
String _message = '—';

@override
void dispose() {
_controller.dispose();
super.dispose();
}

void _greet() {
final name = _controller.text.trim();
setState(() {
_message = name.isEmpty ? 'Введите имя' : 'Здравствуй, $name!';
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Приветствие')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Ваше имя',
border: OutlineInputBorder(),
hintText: 'Анна',
),
textInputAction: TextInputAction.done,
onSubmitted: (_) => _greet(),
),
const SizedBox(height: 12),
Text(_message, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _greet,
child: const Text('Приветствовать'),
),
],
),
),
);
}
}

Разбор.

ЭлементСмысл
TextEditingController«Мост» к тексту внутри TextField; читают через .text
final _controller = ...Создаётся один раз в State, не в build
dispose() + _controller.dispose()Освобождает ресурсы при закрытии экрана — обязательная привычка
trim()Убирает пробелы по краям — пустое « » не считается именем
InputDecorationРамка, подпись labelText, подсказка hintText
OutlineInputBorder()Прямоугольная рамка вокруг поля
onSubmitted: (_) => _greet()Клавиша «Готово» / Enter на клавиатуре = та же логика, что у кнопки
Column + crossAxisAlignment: stretchКнопка на всю ширину экрана
SizedBox(height: 12)Вертикальный зазор 12 логических пикселей

Типичные ошибки.

  • Создают TextEditingController() внутри build — при каждой перерисовке теряется текст и течёт память.
  • Забывают dispose() — предупреждения в консоли при Hot Reload.

Попробуйте сами. Второе поле «Фамилия» и вывод Здравствуй, $name $surname!.


Конвертер °C → °F

Задача. Классическая учебная программа: число → формула → результат на экране (часто встречается в заданиях вместе с Tkinter-конвертером).

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const ConverterScreen());
}
}

class ConverterScreen extends StatefulWidget {
const ConverterScreen({super.key});

@override
State<ConverterScreen> createState() => _ConverterScreenState();
}

class _ConverterScreenState extends State<ConverterScreen> {
final _controller = TextEditingController();
String _result = '—';

@override
void dispose() {
_controller.dispose();
super.dispose();
}

void _convert() {
final raw = _controller.text.trim().replaceAll(',', '.');
final celsius = double.tryParse(raw);
if (celsius == null) {
setState(() => _result = 'Введите число, например 25');
return;
}
final fahrenheit = celsius * 9 / 5 + 32;
setState(() {
_result =
'${celsius.toStringAsFixed(1)} °C = ${fahrenheit.toStringAsFixed(1)} °F';
});
}

void _clear() {
_controller.clear();
setState(() => _result = '—');
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Конвертер')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Температура (°C)',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _convert(),
),
const SizedBox(height: 16),
Text(_result, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _convert,
child: const Text('Перевести'),
),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: _clear,
child: const Text('Очистить'),
),
],
),
],
),
),
);
}
}

Разбор формулы и проверок.

СтрокаСмысл
replaceAll(',', '.')«25,5» → «25.5» для double.tryParse
double.tryParse(raw)Вернёт null, если введены буквы — без try/catch
celsius * 9 / 5 + 32Формула Фаренгейта: $F = C \times \frac{9}{5} + 32$
toStringAsFixed(1)Один знак после запятой в выводе
keyboardType: ... decimal: trueНа телефоне — цифровая клавиатура с точкой
OutlinedButtonВторичная кнопка «Очистить» — визуально легче основной

Попробуйте сами. Обратный перевод °F → °C: $C = (F - 32) \times \frac{5}{9}$.


Флажок, переключатель и радио

Задача. Несколько настроек «вкл/выкл» и выбор одной роли — как Checkbutton и Radiobutton в Tkinter.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const SettingsScreen());
}
}

class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});

@override
State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
bool _notify = true;
bool _sound = false;
String _role = 'user';

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Настройки')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Уведомления'),
subtitle: const Text('Push о новых сообщениях'),
value: _notify,
onChanged: (v) => setState(() => _notify = v),
),
SwitchListTile(
title: const Text('Звук'),
value: _sound,
onChanged: (v) => setState(() => _sound = v),
),
const Divider(),
const Text('Роль', style: TextStyle(fontWeight: FontWeight.bold)),
RadioListTile<String>(
title: const Text('Пользователь'),
value: 'user',
groupValue: _role,
onChanged: (v) => setState(() => _role = v!),
),
RadioListTile<String>(
title: const Text('Администратор'),
value: 'admin',
groupValue: _role,
onChanged: (v) => setState(() => _role = v!),
),
const SizedBox(height: 16),
Text(
'Итог: роль «$_role»; уведомления: $_notify; звук: $_sound',
style: TextStyle(color: Colors.grey.shade700),
),
],
),
);
}
}

Разбор.

ВиджетКогда использовать
SwitchListTileОдна строка «название + переключатель»
RadioListTile<String>Один вариант из группы; groupValue общий для всех
value / onChanged у SwitchonChanged: null — переключатель серый и неактивен
ListViewПрокрутка, если пунктов больше, чем высота экрана

Попробуйте сами. Третья роль «Гость» с value: 'guest'.


Список задач

Задача. Добавлять строки и удалять их — flutter todo list, частый мини-проект для отчёта.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const TodoScreen());
}
}

class TodoScreen extends StatefulWidget {
const TodoScreen({super.key});

@override
State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
final _controller = TextEditingController();
final List<String> _items = [];

@override
void dispose() {
_controller.dispose();
super.dispose();
}

void _addItem() {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() {
_items.add(text);
_controller.clear();
});
}

void _removeAt(int index) {
setState(() => _items.removeAt(index));
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Список задач')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Новая задача',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _addItem(),
),
),
IconButton(
onPressed: _addItem,
icon: const Icon(Icons.add_circle),
tooltip: 'Добавить',
),
],
),
),
Expanded(
child: _items.isEmpty
? const Center(child: Text('Список пуст — добавьте задачу'))
: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(_items[index]),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _removeAt(index),
),
);
},
),
),
],
),
);
}
}

Разбор ListView.

ЭлементСмысл
List<String> _itemsДанные списка в state
_items.add(text)Добавление в конец
_items.removeAt(index)Удаление по индексу
ListView.builderСтроит только видимые строки — важно для длинных списков
itemCount: _items.lengthСколько элементов рисовать
itemBuilder: (context, index)Виджет для строки index
Expanded вокруг спискаСписок занимает всё место под полем ввода
Условие _items.isEmpty ? ... : ...Пустое состояние — подсказка пользователю

Типичные ошибки.

  • ListView без Expanded внутри Column — ошибка unbounded height.
  • Меняют _items, но без setState — UI не обновляется.

Попробуйте сами. Checkbox в ListTile для отметки «выполнено» (второй список или List<Map>).


Ползунок

Задача. Выбрать число в диапазоне — аналог Scale в Tkinter.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const SliderScreen());
}
}

class SliderScreen extends StatefulWidget {
const SliderScreen({super.key});

@override
State<SliderScreen> createState() => _SliderScreenState();
}

class _SliderScreenState extends State<SliderScreen> {
double _volume = 50;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Громкость')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Уровень: ${_volume.round()}',
style: const TextStyle(fontSize: 22),
),
Slider(
min: 0,
max: 100,
divisions: 20,
label: _volume.round().toString(),
value: _volume,
onChanged: (v) => setState(() => _volume = v),
),
],
),
),
);
}
}

Разбор.

ПараметрСмысл
min / maxДиапазон значений
value: _volumeТекущая позиция ползунка — должна быть между min и max
onChangedВызывается при перетаскивании; обязан обновить state
divisions: 2020 дискретных шагов (0, 5, 10, … 100)
labelВсплывающая цифра над ползунком при движении

Форма входа

Задача. Логин и пароль с проверкой — типичная лабораторная «форма авторизации».

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const LoginScreen());
}
}

class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});

@override
State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _loginController = TextEditingController();
final _passwordController = TextEditingController();

@override
void dispose() {
_loginController.dispose();
_passwordController.dispose();
super.dispose();
}

void _submit() {
if (!_formKey.currentState!.validate()) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Вход: ${_loginController.text}')),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Вход')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _loginController,
decoration: const InputDecoration(
labelText: 'Логин',
prefixIcon: Icon(Icons.person),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Введите логин' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Пароль',
prefixIcon: Icon(Icons.lock),
),
validator: (v) =>
(v == null || v.length < 4) ? 'Минимум 4 символа' : null,
),
const SizedBox(height: 24),
FilledButton(
onPressed: _submit,
child: const Text('Войти'),
),
],
),
),
),
);
}
}

Разбор валидации.

ЭлементСмысл
Form + GlobalKey<FormState>Общая форма; ключ нужен, чтобы вызвать .validate()
TextFormFieldКак TextField, но с validator
validator: (v) => ...Верните null — поле OK; строку — текст ошибки под полем
_formKey.currentState!.validate()Проверяет все поля; false, если хоть одно неверно
obscureText: trueСимволы пароля скрыты точками
FilledButtonАкцентная кнопка Material 3

Боковое меню

Задача. Пункты «Главная», «Настройки» в выезжающей панели — иконка ☰ появляется в AppBar автоматически.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const MenuScreen());
}
}

class MenuScreen extends StatelessWidget {
const MenuScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Меню')),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.indigo),
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'Моё приложение',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Главная'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Настройки'),
onTap: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Раздел «Настройки»')),
);
},
),
],
),
),
body: const Center(
child: Text('Откройте меню ☰ слева в AppBar'),
),
);
}
}

Разбор.

  • drawer: у Scaffold — Flutter сам рисует кнопку-«гамбургер».
  • Navigator.pop(context) закрывает drawer после выбора пункта.
  • DrawerHeader — цветная шапка боковой панели.

Второй экран

Задача. Перейти на новый экран и вернуться — flutter navigator push.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const HomeScreen());
}
}

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Главная')),
body: Center(
child: FilledButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DetailsScreen()),
);
},
child: const Text('Подробнее'),
),
),
);
}
}

class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Детали')),
body: Center(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Назад'),
),
),
);
}
}

Разбор навигации.

ВызовЧто происходит
Navigator.push(...)Новый экран поверх текущего; стек растёт
MaterialPageRoute(builder: ...)Анимация «слайд справа» в стиле Android
Navigator.pop(context)Снять верхний экран; вернуться назад
Системная кнопка «Назад» на AndroidТо же, что pop

Смысл context: по нему Flutter находит ближайший Navigator в дереве виджетов. Поэтому context передают в методы навигации.


Вкладки

Задача. Два экрана в одном — переключение по табам или свайпу.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const TabsScreen());
}
}

class TabsScreen extends StatelessWidget {
const TabsScreen({super.key});

@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Вкладки'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: 'Главная'),
Tab(icon: Icon(Icons.settings), text: 'Настройки'),
],
),
),
body: const TabBarView(
children: [
Center(child: Text('Содержимое главной')),
Center(child: Text('Содержимое настроек')),
],
),
),
);
}
}

Разбор.

  • DefaultTabController(length: 2) — число вкладок должно совпадать с длиной TabBar и TabBarView.
  • Свайп влево/вправо на телефоне переключает вкладки без нажатия.

Диалог подтверждения

Задача. Спросить «Удалить?» перед действием — как askyesno в Tkinter.

Future<void> confirmDelete(BuildContext context) async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Удалить?'),
content: const Text('Это действие нельзя отменить.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Удалить'),
),
],
),
);

if (ok == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Удалено')),
);
}
}

Вызовите confirmDelete(context) из onPressed кнопки «Удалить» на любом экране.

Разбор async.

СтрокаСмысл
async / awaitЖдём, пока пользователь нажмёт кнопку в диалоге
showDialog<bool>Возвращает Future<bool?> — результат закрытия
Navigator.pop(ctx, true)Закрыть диалог и вернуть true вызывающему коду
context.mountedПосле await экран мог закрыться — проверка перед SnackBar

Загрузка списка с API

Задача. GET-запрос и список имён — параллель Fetch / axios на Dart.

Шаг 1. В pubspec.yaml в секции dependencies::

dependencies:
flutter:
sdk: flutter
http: ^1.2.0

Терминал: flutter pub get.

Шаг 2. Полный lib/main.dart:

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

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(home: const UsersScreen());
}
}

class UsersScreen extends StatefulWidget {
const UsersScreen({super.key});

@override
State<UsersScreen> createState() => _UsersScreenState();
}

class _UsersScreenState extends State<UsersScreen> {
late Future<List<String>> _future;

@override
void initState() {
super.initState();
_future = _loadUsers();
}

Future<List<String>> _loadUsers() async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/users');
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Ошибка сервера: ${response.statusCode}');
}
final data = jsonDecode(response.body) as List;
return data.map((u) => u['name'] as String).toList();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Пользователи')),
body: FutureBuilder<List<String>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Ошибка: ${snapshot.error}'),
),
);
}
final items = snapshot.data ?? [];
if (items.isEmpty) {
return const Center(child: Text('Список пуст'));
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => ListTile(
leading: const Icon(Icons.person_outline),
title: Text(items[i]),
),
);
},
),
);
}
}

Разбор FutureBuilder.

Состояние snapshotЧто показать
ConnectionState.waitingКрутилка загрузки
hasErrorТекст ошибки (нет сети, 404…)
hasDataListView с результатом
initState + _future = ...Запрос один раз при открытии экрана, не при каждом build

Без интернета замените _loadUsers() на:

Future<List<String>> _loadUsers() async {
await Future.delayed(const Duration(seconds: 1));
return ['Алиса', 'Борис', 'Вика'];
}

Частые ошибки

СимптомПричинаЧто сделать
setState() called after dispose()После await обновили UI закрытого экранаif (!mounted) return; перед setState
RenderFlex overflowed by … pixelsColumn/Row не влезает по высотеSingleChildScrollView или Expanded / Flexible
Vertical viewport was given unbounded heightListView в Column без ExpandedОберните список в Expanded
No Material widget foundTextField вне MaterialAppКорень — MaterialAppScaffold
Цифра счётчика не меняетсяЗабыли setStateОберните изменение поля в setState(() { ... })
Кнопка сераяonPressed: nullПередайте функцию или уберите null
Hot Reload не помогМеняли main() или staticHot Restart (R)
MissingPluginExceptionПлагин не подтянулсяflutter pub get, flutter clean, пересборка

RenderFlex overflow — типичный случай

Плохо — длинный Column без прокрутки:

body: Column(
children: [
/* много виджетов — на маленьком экране красная ошибка */
],
),

Хорошо:

body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [ /* те же виджеты — прокручиваются */ ],
),
),

Частые вопросы

Чем Flutter отличается от React?
React — UI в браузере через DOM; Flutter — свой движок рисования, одна кодовая база на телефон и десктоп. Идея state похожа: см. React — компоненты-рецепты.

Нужен ли Mac для Android?
Нет. Android-сборка работает на Windows/Linux/macOS. iOS-сборка — только на macOS с Xcode.

Можно ли без эмулятора?
flutter run -d chrome — быстрая проверка вёрстки. Для отчёта по «мобильной» лабораторной лучше скриншот с эмулятора.

Как сдать работу учителю?
ZIP проекта (без build/), скриншоты экранов, краткое описание виджетов в отчёте: «счётчик на StatefulWidget, список на ListView.builder».

Где полная теория?
Flutter, Dart — раздел.


Что изучить дальше

ТемаКуда перейти
Архитектура, pub.dev, сборка APKFlutter — энциклопедия
Async, Future, StreamAsync в Dart
Null safety, классыООП в Dart
Публикация в storeМобильные приложения
Веб-аналог UIReact — рецепты
Десктоп на PythonTkinter — виджеты

Для отчёта и лабораторной

В описании укажите три виджета, которые использовали: «Scaffold — каркас экрана, TextField + TextEditingController — ввод имени, setState — обновление надписи». Одно точное имя API показывает, что вы понимаете код, а не только скопировали шаблон — как с тегами в HTML-страницах целиком.


См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").