modular
modular copied to clipboard
Módulo permanece ativo indeterminadamente após navegação em rotas filhas
Describe the bug Referente a issue https://github.com/Flutterando/modular/issues/899. O problema ocorrido não era intrínseco ao RouterOutlet Qualquer navegação em rotas filhas ocorre esse problema.
Eu encontrei esse problema tbm, quando você entra no módulo principal do RouterOutlet sem navegar entre os módulos/rotas filhos e sai para o módulo login por exemplo o dispose do módulo principal do RouterOutlet é feito, porém se navegar internamente nos módulos filho, só esses são disposed e o do módulo principal do RouterOutlet não. Senhores, @jacobaraujo7, @Bwolfs2, tem alguém olhando isso? Obrigado
Mesmo problema aqui com a versão 3.13.6 do Flutter
Mesmo problema aqui. Quando saio do módulo e volto as rotas filhas ainda estão ativas porquê o módulo não foi desativado.
Bom dia @jacobaraujo7, alguma novidade?
Olá @GuilhermeVVeiga @migdev-br @Dansp @Zeca-dev Eu fiz um um pull request #911 para resolver esse problema e está aguardando análise. Por favor, se puderem testar a solução e colocar um comentário no pull request, eu agradeceria.
Para testar, como a alteração é referente ao modular_core, acrescente isso ao seu pubspec (não precisa alterar a dependência flutter_modular
):
dependency_overrides:
modular_core:
git:
url: https://github.com/eduardoflorence/modular.git
ref: module_not_dispose
path: modular_core
@eduardoflorence , boa tarde. Eu verifiquei que o dispose ocorre com a sua alteração, porém o mesmo só ocorre se estiver usando Modular.to.navigate. Se utilizar, por exemplo, Navigator.of(context, rootNavigator: true).pop(), o dispose não ocorre. A sua solução não resolve o meu caso, pois todas as rotas são eliminadas ao utilizar o Modular.to.navigate. Meu cenário exije que as rotas sejam eliminadas só do módulo que quero dar o dispose. Observe o seguinte cenário:
AppModule [HomeModule] -> navega para o módulo Home. HomeModule [HomePage] -> Navega para HomeBank. HomeBankModule [HomeBankPage] -> Navega para contaCorrente ou Pix. ContaCorrenteModule [Page1, Page2, Page3] -> Ao fehcar volta para HomeBank. PixModule [PixPage] -> Ao fechar volta para HomeBank.
Veja que os módulos de conta corrente e pix, ao serem fechados devem navegar de volta para HomeBankPage, que pertencem ao HomeBankModule. Se eu utilizar Modular.to.navigate('/homeBank') todas as rotas iniciais serão perdidas (nesse caso HomeModule).
Eu preciso navegar de volta mantendo as rotas de HomeModule e HomeBakn ativas, fechando e dando dispose apenas no módulo acima, que nesse caso poderia ser ContaCorrenteModule ou PixModule.
Boa tarde @Zeca-dev,
Pelo que entendi do exemplo do seu cenário, basta utilizar o Modular.to.pushNamed
ao invés do navigate. No PR que coloquei de correção (#911), tem um exemplo com os dois tipos de navegação (navigate e pushNamed).
Verifica e me diz se resolveu.
@eduardoflorence , boa tarde.
Nesse caso eu estaria empilhando uma nova página/módulo acima do que já tenho. O que preciso é fechar/sair do módulo atual, dando dispose no mesmo e portanto "matando" todas as suas rotas. Ao fazer isso eu já estaria no módulo abaixo que o chamou, dessa forma:
AppModule [HomeModule] -> navega para o módulo Home. HomeModule [HomePage] -> Navega para HomeBank. HomeBankModule [HomeBankPage] -> Navega para contaCorrente ou Pix. ContaCorrenteModule [Page1, Page2, Page3] -> Ao fehcar volta para HomeBank. PixModule [PixPage] -> Ao fechar volta para HomeBank.
Suponha que estando em HomeBank e chame ContaCorrenteModule, em seguida navego até a conclusão de um fluxo dentro deste módulo (page3). Terminando esse fluxo eu dou um pop na page. Eu espero que esse pop feche todo o módulo ContaCorrenteModule, dando dispose em tudo, fazendo com que eu continue com todas as demais rotas abaixo desse módulo, ou seja, a página dentro do HomeBankModule que chamou o ContaCorrenteModule.
Boa tarde @Zeca-dev,
Como você quer que continue as rotas iniciais, tem que ser com o pushNamed
mesmo. Em relação ao módulo que tem que ser fechado após navegar pelas páginas filhas dele, é só utilizar o popUntil
.
Eu fiz o exemplo abaixo para você testar (atenção para o forRoot
nas rotas children e para o popUnitl
):
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
void main() {
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
}
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: Modular.routerConfig,
);
}
}
class AppModule extends Module {
@override
void routes(r) {
r.module('/', module: HomeModule());
}
}
class HomeModule extends Module {
@override
void routes(r) {
r.child('/', child: (_) => const HomePage());
r.module('/homebank', module: HomeBankModule());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HomePage')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/'),
child: const Text('HomeBank'),
),
),
);
}
}
class HomeBankModule extends Module {
@override
void routes(r) {
r.child('/', child: (_) => const HomeBankPage());
r.module('/contacorrente', module: ContaCorrenteModule());
//r.module('/pix', module: PixModule());
}
}
class HomeBankPage extends StatelessWidget {
const HomeBankPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HomeBank')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/contacorrente/'),
child: const Text('Conta Corrente'),
),
ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/pix/'),
child: const Text('Pix'),
),
],
),
),
);
}
}
class ContaCorrenteModule extends Module {
@override
void routes(r) {
r.child('/', child: (_) => const ContaCorrentePage(), children: [
ChildRoute('/page1', child: (_) => const Page1()),
ChildRoute('/page2', child: (_) => const Page2()),
]);
//r.module('/pix', module: PixModule());
}
}
class ContaCorrentePage extends StatelessWidget {
const ContaCorrentePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Conta Corrente')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/contacorrente/page1', forRoot: true),
child: const Text('Go Page 1'),
),
),
);
}
}
class Page1 extends StatelessWidget {
const Page1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page 1')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/contacorrente/page2', forRoot: true),
child: const Text('Go Page 2'),
),
),
);
}
}
class Page2 extends StatelessWidget {
const Page2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page 2')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.popUntil(ModalRoute.withName('/homebank/')),
child: const Text('Return HomeBank'),
),
),
);
}
}
@eduardoflorence , boa noite. Dessa forma vai funcionar porquê a navegação está apontando para o navegador root (forRoot) e assim ocorre o dispose, mas se colocar essa navegação de page1 e page2 dentro de um RouterOutlet irá quebrar e não fará o dispose. Se usar o root faz o dispose mas a navegação quebra. Se navegar normal, sem forRoot, não faz o dispose.
@Zeca-dev, você consegue alterar o exemplo que eu fiz para representar o problema e colocar aqui?
Farei isso hoje à noite. Obrigado pela atenção. As features aqui na empresa são construídas baseadas em fluxo como esse citado, então ter isso funcionando no modular ajudaria muito.O modular é a minha primeira opção quando penso em injeção de dependências e navegação.
@eduardoflorence , boa noite. Segue o exemplo solicitado:
No fluxo representando o usuário entra no módulo de conta corrente e tem uma navegação interna, por exemplo um cadastro, e no final sai do módulo. O problema é que o dispose não ocorre, e portanto, ao entrar novamente no módulo de conta corrente todas as rotas estão ativas, o que acaba quebrando o fluxo original.
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: Modular.routerConfig,
);
}
}
class AppModule extends Module {
@override
void routes(r) {
r.module('/', module: HomeModule());
}
}
class HomeModule extends Module {
@override
void routes(r) {
r.child('/', child: (_) => const HomePage());
r.module('/homebank', module: HomeBankModule());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HomePage')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/'),
child: const Text('HomeBank'),
),
),
);
}
}
class HomeBankModule extends Module {
@override
void routes(r) {
r.child('/', child: (_) => const HomeBankPage());
r.module('/contacorrente', module: ContaCorrenteModule());
// r.module('/pix', module: PixModule());
}
}
class HomeBankPage extends StatelessWidget {
const HomeBankPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HomeBank')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 110,
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('/homebank/contacorrente/'),
child: const Text('Account'),
),
),
// ElevatedButton(
// onPressed: () => Modular.to.pushNamed('/homebank/pix/'),
// child: const Text('Pix Payment'),
// ),
],
),
),
);
}
}
class ContaCorrenteModule extends Module {
@override
void routes(r) {
r.child('/',
child: (_) => const ContaCorrentePage(),
transition: TransitionType.downToUp,
children: [
ChildRoute('/page1',
child: (_) => const Page1(),
transition: TransitionType.rightToLeft,
isFullscreenDialog: true),
ChildRoute('/page2', child: (_) => const Page2(), transition: TransitionType.rightToLeft),
ChildRoute('/page3', child: (_) => const Page3(), transition: TransitionType.rightToLeft),
]);
//r.module('/pix', module: PixModule());
}
}
///A conta corrente é um RouterOutlet que contem um fluxo interno
///que navega da Page1 até a Page3, sendo que nesta é feita a finalização
///do fluxo. Porém o dispose não ocorre e ao entrar novamente em conta corrente
///todas as Pages que foram colocadas na pilha ainda estão lá, quando deveria
///reiniciar o fluxo e os empilhamentos.
///
///Ao colocar o forRoot o dispose ocorre, porém quebra o fluxo da navegação do
///RouterOutlet, surgindo inclusive algumas telas pretas entre a navegação, por
///estarem em um Navigator diferente do interno ao RouterOutlet.
///
///Se utilizar Modular.to.navigate todas as rotas anteriores serão perdidas e
///o usuário não poderá voltar a home, por exemplo, mantendo o estado que havia
///antes de iniciar as navegações.
class ContaCorrentePage extends StatelessWidget {
const ContaCorrentePage({super.key});
@override
Widget build(BuildContext context) {
Modular.to.pushNamed('./page1');
return const Scaffold(
// appBar: AppBar(title: const Text('Conta Corrente - RouterOutlet')),
body: Center(
child: Expanded(
child: RouterOutlet(),
),
),
);
}
}
class Page1 extends StatelessWidget {
const Page1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Account - Page 1'),
actions: [IconButton(onPressed: () => Modular.to.pop(), icon: const Icon(Icons.close))],
),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('./page2'),
child: const Text('Go Page 2'),
),
),
);
}
}
class Page2 extends StatelessWidget {
const Page2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Account - Page 2')),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.pushNamed('./page3'),
child: const Text('Go Page 3'),
),
),
);
}
}
class Page3 extends StatelessWidget {
const Page3({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Account - Page 3'),
),
body: Center(
child: ElevatedButton(
onPressed: () => Modular.to.popUntil(ModalRoute.withName('/homebank/')),
child: const Text('Return HomeBank'),
),
),
);
}
}
Boa noite @Zeca-dev,
Agora com seu exemplo ficou mais fácil ver o problema. Ele não tem relação com a correção que eu estou propondo no PR.
Após uma pesquisa vi que esse problema, que está no seu exemplo, tem relação com o fato de tanto o Modular.to.pop
quanto o Modular.to.popUntil
não conseguirem agir no navegador do RouterOutlet
. Eles só agem no Navegador principal. Por isso que as páginas do módulo de conta corrente ainda continuam na pilha e consequentemente o módulo não é disposado.
Para te ajudar, eu fiz as duas funções abaixo para usar em rotas filhas (children). No seu exemplo acima, para dar o dispose no módulo de forma correta, bastaria alterar :
onPressed: () => Modular.to.popUntil(ModalRoute.withName('/homebank/')),
Por:
onPressed: () => modularChildrenPopUntil('/homebank/'), // Se for módulo a string tem que terminar com /
Seguem as funções:
Future<void> modularChildrenPop() async {
final currentConfiguration = Modular.routerDelegate.currentConfiguration;
final currentRoutes = [...currentConfiguration!.routes];
final newCurrentRoutes = currentRoutes..removeLast();
await Modular.routerDelegate.setNewRoutePath(currentConfiguration.copyWith(routes: newCurrentRoutes));
}
Future<void> modularChildrenPopUntil(String routeName) async {
final currentConfiguration = Modular.routerDelegate.currentConfiguration;
final currentRoutes = [...currentConfiguration!.routes];
final indexRoute = currentRoutes.lastIndexWhere((element) => element.name == routeName);
final newCurrentRoutes = indexRoute > -1 ? currentRoutes.getRange(0, indexRoute + 1).toList() : [currentRoutes.first];
await Modular.routerDelegate.setNewRoutePath(currentConfiguration.copyWith(routes: newCurrentRoutes));
}
@eduardoflorence, boa noite. Muito obrigado. Vou testar assim que puder. Teria como fazer algo semelhante para integrar ao Modular em outra PR? Esse cenário é bem comum em alguns fluxos dentro de projetos diversos, então seria algo que agregaria bastante valor ao package.
@jacobaraujo7 , boa tarde.
Poderia avaliar junto com o @eduardoflorence a possibilidade de adicionar esse tratamento de dispose e encerramento de rotas filhas, quando estamos usando o RouterOutlet?
Obrigado.
@eduardoflorence , boa tarde. Fiz o teste e o dispose realmente funciona, porém seria necessário sobrescrever o WillPopScope para adicionar esse comportamento de pop das páginas filhas. Além disso a animação da rota-mãe é perdida, o que não é interessante, pois ao entrarmos na rota principal com um tipo de animação queremos sair da mesma utilizando o mesmo tipo. Com essa função "modularChildrenPopUntil" as animações são perdidas.
Diante do exposto, e considerando que essa correção inicial seja aceita, precisaria criar uma nova PR considerando esses cenários, de modo a manter o compportamento das animações e saídas de rota pelo botão físico ou seta de voltar da AppBar.