Flutterando / modular

A smart project structure
https://pub.dev/packages/flutter_modular
Other
1.31k stars 253 forks source link

Módulo permanece ativo indeterminadamente após navegação em rotas filhas #902

Open GuilhermeVVeiga opened 1 year ago

GuilhermeVVeiga commented 1 year ago

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.

migdev-br commented 1 year ago

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

Dansp commented 1 year ago

Mesmo problema aqui com a versão 3.13.6 do Flutter

Zeca-dev commented 1 year ago

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.

migdev-br commented 12 months ago

Bom dia @jacobaraujo7, alguma novidade?

eduardoflorence commented 12 months ago

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
Zeca-dev commented 12 months ago

@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.

eduardoflorence commented 11 months ago

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.

Zeca-dev commented 11 months ago

@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.

eduardoflorence commented 11 months ago

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'),
        ),
      ),
    );
  }
}
Zeca-dev commented 11 months ago

@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.

eduardoflorence commented 11 months ago

@Zeca-dev, você consegue alterar o exemplo que eu fiz para representar o problema e colocar aqui?

Zeca-dev commented 11 months ago

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.

Zeca-dev commented 11 months ago

@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'),
        ),
      ),
    );
  }
}
eduardoflorence commented 11 months ago

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));
}
Zeca-dev commented 11 months ago

@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.

Zeca-dev commented 11 months ago

@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.

Zeca-dev commented 11 months ago

@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.