Flutter GetX state management: how to update color attribute in ListView.builder and refresh the list? - flutter-getx

I am trying to make an observable list. Specifically:
Show a list with names. The list is pulled from a list of objects containing a name key and a isSelected bool key.
List<Student> students = [
Student(name: "John", isSelected: false),
Student(name: "Boris", isSelected: false),
Student(name: "Max", isSelected: false)
];
The names are displayed in a ListView.builder. Clicking on the name should set the corresponding isSelected variable to true. This all works up to this point.
I want the list to refresh so that if isSelected is set to true, the item will show in a different color.
child: Text(
students[index].name,
style: TextStyle(
color: students[index].isSelected == true
? Colors.red
: Colors.black87,
),
),
The problems I run into are:
I get the error "Improper use of GetX. You should only use GetX or Obx for the specific widget that will be updated" no matter which widget I wrap into Obx. I have not included Obx in the code below as it leads to an error.
I have read that lists are reactive but the items inside it are not and need to be made observable. I am not clear how this is done in this instance.
I paste the complete code below. Thank you for your help and apologies for asking something that's likely pretty basic.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() => runApp(testApp());
class Student {
String name;
bool isSelected;
Student({required this.name, required this.isSelected});
}
class Controller extends GetxController {
var students = <Student>[].obs;
}
class testApp extends StatelessWidget {
#override
List<Student> students = [
Student(name: "John", isSelected: false),
Student(name: "Boris", isSelected: false),
Student(name: "Max", isSelected: false)
];
Widget build(BuildContext context) {
final controller = Get.put(Controller());
return MaterialApp(
home: Material(
child: ListView.builder(
itemCount: students.length,
itemBuilder: (BuildContext ctxt, int index) {
return InkWell(
onTap: () {
students[index].isSelected = true;
// refresh does not work
// controller.students.refresh();
},
child: Text(
students[index].name,
style: TextStyle(
color: students[index].isSelected == true
? Colors.red
: Colors.black87,
),
),
);
}),
));
}
}

The solution was to controller.students.refresh() the list. Posting the complete working code below.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() => runApp(testApp());
class Student {
String name;
bool isSelected;
Student({required this.name, required this.isSelected});
}
class Controller extends GetxController {
Rx<List<Student>> students = Rx<List<Student>>([
Student(name: "John", isSelected: false),
Student(name: "Boris", isSelected: false),
Student(name: "Max", isSelected: false)
]);
}
class testApp extends StatelessWidget {
#override
//
final controller = Get.put(Controller());
Widget build(BuildContext context) {
final controller = Get.put(Controller());
return MaterialApp(
home: Material(
child: Obx(
() => ListView.builder(
itemCount: controller.students.value.length,
itemBuilder: (BuildContext ctxt, int index) {
return InkWell(
onTap: () {
controller.students.value[index].isSelected = true;
controller.students.refresh();
},
child: Text(
controller.students.value[index].name,
// students[index].name,
style: TextStyle(
color: controller.students.value[index].isSelected == true
? Colors.red
: Colors.black87,
),
),
);
}),
),
));
}
}

Related

ToggleButton in Bloc

