Lesson 5

Урок 5. Навигация и маршрутизация в приложении, class Navigator

Егор Долгов · 05.07.2019
This article was written for an older Flutter version and is being updated. Some code may not compile on the current version.

Редкое приложение может обойтись одним окном или одной страницей. Во Flutter и то и другое – виджеты. Для переключения между окнами или виджетами нужно использовать Navigator .

Navigator – виджет-класс, позволяющий управлять стеком дочерних виджетов, т.е. открывать, закрывать и переключать окна или страницы. Когда мы используем MaterialApp , то экземпляр класса Navigator уже создан, и его не надо объявлять с помощью слова new . А можно просто вызывать методы, для управления стеком виджетов:

  • Navigator.push;
  • Navigator.pushNamed;
  • Navigator.pop;
  • и другие.

В этом уроке мы рассмотрим некоторые основные возможности класса Navigator:

  1. Открытие нового окна и возврат к предыдущему.
  2. Использование маршрутов для навигации – routes.
  3. Передача параметров в маршруте с помощью события onGenerateRoute.
  4. Открытие диалогового окна.
  5. Анимация диалогового окна.
  6. Возвращаемое значение из диалогового окна.

1. Открытие нового окна и возврат к предыдущему

Чтобы открыть новое окно, нам нужно добавить в стек маршрутов новый маршрут. Это можно сделать с помощью Navigator.push , с указанием двух обязательных параметров: context и виджета MaterialPageRoute (или PageRouteBuilder).

Чтобы вернуться к предыдущему окну, используем метод Navigator.pop .

Создадим простой пример из двух окон.

В главном окне у нас будет кнопка, при нажатии на которую выполнится код «открытия окна»:

Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));

Класс MaterialPageRoute позволяет открыть полноэкранное окно с эффектом присущим для вашей мобильной системы, это простой и удобный способ. Можно воспользоваться другим виджетом PageRouteBuilder , он более сложный по конструкции, мы рассмотрим его в конце этого урока.

Во втором окне будет кнопка возвращения к первому окну с помощью:

Navigator.pop(context);
class Navigator.push, class Navigator.pop
class Navigator.push, class Navigator.pop

В виджете AppBar кнопка возврата добавляется автоматически при использовании Navigator.push , так же автоматически обрабатывается событие системной кнопки «Назад» (Back).

Листинг кода примера из двух окон

import 'package:flutter/material.dart';

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Главное окно')),
      body: Center(child: RaisedButton(onPressed: (){
        Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
      }, child: Text('Открыть второе окно'))),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Второе окно')),
      body: Center(child: RaisedButton(onPressed: (){
        Navigator.pop(context);
      }, child: Text('Назад'))),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: MainScreen(),
  ));
}

Минус такого подхода в том, что в большом приложении с большим количеством классов и виджетов, управлять такой структурой будет очень сложно. В каждый файл где вы будет использовать тот или иной виджет «окна» вам придется подключать его через import, и в случае замены на другой производить переименование виджета во всем проекте.

2. Использование маршрутов для навигации – routes

Поэтому чтобы избавиться от вышеуказанных проблем нужно использовать маршруты. Имена маршрутов принято использовать как пути в директориях: '/', '/client', '/client/123', и т.п.

Маршрут главного окна по умолчанию: '/'.

Когда в предыдущем примере в параметре home мы указали виджет MainScreen() – тем самым мы задали маршрут '/'.

Зададим маршруты в виджете MaterialApp через параметр routes.

routes: {
  '/':(BuildContext context) => MainScreen(),
  '/second':(BuildContext context) => SecondScreen()
},

Теперь параметр home со значением MainScreen() – можно удалить.

Если нам нужно изменить маршрут по умолчанию, при открытии приложения, нужно указать параметр initialRoute со значением маршрута, к примеру: initialRoute: '/second' .

Для открытия окна по маршруту нужно использовать Navigator.pushNamed .

Сделаем замену кода открытия окна с использованием маршрута:

Navigator.pushNamed(context, '/second');

Для возвращения ничего менять в коде не надо, оставляем Navigator.pop(context);

Листинг кода, пример с маршрутами:

import 'package:flutter/material.dart';

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Главное окно')),
      body: Center(child: Column(children: [
        RaisedButton(onPressed: (){Navigator.pushNamed(context, '/second');}, child: Text('Открыть второе окно'))
      ],)),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Второе окно')),
      body: Center(child: RaisedButton(onPressed: (){
        Navigator.pop(context);
      }, child: Text('Назад'))),
    );
  }
}

void main() {
  runApp(MaterialApp(
    initialRoute: '/',
    routes: {
      '/':(BuildContext context) => MainScreen(),
      '/second':(BuildContext context) => SecondScreen()
    }
  ));
}

3. Передача параметров в маршруте с помощью события onGenerateRoute

Следующая проблема, которую нужно решить – это передача параметров в маршруте. Например мы хотим передавать, в некоторых случает, число во второй виджет. К примеру с помощью такого маршрута '/second/123'.

Если в предыдущем примере мы вызовем маршрут '/second/123' – то мы ничего не откроем: такого маршрута нет. Так как прописать все такие маршруты невозможно, то мы должны добавить обработчик onGenerateRoute в виджете MaterialApp :

routes: {
    '/':(BuildContext context) => MainScreen(),
    '/second':(BuildContext context) => SecondScreen()
},
onGenerateRoute: (routeSettings){
    var path = routeSettings.name.split('/');

    if (path[1] == "second") {
        return new MaterialPageRoute(
          builder: (context) => new SecondScreen(id:path[2]),
          settings: routeSettings,
        );
    }
}

