admin管理员组文章数量:1394593
Please note that the example we'll be looking at is purelly for illustration purposes. If you would like to answer using a different example that's ok, as we're trying to get to an idiomatic way of solving this type of problem rather that this problem.
Setup:
- The environment we're using has the
Provider
library availabe. We also useflutter_hooks
but that's not required for the answer. - We have a top-level widget class that contains a
PageView
and aContinue
button. - The
PageView
is not "swippable", the idea is that you navigate through some sort of onboarding using the "Continue" button. ThePageView
is coupled to aPageController
. - Some of the individual pages have interactions. For example, each individual page could either / or:
- Disable the "Continue" button until some action is performed.
- Change the title of the "Continue" button, for example to "Skip".
- Run a task when the continue button is clicked, which must be successful for the next page to be moved to.
Currently, it's the top-level widget that controls this continue button... And it's very cumbersome, as every time the page index changes, the conditions are in the top-level widget to change the title of the button, to disable it, run the async actions between pages, etc.
We are looking for a cleaner way of doing this - for example, what would be very nice would be to design an architecture that would instead allow:
- Each page to define the button's title and disabled state.
- Each page to "tell" the top-level widget that a particular piece of logic should run when "Continue" is pressed - but that logic would be contained somewhere else.
Something like this:
- When the current page changes, the top-level widget would query the page and ask "What is the title for the button? What is the disabled state? Are there actions you need to run when the button is pressed?" and the page itself would be able to tell.
- Upon pressing the button, it would tell the page "I've been clicked" upon which the page would decide to run async actions or not, disable the button, set it to a loading state, etc.
The communication from the pages to the top-level widget is easy - a context.read<OnboardingProvider>().doSomething()
does the trick. It's the other way around which seems difficult.
The idea would be to have each page have some degree of control (both visual and business logic) over the top-level widget button.
Any ideas? Thanks!
Please note that the example we'll be looking at is purelly for illustration purposes. If you would like to answer using a different example that's ok, as we're trying to get to an idiomatic way of solving this type of problem rather that this problem.
Setup:
- The environment we're using has the
Provider
library availabe. We also useflutter_hooks
but that's not required for the answer. - We have a top-level widget class that contains a
PageView
and aContinue
button. - The
PageView
is not "swippable", the idea is that you navigate through some sort of onboarding using the "Continue" button. ThePageView
is coupled to aPageController
. - Some of the individual pages have interactions. For example, each individual page could either / or:
- Disable the "Continue" button until some action is performed.
- Change the title of the "Continue" button, for example to "Skip".
- Run a task when the continue button is clicked, which must be successful for the next page to be moved to.
Currently, it's the top-level widget that controls this continue button... And it's very cumbersome, as every time the page index changes, the conditions are in the top-level widget to change the title of the button, to disable it, run the async actions between pages, etc.
We are looking for a cleaner way of doing this - for example, what would be very nice would be to design an architecture that would instead allow:
- Each page to define the button's title and disabled state.
- Each page to "tell" the top-level widget that a particular piece of logic should run when "Continue" is pressed - but that logic would be contained somewhere else.
Something like this:
- When the current page changes, the top-level widget would query the page and ask "What is the title for the button? What is the disabled state? Are there actions you need to run when the button is pressed?" and the page itself would be able to tell.
- Upon pressing the button, it would tell the page "I've been clicked" upon which the page would decide to run async actions or not, disable the button, set it to a loading state, etc.
The communication from the pages to the top-level widget is easy - a context.read<OnboardingProvider>().doSomething()
does the trick. It's the other way around which seems difficult.
The idea would be to have each page have some degree of control (both visual and business logic) over the top-level widget button.
Any ideas? Thanks!
Share Improve this question edited Mar 27 at 10:56 Doodloo asked Mar 27 at 10:50 DoodlooDoodloo 9195 silver badges19 bronze badges 2 |2 Answers
Reset to default 0What you're suggesting is to send data upward in the widget tree. Flutter isn't designed to do that, so you're trying to work against the framework. Considering that the Continue/Skip-button is supposed to be so different for each individual PageView, I would move it as a child of the PageView instead of the top-level widget. You could do something like this.
class TopLevelWidget extends StatefulWidget {
const TopLevelWidget({super.key});
@override
State<TopLevelWidget> createState() => _TopLevelWidgetState();
}
class _TopLevelWidgetState extends State<TopLevelWidget> {
int currentPageIndex = 0;
@override
Widget build(BuildContext context) {
var pageViewList = [
PageView(goNextPage: goNextPage),
PageView(goNextPage: goNextPage),
PageView(goNextPage: goNextPage),
];
return Scaffold(
body: pageViewList[currentPageIndex],
);
}
goNextPage() {
setState(() {
currentPageIndex = currentPageIndex + 1;
});
}
}
class PageView extends StatelessWidget {
const PageView({super.key, required this.goNextPage});
final Function() goNextPage;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween
children: [
//put whatever here
ElevatedButton(
onPressed: isDisabled ? null : () {
// do whatever operations you would like
goNextPage();
},
child: const Text("Continue")),
]
);
}
}
Not sure if this code compiles, it is just a quick example and should just be used as inspiration. Personally I wouldn't implement the top-level widget entirely like this, but rather use a stream and stream controller to change the currentPageIndex. But this example at least gives you an idea of what it could look like.
In the end this is what we came up with:
We created an interface for all pages to comply with. Something like:
typedef ContinueCallback = Future<void> Function(BuildContext);
@immutable
abstract class IOnboardingView extends StatelessWidget {
const IOnboardingView({super.key});
ContinueCallback get onContinue;
String buttonLabel(BuildContext context);
}
The idea is to make sure that all children pages implement it, and can provide both a callback to provide validation rules when the button is pushed (Or null to disable the button), and the title of the button itself.
On the top-level page, the build
function can now look like this:
@override
Widget build(BuildContext context) {
final state = context.watch<ob_state.OnboardingState>();
final pageIndex = state.index;
final pagerMessage = state.message;
final page = state.pages[state.index];
void nextPage() => state.nextPage(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Pager(total: _OnboardingPage.values.length, index: pageIndex),
if (pagerMessage != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: AppTheme.spaceS),
child: Text(pagerMessage),
),
Container(
width: double.infinity,
height: 50,
margin: const EdgeInsets.all(20),
child: FilledButton(
onPressed: state.enabled ? nextPage : null,
child: Text(page.buttonLabel(context)),
),
),
],
);
}
Note that the state is provided via a context
provider (See context.watch<ob_state.OnboardingState>()
).
The provider looks like something like this (Note how all children pages are List<IOnboardingView> pages
and therefore "provide" what the top-level page needs to know):
ChangeNotifierProvider<OnboardingState> provider() => ChangeNotifierProvider(create: (context) => OnboardingState());
class OnboardingState with ChangeNotifier {
bool _disposed = false;
bool _enabled = true;
String? _message;
final List<IOnboardingView> pages = [
const SafetyView(),
const PreferencesView(),
const PermissionsPage(),
const VerifyAccountView()
];
final PageController pageCtrl = PageController();
int _index = 0;
OnboardingState();
@override
void dispose() {
_disposed = true;
pageCtrl.dispose();
super.dispose();
}
bool get enabled => _enabled;
set enabled(bool value) => value ? enable() : disable();
void enable() {
if (_enabled && _message == null) return;
_enabled = true;
_message = null;
_maybeNotifyListeners();
}
void disable([String? message]) {
if (!_enabled && _message == message) return;
_enabled = false;
_message = message;
_maybeNotifyListeners();
}
void nextPage(BuildContext context) {
disable();
page
.onContinue(context)
.then((value) => pageCtrl.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
))
.whenComplete(enable);
}
String? get message => _message;
IOnboardingView get page => pages[_index];
int get index => _index;
set index(int i) {
if (_index == i) return;
_index = i;
_maybeNotifyListeners();
}
void _maybeNotifyListeners() {
if (_disposed) return;
notifyListeners();
}
}
本文标签: flutterIdiomatic way of checking the widget tree downwardStack Overflow
版权声明:本文标题:flutter - Idiomatic way of checking the widget tree downward - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744095569a2590220.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
void Function(String buttonName, bool enabled, ...)
– Jannik Commented Mar 27 at 14:52