I am trying to implement a ToggleButton using the Bloc pattern provided by Flutter. The code is very simple, but there is a problem. When I try to update my index so that it can be emitted from the Bloc to change my value from bool, the whole thing will do nothing. I have tried rewriting the code to look for the problem, but my guess is that the update will not go through. Does anybody know how to solve this issue?
Here is my Bloc:
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
part 'calculate_event.dart';
part 'calculate_state.dart';
class CalculateBloc extends Bloc<CalculateEvent, CalculateState> {
///Constructor
CalculateBloc()
: super(CalculateInitial(
selection: List.filled(3, false),
totalAmount: '',
textEditingController: TextEditingController())) {
///On-Method to emit the State
on<CalculateEvent>((event, emit) {
///UpdateSelectionEvent
if (event is UpdateSelectionEvent) {
///emit the Index of the button-press
final updateSelect = List.of(state.selection);
for (int i = 0; i < updateSelect.length; i++) {
updateSelect[i] = event.selectIndex == i;
}
emit(UpdateSelectionState(
selection: updateSelect,
totalAmount: state.TotalAmount,
tip: state.tip,
textEditingController: state.textEditingController));
}
///CalculatePercentageEvent
if (event is CalculatePercentageEvent) {
final controller = state.textEditingController;
final totalAmount = double.parse(controller.text);
final selectedIndex = state.selection.indexWhere((element) => element);
final tipPercentage = [0.1, 0.15, 0.2][selectedIndex];
final tipTotal = (totalAmount * tipPercentage).toStringAsFixed(2);
emit(CalculatePercentageState(
selection: state.selection,
totalAmount: totalAmount.toString(),
tip: tipTotal,
textEditingController: state.textEditingController));
}
});
}
}
Here is my State-class:
part of 'calculate_bloc.dart';
#immutable
abstract class CalculateState extends Equatable {
final List<bool> selection;
final String? tip;
final String TotalAmount;
final TextEditingController textEditingController;
CalculateState(
{this.tip = '',
required this.TotalAmount,
required this.selection,
required this.textEditingController});
#override
List<Object?> get props => [selection, tip, TotalAmount];
}
class CalculateInitial extends CalculateState {
CalculateInitial(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
textEditingController: textEditingController);
}
class CalculatePercentageState extends CalculateState {
CalculatePercentageState(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
tip: tip,
textEditingController: textEditingController);
}
class UpdateSelectionState extends CalculateState {
UpdateSelectionState(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
tip: tip,
textEditingController: textEditingController);
}
Here is my Event-class:
part of 'calculate_bloc.dart';
#immutable
abstract class CalculateEvent {}
class CalculatePercentageEvent extends CalculateEvent {}
class UpdateSelectionEvent extends CalculateEvent {
final int selectIndex;
UpdateSelectionEvent(this.selectIndex);
}
And my UI:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_day_1/bloc/calculate_bloc.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CalculateBloc(),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<CalculateBloc, CalculateState>(
bloc: BlocProvider.of<CalculateBloc>(context),
builder: (context, state) {
if (state.tip != null) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(state.tip ?? '',
style: const TextStyle(fontSize: 30)),
),
const Text('Total Amount'),
SizedBox(
width: 80,
child: TextField(
controller: state.textEditingController,
keyboardType:
const TextInputType.numberWithOptions(),
textAlign: TextAlign.center,
decoration:
const InputDecoration(hintText: '\$100.00'),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: ToggleButtons(
onPressed: (index) =>
BlocProvider.of<CalculateBloc>(context)
.add(UpdateSelectionEvent(index)),
isSelected: state.selection,
children: const [
Text('10%'),
Text('15%'),
Text('20%'),
],
),
),
],
);
} else {
return Container(
child: const Text('flase'),
);
}
}),
ElevatedButton(
onPressed: () {
BlocProvider.of<CalculateBloc>(context)
.add(CalculatePercentageEvent());
},
child: const Text('Calculate Amount'),
),
],
),
),
),
);
}
}
I have tried to utalize some of the BlocListener but it will not work. My other guess is to use additionl states to emit it.
I have found the problem. On the HomePage, the BlocProvider keeps building the app with the CalculateInitialState so that the new value is not passed through.

Infinite-scroll listview.builder - to expand or not to expand... and more provider value not updating and how to fix "RenderFlex overflowed"

