getx icon indicating copy to clipboard operation
getx copied to clipboard

[Feature Request] Mixin equivalent to AutomaticKeepAliveClientMixin

Open lpylpyleo opened this issue 3 years ago • 30 comments

AutomaticKeepAliveClientMixin is useful it comes to TabView or PageView.

Tried to use TextEditingController and PageStorageKey to keep user input and ListView's scroll position. But it will be more convinient if we have AutomaticKeepAliveClientMixin.

I have noticed that there is a SingleGetTickerProviderMixin equivalent to SingleTickerProviderMixin in Getx. So is it possible to make a mixin like AutomaticKeepAliveClientMixin as well or I just don't know the correct usage?

Not sure if it is appropriate to use Feature request. Thank you advance.

lpylpyleo avatar Nov 26 '20 19:11 lpylpyleo

Hy @lpylpyleo,

  • AutomaticKeepAliveClientMixin is used for the initState not to run more than once. In Getx you can use a controller for each view and you will not need to use StatefullWidget. The onInit method of the controller will be called only once. See an example:
import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(GetMaterialApp(home: Home()));
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(HomeController());

    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: controller.tabController,
          tabs: [
            Tab(icon: Icon(Icons.directions_car)),
            Tab(icon: Icon(Icons.directions_bike)),
          ],
        ),
        title: Text('Tabs Demo'),
      ),
      body: TabBarView(
        controller: controller.tabController,
        children: [
          CarPage(),
          BikePage(),
        ],
      ),
    );
  }
}

class HomeController extends GetxController with SingleGetTickerProviderMixin {
  TabController tabController;

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

class CarPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(CarPageController());
    return Center(
      child: Obx(() => Text(controller.car.value)),
    );
  }
}

class CarPageController extends GetxController {
  final car = ''.obs;

  @override
  void onInit() {
    print('Call API Car');  // called only once
    car.value = 'Ferrari';
    super.onInit();
  }
}

class BikePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(BikePageController());
    return Center(
      child: Obx(() => Text(controller.bike.value)),
    );
  }
}

class BikePageController extends GetxController {
  final bike = ''.obs;

  @override
  void onInit() {
    print('Call API Bike');  // called only once
    bike.value = 'BMC';
    super.onInit();
  }
}

eduardoflorence avatar Nov 26 '20 22:11 eduardoflorence

Thanks for you sample code. @eduardoflorence

I edited your second TabView's using a ListView. When I switched to this tab, scroll the list, then switched to first tab, then back. The ListView's position is reset to 0.0. Also, the build is called every time I switched to this tab.

class BikePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('BikePage build'); // called every time
    final controller = Get.put(BikePageController());
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Text(index.toString());
      },
    );
  }
}

class BikePageController extends GetxController {
  final bike = ''.obs;

  @override
  void onInit() {
    print('Call API Bike'); // called only once
    bike.value = 'BMC';
    super.onInit();
  }
}

As a comparison, the AutomaticKeepAliveClientMixin version will automatically store the ListView's position, as well as other state like TextField. Also, the build method only fired once. So should I just use StatefulWidget + AutomaticKeepAliveClientMixin instead of GetView + GetxController in cases like this?

class BikePage1 extends StatefulWidget {
  @override
  _BikePage1State createState() => _BikePage1State();
}

class _BikePage1State extends State<BikePage1>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    print('BikePage1 build'); //called only once
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Text(index.toString());
      },
    );
  }

  @override
  bool get wantKeepAlive => true;
}

lpylpyleo avatar Nov 27 '20 08:11 lpylpyleo

You're right, AutomaticKeepAliveClientMixin does not execute the build method again on StatefullWidget. I will mark this issue as a feature request

eduardoflorence avatar Nov 27 '20 14:11 eduardoflorence

I think this should not be on the controller side, since it changes how the build method behaves. The view is responsible for this. I think it is possible to create a class of its own for this, maybe it is even better than a mixin, as it does not need to overwrite the super.build, and because it is possible to add the getter to the controller. Let's implement this, I just don't know if it would be better to create it inside a GetX/Obx/GetBuilder (as a widget wrapper), or as an abstract class (like GetView/GetWiget)

jonataslaw avatar Nov 27 '20 15:11 jonataslaw

Any update? I wanna keep state between two tab. How to?

dev-phamquoctrong avatar Dec 03 '20 04:12 dev-phamquoctrong

@pqtrong17, use the example I provided above, but do what @lpylpyleo informed, replacing the StatelessWidget of each page with a Statefulwidget with AutomaticKeepAliveClientMixin

eduardoflorence avatar Dec 03 '20 18:12 eduardoflorence

@eduardoflorence I use StatefulWidget and work for me but I wanna use GetView or GetWidget, any solution?

dev-phamquoctrong avatar Dec 04 '20 02:12 dev-phamquoctrong

Hello, I think AutomaticKeepAliveClientMixin is useful. If you add it on GetxController,please notice me.Thanks

jy6c9w08 avatar Jan 03 '21 12:01 jy6c9w08

@jy6c9w08 If you made it, you will get the error:

'AutomaticKeepAliveClientMixin<StatefulWidget>' can't be mixed onto 'GetxController' because 'GetxController' doesn't implement 'State<StatefulWidget>'.

dev-phamquoctrong avatar Jan 04 '21 07:01 dev-phamquoctrong

