I am trying to create a comment page. The list of comments are listed using ListView.builder. And when the user enter a comment, it will rebuild the list again to include the newly added comment. But somehow the list is not rebuild and i'm getting this message in terminal:
Changing the content within the the composing region may cause the
input method to behave strangely, and is therefore discouraged. See
https://github.com/flut ter/flutter/issues/78827 for more details
The newly added comment only shows when i close the comment page and reopen it again. Please help me, as i am not sure what is the issue and how to fix it.
Comment Page:
import 'package:flutter/material.dart';
import '../model/model_comment.dart';
class CommentsPage extends StatefulWidget {
#override
_CommentsPageState createState() => _CommentsPageState();
}
class _CommentsPageState extends State<CommentsPage> {
ValueNotifier<int> _counter = ValueNotifier<int>(0);
TextEditingController _controllerComment = TextEditingController();
bool _hasComment = false;
#override
void dispose() {
_controllerComment.dispose();
super.dispose();
}
_commentOnSend() {
setState(() {
var value = CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/women/34.jpg",
name: "Laurent Oslo",
dateTime: "30 Dec 20 08:00",
comment: _controllerComment.text,
);
CommentModel.dummyData.insert(0, value);
});
_controllerComment.clear();
FocusScope.of(context).unfocus();
}
Widget _listView = ListView.builder(
itemCount: CommentModel.dummyData.length,
itemBuilder: (context, index) {
CommentModel _model = CommentModel.dummyData[index];
return Column(
children: <Widget>[
Divider(
height: 12.0,
),
Container(
padding: EdgeInsets.fromLTRB(3.0, 3.0, 3.0, 3.0),
child: ListTile(
leading: CircleAvatar(
radius: 24.0,
backgroundImage: NetworkImage(_model.avatarUrl),
),
title: Text(_model.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_model.comment),
Text(_model.dateTime),
],
),
),
),
],
);
},
);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Comments"),
titleSpacing: 40.0,
),
body: Container(
child: Column(
children: [
Expanded(child: _listView),
Divider(
height: 1.0,
),
ListTile(
leading: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.all(Radius.circular(50.0))),
child: CircleAvatar(
radius: 50.0,
backgroundImage: NetworkImage(
"https://randomuser.me/api/portraits/men/83.jpg")),
),
title: TextField(
decoration: (InputDecoration(
hintText: "Add Comment"
)),
minLines: 1,
maxLines: 5,
controller: _controllerComment,
onChanged: (val) {
setState(() {
_counter.value += 1;
if (val.isNotEmpty) {
_hasComment = true;
} else {
_hasComment = false;
}
});
}),
trailing: ValueListenableBuilder(
valueListenable: _counter,
builder: (BuildContext context, int value, Widget? child) {
return IconButton(
onPressed: _hasComment
? () {
_commentOnSend();
}
: null,
icon: Icon(Icons.send_sharp,
color: _hasComment ? Colors.deepPurple : null),
);
},
),
),
],
),
),
);
}
}
Comment Model Class:
class CommentModel {
final String avatarUrl;
final String name;
final String dateTime;
final String comment;
CommentModel(
{required this.avatarUrl,
required this.name,
required this.dateTime,
required this.comment});
static List<CommentModel> dummyData = [
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/women/34.jpg",
name: "Laurent Oslo",
dateTime: "30 Dec 20 08:00",
comment:
"There is a reason why I implemented it like this. In a comment section, the same comment widget can appear multiple times. So, the keys assigned to each widget needs to be different. Otherwise I won’t be able to refer to a specific widget later on",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/women/49.jpg",
name: "Tracy Wilbur",
dateTime: "01 Oct 20 17:00",
comment: "First Comment!",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/women/23.jpg",
name: "Michael Scott",
dateTime: "30 Sept 20 06:00",
comment:
"The idea is simple. Use the prefix with something else to make the key unique. In this case, I’ve used the index value to make them unique. I used the keys in line 25, 51, 56, and 60. See how I’ve done it in these lines.",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/45.jpg",
name: "Williams John",
dateTime: "17 Sept 20 02:00",
comment: "Join!",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/women/77.jpg",
name: "Claire Rach",
dateTime: "15 Aug 20 19:00",
comment:
"I want the comment section to be hidden away. A user can view comments by tapping to expand a widget. Meaning, the comment section should be collapsible. It will toggle between expanded and collapsed mode when being tapped.",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/81.jpg",
name: "Joe Panama",
dateTime: "05 Jul 20 03:00",
comment:
"A comment will have 3 data values which are commenting user details, time of comment posting and the actual text of the comment. I’ve created a “CommentModel” class to create this model.",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/83.jpg",
name: "Mark Hamill",
dateTime: "09 Jun 20 15:00",
comment:
"Because comments are part of a post, “PostModel” needs to have a list of comment data. So I’ve modified “PostModel” to have a list of “CommentModel” objects. Refer to the code changes to see what I’ve done.",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/85.jpg",
name: "Williams Dafoe",
dateTime: "25 May 20 20:00",
comment:
"Notice lines 18 to 29. I’ve used the “ExpansionTile” widget to create a collapsible list of comments. Each comment is a “_SingleComment” widget implemented in lines 34 to 67.",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/98.jpg",
name: "Phillips Mach",
dateTime: "01 Apr 20 17:00",
comment:
"New to app development and flutter in general(high schooler). Can I use this template? Do I have to give credit or can I just use it? At the very least, can I see the source code so I can learn from it?",
),
CommentModel(
avatarUrl: "https://randomuser.me/api/portraits/men/12.jpg",
name: "Joe Snowden",
dateTime: "04 Mar 20 16:00",
comment: "PM ME!",
),
];
}
This is because you are putting your Listview.builder in a state variable which is expected behavior since state variables do not get reinitialized in rebuilds.
If you want to refactor you can create a new function to return it:
ListView getList(){
return ListView.builder(
itemCount: CommentModel.dummyData.length,
itemBuilder: (context, index) {
CommentModel _model = CommentModel.dummyData[index];
return Column(
children: <Widget>[
Divider(
height: 12.0,
),
Container(
padding: EdgeInsets.fromLTRB(3.0, 3.0, 3.0, 3.0),
child: ListTile(
leading: CircleAvatar(
radius: 24.0,
backgroundImage: NetworkImage(_model.avatarUrl),
),
title: Text(_model.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_model.comment),
Text(_model.dateTime),
],
),
),
),
],
);
},
);
}
Related
I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)
I am working on creating a hashMap for my flutter program and would like some input on it. As I created my hashMap in another dart file that is not the main dart file and I have no idea on how to connect it even when I created constrictors for the hashMaps. This is very important as the hashMap will be used on several files within the program hence why it is not in the main dart. Therefore I would like your guys input on how I could connect the two files.
This is part of my code:
main dart file:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
//Always use Stateless first then use stateful or stateless widgets afterward
#override
Widget build(BuildContext context) {
return MaterialApp(
//only used at the beginning of the program
title: 'The Cafe',
//just a title to the app it does not show for there is nothing telling it to show on the screen
debugShowCheckedModeBanner: false,
//takes out the ribbon at the top right corner of the screen and app
theme: ThemeData(
primarySwatch: Colors.green,
brightness: Brightness.dark,
fontFamily: 'georgia',
textTheme: TextTheme(headline1: TextStyle(fontSize: 100))
//controls the color of the very top part of the application
),
home: StartPage(),
//used to connect the Stateless widget to the Stateful widget below
);
}
}
class StartPage extends StatefulWidget {
#override
_StartPageState createState() => _StartPageState();
}
// do not forget the } prior to this comment if you do it will result in error and the program does not known why either
class _StartPageState extends State<StartPage> {
String value = "";
//stating the string is not seen until you have started to compute the drop-downs
//have the drop down's take you to the item page
//void main(){
//HashMap map = new HashMap<String, double>();
// LinkedHashMap linkedHashMap = new LinkedHashMap<int, String>();
// SplayTreeMap treeMap = new SplayTreeMap<int, String>();
//}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('The Campus Cafe'),
//where the main title is computed to be shown on the screen
centerTitle: true,
//centers the title
),
body: Center(
//This is Header that is after the main Title
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
//Header Container
Expanded(
child: Image.asset('assets/images/campus-cafe-logo-350sidebar.png',)
),
Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Text("Our Menu",style: TextStyle(fontSize: 30),
),
),
Expanded(
//Expands is used to create a body if you want a header and body...can also be used for other things but at the moment this is all I know
child: Column(
//there can be different types of Columns
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
//Padding is how far away one container or item is away from another as shown below
flex:3,
child: DropdownButton<String>(
//items are basically like an array or list
items: [
DropdownMenuItem<String>(
value: "1",
child: Center(
child: Text('Grilled Cheese'),
),
),
DropdownMenuItem<String>(
value: "2",
child: Center(
child: Text('Grilled Ham & Cheese'),
),
),
DropdownMenuItem<String>(
value: "3",
child: Center(
child: Text('BLT'),
),
),
DropdownMenuItem<String>(
value: "4",
child: Center(
child: Text('Western Chicken Sandwich'),
),
),
DropdownMenuItem<String>(
value: "5",
child: Center(
child: Text('Crispy Chicken Wrap'),
),
),
DropdownMenuItem<String>(
value: "6",
child: Center(
child: Text('Cheese Steak'),
),
),
],
onChanged: (_value) => {
print(_value.toString()),
setState(() {
value = _value;
}),
},
hint: Text('Sandwiches')
//This hint displays on your drop-box before you open it to see the items list
),
),
Flexible(
flex:3,
child: DropdownButton<String>(
items: [
DropdownMenuItem<String>(
value: "1",
child: Center(
child: Text('Quantum Burger'),
),
),
DropdownMenuItem<String>(
value: "2",
child: Center(
child: Text('Cheeseburger'),
),
),
DropdownMenuItem<String>(
value: "3",
child: Center(
child: Text('Double Cheeseburger 1/4'),
),
),
DropdownMenuItem<String>(
value: "4",
child: Center(
child: Text('Hamburger 1/4'),
),
),
DropdownMenuItem<String>(
value: "5",
child: Center(
child: Text('Cheeseburger'),
),
),
DropdownMenuItem<String>(
value: "6",
child: Center(
child: Text('Veggie Burger'),
),
),
],
onChanged: (_value) => {
print(_value.toString()),
setState(() {
value = _value;
}),
},
hint: Text('Burgers')),
),
This is my hashMap file:
List<String> sandwich = ["Veggie Melt", "Crispy Chicken Wrap", "Italian Meatball Sub",
"Chicken Parm Grinder", "Grill Cheese", "Grilled Ham & Cheese", "Bacon Bagel Melt"];
List<double> sandwichPrice = [4.50, 6.95, 6.99, 6.59, 3.59, 4.59, 5.29];
Map<String, double> map1 = Map.fromIterables(sandwich, sandwichPrice);
List<String> burgers = ["Veggie Burger", "The Quantum Burger", "Cafe Melt",
"The Bull Rider", "Double Cheese Burger", "Hamburger"];
List<double> burgerPrice = [4.99, 7.25, 6.59, 5.79, 5.89, 3.99, 3.79];
Map<String, double> map2 = Map.fromIterables(burgers, burgerPrice);
List<String> otherItems = ["Chicken Quesadilla", "Cheese Quesadilla",
"Chicken Strips", "Popcorn Chicken", "Jalapeno Poppers"];
List<double> otherItemsPrice = [6.79, 6.29, 4.99, 4.59, 3.49];
Map<String, double> map3 = Map.fromIterables(otherItems, otherItemsPrice);
List<String> sides = ["French Fries", "Onion Rings", "Jalapeno Cheese Curds",
"Tater Tots", "Pretzel Bites", "Nachos & Cheese"];
List<double> sidesPrice = [3.29, 4.79, 4.99, 3.19, 4.59, 3.50];
Map<String, double> map4 = Map.fromIterables(sides, sidesPrice);
List<String> pizza = ["7-inch Cheese", "7-inc with topping"];
List<double> pizzaPrice = [4.59, 4.99];
Map<String, double> map5 = Map.fromIterables(pizza, pizzaPrice);
class Menu {
String sandwich;
String burger;
String otherItems;
String sides;
String pizza;
double sandwichPrice;
double burgerPrice;
double otherItemsPrice;
double sidesPrice;
double pizzaPrice;
Menu.s(this.sandwich, this.sandwichPrice){}
Menu.b(this.burger, this.burgerPrice){}
Menu.o(this.otherItems, this.otherItemsPrice){}
Menu.q(this.sides, this.sidesPrice){}
Menu.p(this.pizza, this.pizzaPrice){}
}
First of all, you need to import your hashMap file. Then update your MyWidget as follows:
I have displayed here an example of how you can use the Dropdown with map1 (i.e, Sandwiches).
You have to just iterate over the keys of the hashmap 'map1' & create the list of DropdownMenuItem from it & pass this list to the items property.
For each dropdown, you will need to save the selected option, so instead of using String value = '';, I have changed it to String selectedSandwich = 'Sandwiches';. This part is crucial as the Dropdown widget can have its value only as one of the options available. So, if you do not have the Sandwiches option in your dropdown list, there will be an error. Hence, I have added the Sandwiches option in the initState of your MyAppWidget.
You have to do the same for the remaining of the hashmaps. Let me know if you need any more help.
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Instead of value use selectedSandwich
String selectedSandwich = 'Sandwiches';
// List of sandwiches, you have to do the same for rest of the maps
// like: burgers, otherItems etc.
List<String> sandwiches = map1.keys.toList();
#override
void initState() {
super.initState();
// Adding sandwiches as an option is necessay as the dropdown's value
// must be equal to one of its options.
// I have done this only for sandwiches, but you need to do the same
// for rest.
sandwiches.insert(0, 'Sandwiches');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DropdownButton<String>(
items: sandwiches // using map1.keys.toList
.map(
(e) => DropdownMenuItem<String>(
value: e,
child: Center(
child: Text(e),
),
),
)
.toList(),
onChanged: (_value) => {
print(_value.toString()),
setState(() {
selectedSandwich = _value; // Set selected sandwich
}),
},
value: selectedSandwich, // Use value
),
],
),
);
}
}
I'm using a listview to display images and text in my flutter app. I've stored the asset path and text in a Json file and I convert it to a list. Getting the image asset path and displaying the correct one seems to work with no issues but thats not the case in playing the audio files from their assets.
I'm using this package import 'package:assets_audio_player/assets_audio_player.dart';
declaration final AssetsAudioPlayer playAudio = AssetsAudioPlayer();
and this is main widget
#override
Widget build(BuildContext context) {
Widget _buildRow(int idx) {
for (var translations in widget.category.translations) {
_wordList = widget.category.translations[idx];
return Container(
height: 88.0,
child: Card(
child: ListTile(
onTap: () {
playAudio.open(
Audio(_wordList.audio),
);
// player.play(_wordList.audio);
log(_wordList.audio, name: 'my.other.category');
},
onLongPress: () {},
leading: SizedBox(
width: 50.0,
height: 88.0,
child: Image(
image: AssetImage(_wordList.emoji),
fit: BoxFit.contain,
),
),
title: Text(
_wordList.akan,
style: TextStyle(fontSize: 18),
),
subtitle: Text(
_wordList.english,
style: TextStyle(fontSize: 18, color: Colors.black),
),
trailing: const Icon(Icons.play_arrow, size: 28),
),
),
);
}
}
Since the image assets in the json file have no issues I don't get why the audio does
I've stored them like this,
{
"english": "mother",
"akan": "ɛna",
"emoji": "assets/icons/family_mother.png",
"audio": "assets/audio/family_mother.mp3"
},
Solved it by generating a new listtile widget through iteration and then putting it into a listview
I have implemented an listener in the following way:
#override
void didChangeDependencies() {
final SlotDataProvider slotDevice = Provider.of<SlotDataProvider>(context);
spptask.streamdevice.listen((device) {
setState(() {
slotDevice._devices[0].name = device.name;
print("Device data received: ${device.name} ");
});
}, onError: (error) {
print("Error: $error.message");
});
super.didChangeDependencies();
}
I listen on a splitted controller and the print "Device data received:..." is called but the widget is not actualized. In the build method I do the following:
...
#override
Widget build(BuildContext context) {
final slotProvider = Provider.of<SlotDataProvider>(context);
final deviceProvider = Provider.of<DeviceDataProvider>(context);
Device slotDevice = slotProvider.getDevice(widget.slot);
Device device = deviceProvider.getDevice(widget.slot);
_dropdownMenuItems = buildDropdownMenuItems(deviceProvider.get());
return ListTile(
title: Row(
children: <Widget>[
SizedBox(
width: 140,
child: DropdownButton(
isExpanded: true,
disabledHint: Text(slotDevice.name),
hint: Text(slotDevice.name),
value: device,
items: _dropdownMenuItems,
onChanged: (value) {
device.setDevice(value);
slotDevice.setDevice(value);
}),
),
SizedBox(width: 10),
SizedBox(width: 60, child: Text('SLOT#${slotDevice.slot}')),
],
),
subtitle: Text(slotDevice.bdaddr, style: TextStyle(fontSize: 10.0)),
leading: SizedBox(
height: 40,
width: 35,
child: UsbBatteryImageAsset(slot: widget.slot),
),
trailing: Icon(Icons.keyboard_arrow_right),
);
}
}
What is missing in the above code. The SlotDataProvider is a fix list of "Device" with attributes such as name, id and so on.
#EDIT
The problem has to do with the combobox. If I change an other field, it works.
Usually for widgets to be rebuilt based on the data updated we use streambuilders
this will cause the widget to rebuild every time there is a change in the stream
it seams that your widget is being built once with the first listening of the data
have you tried wrapping the gridview in a stateful builder ?
I'm trying to create stacks of cards in my Flutter project. Each card contains different data/information and when I try visualize with a dummy data, I have to use a lot of variables which is pretty much repeating variable name for each card. Is there aways to make a reusable card component in flutter so that I can make it clear and simple because when I use real data in the future, I might have more than 2 cards in a group and they will also have different data. Any suggestion will be really appreciated.
class MyConstructor {
MyConstructor({this.jonathan1,this.jonathan2,this.jonathan3});
}
class StackedCardsState extends State<HomePage> {
List<MyConstructor> cards = [
MyConstructor(h1: "Hello", h2: "hello3")
];
/////
Padding(
padding: EdgeInsets.all(15.0),
child: Column(children: [
Text(MyConstructor.hey, style: TextStyle(fontWeight: FontWeight.bold),),
Text(MyConstructor.hey),
Text(MyConstructor.hey, style: TextStyle(color: Colors.red[500]),),
VerticalDivider(color: Colors.blue),
])),
Your problem is first of all rather simple, you are violating the DRY concept (Don't repeat yourself, https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ).
As soon as you start copy pasting code take a moment and think about your code and how you can abstract it into a reusable component.
Another big issue that I think you are lacking is variable naming. It is a very very important part of writing code. Might seem trivial but it will be very hard to understand what a variable named cardOne1 and cardTwo2 actually mean. What is the purpose of that variable? What does it do?
Now with that said I understand your app has something to do with car sales but other than that I'm not really sure what I'm looking at. There for I will have a harder time finding a good variable for this code but here is an example.
So lets break down the contents in the card to a single reusable widget, we can also make a data class (or model) for storing the data that we then give to the widget.
//car_details.dart
class CarDetails {
String title;
String diffNumber;
String diffPercent;
Color colorIndicator;
CarDetails({
this.title,
this.diffNumber,
this.diffPercent,
this.colorIndicator,
});
}
//car_card_details.dart
class CarCardDetails extends StatelessWidget {
final double padding;
final CarDetails carDetails;
CarCardDetails({
this.carDetails,
this.padding = 15,
});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
carDetails.colorIndicator != null
? Container(
color: carDetails.colorIndicator,
height: 60,
width: 2,
)
: Container(),
Padding(
padding: EdgeInsets.all(padding),
child: Column(children: [
Text(carDetails.title),
Text(carDetails.diffNumber),
Text(carDetails.diffPercent),
VerticalDivider(color: Colors.blue),
])),
],
);
}
}
To use this component we make a CarCard Widget that takes a title and a list of CarDetails like so:
// car_card.dart
class CarCard extends StatelessWidget {
final String title;
final List<CarDetails> carDetails;
CarCard({this.title, this.carDetails});
#override
Widget build(BuildContext context) {
List<Widget> detailRow = List();
if (carDetails != null) {
carDetails.forEach((element) {
detailRow.add(CarCardDetails(
top: element.title,
middle: element.diffNumber,
bottom: element.diffPercent,
lineColor: element.colorIndicator,
));
});
}
return Container(
//height: 150, //I would not hardcode the height, let the childrent expand the widget instead
child: SingleChildScrollView(
child: Card(
elevation: 8.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
child: InkWell(
child: Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Spacer(),
Icon(Icons.favorite)
]),
),
Divider(color: Colors.black),
Row(children: detailRow),
]),
),
),
),
);
}
}
And instead of saving all the variables you had in app we can now make them into a list of CarDetails where each element contains the strings.
// some other widget
...
List<CarDetails> carDetails = [
CarDetails(
title: "2 hrs ago",
diffNumber: "+/ TRACK",
diffPercent: "% to DBJ",
),
CarDetails(
title: "CHEVEROLET",
diffNumber: "-2706",
diffPercent: "42.2%",
colorIndicator: Colors.red,
),
CarDetails(
title: "BUICK",
diffNumber: "+300",
diffPercent: "50%",
colorIndicator: Colors.green,
),
CarDetails(
title: "GMC",
diffNumber: "-712",
diffPercent: "52.1%",
colorIndicator: Colors.black26,
),
];
#override
Widget build(BuildContext context) {
return CarCard(
title: "US Daily Retail Delieveries by Brand",
carDetails: carDetails,
);
}
...
This can of course be abstracted even further with the groups of cards etc, etc. But I hope you get the idea.
This is an example of how you could do it, with that said I do not know what data you are intending to use and how you want to structure it. So consider this a starting point and take it from there. :)