5

I created a widget named InfiniteScroll which handles asynchronously loaded data and renders it with ListView.builder. However I am having trouble creating a controller for it (for example for clearing all the loaded data). I read through the implementation of existing controllers such as TextEditingController but I can't seem to wrap my head around it. Here's an example of what I'm trying to achieve:

// I have
InfiniteScroll(
  fetchMore: () async {}, // fetching more data
  builder: (data) {}, // building an item
)

// need
InfiniteScroll(
  controller: _infiniteScrollController,
  fetchMore: () async {}, // fetching more data
  builder: (data) {} // building an item
)
// later
_infiniteScrollController.clearItems();

How to create such a controller? I am using flutter_hooks for local state management if that matters.

Marcin
  • 184
  • 1
  • 2
  • 12
  • I don't see how is this related to my problem. I'm looking for a way to create a controller, the widget itself is just an example. – Marcin Sep 18 '20 at 11:45

3 Answers3

23

I don't think the answer of @ValdaXD describes a good solution.

The way this is usually solved, also in native Flutter widgets like TextField or ScrollController is a controller class that extends ChangeNotifier.

The controller would handle the items and provide a public API to clear them:

class InfiniteScrollController extends ChangeNotifier {
  List<Widget> items = [];

  void clearItems() {
    items.clear();
    notifyListeners();
  }
}

The widget could then display the items via the injected controller:

class InfiniteScroll extends StatefulWidget {
  InfiniteScroll({
    required this.controller
  });

  final InfiniteScrollController controller;

  @override
  State<StatefulWidget> createState() {
    return _InfiniteScrollState();
  }
}

class _InfiniteScrollState extends State<InfiniteScroll> {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: widget.controller.items,
    );
  }
}

I have created a blog post with a different example but the same topic: a controller for a custom widget: https://www.flutterclutter.dev/flutter/tutorials/create-a-controller-for-a-custom-widget/2021/2149/

Schnodderbalken
  • 3,257
  • 4
  • 34
  • 60
  • 4
    This answer is a better solution. The accepted answer goes outside the scope of the way native widgets solve the problem. The blog post from @Schnodderbalken is an excellent guide to solving this. – TitanKing Apr 30 '21 at 14:17
  • 1
    Thank you for your answer, I have marked as the accepted one. – Marcin Aug 21 '21 at 13:56
  • your blog article is very helpful. – abdalmonem Apr 21 '22 at 09:18
  • Good answer but I have one doubt about it. I believe this solution implies storing state in the controller. What if you rather want the controlled widget to contain all state and not split it inside the controller? Wouldn't ValdaXDs solution be better then? – Martin Jul 27 '22 at 09:35
11

I just pass the functions that i want to expose to the controller.

typedef MyTypedef(int value);

class MyController {
  VoidCallback myFunction;
  VoidCallback mySecondFunction;
  MyTypedef functionThatReturns;

  void dispose() {
    //Remove any data that's will cause a memory leak/render errors in here
    myFunction = null;
    mySecondFunction = null;
    functionThatReturns = null;
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({this.controller});
  final MyController controller;
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    MyController _controller = widget.controller;
    if (_controller != null) {
      _controller.myFunction = firstFunction;
      _controller.mySecondFunction = secondFunction;
      _controller.functionThatReturns = functionWithInt;
    }
  }

  void firstFunction() {
    print('Calling first function');
  }

  void secondFunction() {
    print('Calling second function');
  }

  void functionWithInt(int value) {
    print('Calling third function with value $value');
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Then the usage is easy

//We create a variable somewhere
  ...
  MyController controller;
  ...

  //We initialize it
  ...
  controller = MyController();
  ...

  //We assign it
  @override
  Widget build(BuildContext context) {
    return MyWidget(controller: controller);
  }
}

//When we cant to call a function
...
controller.myFunction();
...

//When we want to dispose it
...
controller.dispose();
...

There is a little work to be done to avoid null exceptions , per example we could check if the controller references are null before calling the functions, and throw an error, but that's up to you to decide.

RegularGuy
  • 3,516
  • 1
  • 12
  • 29
1

You could just use a GlobalKey. I dont really know how to make controllers, but this would be my solution. It is really similar to a GlobalKey, but I hope it works for you. But I found nothing on how to do Controllers soooo... yeah.

class InfiniteScroll extends StatefulWidget {
  InfiniteScrollController controller;
  InfiniteScroll({@required this.controller}) {}

  InfiniteScrollState createState() => InfiniteScrollState(controller);
}

class InfiniteScrollState extends State<InfiniteScroll> {
  InfiniteScrollController controller;

  InfiniteScrollState(this.controller) {
    this.controller.setParent(this);
  }

  Widget build(BuildContext context) {}
}


class InfiniteScrollController {
  InfiniteScrollState infiniteScroll;

  void setParent(InfiniteScrollState infiniteScroll) {
    this.infiniteScroll = infiniteScroll;
  }

  void clearItems() {
    infiniteScroll.setState(() {
      //clear Items
    });
  }

}
Quasi
  • 576
  • 4
  • 13
  • Thanks for your answer, this seems like a nice workaround, though the accepted answer is cleaner – Marcin Sep 18 '20 at 15:16
  • Good one, Just one correction is needed, should not pass the controller using the state class constructor. – Deven May 21 '23 at 06:24