Урок 5

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

Егор Долгов · 05.07.2019
Статья написана для старой версии Flutter и сейчас обновляется. Часть кода может не компилироваться на актуальной версии.

Редкое приложение может обойтись одним окном или одной страницей. Во 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()));}