rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.28k stars 955 forks source link

How to implement filter/search in state ? #60

Closed zgramming closed 4 years ago

zgramming commented 4 years ago

I have simple example to search/ filter data in state , my type data state is List [UtangModel]. I can filtered list depending of what user type.

The problem is , although i success filtered the data but i lost data not what i type and it can't restore again. How best practice to implement search using state ?

Problem

Filter code

    void filterList(String query) {
    var fullList = [...state];
    var tempList = [];
    if (query.isEmpty) {
      tempList = [];
    } else {
      for (final item in fullList) {
        if (item.pengutang.nameUser.toLowerCase().contains(query.toLowerCase())) {
          tempList.add(item);
        }
      }
    }

    state = [...tempList];
    print('Total Filtered list ${fullList.length}');
  }

It's how i call the function into onChanged textfield

 TextFormFieldCustom(
              onSaved: (value) => '',
              onChanged: (value) => utangProvider.read(context).filterList(value),
              prefixIcon: Icon(Icons.search),
              hintText: 'Cari...',
            ),

Update

After reading carefully on docs , I think i can implement it using Computed.Family . So i changed previous code to this :


final showFilteredList = Computed.family<List<UtangModel>, String>((read, query) {
  final utang = read(utangProvider.state);
  // var tempList = [...utang];

  return utang
      .where((element) => element.pengutang.nameUser.toLowerCase().contains(query.toLowerCase()))
      .toList();
});

this how i call in UI

Consumer((ctx, read) {
              final utang = read(showFilteredList(_queryController.text));
              return ListView.separated(
                shrinkWrap: true,
                itemCount: utang.length,
                separatorBuilder: (BuildContext context, int index) {
                  return Divider();
                },
                itemBuilder: (BuildContext context, int index) {
                  final result = utang[index];
                  return Card(
                    child: Center(
                      child: Text(result.pengutang.nameUser),
                    ),
                  );
                },
              );
            }),

But i get same result from previous code, I missing something ?

rrousselGit commented 4 years ago

Computed.family as you've described should work.

I'm not sure why it doesn't in your case. Try a flutter clean?

zgramming commented 4 years ago

@rrousselGit strangely , after flutter clean and try uninstall application. it doesn't work what as expected.

Expectation

Result data when typing will be seen immediately.

Reality

It will show data when keyboard closed, and i think data show when keyboard close because when keyboard close it will rebuild entire widget. You can see my Gif implementation. I missed something ?

tanya riverpod 4

rrousselGit commented 4 years ago

Have you tried printing the unfiltered list, to see if it changes?

TimWhiting commented 4 years ago

I believe the issue is that you are watching the family provider, but you aren't rebuilding when the queryController.text changes so it isn't getting the new computed. So you probably want to listen to the controller and rebuild based on that as well? I'm not sure if there is a better solution though.

rrousselGit commented 4 years ago

Oh that's possible.

A solution is to save the filter in a StateProvider, then instead of a Computed.family, have a simple Computed that reads the filter

zgramming commented 4 years ago

Another testing for make it sure.


class TestingFilterModel {
  String id;
  String name;
  int price;
  TestingFilterModel({this.id, this.name, this.price});
}

class TestingFilterProvider extends StateNotifier<List<TestingFilterModel>> {
  TestingFilterProvider([List<TestingFilterModel> state]) : super(state ?? []);
}

final testingFilterProvider = StateNotifierProvider<TestingFilterProvider>((ref) {
  return TestingFilterProvider([
    TestingFilterModel(id: '1', name: 'chair', price: 20000),
    TestingFilterModel(id: '2', name: 'book', price: 50000),
    TestingFilterModel(id: '3', name: 'handphone', price: 1000),
    TestingFilterModel(id: '4', name: 'money', price: 60000),
    TestingFilterModel(id: '5', name: 'bycle', price: 88000),
  ]);
});

final filterTesting = Computed.family<List<TestingFilterModel>, String>((read, query) {
  final testing = read(testingFilterProvider.state);
  return testing.where((element) => element.name.toLowerCase().contains(query)).toList();
});

Consumer((ctx, read) {
              // final utang = read(showFilteredList(_queryController.text));
              final filter = read(filterTesting(_queryController.text));
              return Expanded(
                child: ListView.separated(
                  shrinkWrap: true,
                  itemCount: filter.length,
                  separatorBuilder: (BuildContext context, int index) {
                    return Divider();
                  },
                  itemBuilder: (BuildContext context, int index) {
                    final result = filter[index];
                    return Card(
                      child: Center(
                        // child: Text(result.pengutang.nameUser),
                        child: Text(result.name),
                      ),
                    );
                  },
                ),
              );
            }),
zgramming commented 4 years ago

@rrousselGit can you give me example to implement that ? Because i don't get what you think. Because in documentation , Computed don't have parameter to send right ? the option my understanding is Computed.Family / Provider.Family

@TimWhiting problem is in my code because it not rebuild the state ?

TimWhiting commented 4 years ago

Yes, the problem as it currently stands is in your code. However, you can use Remy's suggestion to make it work without a manual rebuild on a listener on your controller.

class TestingFilterModel {
  String id;
  String name;
  int price;
  TestingFilterModel({this.id, this.name, this.price});
}

