SetState in Flutter - flutter

I have dynamically created a list view by getting data from an API and putting it into a list of objects, however, when I open the screen, which is opened by pressing a button on another screen, the only way to get the ListView to show on screen is to do a hot refresh. I've tried setting the state in the init_state function but it doesn't seem to be working.
Thanks for any help :)
class _PlanetListState extends State<PlanetList> {
List<Planet> planets = [];
String urlWeb = 'https://swapi.dev/api/planets/';
Future getData(String url) async {
var response = await http.get(url);
if (response.statusCode == 200) {
var data = json.decode(response.body);
Planet obj = Planet(
name: data['name'],
rotationPeriod: data['rotation_period'],
orbitalPeriod: data['orbital_period'],
diameter: data['diameter'],
climate: data['climate'],
gravity: data['gravity'],
terrain: data['terrain'],
population: data['population']
);
planets.add(obj);
print(planets);
}
}
#override
void initState() {
for (var i=0;i<62;i++){
getData('$urlWeb$i/');
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Planets"),
backgroundColor: Colors.amber[800],
),
body: Container(
color: Colors.black,
child: ListView.builder(
itemCount: planets.length,
itemBuilder: (BuildContext context, int index){
return Card(
color: Colors.amber[600],
margin:EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: getColor(planets[index].climate),
backgroundImage: AssetImage('assets/planet.png'),
),
contentPadding: EdgeInsets.all(5),
title: Text('$index: ${planets[index].name}'),
subtitle:Text(planets[index].climate),
),
);
},
),
),
);
}
}

What's happening is that your screen is loading before your data are added to the list, this is because your getData function is async. So when your screen first loads the list is empty, and as you may know in order to show changes on the screen you must do a setState(), this is why when you hot refresh the changes appear.
Now you have multiple options here, first you can take a look at the FutureBuilder, this is a widget that would build twice, once before your Future is resolved and once afterwards, meaning it "auto refreshes". In this case your list of data would become the Future type and your ListView would be wrapped with a FutureBuilder so it's built after the data are ready.
Another option would be to do a setState() every time you add an item to the List of objects you have.

Related

Pass On Get Results from one class to another - Flutter

