Come costruire un TabView usando Flutter GetX

Sep 17 2020

Sto cercando di creare un'app utilizzando Flutter con GetX per la gestione dello stato e in una delle mie pagine ho bisogno di creare un widget TabView al centro della pagina, ho già trovato molte cose che spiegano come costruire un TabView come widget, come questo post e questo articolo , ma tutti questi estendono un controller di stato con un SingleTickerProviderStateMixin .

Da quanto ho capito leggendo la documentazione, non dovrei usare StatefulWidgets e States del genere, ma non riesco a capire come elaborare una soluzione utilizzando l'architettura GetX.

Ho già provato nella mia pagina cose come:

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

e

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

Ma l'argomento VSYNC per TabController non può essere nullo e non riesco a capire come non riesco a ottenere un TickerProvider per popolarlo.

Risposte

1 Baker Dec 02 2020 at 12:07

La seguente sarebbe una soluzione per utilizzare un controller GetX per controllare un TabView?

GetTicker

Esiste una versione Get di SingleTickerProviderMixin che implementa l'interfaccia TickerProvider (utilizzando la stessa classe Ticker da Flutter SDK).

Ha un nome accattivante: SingleGetTickerProviderMixin

L'esempio seguente è fondamentalmente tratto dalla documentazione di Flutter per TabController .

Dall'esempio collegato StatefulWidget ho trasferito il suo contenuto in un GetxController ( MyTabController) e aggiunto un mix di 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();
  }
}

Per utilizzare MyTabControllerall'interno di un widget Stateless:

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(),
      ),
    );
  }
}

Ecco il resto dell'esempio di app da copiare e incollare in Android Studio / VisualStudio Code per l'esecuzione:

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

Come hai detto in tutti gli articoli si estendono Statecon SingleTickerProviderStateMixinperché non sarai in grado di inizializzare il tuo TabControlleresterno a un Stateas TabController manage a state ( doc ).

Una soluzione alternativa sarebbe quella di non utilizzare una variabile per il controller e di racchiudere l' albero dei widget TabBare TabViewall'interno di un file DefaultTabController.

Ecco un esempio dal documento ufficiale :

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(),
        ),
      ),
    );
  }
}

In questo modo non avrai bisogno di avere il tuo TabViewinterno a Statema non lo userai GetXneanche.

HamedHamedi Nov 24 2020 at 21:16

Mi sono imbattuto nel problema esatto e mostra che il framework GetX non è pronto per molti possibili scenari e dovresti usarlo con cautela.

La soluzione che ho trovato è un widget wrapper. Nell'esempio proposto ho usato HookWidgetma sei libero di usare anche il 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);
  }
}

Come puoi vedere c'è una variabile Rx che controlla lo stato del tabView. Quindi devi passare un RxInt dall'esterno e ogni volta che il suo valore cambia, il tabView si aggiorna rispettivamente.

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,
             ...,
          ],
        ),
      ),
    );
  }

In questo momento ci prendiamo cura del controller con l'aiuto di HookWidget. Se stai usando il widget Stateful devi smaltirlo correttamente.