how to fix "A non-null String must be provided to a Text widget." error in flutter? - flutter

I am implementing a basic code given in flutter website [link]https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html I am getting the error: "A non-null string must be provided to the text widget". The text widget is being given the value of the increment counter. So could you please help why the null issue is arising?
Code:
import 'package:flutter/material.dart';
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
final Widget goodJob = const Text('Good job!');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title)
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
}
}

You are getting the error because you are using the title String as the AppBar's title and you aren't giving it any value:
I have added a demo using your code as an example:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(
title: 'My Home Page',
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
final Widget goodJob = const Text('Good job!');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
}
}

Related

ListView.builder inside a Column inside a SingleChildScrollView

all. I am trying to add a ListView.builder to a Column that is inside a SingleChildScrollView. However, I am getting an exception, likely due to the fact that there is no constraint for the ListView.builder. Here is my code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ListView.builder(
itemBuilder: (context, index) => const Text('a'),
itemCount: 2,
),
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
If I use a Container and set a defined height, the code above works. However, I am trying to get the ListView.builder to not have a fixed size. I've tried using the Expanded widget and I still get this error. Is there a way to make this work without a defined height? Thanks
In the column widget add mainAxisSize:MainAxisSize.min and in List view.builder add shrinkWrap:true and physics:NeverScrollablePhysics(). That should solve the issue and instead of center widget use SafeArea or a container with specific height.
Here's your working code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ListView.builder(
shrinkWrap:true,// -> Add this here
physics:NeverScrollablePhysics(),// -> And this one
itemBuilder: (context, index) => const Text('a'),
itemCount: 2,
),
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}

What argument should i give?

In the application, the home page is ResultScreen, which displays the entered data. If they are not there, then when you click on the button, we go to the screen with the input. When I enter text into the input and click on the Display Result button, the data should be substituted into the text field on the first screen. I implemented such functionality, but I don’t understand what argument I should substitute in main.dart. Tell me please
Text Screen:
import 'package:flutter/material.dart';
import 'package:flutter_application_1/screens/result_screen.dart';
class TextScreen extends StatefulWidget {
const TextScreen({Key? key}) : super(key: key);
#override
State<TextScreen> createState() => _TextScreenState();
}
class _TextScreenState extends State<TextScreen> {
TextEditingController textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Enter data'),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: textController,
decoration: InputDecoration(labelText: 'Message'),
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ResultScreen(textController.text)));
},
child: Text('Display result'))
],
)),
);
}
}
Result Screen:
import 'package:flutter/material.dart';
import 'package:flutter_application_1/screens/text_screen.dart';
class ResultScreen extends StatefulWidget {
final String valueText;
ResultScreen(this.valueText);
#override
State<ResultScreen> createState() => _ResultScreenState();
}
class _ResultScreenState extends State<ResultScreen> {
// navigation to text_screen
void _buttonNav() {
Navigator.push(
context, MaterialPageRoute(builder: (context) => const TextScreen()));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Results'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _buttonNav, child: const Text('Enter data')),
const SizedBox(
height: 50,
),
Text(valueText),
const SizedBox(
height: 20,
),
],
)),
);
}
}
Main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_application_1/screens/result_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ResultScreen(),
);
}
}
Use the following code.
What is does is, when we enter the first screen i.e. ResultScreen, we pass an empty value for the first time.
Use this in main.dart
home: ResultScreen(''),
And as you are using statefull widget for ResultScreen, you need to use widget.valueText to access it like:
Text(widget.valueText),

Flutter: scoped model access in StatefulWidget

