On my Page, I have a button to take picture. Once that picture is taken, it'll update my Model (it's using Provider ChangeNotifier). And once picture is taken, the Page gets rebuild, and in the Scaffold main I'm build the widgets:
Widget build(BuildContext context) {
return SingleChildScrollView(
// Somewhere in the middle of this
getPicturesSection(),
// Continue with other widgets
)
}
Widget getPicturesSection(BuildContext context) {
var imagesPath = Provider.of<MyModel>(context, listen:false).imagesPath;
var wids = <Widget>[]
// Basically show all the taken pictures
imagesPath.forEach((f) {
wids.add(
Image.file(
File(f)
)
)
})
return Row(children: wids);
}
What I want to do is allow users to delete each image. So I want to add a delete icon below each image:
imagesPath.forEach((f) {
wids.add(
Column(
children: <Widget> [
Image.file(
File(f)
),
IconButton(
onTap: () {
// How do I delete from the very same list that I am using to build this list?
}
),
],
)
)
})
Never mind, I figured out the answer. Since I'm already using ChangeNotifier, I just need to add the function to remove entry from the model, and the changes will propagate downwards.
List<String> imagesPath = new List<String>();
removeRejectionPicturePath(int ind) {
this.imagesPath.removeAt(ind);
notifyListeners(); // This will basically ask all the widgets that is the listener to rebuild the widget tree
}
You can try this !
imagesPath.forEach((f) {
wids.add(
Column(
children: <Widget> [
Image.file(
File(f)
),
IconButton(
icon:Icon(Icons.remove_circle),
onPressed: () {
setState((){
wids.removeAt(imagesPath.values.toList().indexOf(f));
});
}
),
],
)
);
});
Related
I have a list of news that comes from the API on the screen.
Output code for one card with news(I shortened the code and left only the necessary):
Widget customListTile(Article article, BuildContext context) {
final newsController = Get.put(FavoritesController());
return InkWell(
Container(
child:Row(
textDirection: TextDirection.ltr,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
article.source.name,
style: TextStyle(
color: Colors.white,
backgroundColor: Colors.red,
),
),
IconButton(
onPressed: () {
newsController.addNews(article);
},
icon: const Icon(Icons.favorite_border)),
]
)
),
);
}
There is an icon, when clicked, the addNews() function is called.And there is a controller code where there is this function:
class FavoritesController extends GetxController {
var _news = {}.obs;
void addNews(Article article) {
if(_news.containsKey(article)) {
}
}
get news => _news;
}
I need when clicking on the icon, the news was added to a separate Saved page.
How to write a function for adding news in a controller? And did I write the code correctly? Can you please tell me how this can be implemented?
First of all, you either use
.obs
or
GetxController function update()
There is no need to change your code tho because this will work as well but you're not using GetxController correctly in this case.
Let's focus on the .obs
Move the
Now, make a ListView that is wrapped with Obx(() => ...) which uses the news obs.
Obx(() {
return ListView(
children: Get.find<FavoritesController>().news.map((e) => customListTile(e)).toList(),
);
});
Let's move to the addNews function.
When you add an article use the update function.
if(_news.containsKey(article)) {
_news.update((e) => e[key]=value);
}
Also, move
final newsController = Get.put(FavoritesController());
outside of this function, even though is not necessary, it makes no sense for it to be there.
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?
I was trying to make a form that shows one field at the time that acomplishes the next things:
When pressing a "next" button, it validates the field and let the user go to the next field if everything is ok.
When the user presses the next button in the keyboard it should do the same.
If possible, a sliding animation between fields.
What i came up with was something like this:
List<Widget> questions = [List of textFieldForm]
int pageCount = 0;
GlobalKey _formKey = GlobalKey<FormState>();
pickSection(pageCount) {
return questions[pageCount];
}
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
pickSection(widget.pageCount),
Container(
child: Row(children: [
Container(
width: screenWidth * 0.4,
child: TextButton(
onPressed: widget.pageCount == 0
? null
: () {
setState(() {
widget.pageCount--;
}),
child: Text("BACK"),
),
),
Container(
width: screenWidth * 0.4,
child: TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
if (widget.pageCount == questions.length - 1) {
print("FORM ENDS");
} else {
setState(() {
widget.pageCount++;
});
}
}
},
child: Text("NEXT"),
),
),
])
),
]),
),
}
This way i validate every field when it's index is called since i treat every widget as it's own form, but i couldn't think of a way to implement an animation, or make the field advance using the next button on the keyboard(again, because this treats every field as it's own form).
Please help me figure out how to implement this. Animation is not important but using the next button to advance fields is crucial.
Thanks in advance.
You can achieve this using Visibility widget(which is used to show a widget on condition) and maintaining flags.
bool goToNextField = false;
TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
goToNextField = true;
if (widget.pageCount == questions.length - 1) {
print("FORM ENDS");
} else {
setState(() {
widget.pageCount++;
});
}
}
},
And in the next widget use a Visibility widget
Widget nextField () {
return Visibility(
visible: goToNextField,
child :
// what you want to show when next button is pressed
);
}
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;
}
)
I have a list of strings defined like this:
var list = ["one", "two", "three", "four"];
I want to render the values on the screen side by side using text widgets. I have attempted to use the following code to attempt this:
for (var name in list) {
return new Text(name);
}
However, when I run this code, the for loop only runs once and there is only one text widget that gets rendered that says one (the first item in the list). Additionally, when I add a log message inside my for loop, it gets triggered once as well. Why isn't my for loop looping based on the length of the list? It seems to run only once and then quit.
Basically when you hit 'return' on a function the function will stop and will not continue your iteration, so what you need to do is put it all on a list and then add it as a children of a widget
you can do something like this:
Widget getTextWidgets(List<String> strings)
{
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(new Text(strings[i]));
}
return new Row(children: list);
}
or even better, you can use .map() operator and do something like this:
Widget getTextWidgets(List<String> strings)
{
return new Row(children: strings.map((item) => new Text(item)).toList());
}
It is now possible to achieve that in Flutter 1.5 and Dart 2.3 by using a for element in your collection.
var list = ["one", "two", "three", "four"];
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for(var item in list ) Text(item)
],
),
This will display four Text widgets containing the items in the list.
NB. No braces around the for loop and no return keyword.
The Dart language has aspects of functional programming, so what you want can be written concisely as:
List<String> list = ['one', 'two', 'three', 'four'];
List<Widget> widgets = list.map((name) => new Text(name)).toList();
Read this as "take each name in list and map it to a Text and form them back into a List".
For googler, I wrote a simple Stateless Widget containing 3 method mentioned in this SO. Hope this make it easier to understand.
import 'package:flutter/material.dart';
class ListAndFP extends StatelessWidget {
final List<String> items = ['apple', 'banana', 'orange', 'lemon'];
// for in (require dart 2.2.2 SDK or later)
Widget method1() {
return Column(
children: <Widget>[
Text('You can put other Widgets here'),
for (var item in items) Text(item),
],
);
}
// map() + toList() + Spread Property
Widget method2() {
return Column(
children: <Widget>[
Text('You can put other Widgets here'),
...items.map((item) => Text(item)).toList(),
],
);
}
// map() + toList()
Widget method3() {
return Column(
// Text('You CANNOT put other Widgets here'),
children: items.map((item) => Text(item)).toList(),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: method1(),
);
}
}
To loop through a for-loop with multiple widgets in children,
children: [
for(int i = 0; i < item.length; i++) ...[
Widget1,
Widget2,
...
],
],
The simplest way is to map your list inside a Row or a Column widget :
var list = ["one", "two", "three", "four"];
Widget build(BuildContext context) {
return Row(children: List.from(list.map((name) => Text(name))));
}
One line
Column( children: list.map((e) => Text(e)).toList() )
You can use ListView to render a list of items. But if you don't want to use ListView, you can create a method which returns a list of Widgets (Texts in your case) like below:
var list = ["one", "two", "three", "four"];
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('List Test'),
),
body: new Center(
child: new Column( // Or Row or whatever :)
children: createChildrenTexts(),
),
),
));
}
List<Text> createChildrenTexts() {
/// Method 1
// List<Text> childrenTexts = List<Text>();
// for (String name in list) {
// childrenTexts.add(new Text(name, style: new TextStyle(color: Colors.red),));
// }
// return childrenTexts;
/// Method 2
return list.map((text) => Text(text, style: TextStyle(color: Colors.blue),)).toList();
}
You can make use of ListView.Builder() if you are receiving response from a http request that as an array
List items = data;
Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (BuildContext context, int index){
return Container(
child: Text(
items[index]['property']
),
);
},
),
);
Where
data is content returned from a http request using post or get
item is the array
'property' is one of the property of each item in the array assuming your are receiving back a list of objects
An easier approach may be to use expand:
For example
var paragraphs = ['Para1','Para2','Para3'];
Somewhere in your widget tree you can do this:
...paragraphs.expand((value) => [
SizedBox(
height: 10.0,
),
Text(
value,
// Add some styling if necessary
),
SizedBox(
height: 20.0,
),
]),
Here expand returns an iterable of widgets which is then spread using the Spread Operator(...).
when you return some thing, the code exits out of the loop with what ever you are returning.so, in your code, in the first iteration, name is "one". so, as soon as it reaches return new Text(name), code exits the loop with return new Text("one"). so, try to print it or use asynchronous returns.
Below works for me using the collection package :
https://pub.dev/packages/collection
children: <Widget>[
...languages.mapIndex((idx, item) {
return InkWell(
child: CustomCheckbox(Skill(item, _languageSelections[idx])),
onTap: () {
setState(() {
_languageSelections[idx] = !_languageSelections[idx];
});
});
})
],