Simple ways to pass to and share data with widgets/pages


This article is going to be more practical than theoretical, I just wanted to show in a simple way, various methods to share data. This won’t cover more complex ways like scoped BLoC pattern (talked about in previous articles) or other (like ScopedModel or Redux, that I’ve never used).

The example app you can find in my repository.

Data class

As first thing we define a class with some properties that are going to be used in the next examples:

class Data {
String text;
int counter;
String dateTime;
  Data({this.text, this.counter, this.dateTime});
}

Passing data through the widget constructor

The simplest way to send data from a widget to another is by its constructor. Let’s say, for example, we need to pass an instance of the above Data class from a screen (PageOne) to another (PageTwo).

If you change the data on this page, the updated data is sent to the second page.

In the first screen, we declare an instance of the Data class and initialize it with some data, then in the onPressed callback of a RaisedButton, we call the Navigator.push to navigate to the second screen, passing the data object directly to the constructor of the second page.

  • Data
final data = Data(
counter: 1,
dateTime: DateFormat(“dd/MM/yyyy — HH:mm:ss:S”).format(DateTime.now()),
text: “Lorem ipsum”);
  • RaisedButton:
RaisedButton(
child: Text(‘Send data to the second page’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(
data: data,
)),
);
},
),

How can we consume this data? Let’s examine the SecondPage class:

class SecondPage extends StatelessWidget {
final Data data;
  SecondPage({this.data});
  @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(‘Constructor — second page’),
),
body: Container(
padding: EdgeInsets.all(12.0),
alignment: Alignment.center,
child: Column(
children: <Widget>[
Container(
height: 54.0,
padding: EdgeInsets.all(12.0),
child: Text(‘Data passed to this page:’,
style: TextStyle(fontWeight: FontWeight.w700))),
Text(‘Text: ${data.text}’),
Text(‘Counter: ${data.counter}’),
Text(‘Date: ${data.dateTime}’),
],
),
),
);
}
}

As you can see, the only thing to do is to declare as final a property of type Data and add it to the constructor as a named parameter. Now the data it’s accessible in the build function at the same way we consumed it in the first screen.


Passing and receive data

What if we need to pass the data, modify it and turn it back to the first page? In this case, we can add some data to the Navigator.pop and send it back to the first screen (you can find a detailed explanation on this article on the Flutter website).

Basically, you accomplish this by:

1) Wrap the Navigator.push inside an async function and await for its result.

2) On the second screen, passing the data to the Navigator.pop back to the first screen.

Example:

While in the previous section we passed a data instance to the second screen, in this one, we are going to push the second screen and wait for the second screen to pop, receiving from it the data.

  • Pushing the second screen and wait for the result
_secondPage(BuildContext context, Widget page) async {  
final dataFromSecondPage = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => page),
) as Data;
  // Here we have the data from the second screen
  data.counter = dataFromSecondPage.counter;
data.dateTime = dataFromSecondPage.dateTime;
data.text = dataFromSecondPage.text;
}
  • Passing the data from the second screen