I have scoped model lib/scoped_models/main.dart:
import 'package:scoped_model/scoped_model.dart';
class MainModel extends Model {
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
notifyListeners();
}
void setCount(int value) {
_count = value;
notifyListeners();
}
And very simple app lib/main.dart:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<MainModel>(
model: MainModel(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
)
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final MainModel _model = MainModel();
void initState() {
super.initState();
// _model.incrementCount(); // <-- doesn't work !!!
}
void _incrementCounter() {
setState(() {
// _model.incrementCount(); // <-- doesn't work !!!
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
}
)
],
),
),
floatingActionButton: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return FloatingActionButton(
onPressed: () {
model.incrementCount(); // <-- only this works !!!
// _incrementCounter(); // <-- doesn't work !!!
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
)
);
}
}
The problem that I can't access MainModel outside of ScopedModelDescendant widget.
How to call MainModel methods at the beginning of _MyHomePageState class?
I believe it is possible because I don't want to keep all logic just in MainModel class and call every method in ScopedModelDescendant widget because it would be very inconvenient if there were many nested widgets.
So, how to get access to scoped model in StatefulWidget?
Use Scoped Model as provider
add ScopedModel just before the widget which use it (MyHomePage)
use ScopedModel.of<MainModel>(context) to control the model
use ScopedModelDescendant<MainModel> to listen the model
The advantage of using this:
You can access the same model in the descendants and share data easily
rebuild widget as small as possible (only ScopedModelDescendant part will be rebuilt)
code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ScopedModel<MainModel>(
model: MainModel(),
child: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
}
void _incrementCounter() {
ScopedModel.of<MainModel>(context).incrementCount();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ScopedModelDescendant<MainModel>(
builder: (context,child, model){
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Put MainModel as a Singleton
As your solution, you create MainModel once and make it final. This can be more simple like below:
MainModel
final MainModel mainModel = MainModel();
class MainModel{
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
}
void setCount(int value) {
_count = value;
}
}
MyHomePage
MainModel even no need to extend Model or use notifyListeners becaue the widget use setState to rebuild
code:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
}
void _incrementCounter() {
setState(() {
mainModel.incrementCount();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${mainModel.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
After watching into my code for a while I realized how stupid simple it was to fix.
So, obviously there should be just one instance of MainModel() for all widgets and files of the project and for convenience it should be placed in scoped model file lib/scoped_models/main.dart like this:
import 'package:scoped_model/scoped_model.dart';
final MainModel mainModel = MainModel(); // <-- create instance once for all files which require scoped model import
class MainModel extends Model {
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
notifyListeners();
}
void setCount(int value) {
_count = value;
notifyListeners();
}
And then you can use mainModel instance anywhere you import the model import 'package:<app_name>/scoped_models/main.dart';
So that, this code will be valid lib/main.dart:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<MainModel>(
model: mainModel, // <-- instance of model from 'lib/<app_name>/scoped_models/main.dart'
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
)
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
}
void _incrementCounter() {
setState(() {
mainModel.incrementCount(); // <-- now it works !!!
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
}
)
],
),
),
floatingActionButton: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return FloatingActionButton(
onPressed: () {
// model.incrementCount(); // <-- works !!!
_incrementCounter(); // <-- now it's working too !!!
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
)
);
}
}
Despite that fact that is seems reasonable, it can be overwhelming as well for the first time due to lack of examples.

How to invoke a rebuild of a stateless widget?