I am trying to build a view/route that will list items fetched from a REST source.
I want to show a notification item below the list while the data is being fetched.
But my ListView builder is constructed around the fetched data's structure, so I figured just have a ListTile fit some appropriate UX elements below the generated list inside a Column - which was kinda working great - or so I thought - until the list grows to fill the screen causing RenderFlex overflowed error. Wrapping the ListView builder in Expanded fixed that but moved the indicator to the bottom of the screen.
In trying to fix it I seem to have broken more of the plumbing and the boolean variable that should control the idicator widget; isLoading: stockSet.isBusyLoading doesn't seem to update.
At the moment if I hardcode it as `` it does sit in the appropraite position but I am back with the RenderFlex overflow.
Once all of this is working I'll be wanting to automatically load items untill the screen is full - not sure where I'll be triggering that from yet.
class MyStockSet extends StatefulWidget {
const MyStockSet({super.key});
static const indexStr = 'stocks';
static const labelStr = 'Properties';
#override
State<MyStockSet> createState() => _MyStockSetState();
}
class _MyStockSetState extends State<MyStockSet> {
#override
Widget build(BuildContext context) {
const String imagePath = 'assets/images/${MyStockSet.indexStr}.png';
var assetImage = const AssetImage(imagePath);
//var stockSet = context.watch<StockSet>(); <- didn't work either
var stockSet = Provider.of<StockSet>(context,listen: false);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
AscHero(
assetImage: assetImage,
tag: MyStockSet.indexStr,
title: MyStockSet.labelStr,
radius: 32,
),
const SizedBox(width: 12),
const Text(MyStockSet.labelStr),
],
),
actions: [
IconButton(
onPressed: () {
var stockSet = context.read<StockSet>();
int newNr = stockSet.stocks.length + 1;
Stock tmpstock = Stock(
id: newNr,
title: 'test$newNr',
thumbUrl: 'url',
description: 'desc');
stockSet.add(tmpstock);
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () {
developer.log('btn before isBusyLoading ${stockSet.isBusyLoading}');
stockSet.fetch();
developer.log('after btn isBusyLoading ${stockSet.isBusyLoading}');
},
icon: const Icon(Icons.handshake),
),
],
),
body: Column(
children: [
Row(
// these will be filters, order toggle etc.
children: [
ElevatedButton(
onPressed: () => developer.log('Btn pressed.'),
child: Text('Btn')),
],
),
Expanded(
child: Column(children: [
_StockListView(),
LoadingStockListItemNotif(
isLoading: true,
),
]),
),
],
),
);
}
}
class _StockListView extends StatefulWidget {
#override
State<_StockListView> createState() => _StockListViewState();
}
class _StockListViewState extends State<_StockListView> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
developer.log('_StockListView didChangeDependencies()');
// developer.log('scroll pos ${_scrollController.position}');
}
#override
Widget build(BuildContext context) {
var stockSet = context.watch<StockSet>();
return ListView.builder(
// controller: _scrollController,
shrinkWrap: true,
itemCount: stockSet.stocks.length,
itemBuilder: (context, index) => InkWell(
child: StockListItem(
stock: stockSet.stocks[index],
),
onTap: () => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(stockSet.stocks[index]),
),
),
);
}
void _scrollListener() {
developer.log('_scrollListener');
}
}
and
class StockSet extends ChangeNotifier {
final List<Stock> _stocks = [];
late bool isBusyLoading = false;
List<Stock> get stocks => _stocks;
void add(Stock stock) {
_stocks.add(stock);
developer.log('added stock :${stock.title}');
notifyListeners();
}
void remove(Stock stock) {
_stocks.remove(stock);
notifyListeners();
}
Future<void> fetch() async {
developer.log('fetch() iL T');
isBusyLoading = true;
notifyListeners();
Stock tmpStock = await _fetchAStock();
developer.log('fetch() iL F');
isBusyLoading = false;
notifyListeners();
add(tmpStock);
}
Future<Stock> _fetchAStock() async {
developer.log('fetch stock ');
final response = await http.get(
Uri.https(
//...
),
);
developer.log('response.statusCode:${response.statusCode}');
if (response.statusCode == 200) {
final Map<String, dynamic> map = json.decode(response.body);
return Stock(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
thumbUrl: map['thumbUrl'] as String,
);
}
throw Exception('error fetching stocks:');
}
}
Apologies for the convoluted question.
Add mainAxisSize : MainAxisSize.min for the column inside the expanded widget. The expanded doesn't have any bounds and that's why it throws an error. You can wrap the column with a SingleChildScrollView if you have long content to display
This worked for me!
Just set the shrinkWrap attribute to true
Main lesson:
Don't fight the framework.
Answer:
Instead of tying yourself into Möbius knots trying to put the ListView's functionality outside of itself; use the fact that the ListView.builder allows you to sculpt the logic of how it gets built and what it will contain - given that the provider can trigger its rebuild when the variable in the data set changes.
In other words; by increasing the loop of the builder, you can insert a kind of footer to the Listview. The appearance (or not) of that can depend on the provider, provided it fires the appropriate notifyListeners()s etc.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:equatable/equatable.dart';
import 'dart:async';
class ItemSetRoute extends StatefulWidget {
const ItemSetRoute({Key? key}) : super(key: key);
#override
State<ItemSetRoute> createState() => _ItemSetRouteState();
}
class _ItemSetRouteState extends State<ItemSetRoute> {
#override
Widget build(BuildContext context) {
var itemSet = Provider.of<ItemSet>(
context,
listen: true /* in order to rebuild */,
);
return Scaffold(
appBar: AppBar(title: const Text('Test'), actions: [
IconButton(
onPressed: () {
itemSet.fetch();
},
icon: const Icon(Icons.download),
)
]),
body: Column(
//screen
children: [
Row(
children: [
ElevatedButton(
onPressed: () {
itemSet.fetch();
},
child: const Text('Btn')),
],
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: itemSet.items.length + 1,
itemBuilder: (context, index) {
/* logic here to create a kind of footer of the ListView */
if (index <= itemSet.items.length - 1) {
return InkWell(
child: ItemListItem(
item: itemSet.items[index],
),
onTap: () {
//('Item tapped, navigate etc.');
});
} else {
return LoadingItemNotifier(
isLoading: itemSet.isBusyLoading,
);
}
},
),
),
],
),
);
}
}
//Models
class ItemListItem extends StatelessWidget {
const ItemListItem({Key? key, required this.item}) : super(key: key);
final Item item;
#override
Widget build(BuildContext context) {
return Material(
child: ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
);
}
}
class LoadingItemNotifier extends StatefulWidget {
const LoadingItemNotifier({Key? key, this.isLoading = false})
: super(key: key);
final bool isLoading;
#override
State<LoadingItemNotifier> createState() => _LoadingItemNotifierState();
}
class _LoadingItemNotifierState extends State<LoadingItemNotifier> {
#override
Widget build(BuildContext context) {
if (widget.isLoading) {
return Material(
child: ListTile(
leading: SizedBox(
width: 48,
height: 48,
child: ClipOval(
child: Material(
color: Colors.lightBlue.withOpacity(0.25),
child: const Center(
child: Icon(Icons.download),
),
),
),
),
title: const Text('Loading'),
isThreeLine: true,
subtitle: const Text('One moment please...'),
dense: true,
),
);
} else {
return const SizedBox(height: 0);
}
}
}
class ItemSet extends ChangeNotifier {
final List<Item> _items = [];
late bool isBusyLoading = false;
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
Future<void> fetch() async {
isBusyLoading = true;
notifyListeners();
/* handling REST call here */
await Future.delayed(const Duration(milliseconds: 500));
Item newItem = const Item(id: 123, title: 'Title', description: 'Desc');
isBusyLoading = false;
add(newItem);
}
}
class Item extends Equatable {
const Item({
required this.id,
required this.title,
required this.description,
});
final int id;
final String title;
final String description;
#override
List<Object> get props => [id, title, description];
}
Caveats
I don't know if this is the most efficient way of doing this - perhaps there should be fewer states, etc. ...

