Flutter - How to access child data on parent event or action - dart

I have a longer input form where the user can add a number of identical subforms.
For e.g. think about a form where parents can enter their data (parentform) and afterwards add their childs. Since i don't know how many childs a user has they can dynamically add more child forms using a button.
For each child the requested data is identical so e.g. name, birthday, gender, in reality its about 10 fields. So i created a separate widget (childform).
If the user now saves the outer form i need to gather all information from the childforms. I now i can do this by creating all TextControllers inside the parentform, save them in a list and insert them when creating the childforms, like so:
void addTextField() {
int index = _childNameControllers.length;
_childNameControllers.add( TextEditingController());
_childforms.add(
RemovableEntityField(
nameController: _childNameControllers[index]
)
);
}
And then on save something like
void onSave() {
List<Childs> _childs = [];
_childNameControllers.forEach((controller) {
if (controller.text.trim().isNotEmpty) {
_childs.add(Child(name: name));
}
});
// do something with the data
}
But as i said i have about 10 Fields per childform and i would have to create 10 controllers for each form and would need 10 parameters in the childform just to read this information.
Is there an easier way to do this ?
PS: i know i could make the child state public but i don't really want to do this either

I believe you are supposed to add variable updater in the child and push the value up to the parent.
https://github.com/flutter/flutter/tree/4b4cff94bf6631bf6326c9239c10b286b4fdb08c/dev/benchmarks/test_apps/stocks
The flutter stock application has an example.
In the child you need this.
class StockSettings extends StatefulWidget {
const StockSettings(this.configuration, this.updater);
final StockConfiguration configuration;
final ValueChanged<StockConfiguration> updater;
#override
StockSettingsState createState() => StockSettingsState();
}
class StockSettingsState extends State<StockSettings> {
void _handleBackupChanged(bool value) {
sendUpdates(widget.configuration.copyWith(backupMode: value ? BackupMode.enabled : BackupMode.disabled));
}
void sendUpdates(StockConfiguration value) {
if (widget.updater != null)
widget.updater(value);
}
In the parent, you pass down configuartion updator which is just a wrapper around set state
class StocksAppState extends State<StocksApp> {
StockData stocks;
StockConfiguration _configuration = StockConfiguration(
stockMode: StockMode.optimistic,
backupMode: BackupMode.enabled,
debugShowGrid: false,
debugShowSizes: false,
debugShowBaselines: false,
debugShowLayers: false,
debugShowPointers: false,
debugShowRainbow: false,
showPerformanceOverlay: false,
showSemanticsDebugger: false
);
#override
void initState() {
super.initState();
stocks = StockData();
}
void configurationUpdater(StockConfiguration value) {
setState(() {
_configuration = value;
});
}
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => StockHome(stocks, _configuration, configurationUpdater),
'/settings': (BuildContext context) => StockSettings(_configuration, configurationUpdater)
},

Related

Flutter unable to set state of list<object>

Here is how the main TodoApp widget looks like:
class TodoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(title: 'Todo List', home: new TodoList());
}
}
I have created a class for a list item which is called Task:
class Task {
String name;
int number;
Task(this.name, this.number);
}
The list widget looks like this:
class TodoList extends StatefulWidget {
#override
createState() => new TodoListState();
}
The TodoListState class has a method which adds new items to the list:
class TodoListState extends State<TodoList> {
List<Task> _todoItems = [];
List<bool> checkedItems = []; // this was created for testing purposes
void _addTodoItem(Task task) {
if (task.name.length > 0) {
setState(() => checkedItems.add(false)); // works fine
setState(() => _todoItems.add(new Task('abc', 12))); // does not work
}
}
...
}
Why when _addTodoItem method is called, one item is added to the checkedItems list but _todoItems list is left unchanged? What am I missing?
It's working as it should, setState is only called once, hence the first method is executed. You should add both methods in the setState method, since it's called once per run or state, you can write it like this:
....
setState(() => {
checkedItems.add(false);
_todoItems.add(new Task('abc', 12);
});
....

How to call setState outside class/stateful widget?

I have the following variables
String _warningMessage;
bool _warningVisibility;
Which I want to update via a Class which implements an interface
class _UserSignupInterface extends _SignupSelectUsernamePageState
implements UserSignupInterface {
#override
void onSuccess() {
_hideWarning();
_navigateToUserPage();
}
#override
void onError(String message) {
_isSignupClickable = true;
if(message != null) {
_displayWarning(message);
}
}
}
with the _displayWarning code (which is inside the _SignupSelectUsernamePageState)
void _displayWarning(String message) {
if (message != null) {
setState(() {
widget._warningMessage = message;
widget._warningVisibility = true;
});
}
}
However, whenever I call the _displayWarning(message) from outside the _SignupSelectUsernamePageState. I get an error saying
Unhandled Exception: setState() called in constructor
Is there a proper way of updating these variable states outside their class? Which in my case, I'm calling the _displayWarning(message) from another class that implements an interface
You have to decide whether this is a value that is changed internally within the widget, or if that's a value that changes externally to it.
If it's internal, the common thing is to place them in the State class with the _ on them, they could start with a value for instance set on initState and every time they change you call setState to indicate that.
However, if they change outside the widget, then you place them on the StatefulWidget class (as you seem to have done), you leave them without the _ as they are actually public and you even make them final and place them in the constructor to allow them to be set.
In this last case, if in the State class you must be aware of a change in the widget, you can implement didUpdateWidget, but that's not mandatory.
Of course you can mix both things, having a _warningMessage in the State, so you can update it with setState, but with an initial value defined in initState that comes from the widget.
Again, if the widget changes externally, you can again update the value of the _warningMessage with the new widgets value.
Something like that: (I didn't test this code)
class YourWidget extends StatefulWidget {
YourWidget({this.warningMessage});
final String warningMessage;
#override
State<YourWidget> createState() => new _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String _warningMessage;
#override
void initState() {
super.initState();
_warningMessage = widget.warningMessage;
}
#override
didUpdateWidget(ReorderableListSimple oldWidget) {
super.didUpdateWidget(oldWidget);
_warningMessage = widget.warningMessage;
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(_warningMessage),
RaisedButton(
child: Text("Change Message"),
onPressed: () {
setState(() {
_warningMessage = "new message from within the State class";
});
}
)
],
);
}
}
So in this example you can change the warningMessage externally, like in the parent Widget you are able to pass a different message. However, if you need, you can also set it internally using setState, as it's happening in the button's onPressed.
What you might check is wether you actually need that property exposed in the Widget, maybe you don't! Then, the example would look like that:
class YourWidget extends StatefulWidget {
#override
State<YourWidget> createState() => new _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String _warningMessage;
#override
void initState() {
super.initState();
_warningMessage = "default message, no need for widget";
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(_warningMessage),
RaisedButton(
child: Text("Change Message"),
onPressed: () {
setState(() {
_warningMessage = "new message from within the State class";
});
}
)
],
);
}
}
Just create a static value in the state of your widget class, then when you build the widget, set it's value to the widget. So whenever you want to call it to setState(), just call the static value.