Context
I have two stateless widgets (pages): HomePage and DetailsPage. Obviously the application starts and launches the HomePage. There is a button the user can press to navigate to the DetailsPage with a Navigator.pop() button to navigate back to the HomePage.
I know when the DetailsPage is done being used with the .whenComplete() method. It is at this point I want to rebuild the HomePage widget.
Code
This is the minimum reproduction of my behavior.
main.dart
import 'package:example/home.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: HomePage());
}
}
home.dart
import 'package:example/details.dart';
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
static const name = 'Home Page';
const HomePage() : super();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: MaterialButton(
color: Colors.blue,
textColor: Colors.white,
child: Text(name),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: DetailsPage.builder),
).whenComplete(() => print('Rebuild now.'));
},
),
),
);
}
}
details.dart
import 'package:flutter/material.dart';
class DetailsPage extends StatelessWidget {
static const name = 'Details Page';
static WidgetBuilder builder = (BuildContext _) => DetailsPage();
const DetailsPage();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(name),
MaterialButton(
color: Colors.blue,
textColor: Colors.white,
child: Text('Go Back'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
}
}
Question
How can I invoke a rebuild of this stateless widget (HomePage) at the .whenComplete() method callback?
You can force rebuild the widget tree as follows:
class RebuildController {
final GlobalKey rebuildKey = GlobalKey();
void rebuild() {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(rebuildKey.currentContext as Element).visitChildren(rebuild);
}
}
class RebuildWrapper extends StatelessWidget {
final RebuildController controller;
final Widget child;
const RebuildWrapper({Key? key, required this.controller, required this.child}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
key: controller.rebuildKey,
child: child,
);
}
In your case,
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final RebuildController controller = RebuildController();
MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: RebuildWrapper(
controller: controller,
child: HomePage(
rebuildController: controller,
),
),
);
}
}
class HomePage extends StatelessWidget {
static const name = 'Home Page';
final RebuildController rebuildController;
const HomePage({Key? key, required this.rebuildController}) : super(key: key);
#override
Widget build(BuildContext context) {
print('Hello there!');
return Scaffold(
body: Center(
child: MaterialButton(
color: Colors.blue,
textColor: Colors.white,
child: const Text(name),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: DetailsPage.builder),
).whenComplete(rebuildController.rebuild);
},
),
),
);
}
}
class DetailsPage extends StatelessWidget {
static const name = 'Details Page';
static WidgetBuilder builder = (BuildContext _) => const DetailsPage();
const DetailsPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(name),
MaterialButton(
color: Colors.blue,
textColor: Colors.white,
child: const Text('Go Back'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
}
}
class RebuildController {
final GlobalKey rebuildKey = GlobalKey();
void rebuild() {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(rebuildKey.currentContext as Element).visitChildren(rebuild);
}
}
class RebuildWrapper extends StatelessWidget {
final RebuildController controller;
final Widget child;
const RebuildWrapper({Key? key, required this.controller, required this.child}) : super(key: key);
#override
Widget build(BuildContext context) => Container(
key: controller.rebuildKey,
child: child,
);
}
But it is unnatural to force rebuild stateless widgets as they are not supposed to be rebuilt. You should use stateful widget or other state management solutions so that your HomePage will only be updated on meaningful state change.
Source - this answer

Flutter: Detect rebuild of any widget which is not visible on screen but is in the widget tree

Summary:
As showing a page/route using the Navigator, a new branch is created from the nearest MaterialApp parent. Meaning both pages (Main & New) will be in memory and will rebuild if they are listening to the same ChangeNotifier.
I am having trouble finding out which widget is on-screen currently visible to the user.
I need this to handle a scenario to skip performing asynchronous or long processes with some side effects, from a widget that might be in the widget tree but currently not visible.
Note: The sample code given here represents the basic architecture of the app I am currently working on, but reproduces the exact problem.
I am having this problem with a very different and complex widget tree that I have in my app, executing the doLongProcess() from a widget that is not visible on the screen. Also doLongProcess() changes some common property in my app which causes an issue, as any background widget can modify the details which are visible on the other widget.
I am looking for a solution to this issue, if there's any other way to achieve the goal except finding which widget is on the screen then please let me know that as well.
My final goal is to allow the long process to be executed from only the visible widget(s).
Please run the app once, to understand the following details properly.
Note 2:
I have tried to use mounted property of the state to determine if it can be used or not but it shows true for both widgets (MainPage TextDisplay and NewPage TextDisplay)
Let me know in the comments if more details or I missed something which is required.
Use the following sample code with provider dependency included for reproducing the problem:
// add in pubspec.yaml: provider: ^4.3.2+1
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
print('MainPage: build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextDisplay(
name: 'MainPage TextDisplay',
),
SizedBox(
height: 20,
),
RaisedButton(
child: Text('Open New Page'),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => NewPage(),
)),
),
],
),
),
);
}
}
class TextDisplay extends StatefulWidget {
final String name;
const TextDisplay({Key key, #required this.name}) : super(key: key);
#override
_TextDisplayState createState() => _TextDisplayState();
}
class _TextDisplayState extends State<TextDisplay> {
#override
Widget build(BuildContext context) {
return Container(
child: ChangeNotifierProvider.value(
value: dataHolder,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(child: Text(widget.name)),
SizedBox(
height: 20,
),
Consumer<DataHolder>(
builder: (context, holder, child) {
// need to detect if this widget is on the screen,
// only then we should go ahead with this long process
// otherwise we should skip this long process
doLongProcess(widget.name);
return Text(holder.data);
},
),
RaisedButton(
child: Text('Randomize'),
onPressed: () => randomizeData(),
),
],
),
),
);
}
void doLongProcess(String name) {
print('$name: '
'Doing a long process using the new data, isMounted: $mounted');
}
}
class NewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('NewPage: build');
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
title: Text('New Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextDisplay(
name: 'NewPage TextDisplay',
),
],
),
),
);
}
}
/////////////////// Data Holder Class and methods ///////////////////
class DataHolder extends ChangeNotifier {
String _data;
String get data => _data ?? 'Nothing to show, Yet!';
setData(String newData) {
print('\n new data found: $newData');
_data = newData;
notifyListeners();
}
}
final dataHolder = DataHolder();
randomizeData() {
int mills = DateTime.now().millisecondsSinceEpoch;
dataHolder.setData(mills.toString());
}
Posting solution for others to refer.
Refer to this flutter plugin/package:
https://pub.dev/packages/visibility_detector
The solution code:
// add in pubspec.yaml: provider: ^4.3.2+1
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
print('MainPage: build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextDisplay(
name: 'MainPage TextDisplay',
),
SizedBox(
height: 20,
),
RaisedButton(
child: Text('Open New Page'),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => NewPage(),
)),
),
],
),
),
);
}
}
class TextDisplay extends StatefulWidget {
final String name;
const TextDisplay({Key key, #required this.name}) : super(key: key);
#override
_TextDisplayState createState() => _TextDisplayState();
}
class _TextDisplayState extends State<TextDisplay> {
/// this holds the latest known status of the widget's visibility
/// if [true] then the widget is fully visible, otherwise it is false.
///
/// Note: it is also [false] if the widget is partially visible since we are
/// only checking if the widget is fully visible or not
bool _isVisible = true;
#override
Widget build(BuildContext context) {
return Container(
child: ChangeNotifierProvider.value(
value: dataHolder,
/// This is the widget which identifies if the widget is visible or not
/// To my suprise this is an external plugin which is developed by Google devs
/// for the exact same purpose
child: VisibilityDetector(
key: ValueKey<String>(widget.name),
onVisibilityChanged: (info) {
// print('\n ------> Visibility info:'
// '\n name: ${widget.name}'
// '\n visibleBounds: ${info.visibleBounds}'
// '\n visibleFraction: ${info.visibleFraction}'
// '\n size: ${info.size}');
/// We use this fraction value to determine if the TextDisplay widget is
/// fully visible or not
/// range for fractional value is: 0 <= visibleFraction <= 1
///
/// Meaning we can also use fractional values like, 0.25, 0.3 or 0.5 to
/// find if the widget is 25%, 30% or 50% visible on screen
_isVisible = info.visibleFraction == 1;
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(child: Text(widget.name)),
SizedBox(
height: 20,
),
Consumer<DataHolder>(
builder: (context, holder, child) {
/// now that we have the status of the widget's visiblity
/// we can skip the long process when the widget is not visible.
if (_isVisible) {
doLongProcess(widget.name);
}
return Text(holder.data);
},
),
RaisedButton(
child: Text('Randomize'),
onPressed: () => randomizeData(),
),
],
),
),
),
);
}
void doLongProcess(String name) {
print('\n ============================ \n');
print('$name: '
'Doing a long process using the new data, isMounted: $mounted');
final element = widget.createElement();
print('\n name: ${widget.name}'
'\n element: $element'
'\n owner: ${element.state.context.owner}');
print('\n ============================ \n');
}
}
class NewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('NewPage: build');
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
title: Text('New Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextDisplay(
name: 'NewPage TextDisplay',
),
],
),
),
);
}
}
/////////////////// Data Holder Class and methods ///////////////////
class DataHolder extends ChangeNotifier {
String _data;
String get data => _data ?? 'Nothing to show, Yet!';
setData(String newData) {
print('\n new data found: $newData');
_data = newData;
notifyListeners();
}
}
final dataHolder = DataHolder();
randomizeData() {
int mills = DateTime.now().millisecondsSinceEpoch;
dataHolder.setData(mills.toString());
}