RaisedButton(
onPressed: () {
Navigator.pop(context, data); // data back to the first screen},
child: Text(‘Back’),
),

InheritedWidget

Passing data through the widget constructor can be useful when we need to pass the data only to one level down. In fact, if the data passed to the second widget needs to be passed to a third widget and so on, things begin to become hard to handle. An easy way to make the data available to all the widgets down the tree is to use an InheritedWidget.

“No. rebuilds” shows how many times the page was rebuilt.
class InheritedDataProvider extends InheritedWidget {
final Data data;
InheritedDataProvider({
Widget child,
this.data,
}) : super(child: child);
@override
bool updateShouldNotify(InheritedDataProvider oldWidget) => data != oldWidget.data;
static InheritedDataProvider of(BuildContext context) =>         context.inheritFromWidgetOfExactType(InheritedDataProvider);
}

Extending the class InheritedWidget, and adding to its constructor the data to be available to the tree, it is then possible to get this data by using the static function of().

InheritedDataProvider(
child: InheritedDataWidget(),
data: data,
),

Then, inside the InheritedDataWidget:

class InheritedDataWidget extends StatelessWidget {
 @override
Widget build(BuildContext context) {
final data = InheritedDataProvider.of(context).data;
   return Container(
child: Column(
children: <Widget>[
Text(‘Parent’),
Text(‘${data.text}’),
InheritedDataWidgetChild()
],
),
);
}
}

Inside the widget InheritedDataWidgetChild, we can do the same:

class InheritedDataWidgetChild extends StatelessWidget {
  @override
Widget build(BuildContext context) {
final data = InheritedData.of(context).data;
    return Container(
child: Column(
children: <Widget>[
Divider(),
Text(‘Child’),
Text(‘${data.text}’),
InheritedDataWidgetGrandchild()
],
),
);
}
}

...and so on. Every widget along the tree can access to the data provided by our InheritedWidget provider.

The downside of this simple method is that if a child widget updates the data, this won’t be reflected upside. You can read this great article, “Managing Flutter Application State With InheritedWidgets” by Hans Muller, on how you can solve this.


InheritedWidget and streams

Another way to make it possible for a child down the widget tree to update the data for all the widgets tree, it is to use streams.

“No. rebuilds” here doesn’t change because using the streams the whole page won’t be rebuilt, but only the changed widgets.

In this example, the Data instance of the previous example it is going to be replaced by a stream, to be more specific, by a StreamedValue (it is built around a RxDart’s BehaviorSubject) of this package the I created to handle streams and other stuff. But the same can be applied to a StreamController or a RxDart’s subject.

class InheritedStreamedDataProvider extends InheritedWidget {
final StreamedValue<Data> data;

InheritedStreamedDataProvider({
Widget child,
this.data,
}) : super(child: child);
   @override
bool updateShouldNotify(InheritedStreamedDataProvider oldWidget) => data.value != oldWidget.data.value;
   static InheritedStreamedDataProvider of(BuildContext context) =>  context.inheritFromWidgetOfExactType(InheritedStreamedDataProvider);
}

As before, we wrap in the InheritedStreamedDataProvider, the widgets tree. Using a StreamBuilder(here I am using a StreamedWidget that is just a customized version of the StreamBuilder), every time the stream emits a new event, the InheritedStreamedDataProviderWidget() is rebuilt:

InheritedStreamedDataProvider(
data: inheritedStream,
child: StreamedWidget(
stream: inheritedStream.outStream,
builder: (context, snapshot) => InheritedStreamedDataProviderWidget(),
),
),

Here is the InheritedStreamedDataProviderWidget:

class InheritedStreamedDataProviderWidget extends StatelessWidget {
  @override
Widget build(BuildContext context) {
    final data = InheritedStreamedDataProvider.of(context).data;

return Container(
child: Column(
children: <Widget>[
Text(‘Parent’),
Text(‘${data.value.text}’),
InheritedStreamedDataProviderWidgetChild()
],
),
);
}
}

and its child InheritedStreamedDataProviderWidgetGrandChild :

class InheritedStreamedDataProviderWidgetGrandchild extends StatelessWidget {
final textController = TextEditingController();
  @override
Widget build(BuildContext context) {
final data = InheritedStreamedDataProvider.of(context).data;
textController.text = data.value.text;

return Container(
margin: EdgeInsets.only(top: 12.0),
padding: EdgeInsets.all(12.0),
color: Colors.blueGrey[50],
child: Column(
children: <Widget>[
Text('Grandchild'),
Text('${data.value.text}'),
Container(height: 12.0,),
TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text',
hintText: 'Insert some text',
border: OutlineInputBorder()),
onSubmitted: (text) {
data.value.text = text;
data.refresh();
},
),
],
),
);
}
}

Every time the data.value.text changes, all the widgets rebuilt showing the data updated.


Generic InheritedWidget provider

In the previous examples, we created a specific InheritedWidget provider for every kind of data we wanted to share. To avoid to write a lot of boilerplates, we can use generics:

class InheritedProvider<T> extends InheritedWidget {
  final T inheritedData;
  InheritedProvider({
Widget child,
this.inheritedData,
}) : super(child: child);
  @override
bool updateShouldNotify(InheritedProvider oldWidget) => inheritedData != oldWidget.inheritedData;
  static T of<T>(BuildContext context) =>   (context.inheritFromWidgetOfExactType(InheritedProvider<T>().runtimeType) as InheritedProvider<T>).inheritedData;
}