Troubles with making a Favorite page with Hive DB Flutter

Hello everyone here's my test app and I have some problems with making a Favorite page section where you can tap on button and add the item into fav page.
I'm receiving a data from API and implementing it by Listview.builder
Here are some photos of how it should look like:
Home page
Favorite page
main.dart, here I'm openning a box called 'favorites_box'
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async{
await GetStorage.init();
await Hive.openBox('favorites_box');
runApp(MainPage());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyApp()),
GetPage(name: '/main-page', page: () => MainPage()),
GetPage(name: '/favorite_page', page: () => FavoritePage()),
// Dynamic route
],
home: MainPage(),
);
}
}
Well here's a code of home page:
main_page.dart
import 'package:flutter/material.dart';
import '../View/listview_api.dart';
class MainPage extends StatefulWidget {
#override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int currentIndex = 0;
List<BottomNavigationBarItem>? items;
final screens = [
HomePage(),
HomePage()
FavoritePage(),
HomePage()
];
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Container(
width: double.infinity,
height: 40,
color: Colors.white,
child: Center(
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
),
hintText: 'Searching',
prefixIcon: Icon(Icons.search),
suffixIcon: Icon(Icons.notifications)),
),
),
),
),
body: screens[currentIndex],
bottomNavigationBar: BottomNavigationBar(
unselectedItemColor: Colors.grey,//AppColors.unselectedBottomNavItem,
selectedItemColor: Colors.blue,//AppColors.assets,
onTap: (index) => setState(() {
currentIndex = index;
}),//controller.setMenu(BottomMenu.values[pos]),
//currentIndex: ,//controller.bottomMenu.index,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
currentIndex: currentIndex,
selectedLabelStyle: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
),
elevation: 8,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
backgroundColor: Colors.blue,
),
BottomNavigationBarItem(
icon: Icon(Icons.add_shopping_cart),
label: 'Shopping cart',
backgroundColor: Colors.red,
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Favorite',
backgroundColor: Colors.green,
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
backgroundColor: Colors.yellow,
),
],
),
),
),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//Image.asset('images/image0.jpg'),
SizedBox(
height: 25.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'New!',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () {},
icon: Icon(
Icons.arrow_forward_outlined,
),
),
],
),
SizedBox(
height: 25.0,
),
SizedBox(
height: 300.0,
width: double.infinity,
child: ListViewAPI(),
),
],
),
),
),
),
);
}
}
And now, below is a code of ListViewAPI(), here I've added the elements which I tap to the box('favorites_box'): listview_api.dart
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
String? stringResponse;
Map? mapResponse;
Map? dataResponse;
List? listResponse;
class ListViewAPI extends StatefulWidget {
const ListViewAPI({Key? key}) : super(key: key);
#override
_ListViewAPIState createState() => _ListViewAPIState();
}
class _ListViewAPIState extends State<ListViewAPI> {
Future apiCall() async {
http.Response response;
response = await http.get(Uri.parse("https://api.client.macbro.uz/v1/product"));
if(response.statusCode == 200) {
setState(() {
// stringResponse = response.body;
mapResponse = jsonDecode(response.body);
listResponse = mapResponse!['products'];
});
}
}
#override
void initState() {
super.initState();
apiCall();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
listResponse![index]['image'],
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box('favorites_box').put(listResponse![index]['image'], listResponse);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: listResponse == null ? 0 : listResponse!.length,
),
),
);
}
}
So here, I created a list, and tried to save the elements from box named "favorites_box" and got data which was added while I tap favorite IconButton upper but without success( :
favorite_page.dart
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../View/gridview_api.dart';
class FavoritePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder(
valueListenable: Hive.box('favorites_box').listenable(),
builder: (context, box, child) {
List posts = List.from(Hive.box('favorites_box').values);
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Column(
children: [
Text(
'List of favorite products'
),
Card(
child: posts[index] == null ? Text('nothing(') : posts[index],
// child: Hive.box('favorites_box').get(listResponse),
),
],
);
},
);
},
),
);
}
}
I'll be grateful if someone could help me with this problem, as I'm trying to fix this issue for a couple of days
P.s. I'm so sorry for some inconveniences, I'm a novice yet that's why hope you'll understand me
Thanks!
Alright. I now have a solution. It is a bit more complex than what you started with but it worked during testing.
Using https://marketplace.visualstudio.com/items?itemName=hirantha.json-to-dart I created a model class from the API data JSON. One for the Product and one for the Price map inside of Product.
product_model.dart
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'price.dart';
part 'product_model.g.dart';
#HiveType(typeId: 1)
class ProductModel extends Equatable {
#HiveField(0)
final String? id;
#HiveField(1)
final String? name;
#HiveField(2)
final String? slug;
#HiveField(3)
final bool? active;
#HiveField(4)
final String? image;
#HiveField(5)
final String? code;
#HiveField(6)
final String? order;
#HiveField(7)
final int? cheapestPrice;
#HiveField(8)
final Price? price;
#HiveField(9)
final int? discount;
const ProductModel({
this.id,
this.name,
this.slug,
this.active,
this.image,
this.code,
this.order,
this.cheapestPrice,
this.price,
this.discount,
});
factory ProductModel.fromMap(Map<String, dynamic> data) => ProductModel(
id: data['id'] as String?,
name: data['name'] as String?,
slug: data['slug'] as String?,
active: data['active'] as bool?,
image: data['image'] as String?,
code: data['code'] as String?,
order: data['order'] as String?,
cheapestPrice: data['cheapest_price'] as int?,
price: data['price'] == null
? null
: Price.fromMap(data['price'] as Map<String, dynamic>),
discount: data['discount'] as int?,
);
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
'slug': slug,
'active': active,
'image': image,
'code': code,
'order': order,
'cheapest_price': cheapestPrice,
'price': price?.toMap(),
'discount': discount,
};
/// `dart:convert`
///
/// Parses the string and returns the resulting Json object as [ProductModel].
factory ProductModel.fromJson(String data) {
return ProductModel.fromMap(json.decode(data) as Map<String, dynamic>);
}
/// `dart:convert`
///
/// Converts [ProductModel] to a JSON string.
String toJson() => json.encode(toMap());
ProductModel copyWith({
String? id,
String? name,
String? slug,
bool? active,
String? image,
String? code,
String? order,
int? cheapestPrice,
Price? price,
int? discount,
}) {
return ProductModel(
id: id ?? this.id,
name: name ?? this.name,
slug: slug ?? this.slug,
active: active ?? this.active,
image: image ?? this.image,
code: code ?? this.code,
order: order ?? this.order,
cheapestPrice: cheapestPrice ?? this.cheapestPrice,
price: price ?? this.price,
discount: discount ?? this.discount,
);
}
#override
bool get stringify => true;
#override
List<Object?> get props {
return [
id,
name,
slug,
active,
image,
code,
order,
cheapestPrice,
price,
discount,
];
}
}
price.dart
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:hive_flutter/hive_flutter.dart';
part 'price.g.dart';
#HiveType(typeId: 2)
class Price extends Equatable {
#HiveField(0)
final int? price;
#HiveField(1)
final int? oldPrice;
#HiveField(2)
final int? uzsPrice;
#HiveField(3)
final int? secondPrice;
#HiveField(4)
final int? secondUzsPrice;
const Price({
this.price,
this.oldPrice,
this.uzsPrice,
this.secondPrice,
this.secondUzsPrice,
});
factory Price.fromMap(Map<String, dynamic> data) => Price(
price: data['price'] as int?,
oldPrice: data['old_price'] as int?,
uzsPrice: data['uzs_price'] as int?,
secondPrice: data['second_price'] as int?,
secondUzsPrice: data['second_uzs_price'] as int?,
);
Map<String, dynamic> toMap() => {
'price': price,
'old_price': oldPrice,
'uzs_price': uzsPrice,
'second_price': secondPrice,
'second_uzs_price': secondUzsPrice,
};
/// `dart:convert`
///
/// Parses the string and returns the resulting Json object as [Price].
factory Price.fromJson(String data) {
return Price.fromMap(json.decode(data) as Map<String, dynamic>);
}
/// `dart:convert`
///
/// Converts [Price] to a JSON string.
String toJson() => json.encode(toMap());
Price copyWith({
int? price,
int? oldPrice,
int? uzsPrice,
int? secondPrice,
int? secondUzsPrice,
}) {
return Price(
price: price ?? this.price,
oldPrice: oldPrice ?? this.oldPrice,
uzsPrice: uzsPrice ?? this.uzsPrice,
secondPrice: secondPrice ?? this.secondPrice,
secondUzsPrice: secondUzsPrice ?? this.secondUzsPrice,
);
}
#override
bool get stringify => true;
#override
List<Object?> get props {
return [
price,
oldPrice,
uzsPrice,
secondPrice,
secondUzsPrice,
];
}
}
I then used https://docs.hivedb.dev/#/custom-objects/generate_adapter to create adapters for both of those. You can read the documentation to see how that is done using build_runner and the hive_generator packages.
In main.dart I registered both of the adapters and opened up a box with the ProductModel type from product_model.dart.
main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:test/product_model/price.dart';
import 'package:test/product_model/product_model.dart';
import 'favorite_page.dart';
import 'homepage.dart';
void main() async {
// await GetStorage.init();
await Hive.initFlutter();
Hive.registerAdapter(PriceAdapter());
Hive.registerAdapter(ProductModelAdapter());
await Hive.openBox<ProductModel>('favorites_box');
runApp(MainPage());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyApp()),
GetPage(name: '/main-page', page: () => MainPage()),
GetPage(name: '/favorite_page', page: () => FavoritePage()),
// Dynamic route
],
home: MainPage(),
);
}
}
listview_api.dart is mostly the same with the exception of mapping the products from listResponse to ProductModel objects.
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:test/product_model/product_model.dart';
String? stringResponse;
Map? mapResponse;
Map? dataResponse;
List? listResponse;
class ListViewAPI extends StatefulWidget {
const ListViewAPI({Key? key}) : super(key: key);
#override
_ListViewAPIState createState() => _ListViewAPIState();
}
class _ListViewAPIState extends State<ListViewAPI> {
Future apiCall() async {
http.Response response;
response =
await http.get(Uri.parse("https://api.client.macbro.uz/v1/product"));
if (response.statusCode == 200) {
setState(() {
// stringResponse = response.body;
mapResponse = jsonDecode(response.body);
listResponse = mapResponse!['products'];
listResponse =
listResponse!.map((e) => ProductModel.fromMap(e)).toList(); // Map all of the products in listResponse to a ProductModel object.
});
}
}
#override
void initState() {
super.initState();
apiCall();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
listResponse![index].image!,
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box<ProductModel>('favorites_box').put(
listResponse![index].image, listResponse![index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: listResponse == null ? 0 : listResponse!.length,
),
),
);
}
}
homepage.dart is unchanged.
favorite_page.dart was changed to a stateful widget and then gets the box values on init.
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:test/product_model/product_model.dart';
class FavoritePage extends StatefulWidget {
#override
State<FavoritePage> createState() => _FavoritePageState();
}
class _FavoritePageState extends State<FavoritePage> {
var posts;
#override
void initState() {
super.initState();
posts = Hive.box<ProductModel>('favorites_box').values.toList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
posts[index].image!,
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box<ProductModel>('favorites_box')
.delete(posts[index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: posts == null ? 0 : posts.length,
),
);
}
}
I really encourage you to read the documentation on Hive as it contains a wealth of information. Another tip when coding with hive is to make sure you are clearing out the storage and cache for your emulator or physical device regularly. I have had too many headaches dealing with errors in Hive simply because I forgot to clear the storage and cache which was resulting in bad data despite having changed my source code.
I don't believe this is a problem with your code. However, I do recommend creating a model class for your data and maybe using a FutureBuilder https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html.
I believe the problem is that you have not updated your AndroidManifest.xml file to allow for internet connectivity.
Try adding:
<uses-permission android:name="android.permission.INTERNET" />
to your android\app\src\main\AndroidManifest.xml, above <application.
Further reading: https://flutter-examples.com/add-permissions-in-androidmanifest-xml-file/
After taking a closer look at your issue, I think I figured out the problem.
Hive requires an init:
void main() async {
// await GetStorage.init(); // Not sure why this was here but doesn't seem to be needed.
await Hive.initFlutter();
await Hive.openBox('favorites_box');
runApp(MainPage());
}
You were also missing a comma in main_page.dart
final screens = [
HomePage(),
HomePage() <----
FavoritePage(),
HomePage()
];
For your favorites page, I replaced the ValueListenableBuilder with just a ListView.builder:
class FavoritePage extends StatelessWidget {
List posts = List.from(Hive.box('favorites_box').values);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
posts[index]['image'],
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box('favorites_box').delete(posts[index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: posts == null ? 0 : posts.length,
),
);
}
}
There is still an error when you try to use this that says that posts[index]['image'] type 'String' is not a subtype of type 'int' of 'index' but you can easily fix this by creating a model class and accessing everything with those properties. Using model class in flutter (dart) here is an example of a model class. Instead of using DocumentSnapshot, you can add a toList() or toMap() method.
Hope this helps. It is working on my emulator. but I am just printing out the full string instead of using the image in the Card child.
Example Model Class:
import 'dart:convert';
void main() async {
String data = '{"id":"626694d4f1ce2a0012f0fe1c","name":"JBL Party Box On-The-Go","slug":"jbl-party-box-on-the-go-juqgil2ep8ult","active":true,"image":"https://cdn.macbro.uz/macbro/1fad4f47-51f4-4f12-975b-657d780c98af","code":"","order":"0","cheapest_price":0,"price":{"price":520,"old_price":0,"uzs_price":5994000,"second_price":0,"second_uzs_price":7012500},"discount":0}';
var test = new ProductModel.fromJson(json.decode(data));
print(test.image);
}
class ProductModel {
String? name;
String? image;
ProductModel.fromJson(Map json) {
this.name = json['id'];
this.image = json['image'];
}
}

