jonataslaw / getx

Open screens/snackbars/dialogs/bottomSheets without context, manage states and inject dependencies easily with Get.
MIT License
10.33k stars 1.62k forks source link

Multiple widgets with GetControllers #97

Closed thencke closed 4 years ago

thencke commented 4 years ago

Hi, it's me again. I really like the Get's approach for state management, it's really simple and straightforward, but I encountered some problems when I tried to create a list (ListView) of widgets that instantiate a GetController for each one. Here's a reproducible snippet:

import 'package:flutter/material.dart';
import 'package:get/get.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        body: ListView(children: [
          MyTile(cId: 'one'),
          MyTile(cId: 'two'),
          MyTile(cId: 'three'),
        ]),
      ),
    );
  }
}

class MyTileController extends GetController {
  int counter = 0;
  void increment() {
    counter++;
    update(this);
  }
}

class MyTile extends StatelessWidget {
  final String cId;

  const MyTile({Key key, this.cId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetBuilder<MyTileController>(
      id: cId,
      init: MyTileController(),
      builder: (controller) {
        return ListTile(
          title: Text('My Counter Value is: ${controller.counter}'),
          trailing: IconButton(onPressed: controller.increment, icon: Icon(Icons.add)),
        );
      },
    );
  }
}

Note that, when you tap the + button, it increments the counter value for every widget. I even tried to shoot in the dark adding an Id to the GetBuilder, but does not work too.

I also tried this approach:

class MyTile extends StatelessWidget {
  final String cId;
  final MyTileController controller = MyTileController();

  MyTile({Key key, this.cId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetBuilder(
      id: cId,
      init: controller,
      builder: (_) {
        return ListTile(
          title: Text('My Counter Value is: ${controller.counter}'),
          trailing: IconButton(onPressed: controller.increment, icon: Icon(Icons.add)),
        );
      },
    );
  }
}

Instantiating the controller outside GetBuilder init, but this causes an exception to be thrown:

════════ Exception caught by gesture ═══════════════════════════════════════════
The following NoSuchMethodError was thrown while handling a gesture:
The method 'forEach' was called on null.
Receiver: null
Tried calling: forEach(Closure: (RealState) => Null)

When the exception was thrown, this was the stack
#0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:53:5)
#1      GetController.update 
package:get/…/state/get_state.dart:18
#2      MyTileController.increment 
package:get_example/main.dart:32
#3      _InkResponseState._handleTap 
package:flutter/…/material/ink_well.dart:779

So, Is there a way to instantiate more than one controller of a type to achieve my goal? Btw, great library in general, I am thinking about getting rid of mobx and start using this, but this problem stopped me from continuing, because in mobx I can have several controllers attached to different siblings widgets and it works very well. Thanks in advance.

jonataslaw commented 4 years ago

Hi, thanks for providing a reproduction code. Do you want each listTile to have its own controller instance? I think a global = false in your GetBuilder would solve that.

thencke commented 4 years ago

Hi, thanks for providing a reproduction code. Do you want each listTile to have its own controller instance? I think a global = false in your GetBuilder would solve that.

Exato, eu vou escrever em português mesmo não sabia que eras brasileiro também. Meu caso de uso é exatamente esse, eu tenho uma lista de widgets que precisam cada um ter seu próprio controller pra controlar o seu estado, só que me parece que o approach do Get visa ter um gerenciamento de estado no padrão "singleton", então eu não consegui instanciar mais de um controller sem dar esses problemas.

Tentei adicionar o global = false pra fazer um teste, mas mesmo assim não funcionou, ocorre outro exception:

flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following NoSuchMethodError was thrown building MyTile:
flutter: The method 'add' was called on null.
flutter: Receiver: null
flutter: Tried calling: add(Instance of 'RealState')
flutter:
flutter: The relevant error-causing widget was:
flutter:   MyTile 
lib/main.dart:19
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:53:5)
flutter: #1      _GetBuilderState.initState 
package:get/…/state/get_state.dart:92
flutter: #2      StatefulElement._firstBuild 
package:flutter/…/widgets/framework.dart:4640
flutter: #3      ComponentElement.mount 
package:flutter/…/widgets/framework.dart:4476
flutter: ...     Normal element mounting (39 frames)
flutter: #42     Element.inflateWidget 
package:flutter/…/widgets/framework.dart:3446
flutter: #43     Element.updateChild 
package:flutter/…/widgets/framework.dart:3214
flutter: #44     SliverMultiBoxAdaptorElement.updateChild 
package:flutter/…/widgets/sliver.dart:1162
flutter: #45     SliverMultiBoxAdaptorElement.createChild.<anonymous closure> 
package:flutter/…/widgets/sliver.dart:1147
flutter: #46     BuildOwner.buildScope 
package:flutter/…/widgets/framework.dart:2607
flutter: #47     SliverMultiBoxAdaptorElement.createChild 
package:flutter/…/widgets/sliver.dart:1140
flutter: #48     RenderSliverMultiBoxAdaptor._createOrObtainChild.<anonymous closure> 
package:flutter/…/rendering/sliver_multi_box_adaptor.dart:354
flutter: #49     RenderObject.invokeLayoutCallback.<anonymous closure> 
package:flutter/…/rendering/object.dart:1866
flutter: #50     PipelineOwner._enableMutationsToDirtySubtrees 
package:flutter/…/rendering/object.dart:918
flutter: #51     RenderObject.invokeLayoutCallback 
package:flutter/…/rendering/object.dart:1866
flutter: #52     RenderSliverMultiBoxAdaptor._createOrObtainChild 
package:flutter/…/rendering/sliver_multi_box_adaptor.dart:343
flutter: #53     RenderSliverMultiBoxAdaptor.addInitialChild 
package:flutter/…/rendering/sliver_multi_box_adaptor.dart:427
flutter: #54     RenderSliverList.performLayout 
package:flutter/…/rendering/sliver_list.dart:79
flutter: #55     RenderObject.layout 
package:flutter/…/rendering/object.dart:1767
flutter: #56     RenderSliverEdgeInsetsPadding.performLayout 
package:flutter/…/rendering/sliver_padding.dart:135
flutter: #57     RenderSliverPadding.performLayout 
package:flutter/…/rendering/sliver_padding.dart:375
flutter: #58     RenderObject.layout 
package:flutter/…/rendering/object.dart:1767
flutter: #59     RenderViewportBase.layoutChildSequence 
package:flutter/…/rendering/viewport.dart:452
flutter: #60     RenderViewport._attemptLayout 
package:flutter/…/rendering/viewport.dart:1444

O código reproduzível desse erro é (embora talvez nem seja necessário, já que a unica coisa diferente é o global = false no GetBuilder)

import 'package:flutter/material.dart';
import 'package:get/get.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        body: ListView(children: [
          MyTile(cId: 'one'),
          MyTile(cId: 'two'),
          MyTile(cId: 'three'),
        ]),
      ),
    );
  }
}