The data unLockCard is properly created in the main class where the button is placed.
When I moved the button to a dialog in a different class - the unLockCard is lost. I receive the error message
What is the best way to pass on unLockCard[number] = tarots[0]; into a different widget or class.
Homepage
List<bool> flips = [false, false, false, false];
List tarots = [];
List unLockCard = [];
Widget _buildTarotCard(key, number, title) {
return Column(
children: [
FlipCard(
key: key,
flipOnTouch: true,
front: GestureDetector(
onTap: () {
tarots.shuffle();
key.currentState.toggleCard();
setState(() {
flips[number] = true;
});
unLockCard[number] = tarots[0];
tarots.removeAt(0);
},
Dialog
Widget _showDialog(BuildContext context) {
Future.delayed(Duration.zero, () => showAlert(context));
return Container(
color: Color(0xFF2C3D50),
);
}
void showAlert(BuildContext context) {
List unLockCard = [];
Dialogs.materialDialog(
color: colorTitle,
msg: 'Congratulations, you won 500 points',
msgStyle: TextStyle(color: Colors.white),
title: 'Congratulations',
titleStyle: TextStyle(color: Colors.white),
lottieBuilder: Lottie.asset('assets/lottie/spirituality.json',
fit: BoxFit.contain,
),
dialogWidth: kIsWeb ? 0.3 : null,
context: context,
actions: [
NeumorphicButton(
onPressed: () => Get.toNamed(Routes.DETAILS,
arguments: unLockCard.sublist(0, 4)),
margin: EdgeInsets.symmetric(horizontal: 8.0),
The code is a bit cluttered. You are building a new List unLockCard = []; in the showAlert() method even though you have one already in the HomePage. I suggest you create a CardController class that deals with everything card related and use it in all the views you need. One very elegant way I found is by using the GetX library (https://github.com/jonataslaw/getx). You can declutter your code using a GetView and a GetController (https://github.com/jonataslaw/getx#getview). However, if you don't want to add a new library to your project, you can achieve the same results by having a single point that deals with card actions (i.e. holds a single instance of the unlockCard list and updates it accordingly).

Flutter - Provider does not updating data

I am using Provider to ensure data consistency between widgets. I just learned that the level of declared Provider object is important when accessing data in widgets. So I created one Provider variable at the top and tried to pass it as an argument to children, stateless & stateful widgets.
In a stateful widget, user selects size for a product. In a stateless widget, user taps button to add the selected item with selected specification to cart. Afterwards, API handles all stuff but to that point, when user selects other sizes, Provider does not update the initial data it holds. It always stuck with initial data or in my case initial size of a product.
Here is my: top parent widget:
#override
Widget build(BuildContext context) {
// initialize
var provider = Provider.of<ProductDetailProvider>(context, listen: true);
provider.detailModel = widget.detailModel!;
provider.seciliBeden = int.parse(widget.detailModel!.Stoks.first.Beden);
provider.stokId = -1;
return Scaffold(
backgroundColor: AppTheme.containerColor,
body: SafeArea(
bottom: false,
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
ImageViewBuilder(
pageController: imagePageController,
productDetailModel: widget.detailModel,
resims: resims,
),
DraggableScrollableSheet(
initialChildSize: 0.3,
maxChildSize: 1,
minChildSize: 0.3,
builder: (context, controller) {
return DraggableProductView(
scrollController: controller,
detailModel: widget.detailModel,
provider: provider, // StatefulWidget
);
},
),
Positioned(
child: Align(
alignment: Alignment.bottomCenter,
child: AddToCartBottomBar(
productDetailModel: widget.detailModel!,
productDetailProvider: provider, //StatelessWidget
),
),
),
],
),
),
),
);
}
StatefulWidget: this is how I change Provider object in DraggableProductView:
onTap: () {
setState(() {
seciliBeden =
widget.detailModel!.Stoks[index].Beden;
});
widget.provider!
.changeSeciliBeden(int.parse(seciliBeden));
},
Inside this StatefulWidget, I can print updated provider with updated datas. ChangeSeciliBeden also triggers ChangeStockId function inside provider.
StatelessWidget: AddToCartBottom widget holds several stateless widgets. This is the onTap method from AddToCartButton:
onTap: () async {
// This line prints only initial size & stock id of the product.
// It does not print updated data
print("yukarı: ${productDetailProvider!.stokId}");
// ...
// ...
// ...
},
Finally, this is my provider class:
class ProductDetailProvider extends ChangeNotifier {
late ProductDetailModel detailModel;
late int seciliBeden;
late int stokId;
// This method works too. It prints updated data.
changeSeciliBeden(int value) {
seciliBeden = value;
prepareStokId();
print("provider secili beden: $seciliBeden");
print("provider stok id: $stokId");
notifyListeners();
}
changeStokId(int value) {
stokId = value;
notifyListeners();
}
// This method works. I checked that. If block is true always once.
prepareStokId() {
detailModel.Stoks.forEach((element) {
if (element.Beden == seciliBeden.toString()) {
changeStokId(element.StokId);
}
});
notifyListeners();
}
}
Can somebody help me to figure out where I did wrong? Isn't this the way to pass data between widgets without using arguments?

Flutter - getx controller not updated when data changed

I am developing an app that has a bottomnavitaionbar with five pages. I use getx. In first page, i am listing data. My problem is that, when i changed data(first page in bottomnavigationbar) manually from database and thn i pass over pages, came back to first page i could not see changes.
Controller;
class ExploreController extends GetxController {
var isLoading = true.obs;
var articleList = List<ExploreModel>().obs;
#override
void onInit() {
fetchArticles();
super.onInit();
}
void fetchArticles() async {
try {
isLoading(true);
var articles = await ApiService.fetchArticles();
if (articles != null) {
//articleList.clear();
articleList.assignAll(articles);
}
} finally {
isLoading(false);
}
update();
}
}
and my UI;
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
thanks to #Baker for the right answer. However, if you have a list and in viewModel and want to update that list, just use the list.refresh() when the list updated
RxList<Models> myList = <Models>[].obs;
when add or insert data act like this:
myList.add(newItem);
myList.refresh();
GetX doesn't know / can't see when database data has changed / been updated.
You need to tell GetX to rebuild when appropriate.
If you use GetX observables with GetX or Obx widgets, then you just assign a new value to your observable field. Rebuilds will happen when the obs value changes.
If you use GetX with GetBuilder<MyController>, then you need to call update() method inside MyController, to rebuild GetBuilder<MyController> widgets.
The solution below uses a GetX Controller (i.e. TabX) to:
hold application state:
list of all tabs (tabPages)
which Tab is active (selectedIndex)
expose a method to change the active/visible tab (onItemTapped())
OnItemTapped()
This method is inside TabX, the GetXController.
When called, it will:
set which tab is visible
save the viewed tab to the database (FakeDB)
rebuild any GetBuilder widgets using update()
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
}
Complete Example
Copy/paste this entire code into a dart page in your app to see a working BottomNavigationBar page.
This tabbed / BottomNavigationBar example is taken from
https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
but edited to use GetX.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
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',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyTabHomePage(),
);
}
}
class FakeDB {
List<int> viewedPages = [0];
void insertViewedPage(int page) {
viewedPages.add(page);
}
}
/// BottomNavigationBar page converted to GetX. Original StatefulWidget version:
/// https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
class TabX extends GetxController {
TabX({this.db});
final FakeDB db;
int selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
List<Widget> tabPages;
#override
void onInit() {
super.onInit();
tabPages = <Widget>[
ListViewTab(db),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
}
/// INTERESTING PART HERE ↓ ************************************
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
// ↑ update() is like setState() to anything inside a GetBuilder using *this*
// controller, i.e. GetBuilder<TabX>
// Other GetX controllers are not affected. e.g. GetBuilder<BlahX>, not affected
// by this update()
// Use async/await above if data writes are slow & must complete before updating widget.
// This example does not.
}
}
/// REBUILT when Tab Page changes, rebuilt by GetBuilder in MyTabHomePage
class ListViewTab extends StatelessWidget {
final FakeDB db;
ListViewTab(this.db);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: db.viewedPages.length,
itemBuilder: (context, index) =>
ListTile(
title: Text('Page Viewed: ${db.viewedPages[index]}'),
),
);
}
}
class MyTabHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
Get.put(TabX(db: FakeDB()));
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
/// ↓ Tab Page currently visible - rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
child: GetBuilder<TabX>(
builder: (tx) => tx.tabPages.elementAt(tx.selectedIndex)
),
),
/// ↓ BottomNavBar's highlighted/active item, rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
bottomNavigationBar: GetBuilder<TabX>(
builder: (tx) => BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
),
],
currentIndex: tx.selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: tx.onItemTapped,
),
),
);
}
}
You don't need GetBuilder here, as its not meant for observable variables. Nor do you need to call update() in the fetchArticles function as that's only for use with GetBuilder and non observable variables.
So you had 2 widgets meant to update UI (GetBuilder and Obx) both following the same controller and all you need is just the OBX. So Rahuls answer works, or you can leave the Obx in place, get rid of of the GetBuilder and declare and initialize a controller in the beginning of your build method.
final exploreController = Get.put(ExploreController());
Then use that initialized controller in your OBX widget as the child of your Expanded.
Obx(() => exploreController.isLoading.value
? Center(
child:
SpinKitChasingDots(color: Colors.deepPurple[600], size: 40),
)
: ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {},
),
)
GetX< ExploreController >(builder: (controller) {
if (controller.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: controller.articleList.length,
separatorBuilder: (BuildContext context, int index) {});
});
If you change the value in the database 'manually', you need a STREAM to listen to the change on the database.
You can't do:
var articles = await ApiService.fetchArticles();
You need to do something like this:
var articles = await ApiService.listenToArticlesSnapshot();
The way you explained is like if you need the data to refresh after navigating to another page and clicking on a button, then navigating to first page (GetBuilder) OR automatically adds data from the within the first page (Obx). But your case is simple, just retrieve the articles SNAPSHOT, then in the controller onInit, subscribe to the snapshot with the bindStream method, and eventually use the function ever() to react to any change in the observable articleList.
Something like this:
create
final exploreController = Get.put(ExploreController());
Add
init: ExploreController();
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
*** here ***
init: ExploreController();
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
using GetxBuilder approch on ui side and where you want update simple called built in function update();
The simplest way I could.
In the controller create an obs (var indexClick = 1.obs;)
On each Tile test the selected==index...;
On the click of each item change the indexClick sequentially
return Obx(() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.dns),
title: const Text('Menu1'),
selected: controller.indexClick.value==1?true:false,
onTap: () {
controller.indexClick.value=1;
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Menu2'),
selected: controller.indexClick.value==2?true:false,
onTap: () {
controller.indexClick.value=2;
Navigator.pop(context);
},
),