class TestingFilterProvider extends StateNotifier<List<TestingFilterModel>> {
  TestingFilterProvider([List<TestingFilterModel> state]) : super(state ?? []);
}

final testingFilterProvider = StateNotifierProvider<TestingFilterProvider>((ref) {
  return TestingFilterProvider([
    TestingFilterModel(id: '1', name: 'chair', price: 20000),
    TestingFilterModel(id: '2', name: 'book', price: 50000),
    TestingFilterModel(id: '3', name: 'handphone', price: 1000),
    TestingFilterModel(id: '4', name: 'money', price: 60000),
    TestingFilterModel(id: '5', name: 'bycle', price: 88000),
  ]);
});
/// No longer a family computed
final filterTesting = Computed<List<TestingFilterModel>>((read) {
  final testing = read(testingFilterProvider.state);
/// Now using filter from a filterProvider
  final filter = read(filterProvider);
  return testing.where((element) => element.name.toLowerCase().contains(filter)).toList();
});
/// filter provider initialized with empty string
final filterProvider = StateProvider((ref) => '');

/// In your widget
// Somewhere you need to put something like this. (Probably in initState if you aren't using hook widgets). 
// filterprovider state set to the new filter based on your filter text controller
// DON'T put this in your build method, since you don't want to create a new listener every build. Memory leak
_controller.listen((value) => context.read(filterProvider).state = value);
// If using hook widgets you can probably use useEffect

Consumer((ctx, read) {

              final filter = read(filterTesting);
              return Expanded(
                child: ListView.separated(
                  shrinkWrap: true,
                  itemCount: filter.length,
                  separatorBuilder: (BuildContext context, int index) {
                    return Divider();
                  },
                  itemBuilder: (BuildContext context, int index) {
                    final result = filter[index];
                    return Card(
                      child: Center(
                        child: Text(result.name),
                      ),
                    );
                  },
                ),
              );
            }),

I haven't tested this so it might have some issues. But this is the general idea.

rrousselGit commented 4 years ago

Tim is correct, modulo the fact that you don't have to listen to the texteditingcontroller

You can use Textfield.onChange:

final filterProvider = StateProvider((_) => '');

... 
Textfield(
  onChange: (value) => filterProvider.read(context).state = value, 
)
rrousselGit commented 4 years ago

Both the Todos and the Marvel example implement a filter. The former uses an enum as filter, while the latter has a search field.

zgramming commented 4 years ago

Ahhh finally i get it. Thank's @rrousselGit and @TimWhiting for both of you to help my case.

TimWhiting commented 4 years ago

@rrousselGit I wonder for some common design patterns like filtering etc, there could be some helper functions (Maybe in another package). Or another provider for filtering that wraps a StateProvider for the filter and a Computed for the filtered results. I know you are planning on creating a retry variant for that common use case, so maybe a filter variant of a provider would be another good common use case to look at?

I'd be willing to help with creating some convenient providers/functions for common use cases and make a few PRs. But I'll probably wait for your reworked version to come out before looking into that.

rrousselGit commented 4 years ago

What would helpers for filter looks like? In my opinion, Computed is the helper (especially combined with StateProvider)

What do you think is missing?

TimWhiting commented 4 years ago

The problem isn't necessarily a feature issue, more of a discoverability issue I think. When you are thinking of a filter feature for your app you aren't necessarily thinking in terms of providers and computed. You have to switch mentalities to try to think how to implement it. So it wouldn't necessarily be a different provider, just some convenience functions or hooks that are in the terminology you would use when describing a filter. i.e.

final myFilter = createFilterProvider(initialQuery: () => '', filter: (ref, query) => //do filter stuff here
);

/// In hook widget
final filter = useFilter(myFilter);

// Changing query
TextField(onChange: (value) => filter.query = value),
// Using filter state
Column(children: filter.state.map(...).toList()),

I'm pretty busy this week, but I can try to prototype such a function / provider. Essentially what gets returned from the function is not a provider but something like this:

class FilterProviderHelper<Query extends StateNotifier, Filter extends Computed> {
   final Query queryProvider;
   final Filter filterProvider;
   FilterProviderHelper(this.queryProvider, this.filterProvider);
}

And the hook functions access the relevant portions of this class.

As a side note. Note that I say discoverability issue, not example/documentaion issue, I know you have examples for it, but I think people don't necessarily look at examples on GitHub. I used to use Bloc before starting to migrate my stuff over to StateNotifier + Freezed + Hooks + Riverpod. I liked the tutorial section of the Bloc documentation: for example: https://bloclibrary.dev/#/fluttertodostutorial. Tutorials are longer than simple examples, but they showcase some design patterns & how to think when working with the API. I'd be willing to turn your todo example into a tutorial for the riverpod website. But it might take a little bit of time.

In general I'd love to see more content, such as a Medium article or YouTube series showing how to use the combination of (StateNotifier + Freezed + Hooks + Riverpod). I really love how they all work together. Again, I'd be willing to do this, but need to find / set aside time for it & this week is going to be pretty busy for me already.

rrousselGit commented 4 years ago

Feel free to make a proposal about such helpers, although I'm not entirely convinced. It sounds like this is more about people not knowing how to use the tools available than a need for a new tool. And adding new APIs is difficult as they require a lot of effort to maintain.

Overall: Tutorials, articles, a search bar, and more examples are planned. They should come right after the big PR gets merged, so August I'd say