Refresh part of the screen - flutter

I'm new at Flutter and still struggling with overall concept of widgets structure especially stateful ones. As far as I understand I should create all widgets stateless unless they can be changed. Then they should be stateful and hold their state.
So I have a simple screen with a list of items that is dynamic and action button:
class WalletWidget extends StatelessWidget {
final GlobalKey<_WalletItemsState> _key = GlobalKey();
void _addWalletItem(BuildContext context) async {
var asset = await Navigator.pushNamed(context, '/add_wallet_item');
if (asset == null) {
return;
}
_key.currentState!._refresh();
}
#override
Widget build(BuildContext context) => Scaffold(
body: _WalletItems(),
floatingActionButton: FloatingActionButton(
onPressed: () => _addWalletItem(context),
child: const Icon(Icons.add)),
);
}
class _WalletItems extends StatefulWidget {
const _WalletItems();
#override
State<StatefulWidget> createState() => _WalletItemsState();
}
class _WalletItemsState extends State<_WalletItems> {
List<Asset> _walletAssets = [];
void _refresh() {
setState(() {});
}
#override
Widget build(BuildContext context) => ListView.builder(
itemCount: _walletAssets.length,
itemBuilder: (context, index) =>
Text(Asset.persistenceMgr.getAll().toList()[index].title),
);
}
in my understanding whole screen should be a stateless as it merely builds a UI structure. Hence class WalletWidget extends StatelessWidget. The list widget is stateful. Adding of new elements is done at separate screen when the action button is pressed. So when I'm back from adding screen the list is updated and I need to refresh the state of the list.
Only way I've found is using a GlobalKey, which points to the _WalletItemsState. But when it comes to line _key.currentState!._refresh(); the currentState is always null. So I assume the _key isn't properly associated with list's state. How do I do that?

_key is just intialized and not used in any widget. So, the currentState is always null
Try passing the _key to Scaffold of WalletWidget.
class WalletWidget extends StatelessWidget {
final GlobalKey<_WalletItemsState> _key = GlobalKey();
void _addWalletItem(BuildContext context) async {
var asset = await Navigator.pushNamed(context, '/add_wallet_item');
if (asset == null) {
return;
}
_key.currentState!.build(context);
}
#override
Widget build(BuildContext context) => Scaffold(
key:_key,
body: _WalletItems(),
floatingActionButton: FloatingActionButton(
onPressed: () => _addWalletItem(context),
child: const Icon(Icons.add)),
);
}

Related

How do I call method initState() from a stateless widget extending class?

I have a splash screen in my homepage activity which should then redirect to my second activity:
class _MyHomePageState extends State<MyHomePage> {
#override
void initState(){
super.initState();
Timer(const Duration(seconds: 3),
()=>Navigator.pushReplacement(context,
MaterialPageRoute(builder:
(context) =>
SecondScreen()
)
)
);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child:FlutterLogo(size:MediaQuery.of(context).size.height)
);
}
}
class SecondScreen extends StatelessWidget { //checking if internet connection exists here
late StreamSubscription subscription;
var isDeviceConnected = false;
bool isAlertSet = false;
#override
void initState(){
getConnectivity();
super.initState(); //initState() is undefined
}
getConnectivity() =>
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() => isAlertSet = true); //setState() is undefined
}
},
);
#override
void dispose() {
subscription.cancel();
super.dispose(); //dispose() is undefined
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment:MainAxisAlignment.center,
children:[
Image(
image: const AssetImage('images/logo.png'),
height: AppBar().preferredSize.height,),
const SizedBox(
width: 15,
),
Text(
widget.title
),
]
)
)
);
}
showDialogBox() => showCupertinoDialog<String>(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
title: const Text('No internet connection'),
content: const Text('Please make sure you have an active internet connection to continue'),
actions: <Widget>[
TextButton(
onPressed: () async {
Navigator.pop(context, 'Cancel');
setState(() => isAlertSet = false);
isDeviceConnected =
await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() => isAlertSet = true);
}
},
child: const Text('OK'),
),
],
),
);
}
The flow is such that, in the homepage activity a splash screen will open and then it will redirect to the second activity which will check if the user has an active internet connection.
I tried changing the SecondScreen to statefulWidget, but I still keep getting the same error.
Stateless: A stateless widget is like a constant. It is immutable. If you want to change what is displayed by a stateless widget, you'll have to create a new one.
Stateful: Stateful widgets are the opposite. They are alive and can interact with the user. Stateful widgets have access to a method named setState, which basically says to the framework "Hello, I want to display something else. Can you redraw me please ?".
A stateless widget can only be drawn once when the Widget is loaded/built and cannot be redrawn based on any events or user actions.
This kind of widget has no state, so they can’t change according to an internal state, they only react to higher widget changes.
more information read this documentation StatefulWidget and StatelessWidget
convert in stateful widget
class SecondScreen extends StatefulWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
there is no initState in a stateless widget but you can call a function after rebuild of a stateless widget using this:
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
// do something
print("Build Completed");
});
return Container(
color: Colors.blue,
child: WhatEverWidget()
);
}
}

