Cómo construir un TabView usando Flutter GetX

Sep 17 2020

Estoy tratando de crear una aplicación usando Flutter con GetX para State Management, y en una de mis páginas necesito construir un widget TabView en el medio de la página, ya encontré muchas cosas que explican cómo construir un TabView como widget, como esta publicación y este artículo , pero todos ellos extienden un controlador de estado con un SingleTickerProviderStateMixin .

Según tengo entendido al leer la documentación, se supone que no debo usar StatefulWidgets y Estados como ese, pero no puedo entender cómo elaborar una solución usando la arquitectura GetX.

Ya probé en mi página cosas como:

class CourseDetailPage extends StatelessWidget {
  final TabController _tabController = Get.put(TabController(vsync: null, length: 2));
}

y

class CourseDetailPage extends StatelessWidget {
  final TabController _tabController = TabController(vsync: null, length: 2);
}  

Pero el argumento VSYNC para el TabController no puede ser nulo, y no entiendo cómo no puedo obtener un TickerProvider para completarlo.

Respuestas

1 Baker Dec 02 2020 at 12:07

¿Lo siguiente sería una solución para usar un controlador GetX para controlar un TabView?

GetTicker

Hay una versión Get de SingleTickerProviderMixin que implementa la interfaz TickerProvider (usando la misma clase Ticker de Flutter SDK).

Tiene un nombre pegadizo: SingleGetTickerProviderMixin

El siguiente ejemplo es básicamente de los documentos de Flutter para TabController .

Desde el StatefulWidget del ejemplo vinculado, trasplante su contenido a un GetxController ( MyTabController) y agregué una combinación de SingleGetTickerProviderMixin:

class MyTabController extends GetxController with SingleGetTickerProviderMixin {
  final List<Tab> myTabs = <Tab>[
    Tab(text: 'LEFT'),
    Tab(text: 'RIGHT'),
  ];

  TabController controller;

  @override
  void onInit() {
    super.onInit();
    controller = TabController(vsync: this, length: myTabs.length);
  }

  @override
  void onClose() {
    controller.dispose();
    super.onClose();
  }
}

Para usar MyTabControllerdentro de un widget sin estado:

class MyTabbedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyTabController _tabx = Get.put(MyTabController());
    // ↑ init tab controller
    
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabx.controller,
          tabs: _tabx.myTabs,
        ),
      ),
      body: TabBarView(
        controller: _tabx.controller,
        children: _tabx.myTabs.map((Tab tab) {
          final String label = tab.text.toLowerCase();
          return Center(
            child: Text(
              'This is the $label tab',
              style: const TextStyle(fontSize: 36),
            ),
          );
        }).toList(),
      ),
    );
  }
}

Aquí está el resto del ejemplo de la aplicación para simplemente copiar y pegar en Android Studio / VisualStudio Code para ejecutar:

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

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Tab Example'),
      ),
      body: Column(
        children: [
          Expanded(
            flex: 1,
            child: Container(
              alignment: Alignment.center,
              child: Text('Some random stuff'),
            ),
          ),
          Expanded(
            flex: 4,
            child: MyTabbedWidget(),
          )
        ],
      ),
    );
  }
}

class MyTabController extends GetxController with SingleGetTickerProviderMixin {
  final List<Tab> myTabs = <Tab>[
    Tab(text: 'LEFT'),
    Tab(text: 'RIGHT'),
  ];

  TabController controller;

  @override
  void onInit() {
    super.onInit();
    controller = TabController(vsync: this, length: myTabs.length);
  }

  @override
  void onClose() {
    controller.dispose();
    super.onClose();
  }
}

class MyTabbedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyTabController _tabx = Get.put(MyTabController());
    // ↑ init tab controller
    
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabx.controller,
          tabs: _tabx.myTabs,
        ),
      ),
      body: TabBarView(
        controller: _tabx.controller,
        children: _tabx.myTabs.map((Tab tab) {
          final String label = tab.text.toLowerCase();
          return Center(
            child: Text(
              'This is the $label tab',
              style: const TextStyle(fontSize: 36),
            ),
          );
        }).toList(),
      ),
    );
  }
}
GuillaumeRoux Sep 17 2020 at 15:54

Como usted ha dicho en todos los artículos que se extienden Statecon SingleTickerProviderStateMixinporque no va a ser capaz de inicializar el TabControllerexterior de una Statecomo TabController gestionar un estado ( doc ).

Una solución sería la de no utilizar una variable para su controlador y envolver su TabBary TabViewárbol de control dentro de una DefaultTabController.

Aquí hay un ejemplo del documento oficial :

class MyDemo extends StatelessWidget {
  final List<Tab> myTabs = <Tab>[
    Tab(text: 'LEFT'),
    Tab(text: 'RIGHT'),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: myTabs.length,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: myTabs,
          ),
        ),
        body: TabBarView(
          children: myTabs.map((Tab tab) {
            final String label = tab.text.toLowerCase();
            return Center(
              child: Text(
                'This is the $label tab',
                style: const TextStyle(fontSize: 36),
              ),
            );
          }).toList(),
        ),
      ),
    );
  }
}

Al hacerlo, no necesitará tener su TabViewinterior, Statepero tampoco lo usará GetX.

HamedHamedi Nov 24 2020 at 21:16

Me encontré con el problema exacto y muestra que el marco GetX no está listo para muchos escenarios posibles y debe usarlo con precaución.

La solución que se me ocurrió es un widget de envoltura. En el ejemplo propuesto que utilicé, HookWidgetpero también puedes usar el widget Stateful.

class TabWidget extends HookWidget {
  final List<Widget> children;
  final RxInt currentTab;
  final int initialIndex;

  TabWidget({
    @required this.children,
    @required this.currentTab,
    @required this.initialIndex,
  });

  @override
  Widget build(BuildContext context) {
    final controller = useTabController(
      initialLength: children.length,
      initialIndex: initialIndex,
    );
    currentTab.listen((page) {
      controller.animateTo(page);
    });
    return TabBarView(controller: controller, children: children);
  }
}

Como puede ver, hay una variable Rx que controla el estado de tabView. Por lo tanto, debe pasar un RxInt desde el exterior y cada vez que cambia el valor, tabView se actualiza respectivamente.

class TestView extends GetView<TestController> {
  @override
  Widget build(BuildContext context) {
    final screen = 0.obs;
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          actions: [
            IconButton(
              icon: Icon(Icons.forward),
              onPressed: () => screen.value = screen.value + 1,
            )
          ],
        ),
        body: TabWidget(
          initialIndex: 1,
          currentTab: screen,
          children: [
             child1,
             child2,
             child3,
             ...,
          ],
        ),
      ),
    );
  }

Ahora mismo nos ocupamos del controlador con la ayuda de HookWidget. Si está utilizando el widget Stateful, debe eliminarlo correctamente.