Can I use Dismissible without actually dismissing the widget?

I'm trying to make a widget that can be swiped to change the currently playing song in a playlist. I'm trying to mimic how other apps do it by letting the user swipe away the current track and the next one coming in. Dismissible is so close to what I actually want. It has a nice animation and I can easily use the onDismissed function to handle the logic. My issue is that Dismissible actually wants to remove the widget from the tree, which I don't want.
The widget I'm swiping gets updated with a StreamBuilder when the song changes, so being able to swipe away the widget to a new one would be perfect. Can I do this or is there a better widget for my needs?
Here's the widget I'm working on:
class NowPlayingBar extends StatelessWidget {
const NowPlayingBar({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<ScreenState>(
stream: _screenStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final screenState = snapshot.data;
final queue = screenState.queue;
final mediaItem = screenState.mediaItem;
final state = screenState.playbackState;
final processingState =
state?.processingState ?? AudioProcessingState.none;
final playing = state?.playing ?? false;
if (mediaItem != null) {
return Container(
width: MediaQuery.of(context).size.width,
child: Dismissible(
key: Key("NowPlayingBar"),
onDismissed: (direction) {
switch (direction) {
case DismissDirection.startToEnd:
AudioService.skipToNext();
break;
case DismissDirection.endToStart:
AudioService.skipToPrevious();
break;
default:
throw ("Unsupported swipe direction ${direction.toString()} on NowPlayingBar!");
}
},
child: ListTile(
leading: AlbumImage(itemId: mediaItem.id),
title: mediaItem == null ? null : Text(mediaItem.title),
subtitle: mediaItem == null ? null : Text(mediaItem.album),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (playing)
IconButton(
onPressed: () => AudioService.pause(),
icon: Icon(Icons.pause))
else
IconButton(
onPressed: () => AudioService.play(),
icon: Icon(Icons.play_arrow)),
],
),
),
),
);
} else {
return Container(
width: MediaQuery.of(context).size.width,
child: ListTile(
title: Text("Nothing playing..."),
));
}
} else {
return Container(
width: MediaQuery.of(context).size.width,
// The child below looks pretty stupid but it's actually genius.
// I wanted the NowPlayingBar to stay the same length when it doesn't have data
// but I didn't want to actually use a ListTile to tell the user that.
// I use a ListTile to create a box with the right height, and put whatever I want on top.
// I could just make a container with the length of a ListTile, but that value could change in the future.
child: Stack(
alignment: Alignment.center,
children: [
ListTile(),
Text(
"Nothing Playing...",
style: TextStyle(color: Colors.grey, fontSize: 18),
)
],
));
}
},
);
}
}
Here's the effect that I'm going for (although I want the whole ListTile to get swiped, not just the song name): https://i.imgur.com/ZapzpJS.mp4
This can be done by using the confirmDismiss callback instead of the onDismiss callback. To make sure that the widget never actually gets dismissed, you need to return false at the end of the function.
Dismissible(
confirmDismiss: (direction) {
...
return false;
}
)