Now, refactoring the code of the first InheritedWidget example, it becomes:

MainPage

InheritedProvider<Data>(
child: InheritedDataWidget(),
inheritedData: data,
),

InheritedDataWidget

class InheritedDataWidget extends StatelessWidget {
  @override
Widget build(BuildContext context) {
final data = InheritedProvider.of<Data>(context);
    return Container(
child: Column(
children: <Widget>[
Text(‘Parent’),
Text(‘${data.text}’),
InheritedDataWidgetChild()
],
),
);
}
}

AppConfig InheritedWidget

If you noticed, in the first example, to access the inherited data from the second page pushed by the navigator, the page was first put inside an InheritedDataProvider. If you leave this provider and try to access the second page. you get an error. To solve this issue, the InheritedWidgetProvider has to be positioned above the MaterialApp widget. In this example, we are going to use this strategy to make an instance of the class AppConfig, accessible to all the widgets and pages.

models/appconfig.dart

class AppConfig {
Color blocBackground = Colors.grey[200];
Color appDataBackground = Colors.grey[300];
Color backgroundAppConfigPageOne = Colors.indigo[200];
Color textColorAppConfigPageOne = Colors.purple[600];
Color backgroundAppConfigPageTwo = Colors.orange[200];
Color textColorAppConfigPageTwo = Colors.brown[600];
}

main.dart

class MyApp extends StatelessWidget {
  final appConfig = AppConfig();
  @override
Widget build(BuildContext context) {
    return InheritedProvider<AppConfig>(
inheritedData: appConfig,
child: MaterialApp(
title: ‘Data examples’,
theme: ThemeData(primarySwatch: Colors.blue),
home: Home(),
),
);
}
}

Now we create two pages, AppConfigPage and SecondPage. In the first one:

@override
Widget build(BuildContext context) {
final appConfig = InheritedProvider.of<AppConfig>(context);

In the RaisedButton to push the second page, as you can see, there is no provider around SecondPage:

RaisedButton(
child: Text(‘Send to the new page’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(
text: text,
)),
);
},
),

Now, in the SecondPage we can access the AppConfig instance in the same way as before (just adding to the of() function the type we are going to get):