Implementing Search in ListView builder

I'm new to coding and I'm trying to make my own app. I'm trying to have a search feature for my listview building but I am having trouble implementing the search. I've tried different tutorials and youtube videos but I can't seem to implement it correctly.
here's the dummy data i'm using
class.dart
class Disease {
String id, title;
List<String> diagnostics;
List<String> management;
Disease({this.id, this.title, this.diagnostics, this.management});
}
data.dart
import 'class.dart';
class Data {
static List<Disease> disease = [
Disease(
id: '1',
title: 'Dengue',
diagnostics: [
'CBC, Plt',
'Dengue NS1 if less than 4 days',
'Dengue Duo if more than 4 days from onset of symptoms',
], management: [
'Hydration',
'No Dark Colored Foods',
'aoiushdioaushd',
]),
Disease(
id: '1',
title: 'Typhoid Fever',
diagnostics: [
'CBC, Plt',
'Dengue NS1 if less than 4 days',
'Dengue Duo if more than 4 days from onset of symptoms',
],
management: [
'Hydration',
'No Dark Colored Foods',
'aoiushdioaushd',
],
)
];
}
and my home page where search is implemented
import 'package:flutter/material.dart';
import 'package:yellow_book/utils/class.dart';
import 'package:yellow_book/utils/data.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title = ''}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var disease = Data.disease;
TextEditingController editingController = TextEditingController();
final duplicateItems = List<String>.generate(10000, (i) => "Item $i");
var items = List<String>();
#override
void initState() {
items.addAll(duplicateItems);
super.initState();
}
void filterSearchResults(String query) {
List<String> dummySearchList = List<String>();
dummySearchList.addAll(duplicateItems);
if (query.isNotEmpty) {
List<String> dummyListData = List<String>();
dummySearchList.forEach((item) {
if (item.contains(query)) {
dummyListData.add(item);
}
});
setState(() {
items.clear();
items.addAll(dummyListData);
});
return;
} else {
setState(() {
items.clear();
items.addAll(duplicateItems);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
TextField(
onChanged: (value) {
filterSearchResults(value);
},
controller: editingController,
decoration: InputDecoration(
hintText: "Search",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(0.0),
),
),
),
),
Expanded(
child: ListView.builder(
itemCount: disease.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {},
title: Text(disease[index].title),
),
);
},
),
)
],
),
);
}
}
please help. Thank you
Currently your ListView is based on the disease property in your StatefulWidget and you don't change this list in any way (add or remove entries). Your filterSearchResults function as well as in initState you work with your items property so you should reference this one in your ListView or change your overall logic to work on your disease property.
So for now you can change your ListView to:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {},
title: Text(items[index]),
),
);
},
),
This will show all items entries as ListTile and you should be able to search them. If you want to make this work on your disease property, you need to change your functions to make use of the disease property instead of items.