@pqtrong17 Thanks for your reply, I know your mean. So,how can I useing getx to keep page state?I want avoid to use StatefulWidget. I see you mark this issue as a feature request.If you realize this feature,Please notice me.Thsnks.

jy6c9w08 avatar Jan 04 '21 12:01 jy6c9w08

Any update, please? @eduardoflorence

dev-phamquoctrong avatar Jan 08 '21 06:01 dev-phamquoctrong

Any update, please? @eduardoflorence

????

ataknakbulut avatar Jan 31 '21 07:01 ataknakbulut

Any update, please? @eduardoflorence

????

I use StatefulWidget and work for me but I wanna use GetView or GetWidget, any solution?

dev-phamquoctrong avatar Feb 01 '21 03:02 dev-phamquoctrong

@lpylpyleo an alternative to keep the Lisview off set position would be to use PageStorageKey() for every tabBarView or PageView. You can follow this example here.

This feature would be more than appreciated, I'm experiencing this issue myself as my App is nested with TabBars. Thanks again

maares avatar Feb 09 '21 20:02 maares

@maares Thanks. Now I use a simple KeepAliveWrapper as a work around.

TabBarView(
        controller: controller.tabController,
        children: const [
          KeepAliveWrapper(child: CarPage()),
          KeepAliveWrapper(child: BikePage()),
        ],
)

My KeepAliveWrapper:

class KeepAliveWrapper extends StatefulWidget {
  final Widget child;

  const KeepAliveWrapper({Key key, this.child}) : super(key: key);

  @override
  _KeepAliveWrapperState createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }

  @override
  bool get wantKeepAlive => true;
}

lpylpyleo avatar Feb 25 '21 06:02 lpylpyleo

Anyone found a solution to keep the ScrollController level please ?

loic-hamdi avatar Mar 02 '21 05:03 loic-hamdi

Anyone found a solution to keep the ScrollController level please ?

See above (Stateful)

eduardoflorence avatar Mar 02 '21 12:03 eduardoflorence

I've been using a similar approach to the one provided by @lpylpyleo for a while now, but you should consider passing a closure that returns a Widget instead of doing it directly. That way your widget won't be built until you go to this tab for the first time, which for me was what I wanted, because it was fetching some data from database. No need to fetch if user might not go there at all.

Zohenn avatar Mar 02 '21 13:03 Zohenn

Any updates?

AzizMarashly avatar Oct 06 '21 09:10 AzizMarashly

Any updates?

syssam avatar Oct 17 '21 09:10 syssam

Any updates?

qq326646683 avatar Nov 08 '21 14:11 qq326646683

Any updates?

YeFei572 avatar Dec 09 '21 09:12 YeFei572

Update on this ?

pacifio avatar Feb 17 '22 16:02 pacifio

I think this should be taken into consideration.

3xscola avatar May 22 '22 17:05 3xscola

Nice!!!

Update

const KeepAliveWrapper({Key? key, required this.child}) : super(key: key);

DSPerson avatar Jul 05 '22 08:07 DSPerson

Any updates on AutomaticKeepAliveClientMixin in getx ??

Asif-shah786 avatar Sep 18 '22 08:09 Asif-shah786

+1 for this feature :)

wildsurfer avatar Nov 30 '22 12:11 wildsurfer

Thanks for you sample code. @eduardoflorence

I edited your second TabView's using a ListView. When I switched to this tab, scroll the list, then switched to first tab, then back. The ListView's position is reset to 0.0. Also, the build is called every time I switched to this tab.

class BikePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('BikePage build'); // called every time
    final controller = Get.put(BikePageController());
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Text(index.toString());
      },
    );
  }
}

class BikePageController extends GetxController {
  final bike = ''.obs;

  @override
  void onInit() {
    print('Call API Bike'); // called only once
    bike.value = 'BMC';
    super.onInit();
  }
}

As a comparison, the AutomaticKeepAliveClientMixin version will automatically store the ListView's position, as well as other state like TextField. Also, the build method only fired once. So should I just use StatefulWidget + AutomaticKeepAliveClientMixin instead of GetView + GetxController in cases like this?

class BikePage1 extends StatefulWidget {
  @override
  _BikePage1State createState() => _BikePage1State();
}

class _BikePage1State extends State<BikePage1>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    print('BikePage1 build'); //called only once
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Text(index.toString());
      },
    );
  }

  @override
  bool get wantKeepAlive => true;
}

Update

Support for null security

class KeepAliveWrapper extends StatefulWidget {
  final Widget child;
  const KeepAliveWrapper(this.child, {super.key});

  @override
  State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }

  @override
  bool get wantKeepAlive => true;
}

leggod avatar Jan 11 '23 10:01 leggod

Try this

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

abstract class GetViewKeepAlive<T> extends StatefulWidget {
  const GetViewKeepAlive({super.key, this.tag});

  @override
  State<GetViewKeepAlive<T>> createState() => _GetViewKeepAliveState<T>();

  @protected
  Widget build(BuildContext context);

  @protected
  final String? tag;

  @protected
  T get controller => GetInstance().find<T>(tag: tag)!;
}

class _GetViewKeepAliveState<T> extends State<GetViewKeepAlive<T>> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.build(context);
  }

  @override
  bool get wantKeepAlive => true;
}

Change your existed view from class RickRollView extends GetView<RickRollController> to class RickRollView extends GetViewKeepAlive<RickRollController>

liemfs avatar Apr 18 '23 06:04 liemfs