class SecondPage extends StatelessWidget {
  @override
Widget build(BuildContext context) {
final appConfig = InheritedProvider.of<AppConfig>(context);

Singletons

Another simple way to share data across widgets (and pages), is to use a global singleton.

First, we create a singleton class and add a property.

singletons/appdata.dart

class AppData {
static final AppData _appData = new AppData._internal();

String text;
  factory AppData() {
return _appData;
}
  AppData._internal();
}
final appData = AppData();

Now, in the files we want to access this appData instance, we just import the appdart.dart file:

import ‘../singletons/appdata.dart’;
class AppDataPage extends StatefulWidget {
  @override
AppDataPageState createState() {
return new AppDataPageState();
}
}
class AppDataPageState extends State<AppDataPage> {
  final textController = TextEditingController();
  @override
Widget build(BuildContext context) {
textController.text = appData.text;

return Scaffold(
appBar: AppBar(
title: Text(‘AppData PageOne’),
),
body: Container(
padding: EdgeInsets.all(12.0),
alignment: Alignment.center,
child: Column(
children: <Widget>[
TextField(
controller: textController,
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (text) {
appData.text = text;
},
),
Divider(),
RaisedButton(
child: Text(‘PageTwo’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
),
],
),
),
);
}
}

Singleton BLoC

The same method can be applied to an instance of a BLoC class, with the difference that here we are going to use streams:

Create the BLoC class and add some streams:

singletons/bloc.dart

import ‘package:frideos/frideos.dart’;
class SingletonBloc {
  static final SingletonBloc _singletonBloc = new    SingletonBloc._internal();
  final dateTime = StreamedValue<String>();
final text = StreamedValue<String>();
  factory SingletonBloc() {
return _singletonBloc;
}
  SingletonBloc._internal() {
text.value = “Lorem ipsum”;
}
  dispose() {
print(‘Disposing bloc’);
dateTime.dispose();
text.dispose();
}
}
final bloc = SingletonBloc();

And on the page, we can use the streams in the bloc instance along with the StreamBuilder, to build our widgets, without the need to call setState() to update them whenever the data changes:

screens/bloc_page.dart

import ‘package:frideos/frideos.dart’;
import ‘../singletons/bloc.dart’;
class SingletonBlocPage extends StatefulWidget {
@override
SingletonBlocPageState createState() {
return new SingletonBlocPageState();
}
}
class SingletonBlocPageState extends State<SingletonBlocPage> {
  final textController = TextEditingController();
  @override
Widget build(BuildContext context) {

textController.text = bloc.text.outStream.value;

return Scaffold(
appBar: AppBar(
title: Text(‘SingletonBloc PageOne’),
),
body: Container(
padding: EdgeInsets.all(12.0),
alignment: Alignment.center,
child: Column(
children: <Widget>[
StreamedWidget(
stream: bloc.text.outStream,
builder: (context, snapshot) {
return TextField(
controller: textController,
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: bloc.text.inStream,
);
}),
Divider(),
RaisedButton(
child: Text(‘PageTwo’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
),
],
),
),
);
}
}


CallBacks

The last method I’d like to talk about, to share data between two widgets, it is the use of the callbacks.

Let’s say we have a main widget that has in its children a widget from which we want to take some data back. For example, in the first widget you have two Text widgets that show some text and a datetime and in its CallbacksWidget, there are a TextField and a RaisedButton. On the onChanged callback of the first, we pass the onChangeText function the take as a parameter a string, while in the second, we pass the onChangeDate function (but in the onPressed callback).

screens/callbacks.dart

class CallbacksWidget extends StatelessWidget {
  final Function(String dateTime) onChangeDate;
final Function(String text) onChangeText;
  CallbacksWidget({this.onChangeDate, this.onChangeText});
  @override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(height: 6.0),
TextField(
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (value) {
onChangeText(value);
},
),
Container(height: 6.0),
RaisedButton(
child: Text(“GetTime”),
onPressed: () {
var dateTime = DateFormat(“dd/MM/yyyy — HH:mm:ss:S”).format(DateTime.now());
onChangeDate(dateTime);
},
),
],
),
);
}
}

As you can see, the two callbacks are declared as Function(String string) and defined in the constructor as named parameters:

final Function(String dateTime) onChangeDate;
final Function(String text) onChangeText;
CallbacksWidget({this.onChangeDate, this.onChangeText});

Let’s see the TextField:

TextField(
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (value) {
onChangeText(value);
},
),

The value from the onChanged callback is passed to the onChangeText callback. The same happens in the RaisedButton, with the difference that we are going to use its onPressed callback:

RaisedButton(
child: Text(“GetTime”),
onPressed: () {
var dateTime = DateFormat(“dd/MM/yyyy — HH:mm:ss:S”).format(DateTime.now());

onChangeDate(dateTime);
},
),

Okay, now how can we use the CallbacksWidget to access this data? Let’s examine how it is used in the callbacks.dart file:

  • Using setState:
CallbacksWidget(
onChangeDate: (newdate) {
setState(() {
dateTime = newdate;
});
},
onChangeText: (newtext) {
setState(() {
text = newtext;
});
},
),
  • Using streams
CallbacksWidget(
onChangeDate: (newdate) {
streamedTime.value = newdate;
},
onChangeText: (newtext) {
streamedText.value = newtext;
},
),

Just we pass to the CallbacksWidget a function to retrieve the data in the same way we do with a TextField. In the first case, we assign the value inside a setState function to rebuild the widget and update its state, in the second one we don’t need to call the setState because the values are given to two StreamedValue objects that trigger the rebuild of the StreamBuilder associated with them without affecting the whole CallBacksPage but only the widgets that we need to update.


Conclusion

These are just simple ways that I found useful to use in my experiments with Flutter. They are no the best or most sophisticated, but in various scenarios can let you accomplish your task in an easy way. I hope this could be useful.

You can find the example app in my repository on GitHub.