createState method not always called when creating new StatefulWidget

I have a collection of simple objets that sometimes changes. I am using a ListView to render those objects, basically text. When my collection changes the list is rebuild with the new objects, so if the list changes from 1 to 3 items, I see 3 items, but the first one keeps its previous value.
I've noticed that the method "createState" is not called in all cases when I create a new CustomTextField (in the example above, it is called only when new elements are added to the list).
How do I make sure my list is updated properly when my collection changes?
My parent widget builds a list of text fields:
...
#override
Widget build(BuildContext context) {
...
var list = <Widget>[];
collection.forEach((item) {
var widget = CustomTextField(
content: item,
);
list.add(widget);
...
return new ListView(
children: list,
);
});
...
My CustomTextField definition:
class CustomTextField extends StatefulWidget {
final MediaContent content;
CustomTextField({
Key key,
this.content,
}) : super(key: key);
#override
CustomTextFieldState createState() {
return CustomTextFieldState();
}
}
...
MediaContent is a very simple object containing some text:
class MediaContent {
String textData;
ContentType type;
MediaContent(
this.type,
);
}
You have to define unique Key for you collections items, take a look at here:
https://www.youtube.com/watch?v=kn0EOS-ZiIc

Refresh a listview outside of a widget in Flutter?

I have a Flutter application which comprises of a scaffold with a slider and a tabview.
The tabview comprises of a List which is show on each tab as seen in the picture below.
List<Widget> widgetList = <Widget>[
Post(),
Feed(),
Location(),
HomePage(),
Feed(),
];
Now I would like to refresh the current tab on screen when the slider is moved. However, since the classes are private i.e. _HomePageState, I do not know how to access the refreshList() method as shown in the snippet below.
homepage.dart:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
var list;
var random;
var refreshKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
random = Random();
refreshList();
}
Future<Null> refreshList() async {
refreshKey.currentState?.show(atTop: false);
await Future.delayed(Duration(seconds: 2));
setState(() {
list = List.generate(random.nextInt(10), (i) => "Item $i");
});
return null;
}
}
The slider is not baked into each listview Widget i.e. homepage.dart as the slider values is applicable to each individual tab. How can I refresh the inner listview widget when the outer widget with the slider is moved?
There are different ways to deal with this:
You could pass down the PageController and the page index to your page widgets. In your page widgets can then listen to the changes (pageController.addListener(...)) and compare the currently centered page with their page index.
Create one ChangeNotifier for every page that you want to refresh:
final postRefresh = ChangeNotifier();
final feedRefresh = ChangeNotifier();
...
// build method of parent:
List<Widget> widgetList = <Widget>[
Post(refresh: postRefresh),
Feed(refresh: feedRefresh),
...
];
// initState method of parent:
pageController.addListener(() {
final roundedPage = pageController.page.round();
if(roundedPage == 0) {
postRefresh.notifyListeners();
}
else if(roundedPage == 1) {
feedRefresh.notifyListeners();
}
// ...
})
You could also give your page widgets a global key (for that, their State classes must be public):
// class body of parent:
final postPageKey = GlobalKey<PostPageState>();
final feedPageKey = GlobalKey<FeedPageState>();
// build method of parent:
List<Widget> widgetList = <Widget>[
Post(key: postPageKey),
Feed(key: feedPageKey),
...
];
// initState method of parent:
pageController.addListener(() {
final roundedPage = pageController.page.round();
if(roundedPage == 0) {
postPageKey.currentState?.refresh();
}
else if(roundedPage == 1) {
feedPageKey.currentState?.refresh();
}
// ...
})

