Reputation: 1
What I'm trying to understand is the best way to initialize a provider using the parameters passed to the widget. Specifically, I need to set the initial state of a FilterProvider based on the value received by the SearchPage when navigating from the HomePage.
I am developing a Flutter application using Riverpod, auto_route. I have a home screen (HomePage) and a search screen (SearchPage). The filter state is managed by filterProvider, and the search in SearchPage uses this filter via the searchControllerProvider.
The flow is as follows: in the HomePage, when pressing a button, I navigate to the SearchPage and pass an initial Filter to the search screen. What I want is for the SearchPage to initialize the filterProvider with the value from the initialFilter.
Here is the relevant code:
@MaterialAutoRouter(
replaceInRouteName: 'Page,Route',
routes: <AutoRoute>[
AutoRoute(page: HomePage, initial: true),
AutoRoute(page: SearchPage),
],
)
class $AppRouter {}
class Filter {
final bool isFiltered;
Filter({required this.isFiltered});
}
final filterProvider = StateProvider<Filter>((ref) {
return Filter(isFiltered: false);
});
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.pushRoute(SearchRoute(filter: Filter(isFiltered: true)));
},
child: Text('Go to Search'),
),
),
);
}
}
final searchControllerProvider = Provider<List<String>>((ref) {
final filter = ref.watch(filterProvider).state;
if (filter.isFiltered) {
return ['True'];
} else {
return ['False'];
}
});
class SearchPage extends ConsumerStatefulWidget {
final Filter initialFilter;
SearchPage({Key? key, required this.initialFilter}) : super(key: key);
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends ConsumerState<SearchPage> {
@override
Widget build(BuildContext context) {
final results = ref.watch(searchControllerProvider);
return Scaffold(
appBar: AppBar(
title: Text('Search Page'),
),
body: Expanded(
child: ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(results[index]),
);
},
),
),
);
}
}
My question is how to properly initialize the filterProvider with the value of initialFilter from the SearchPage.
Here are the approaches I've tried and the problems I've encountered:
First Approach: Modifying the filterProvider in initState or build
@override
void initState() {
super.initState();
ref.read(filterProvider.notifier).state = widget.initialFilter;
}
I tried modifying the filterProvider in initState or build, but I get the following error:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════
Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDependencies
Second Approach: Setting the filterProvider value in HomePage before navigating
onPressed: () {
ref.read(filterProvider.notifier).state = Filter(isFiltered: true);
context.pushRoute(SearchRoute(filter: Filter(isFiltered: true)));
}
However, when observing the logs, I see that the filterProvider is disposed before the SearchPage loads:
Provider filterProvider was initialized with Filter(isFiltered: false)
Provider filterProvider updated from Filter(isFiltered: true))
Provider filterProvider was disposed
Provider filterProvider was initialized with Filter(isFiltered: false)
I could use KeepAlive here, but in this case, Riverpod would preserve the filter state across screens, which is not the behavior I want. I want the filter state to be kept only within the search screen.
Third Approach: Using AutoRouteWrapper and ProviderScope
In this approach, I tried using the wrappedRoute functionality from auto_route to wrap the screen with a ProviderScope and override the filterProvider before the screen is displayed.
class SearchPage extends ConsumerStatefulWidget implements AutoRouteWrapper {
final Filter initialFilter;
SearchPage({Key? key, required this.initialFilter}) : super(key: key);
@override
Widget wrappedRoute(BuildContext context) {
return ProviderScope(
overrides: [
filterProvider.overrideWith(() => initialFilter),
],
child: this,
);
}
@override
_SearchPageState createState() => _SearchPageState();
}
The idea is that the ProviderScope will override the filterProvider with the initial initialFilter value when the SearchPage is loaded. However, this approach also caused an exception:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════
The following LateError was thrown building SearchScreen(dirty,
dependencies: [UncontrolledProviderScope], state:
SearchScreenState#5931a):
LateInitializationError: Field '_element@119511105' has not been
initialized.
Fourth Approach: Using Future.microtask or addPostFrameCallback
Future.microtask(() {
ref.read(filterProvider.notifier).state = widget.initialFilter;
});
or
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(filterProvider.notifier).state = widget.initialFilter;
});
This approach worked, but with a side effect: the searchControllerProvider makes two calls, one with the default filter and another with the correct filter value.
What would be the best way to initialize the filterProvider with the initialFilter value from SearchPage without encountering the issues mentioned above?
Upvotes: 0
Views: 175
Reputation: 1
if i can ask... you need to pass the value of filter from home page to search page ? by using riverpod ? if that is what you are need ok then
class Filter {
final bool isFiltered;
Filter({required this.isFiltered});
}
final filterProvider = StateProvider<Filter>((ref) => Filter(isFiltered:
false));
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
ref.read(filterProvider.notifier).state = Filter(isFiltered:
true);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const SearchPage(),
));
},
child: const Text('Go to Search'),
),
),
);
}
}
final searchControllerProvider = Provider<List<String>>((ref) {
final filter = ref.watch(filterProvider);
if (filter.isFiltered) {
return ['True'];
} else {
return ['False'];
}
});
class SearchPage extends ConsumerStatefulWidget {
const SearchPage({super.key});
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends ConsumerState<SearchPage> {
late List<String> results;
@override
void initState() {
results = ref.read(searchControllerProvider);
super.initState();
}
@override
Widget build(BuildContext context) {
final results = ref.watch(searchControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Search Page'),
),
body: ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(results[index]),
);
},
),
);
}
}
Upvotes: 0