Маршрут '/second' в параметре routes можно было бы убрать, нужно только правильно написать исключение на отсутствие параметра path[2] . Но в нашем примере мы оставим его.

Передача параметров в маршруте с помощью события onGenerateRoute
Передача параметров в маршруте с помощью события onGenerateRoute

Листинг кода, пример с передачей параметров в маршруте:

import 'package:flutter/material.dart';

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Главное окно')),
      body: Center(child: Column(children: [
        RaisedButton(onPressed: (){Navigator.pushNamed(context, '/second');}, child: Text('Открыть второе окно')),
        RaisedButton(onPressed: (){Navigator.pushNamed(context, '/second/123');}, child: Text('Открыть второе окно 123')),
      ],)),
    );
  }
}

class SecondScreen extends StatelessWidget {
  String _id;

  SecondScreen({String id}):_id = id;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Второе окно $_id')),
      body: Center(child: RaisedButton(onPressed: (){
        Navigator.pop(context);
      }, child: Text('Назад'))),
    );
  }
}

void main() {
  runApp(MaterialApp(
    initialRoute: '/',
    routes: {
      '/':(BuildContext context) => MainScreen(),
      '/second':(BuildContext context) => SecondScreen()
    },
    onGenerateRoute: (routeSettings){
      var path = routeSettings.name.split('/');

      if (path[1] == "second") {
        return new MaterialPageRoute(
          builder: (context) => new SecondScreen(id:path[2]),
          settings: routeSettings,
        );
      }
    },
  ));
}

4. Открытие диалогового окна

С помощью класса Navigator можно так же открывать диалоговые или всплывающие окна.

Создадим диалоговое окно с помощью виджет-класса PageRouteBuilder и параметром opaque: false .

Открытие диалогового окна
Открытие диалогового окна

Листинг кода, открытия диалогового окна:

import 'package:flutter/material.dart';

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Больше или меньше')),
      body: Center(child: Column(children: [
        RaisedButton(onPressed: (){
          Navigator.push(context, PageRouteBuilder(
              opaque: false,
              pageBuilder: (BuildContext context, _, __) => MyPopup()
          ));
          }, child: Text('Загадать число')),
      ],)),
    );
  }
}

class MyPopup extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Ваш ответ:'),
      actions: [
        FlatButton(
          onPressed: () {Navigator.pop(context);},
          child: Text('Больше'),
        ),
        FlatButton(
          onPressed: () {Navigator.pop(context);},
          child: Text('Меньше'),
        ),
      ],
    );
  }
}

void main() {runApp(MaterialApp(home: MainScreen()));}

5. Анимация диалогового окна

Открывающиеся окна можно проанимировать, для этого нужно добавить параметр transitionsBuilder в виджет PageRouteBuilder

Navigator.push(context, PageRouteBuilder(
    opaque: false,
    pageBuilder: (BuildContext context, _, __) => MyPopup(),
    transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    }
));

Нижние подчеркивания играют роль переменных, т.е. вместо « » и «_ » можно было бы задать «varA» и «varB».

Так как мы не используем эти переменные, а в параметрах должны быть указаны названия для них – мы присвоили им названия « » и «_ », визуально они выглядят как прочерк.

  • FadeTransition – анимация прозрачности, opacity – текущие значение прозрачности
  • child – это виджет над которым будет производиться анимация

Интересным моментом является то что можно комбинировать анимации – как бы вкладывать их друг в друга.

Объединим две анимации: изменения прозрачности и увеличения размера.

Navigator.push(context, PageRouteBuilder(
    opaque: false,
    pageBuilder: (BuildContext context, _, __) => MyPopup(),
    transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
      print(animation);
      return FadeTransition(
        opacity: animation,
        child: ScaleTransition(scale: animation, child: child),
      );
    }
));
Анимация при открытии диалогового окна
Анимация при открытии диалогового окна

6. Возвращаемое значение из диалогового окна

Чтобы получить какое-то значение из диалогового окна, нужно в функции pop после context добавить значение, которое мы будем возвращать. Тип значения может быть любым. Для нашего примера пусть будет: Navigator.pop(context,true);

Но чтобы получить значение нужно добавить: await перед Navigator и async в RaisedButton :

//…
RaisedButton(onPressed: () async {
  bool value = await Navigator.push(context, PageRouteBuilder(
//…
Возвращаемое значение из диалогового окна
Возвращаемое значение из диалогового окна

Полный листинг кода

import 'package:flutter/material.dart';

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _scaffoldKey = GlobalKey<ScaffoldState>();

    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text('Больше или меньше')),
      body: Center(child: Column(children: [
        RaisedButton(onPressed: () async {
          bool value = await Navigator.push(context, PageRouteBuilder(
              opaque: false,
              pageBuilder: (BuildContext context, _, __) => MyPopup(),
              transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
                return FadeTransition(
                  opacity: animation,
                  child: ScaleTransition(scale: animation, child: child),
                );
              }
          ));

          if (value) _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text('Больше'), backgroundColor: Colors.green,));// TRUE
          else _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text('Меньше'), backgroundColor: Colors.red,));// FALSE
          }, child: Text('Загадать число')),
      ],)),
    );
  }
}

class MyPopup extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Ваш ответ:'),
      actions: [
        FlatButton(
          onPressed: () {Navigator.pop(context,true);},
          child: Text('Больше'),
        ),
        FlatButton(
          onPressed: () {Navigator.pop(context,false);},
          child: Text('Меньше'),
        ),
      ],
    );
  }
}

void main() {runApp(MaterialApp(home: MainScreen()));}