class MyTileController extends GetController {
  int counter = 0;
  void increment() {
    counter++;
    update(this);
  }
}

class MyTile extends StatelessWidget {
  final String cId;

  MyTile({Key key, this.cId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetBuilder(
      id: cId,
      global: false,
      init: MyTileController(),
      builder: (controller) {
        return ListTile(
          title: Text('My Counter Value is: ${controller.counter}'),
          trailing: IconButton(onPressed: controller.increment, icon: Icon(Icons.add)),
        );
      },
    );
  }
}

Screenshot do exception: https://prnt.sc/sge6zw

De novo, parabéns pela lib, com certeza vai fazer parte de todos os meus projetos daqui pra frente, eu só estou relutante em trocar o mobx pelo modulo de gerência de estado do Get por causa desses probleminhas, assim que estabilizar aí sim entro de vez.

jonataslaw commented 4 years ago

Mano, só tenho a agradecer, na penultima atualização, eu removi acidentalmente um '[]' da lista de ids de controladores não globais (provavelmente porque eu comentei o código acima, deve ter pegado junto) e gerou o erro, testa a versão 2.4.0 com global = false que vai funcionar normalmente.

thencke commented 4 years ago

Perfeito! agora sim está funcionando com um controller dedicado pra cada widget. Obrigado pela rapidez do update. Vou fazer mais uns estudos aqui pra ver se compensa eu refatorar a minha aplicação inteira que usa mobx pra passar a utilizar o Get.

Outra coisa, você pretende implementar algo na pegada do "reaction" do mobx? tipo quando certa variável alterar o valor é executado um evento dentro do controller. Vou fechar essa issue aqui, o problema foi resolvido, essa minha pergunta aí não condiz com o contexto da issue, se quiser eu abro uma nova isso de feature request.

Abraços

jonataslaw commented 4 years ago

Perfeito! agora sim está funcionando com um controller dedicado pra cada widget. Obrigado pela rapidez do update. Vou fazer mais uns estudos aqui pra ver se compensa eu refatorar a minha aplicação inteira que usa mobx pra passar a utilizar o Get.

Outra coisa, você pretende implementar algo na pegada do "reaction" do mobx? tipo quando certa variável alterar o valor é executado um evento dentro do controller. Vou fechar essa issue aqui, o problema foi resolvido, essa minha pergunta aí não condiz com o contexto da issue, se quiser eu abro uma nova isso de feature request.

Abraços

Então, reaction com o Get tradicional não é necessária, porque toda alteração de uma variável depende de um set, e dentro desse set você pode chamar qualquer evento.

Com o GetX (lançado com o mesmo update do fix, mas ainda sem documentação), você pode dar listen em qualquer variável, e não precisa de reaction pra isso. ex:

final name = "JOÃO".obs;

name.listen((newName){ Get.snackbar("Tudo ok", "nome alterado com sucesso pra $newName"); });

name.value = "Pedro";

// snackbar abre

Implementei essa Api que é o meio termo entre o bloc e o mobx. Ela tem tudo que o mobx tem diretamente, sem precisar de gerador de codigo ou marcação, e tem a API do rxDart inteira disponível, se a pessoa quiser pegar o nome joão e adicionar outro nome no final sempre que mudar, ela pode. É todo poder de manipulação de streams do bloc, com a facilidade do MobX, sem gerador de código, mas obviamente, foi lançado hoje, ainda to criando os testes pra só lançar a documentação quando tiver redondo.