Flutter scoped model not updating all descendants"

My app is intended to consume live sensors data from an API using flutter scoped_model. The data is a JSON array like these:
[
{
"id": 4,
"device_name": "fermentero2",
"active_beer": 4,
"active_beer_name": "Sourgobo",
"controller_fridge_temp": "Fridge --.- 1.0 ░C",
"controller_beer_temp": "Beer 28.6 10.0 ░C",
"active_beer_temp": 28.63,
"active_fridge_temp": null,
"active_beer_set": 10,
"active_fridge_set": 1,
"controller_mode": "b"
},
{
"id": 6,
"device_name": "brewpi",
"active_beer": 1,
"active_beer_name": "Amber Ale",
"controller_fridge_temp": null,
"controller_beer_temp": null,
"active_beer_temp": null,
"active_fridge_temp": null,
"active_beer_set": null,
"active_fridge_set": null,
"controller_mode": null
}
]
Those are devices. My Device model is as follow (json annotation):
#JsonSerializable(nullable: false)
class Device {
int id;
String device_name;
#JsonKey(nullable: true) int active_beer;
#JsonKey(nullable: true) String active_beer_name;
#JsonKey(nullable: true) String controller_mode; // manual beer/fridge ou perfil
#JsonKey(nullable: true) double active_beer_temp;
#JsonKey(nullable: true) double active_fridge_temp;
#JsonKey(nullable: true) double active_beer_set;
#JsonKey(nullable: true) double active_fridge_set;
Device({
this.id,
this.device_name,
this.active_beer,
this.active_beer_name,
this.controller_mode,
this.active_beer_temp,
this.active_beer_set,
this.active_fridge_set,
});
factory Device.fromJson(Map<String, dynamic> json) => _$DeviceFromJson(json);
Map<String, dynamic> toJson() => _$DeviceToJson(this);
}
My scoped model class for the Device is as follow:
class DeviceModel extends Model {
Timer timer;
List<dynamic> _deviceList = [];
List<dynamic> get devices => _deviceList;
set _devices(List<dynamic> value) {
_deviceList = value;
notifyListeners();
}
List _data;
Future getDevices() async {
loading = true;
_data = await getDeviceInfo()
.then((response) {
print('Type of devices is ${response.runtimeType}');
print("Array: $response");
_devices = response.map((d) => Device.fromJson(d)).toList();
loading = false;
notifyListeners();
});
}
bool _loading = false;
bool get loading => _loading;
set loading(bool value) {
_loading = value;
notifyListeners();
}
notifyListeners();
}
My UI is intended to be a list of devices showing live data (rebuild ui as sensor data change) and a detail page of each Device, also showing live data. For that I'm using a timer. The page to list Devices is working as expected and "refreshing" every 30 seconds:
class DevicesPage extends StatefulWidget {
#override
State<DevicesPage> createState() => _DevicesPageState();
}
class _DevicesPageState extends State<DevicesPage> {
DeviceModel model = DeviceModel();
Timer timer;
#override
void initState() {
model.getDevices();
super.initState();
timer = Timer.periodic(Duration(seconds: 30), (Timer t) => model.getDevices());
}
#override
Widget build(BuildContext) {
return Scaffold(
appBar: new AppBar(
title: new Text('Controladores'),
),
drawer: AppDrawer(),
body: ScopedModel<DeviceModel>(
model: model,
child: _buildListView(),
),
);
}
_buildListView() {
return ScopedModelDescendant<DeviceModel>(
builder: (BuildContext context, Widget child, DeviceModel model) {
if (model.loading) {
return UiLoading();
}
final devicesList = model.devices;
return ListView.builder(
itemBuilder: (context, index) => InkWell(
splashColor: Colors.blue[300],
child: _buildListTile(devicesList[index]),
onTap: () {
Route route = MaterialPageRoute(
builder: (context) => DevicePage(devicesList[index]),
);
Navigator.push(context, route);
},
),
itemCount: devicesList.length,
);
},
);
}
_buildListTile(Device device) {
return Card(
child: ListTile(
leading: Icon(Icons.devices),
title: device.device_name == null
? null
: Text(
device.device_name.toString() ?? "",
),
subtitle: device.active_beer_name == null
? null
: Text(
device.active_beer_temp.toString() ?? "",
),
),
);
}
}
class UiLoading extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 12),
Text(
'Loading',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
The problem happens with the detail page UI that is also supposed to show live Data but it behaves like a statelesswidget and do not rebuild itself after the Model gets updated:
class DevicePage extends StatefulWidget {
Device device;
DevicePage(this.device);
#override
//State<DevicePage> createState() => _DevicePageState(device);
State<DevicePage> createState() => _DevicePageState();
}
class _DevicePageState extends State<DevicePage> {
DeviceModel model = DeviceModel();
Timer timer;
#override
void initState() {
DeviceModel model = DeviceModel();
super.initState();
timer = Timer.periodic(Duration(seconds: 30), (Timer t) => model.updateDevice());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: new Text(widget.device.device_name),
),
drawer: AppDrawer(),
body: ScopedModel<DeviceModel>(
model: model,
child: _buildView(widget.device),
),
);
}
_buildView(Device device) {
return ScopedModelDescendant<DeviceModel>(
builder: (BuildContext context, Widget child, DeviceModel model) {
if (model.loading) {
return UiLoading();
}
return Card(
child: ListTile(
leading: Icon(Icons.devices),
title: device.device_name == null
? null
: Text(
device.device_name.toString() ?? "",
),
subtitle: device.active_beer_name == null
? null
: Text(
device.active_beer_temp.toString() ?? "",
),
),
);
},
);
}
}
class UiLoading extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 12),
Text(
'Loading',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
}
What am I missing ?
many thanks in advance
It looks like you're building a new DeviceModel for your DevicePage which means that model will be the one your ui would react to, not the one higher up the widget tree - your DevicesPage.
ScopedModel<DeviceModel>(
model: model,
child: _buildView(widget.device),
)
On your DevicePage where you add a the body to your Scaffold replace the ScopedModel with just:
_buildView(widget.device)
That should solve your issue.