Getx argumentsbeing cleared after using showDialog() in Flutter

The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}

Trigger snackbar message on page load in Flutter

I am new to Flutter and trying to trigger a snack bar on page load if a message was returned from the page I navigated from. I have managed to get the message to display on a button click, but get an error stating that my context does not have a Scaffold if I try to do it elsewhere.
I am also struggling to find an example of how to show a sack bar without user interaction, so if anyone has a reference, that would surely go a long way in helping as well.
Here is a simplified version of my view:
class LandingView extends StatefulWidget {
final LandingViewModel viewModel;
LandingView(this.viewModel);
#override
State<StatefulWidget> createState() {
return new _ViewState();
}
}
class _ViewState extends State<LandingView> {
#override
void initState() {
super.initState();
}
void _showSnackbar(context, message) {
final snackBar = SnackBar(
content: Text(message),
);
Scaffold.of(context).showSnackBar(snackBar);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: _buildLayout(context),
),
),
);
}
Widget _buildLayout(BuildContext context) {
Map<String, dynamic> args = getArgs(context); //get value from previous page
if (args != null &&
args["Toast Message"] != null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(
context, args["Toast Message"]); //if so call snack bar function
// this throws an error saying "Scaffold.of() called with a context that does not contain a Scaffold"
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: boxConstraints.maxHeight),
child: RaisedButton(
child: Text(
"Show Snack Bar",
),
onPressed:
() {
if (args != null &&
args["Toast Message"] !=
null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(context,
args["Toast Message"]); //if so call snack bar function
//this works perfectly
}),
),
);
});
}
}
Any advice would be greatly appreciated
You're getting that because your LandingView widget is not in a Scaffold. You can fix this by putting the LandingView widget inside a StatelessWidget with a Scaffold and changing any references to LandingView to LandingViewPage:
class LandingViewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: LandingView()
);
}
}
We can do this with addPostFrameCallback method
#override
void initState(){
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => scaffold.showSnackBar(SnackBar(content: Text("snackbar")));
}
In a stateful widget put:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Error")));
});
}

How to rebuild the Widget onpress of floatingactionbutton?

i'm trying to pull new data from firebase cloud firestore and rebuild the widget on onPress of floating action button. i'm not sure how to rebuild the whole widget. Tried to call getList from the onPressed and setState() but still not rebulding widget evening nameList was updated.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
}),
);
}
}
class MainList extends StatefulWidget {
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
List<Test> nameList = [];
#override
void initState() {
super.initState();
getList();
}
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Once a widget is created initState isn't called again. So your listview is still reflecting the old data.
You could getList in the onPressed which would then update your nameList. You could then pass this nameList to MainList.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<Test> nameList = [];
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(nameList: nameList),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
getList();
}),
);
}
}
Your MainList widget would then look like:
class MainList extends StatefulWidget {
final List nameList;
MainList({this.nameList});
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Just calling setState() is not enough. You'll have to tell Dart what you are going to set. Sample code :
setState ( ()=> nameList = _fetchedList ) ;
In the above code, the variable nameList is assigned within setState().
In your code, you've two options.
Option 1 :
setState(() {
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
}
});
Or option 2, better way, use for loop to add in the data in another list and use setState with one line as below :
List<Test> _fetchedList ;
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
_fetchedList.add(addName);
}
setState( ()=> nameList = _fetchedList ) ;

how to keep the state of my widgets after scrolling?