In Dart/Flutter, how do I use a variable from a method so I can ouput it to a text field

Hope somebody can help - I hit this dead end a few weeks ago and think that I've tried everything within my limited knowledge.
I've set up a database that works OK - that is I can add data on one screen, review the data and edit the data on another screen. Now I want to sum one of the columns (beef) which I've been able to do as proven in the 'debugPrint' to the console. I want to access this variable 'beefTotal' from the 'sumBeef' method and print show this in a text field in the UI. I just can't manage it though. It just returns null.
Thanks in advance for any help.
import 'package:flutter/material.dart';
import 'package:take_note/utils/database_helper.dart';
class Info extends StatefulWidget {
#override
State<StatefulWidget> createState() => _InfoState();
}
DatabaseHelper helper = DatabaseHelper();
var database = DatabaseHelper();
class _InfoState extends State<Info> {
List beefTotal;
#override
Widget build (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Beef Info"),
backgroundColor: Colors.lightBlueAccent,
),
body: Container(
child: Column(
children: <Widget>[
Expanded(
child: Center(
child: RaisedButton(
onPressed: (){
sumBeef();
},
),
),
),
Expanded(
child: Center(
child: Text("Total Beef is: £ $beefTotal", style: TextStyle(
color: Colors.lightBlueAccent,
fontSize: 30.0,
fontWeight: FontWeight.w400
),),
),
),
],
),
)
);
}
void sumBeef () async {
beefTotal = await database.addBeef();
debugPrint("Total beef: $beefTotal");
}
}
The code below is from a class called DatabaseHelper which the method sumBeef() uses
Future<List<Map<String, dynamic>>> addBeef()async{
Database db = await this.database;
var result = await db.rawQuery("SELECT SUM(beef) FROM $table");
return result;
}
```[enter image description here][1]
[1]: https://i.stack.imgur.com/L46Gj.png
Just call
setState({
});
void sumBeef () async {
beefTotal = await database.addBeef();
setState(() {});
debugPrint("Total beef: $beefTotal");
}
and your good! anytime you make a change you have to call setState method to update the ui (rebuild) in flutters case