Route Guards in Flutter

In Angular, one can use the canActivate for route guarding.
In Flutter, how would one go about it? Where is the guard placed? How do you guard routes?
I'm thinking along the lines of this:
User logged in. Their token is stored in Shared Preference (the right way to store tokens? )
User closed the app.
User opens app again. As the application starts, it determines if user is logged in (perhaps a service that checks the storage for token), and then
If logged in, load the homepage route
If not logged in, load the login page
I was stumbling over this problem too and ended up using a FutureBuilder for this. Have a look at my routes:
final routes = {
'/': (BuildContext context) => FutureBuilder<AuthState>(
// This is my async call to sharedPrefs
future: AuthProvider.of(context).authState$.skipWhile((_) => _ == null).first,
builder: (BuildContext context, AsyncSnapshot<AuthState> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.done:
// When the future is done I show either the LoginScreen
// or the requested Screen depending on AuthState
return snapshot.data == AuthState.SIGNED_IN ? JobsScreen() : LoginScreen()
default:
// I return an empty Container as long as the Future is not resolved
return Container();
}
},
),
};
If you want to reuse the code across multiple routes you could extend the FutureBuilder.
I don't think there is a route guarding mechanism per se, but you can do logic in the main function before loading the app, or use the onGenerateRoute property of a MaterialApp. One way to do that in your case is to await an asynchronous function that checks if the user is logged in before loading the initial route. Something like
main() {
fetchUser().then((user) {
if (user != null) runApp(MyApp(page: 'home'));
else runApp(MyApp(page: 'login'));
});
}
But you may also be interested in the way the Shrine app does it. They have the login page as the initial route in any case and remove it if the user is logged in. That way the user sees the login page until it has been determined whether or not they log in. I've included the relevant snippet below.
class ShrineApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shrine',
home: HomePage(),
initialRoute: '/login',
onGenerateRoute: _getRoute,
);
}
Route<dynamic> _getRoute(RouteSettings settings) {
if (settings.name != '/login') {
return null;
}
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => LoginPage(),
fullscreenDialog: true,
);
}
}
If you don't want them to see the login page at all if they are logged in, use the first approach and you can control the splash screen that shows before runApp has a UI by exploring this answer.
You can use auto_route extension. This extension is awesome for dealing with routes and provides good way of using guards. Please refer to the documentation:
https://pub.dev/packages/auto_route#route-guards
I came up with the following solution for a web project, which allows me to easily introduce guarded routes without having to worry that an unauthorized user is able to access sensitive information.
The GuardedRoute class looks like this:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;
Widget _defaultTransitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return child;
}
class GuardedRoute extends PageRouteBuilder {
GuardedRoute({
#required final String guardedRoute,
#required final String fallbackRoute,
#required final Stream<bool> guard,
#required final core.Router router,
final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
final bool maintainState = true,
final Widget placeholderPage,
})
: super(
transitionsBuilder: transitionsBuilder,
maintainState: maintainState,
pageBuilder: (context, animation, secondaryAnimation) =>
StreamBuilder(
stream: guard,
builder: (context, snapshot) {
if (snapshot.hasData) {
// navigate to guarded route
if (snapshot.data == true) {
return router.routes[guardedRoute](context);
}
// navigate to fallback route
return router.routes[fallbackRoute](context);
}
// show a placeholder widget while the guard stream has no data yet
return placeholderPage ?? Container();
}
),
);
}
Using the guarded route is easy. You can define a guarded route and a fallback route (like a login page). Guard is a Stream which decides if the user can navigate to the guarded route. this is my Router class which shows how to use the GuardedRoute class:
class BackendRouter extends core.BackendRouter {
BackendRouter(
this._authenticationProvider,
this._logger
);
static const _tag = "BackendRouter";
core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =
core.Lazy(() => GlobalKey<NavigatorState>());
final core.AuthenticationProvider _authenticationProvider;
final core.Logger _logger;
#override
Map<String, WidgetBuilder> get routes => {
core.BackendRoutes.main: (context) => MainPage(),
core.BackendRoutes.login: (context) => LoginPage(),
core.BackendRoutes.import: (context) => ImportPage(),
};
#override
Route onGenerateRoute(RouteSettings settings) {
if (settings.name == core.BackendRoutes.login) {
return MaterialPageRoute(
settings: settings,
builder: routes[settings.name]
);
}
return _guardedRoute(settings.name);
}
#override
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();
#override
void navigateToLogin() {
_logger.i(_tag, "navigateToLogin()");
navigatorKey
.currentState
?.pushNamed(core.BackendRoutes.login);
}
#override
void navigateToImporter() {
_logger.i(_tag, "navigateToImporter()");
navigatorKey
.currentState
?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
}
GuardedRoute _guardedRoute(
String route,
{
maintainState = true,
fallbackRoute = core.BackendRoutes.login,
}) =>
GuardedRoute(
guardedRoute: route,
fallbackRoute: fallbackRoute,
guard: _authenticationProvider.isLoggedIn(),
router: this,
maintainState: maintainState,
placeholderPage: SplashPage(),
);
}
And your application class looks like this:
class BackendApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
// get router via dependency injection
final core.BackendRouter router = di.get<core.BackendRouter>();
// create app
return MaterialApp(
onGenerateRoute: (settings) => router.onGenerateRoute(settings),
navigatorKey: router.navigatorKey,
);
}
}
You can use flutter_modular package to do that. It´s a library that try to keep the same features than angular. Take a look of it documentation.
Modular Docs
My solution is to build in a route guard system, much like the other libraries out there but where we can still use the original Navigator where needed, open a modal as a named route, chain guards, and add redirects. Its really basic but can be built on quite easily.
It seems like a lot but you'll just need 3 new files to maintain along with your new guards:
- router/guarded_material_page_route.dart
- router/route_guard.dart
- router/safe_navigator.dart
// Your guards go in here
- guards/auth_guard.dart
...
First create a new class that extends MaterialPageRoute, or MaterialWithModalsPageRoute if you're like me and want to open the Modal Bottom Sheet package. I've called mine GuardedMaterialPageRoute
class GuardedMaterialPageRoute extends MaterialWithModalsPageRoute {
final List<RouteGuard> routeGuards;
GuardedMaterialPageRoute({
// ScrollController is only needed if you're using the modals, as i am in this example.
#required Widget Function(BuildContext, [ScrollController]) builder,
RouteSettings settings,
this.routeGuards = const [],
}) : super(
builder: builder,
settings: settings,
);
}
Your route guards will look like this:
class RouteGuard {
final Future<bool> Function(BuildContext, Object) guard;
RouteGuard(this.guard);
Future<bool> canActivate(BuildContext context, Object arguments) async {
return guard(context, arguments);
}
}
You can now add GuardedMaterialPageRoutes to your router file like so:
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homeRoute:
// These will still work with our new Navigator!
return MaterialPageRoute(
builder: (context) => HomeScreen(),
settings: RouteSettings(name: homeRoute),
);
case locationRoute:
// Following the same syntax, just with a routeGuards array now.
return GuardedMaterialPageRoute(
// Again, scrollController is only if you're opening a modal as a named route.
builder: (context, [scrollController]) {
final propertiesBloc = BlocProvider.of<PropertiesBloc>(context);
final String locationId = settings.arguments;
return BlocProvider(
create: (_) => LocationBloc(
locationId: locationId,
propertiesBloc: propertiesBloc,
),
child: LocationScreen(),
);
},
settings: RouteSettings(name: locationRoute),
routeGuards: [
// Now inject your guards, see below for what they look like.
AuthGuard(),
]
);
...
Create your async guard classes like so, as used above in our router.
class AuthGuard extends RouteGuard {
AuthGuard() : super((context, arguments) async {
final auth = Provider.of<AuthService>(context, listen: false);
const isAnonymous = await auth.isAnonymous();
return !isAnonymous;
});
}
Now you'll need a new class that handles your navigation. Here you check if you have access and simply run through each guard:
class SafeNavigator extends InheritedWidget {
static final navigatorKey = GlobalKey<NavigatorState>();
#override
bool updateShouldNotify(SafeNavigator oldWidget) {
return false;
}
static Future<bool> popAndPushNamed(
String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
Navigator.of(navigatorKey.currentContext).pop();
return pushNamed(routeName, arguments: arguments, asModalBottomSheet: asModalBottomSheet);
}
static Future<bool> pushNamed(String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
// Fetch the Route Page object
final settings = RouteSettings(name: routeName, arguments: arguments);
final route = Routes.generateRoute(settings);
// Check if we can activate it
final canActivate = await _canActivateRoute(route);
if (canActivate) {
// Only needed if you're using named routes as modals, under the hood the plugin still uses the Navigator and can be popped etc.
if (asModalBottomSheet) {
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext,
builder: (context, scrollController) =>
(route as GuardedMaterialPageRoute)
.builder(context, scrollController));
} else {
Navigator.of(navigatorKey.currentContext).push(route);
}
}
return canActivate;
}
static Future<bool> _canActivateRoute(MaterialPageRoute route) async {
// Check if it is a Guarded route
if (route is GuardedMaterialPageRoute) {
// Check all guards on the route
for (int i = 0; i < route.routeGuards.length; i++) {
// Run the guard
final canActivate = await route.routeGuards[i]
.canActivate(navigatorKey.currentContext, route.settings.arguments);
if (!canActivate) {
return false;
}
}
}
return true;
}
}
To make it all work you will need to add the SafeNavigator key to your Material app:
MaterialApp(
navigatorKey: SafeNavigator.navigatorKey,
...
)
And now you can navigate to your routes and check if you have access to them like this:
// Opens a named route, either Guarded or not.
SafeNavigator.pushNamed(shortlistRoute);
// Opens a named route as a modal
SafeNavigator.pushNamed(shortlistRoute, asModalBottomSheet: true);
// Pops the current route and opens a named route as a modal
SafeNavigator.popAndPushNamed(shortlistRoute, asModalBottomSheet: true);

Resources