I'm codeing an app with flutter an i'm haveing problems with the development. I'm trying to have a listview with a custom widget that it has a favourite icon that represents that you have liked it product. I pass a boolean on the constructor to set a variables that controls if the icons is full or empty. When i click on it i change it state. It works awesome but when i scroll down and up again it loses the lastest state and returns to the initial state.
Do you know how to keep it states after scrolling?
Ty a lot <3
Here is my code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index){
return new LikeClass(liked: false);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final bool liked;//i want this variable controls how heart looks like
LikeClass({this.liked});
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
bool liked;
#override
void initState() {
liked=widget.liked;
}
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap:((){
setState(() {
liked=!liked;
//widget.liked=!widget.liked;
});
}),
child: new Icon(Icons.favorite, size: 24.0,
color: liked?Colors.red:Colors.grey,
//color: widget.liked?Colors.red:Colors.grey,//final method to control the appearance
),
),
],
),
);
}
}
You have to store the state (favorite or not) in a parent widget. The ListView.builder widget creates and destroys items on demand, and the state is discarded when the item is destroyed. That means the list items should always be stateless widgets.
Here is an example with interactivity:
class Item {
Item({this.name, this.isFavorite});
String name;
bool isFavorite;
}
class MyList extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyListState();
}
class MyListState extends State<MyList> {
List<Item> items;
#override
void initState() {
super.initState();
// Generate example items
items = List<Item>();
for (int i = 0; i < 100; i++) {
items.add(Item(
name: 'Item $i',
isFavorite: false,
));
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListItem(
items[index],
() => onFavoritePressed(index),
);
},
);
}
onFavoritePressed(int index) {
final item = items[index];
setState(() {
item.isFavorite = !item.isFavorite;
});
}
}
class ListItem extends StatelessWidget {
ListItem(this.item, this.onFavoritePressed);
final Item item;
final VoidCallback onFavoritePressed;
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(item.name),
leading: IconButton(
icon: Icon(item.isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: onFavoritePressed,
),
);
}
}
If you don't have many items in the ListView you can replace it with a SingleChildScrollview and a Column so that the Widgets aren't recycled. But it sounds like you should have a list of items where each item has an isFavourite property, and control the icon based on that property. Don't forget to setState when toggling the favorite.
Other answer are better for your case but this an alternative and can be used if you want to only keep several elements alive during a scroll. In this case you can use AutomaticKeepAliveClientMixin with keepAlive.
class Foo extends StatefulWidget {
#override
FooState createState() {
return new FooState();
}
}
class FooState extends State<Foo> with AutomaticKeepAliveClientMixin {
bool shouldBeKeptAlive = false;
#override
Widget build(BuildContext context) {
super.build(context);
shouldBeKeptAlive = someCondition();
return Container(
);
}
#override
bool get wantKeepAlive => shouldBeKeptAlive;
}
ListView.builder & GridView.builder makes items on demand. That means ,they construct item widgets & destroy them when they going beyond more than cacheExtent.
So you cannot keep any ephemeral state inside that item widgets.(So most of time item widgets are Stateless, but when you need to use keepAlive you use Stateful item widgets.
In this case you have to keep your state in a parent widget.So i think the best option you can use is State management approach for this. (like provider package, or scoped model).
Below link has similar Example i see in flutter.dev
Link for Example
Hope this answer will help for you
A problem with what you are doing is that when you change the liked variable, it exists in the Widget state and nowhere else. ListView items share Widgets so that only a little more than are visible at one time are created no matter how many actual items are in the data.
For a solution, keep a list of items as part of your home page's state that you can populate and refresh with real data. Then each of your LikedClass instances holds a reference to one of the actual list items and manipulates its data. Doing it this way only redraws only the LikedClass when it is tapped instead of the whole ListView.
class MyData {
bool liked = false;
}
class _MyHomePageState extends State<MyHomePage> {
List<MyData> list;
_MyHomePageState() {
// TODO use real data.
list = List<MyData>();
for (var i = 0; i < 100; i++) list.add(MyData());
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return new LikeClass(list[index]);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final MyData data;
LikeClass(this.data);
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap: (() {
setState(() {
widget.data.liked = !widget.data.liked;
});
}),
child: new Icon(
Icons.favorite,
size: 24.0,
color: widget.data.liked ? Colors.red : Colors.grey,
),
),
],
),
);
}
}