Understanding and implementing OOP (in flutter/dart) to abstract shared structure and functionality - flutter

(Edited to clarify & update with progress)
When building an app with various lists of items and some cross-referencing between them; e.g. I have a stock class and a StockSet that extends ChangeNotifier as well as holds some basic rest functionality etc. But I'm also going to have Clients, Meetings etc, all of which are basically the same except for minor differences in fields.
So I recon I should define a parent class, say Item and ItemSet that, and then the actual elements in my app will extend those. - Or is that over-engineering?
However, in say my current StockSet I have methods to fetch either a single, or a number of stock items, i.e. ...Future<Stock> _fetchAStock() async {...
the only significant difference between it and say client records will be the REST url, and type of object returned (and give or take a few fields).
Is it better to have individual methods in all the child level classes - seems simpler though more code = more potential errors etc. OR build the fetch functionality into the parent class with some kind of abstraction/specification/parameterization(/or external configuration look-up) of the case specific differences?
New-ish to OOP and Flutter/Dart and it seems clearer some days than others... this is an others :-)
What I have:
class StocksSetRoute extends StatelessWidget with asc_alertBar {
const StocksSetRoute({Key? key}) : super(key: key);
static const indexStr = 'stocks';
static const labelStr = 'Properties';
static GlobalKey myKey = GlobalKey();
static GlobalKey parentKey = GlobalKey();
#override
Widget build(BuildContext context) {
var stockSet = context.watch<StockSet>();
return Scaffold(
key: parentKey,
appBar: AppBar(
title: const TitleRow(indexStr: indexStr, labelStr: labelStr),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// these will be filters, order toggle etc.
children: [
ElevatedButton(
onPressed: (stockSet.isRemoteEmpty)
? null
: () async {
try {
await stockSet.fetch(5);
} catch (e) {
alertBar(
'Could not fetch any more. ', stockSet as ItemSet,
backgroundColor: Colors.grey, context: context);
}
},
child: const Text('Fetch More'),
),
ElevatedButton(
onPressed:
(stockSet.stocks.isEmpty) ? null : () => stockSet.clear(),
child: const Text('Clear'),
),
],
),
Expanded(
child: StockListViewBuilder(
stockSet: stockSet, theKey: myKey, alert: alertBar))
],
),
);
}
}
class StockListViewBuilder extends StatefulWidget {
final StockSet stockSet;
final GlobalKey theKey;
final Function alert;
const StockListViewBuilder({
Key? key,
required this.stockSet,
required this.theKey,
required this.alert,
}) : super(key: key);
#override
State<StockListViewBuilder> createState() => _StockListViewBuilderState();
}
class _StockListViewBuilderState extends State<StockListViewBuilder>
with asc_alertBar {
final ScrollController _scrollController = ScrollController();
late double _scrollPosition;
late double _maxScrollExtent;
late bool isThisTheEnd = false;
_scrollListener() async {
setState(() {
_scrollPosition = _scrollController.position.pixels;
_maxScrollExtent = _scrollController.position.maxScrollExtent;
});
if (!isThisTheEnd && _scrollPosition / _maxScrollExtent > 0.90) {
isThisTheEnd = true;
if (widget.stockSet.isRemoteEmpty) {
alertBar('No more items available', null, context: context);
} else {
await widget.stockSet.fetch(5);
}
}
if (isThisTheEnd && _scrollPosition / _maxScrollExtent <= 0.90) {
isThisTheEnd = false;
}
}
#override
void initState() {
super.initState();
int listCount;
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) async {
listCount = widget.stockSet.stocks.length;
if (listCount < 10 && !widget.stockSet.isRemoteEmpty) {
try {
await widget.stockSet.fetch(10);
} catch (e) {
super.setState(() {
widget.alert("Can't load stock.", widget.stockSet,
backgroundColor: Colors.red);
});
}
}
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
controller: _scrollController,
shrinkWrap: true,
key: widget.theKey,
itemCount: widget.stockSet.stocks.length + 1,
itemBuilder: (context, index) {
if (index <= widget.stockSet.stocks.length - 1) {
return InkWell(
onTap: (() => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(widget.stockSet.stocks[index]),
)),
child: StockListItem(
stock: widget.stockSet.stocks[index],
));
} else {
return LoadingItemNotifier(
isLoading: widget.stockSet.isBusyLoading,
);
}
},
);
}
}
class StockSet extends ItemSet {
final List<Stock> _stocks = [];
List<Stock> get stocks => _stocks;
List<int> getHaveStocksIds() {
final List<int> ids = _stocks.map((stock) => stock.id).toList();
return ids;
}
void add(Stock stock) {
_stocks.add(stock);
notifyListeners();
}
void remove(Stock stock) {
_stocks.remove(stock);
notifyListeners();
}
void clear() {
_stocks.clear();
isBusyLoading = false;
isRemoteEmpty = false;
notifyListeners();
}
Future<void> fetch([int num = 1]) async {
int i = 0;
for (i; i < num; i++) {
if (!isRemoteEmpty) {
try {
Stock tmpStock = await _fetchAStock();
if (getHaveStocksIds().contains(tmpStock.id)) {
throw Exception('We allready have ${tmpStock.id}');
}
add(tmpStock);
} catch (e) {
i = num;
isRemoteEmpty = true;
isBusyLoading = false;
notifyListeners();
throw Exception('No more to fetch $e');
}
}
}
return;
}
Future<Stock> _fetchAStock() async {
List<int> have = getHaveStocksIds();
final queryParameters = {
'exclude': json.encode(have),
};
isBusyLoading = true;
notifyListeners();
try {
final response = await http.post(
Uri.https('onethread.design', 'agency/wp-json/ypagp/v1/pr-get-a',
queryParameters),
);
isBusyLoading = false;
notifyListeners();
if (response.statusCode != 200) {
throw HttpException('${response.statusCode}');
}
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,
);
} on SocketException {
feedback = 'Please enable an internet connection.';
notifyListeners();
} on HttpException {
feedback = "Couldn't find the/a post.";
notifyListeners();
} on FormatException {
feedback = "Bad response format.";
} catch (e, s) {
feedback = 'fetchA catch $e $s ';
}
throw Exception('Could not _fetchAStock');
}
}
class ItemSet extends ChangeNotifier {
bool isBusyLoading = false;
bool isRemoteEmpty = false;
String? feedback;
}
what I'm aiming at (and have partially succeeded in implementing) is something like
class StockSet extends ItemSet and class ItemSet extends ChangeNotifier {...
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];
}
and I think then
class Stock extends Item {
final String thumbUrl;
const Stock(
{required super.id, required super.title, required super.description, required this.thumbUrl});
#override
List<Object> get props => [id, title, description, thumbUrl];
}
though when it comes to the ItemSet's methods, e.g.
been trying things like e.g.
in ItemSet
Future<Type> fetchAItem(String typeSlug, Type type) async {
List<int> have = getHaveItemsIds();
final queryParameters = {
'exclude': json.encode(have),
};
developer.log('_fetchAItem $queryParameters');
isBusyLoading = true;
notifyListeners();
try {
final response = await http.post(
Uri.https('###', 'a###/v1/$typeSlug-get-a',
queryParameters),
);
developer.log('response.statusCode:${response.statusCode}');
isBusyLoading = false;
notifyListeners();
if (response.statusCode != 200) {
throw HttpException('${response.statusCode}');
}
final Map<String, dynamic> map = json.decode(response.body);
return Item(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
) as type;// no! - was worth a guess though
} on...
but I'm getting lost in the plumbing, should I use #override in the child classes? seems clunky - or maybe use more mixins?.
Thanks in advance
and the alertBar mixin...
mixin asc_alertBar {
void alertBar(
String message,
ItemSet? itemSet, {
String label = 'Okay',
Function? action,
Color? backgroundColor,
required BuildContext context,
}) {
if (itemSet != null) {
String? itemSetFeedback = itemSet.feedback;
if (itemSetFeedback != null) {
message += '\n$itemSetFeedback';
itemSet.feedback = null;
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: backgroundColor,
duration: const Duration(seconds: 15),
content: AnimatedText(
textContent: message,
durationFactor: 0.25,
),
action: SnackBarAction(
label: label,
onPressed: () => action,
),
),
);
}
}
I hope the question makes sense.

Related

flutter : display current user information flutter [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 16 days ago.
Improve this question
i try to display user info like email and user name in profile page , how to get user data for current user
i used flutter , php , mysql , thats my code :
1- login controller
abstract class LoginController extends GetxController {
// late TextEditingController phone;
late TextEditingController password;
late TextEditingController username;
LoginData loginData = LoginData(Get.find());
late StatusRequiest statusRequiest;
bool isShowPassword = true;
GlobalKey<FormState> formstate = GlobalKey<FormState>();
toSignUp();
toForgetPassword();
login();
showPassword() {
isShowPassword = isShowPassword == true ? false : true;
update();
}
}
class LoginControllerImp extends LoginController {
#override
login() async {
var formdata = formstate.currentState;
if (formdata!.validate()) {
statusRequiest = StatusRequiest.loading;
var response = await loginData.postdata(username.text,password.text); //getData from "Data folder" not "controller folder"
// if (kDebugMode) {
// print("===================== $response");
// }
statusRequiest = handlingData(response);
if (StatusRequiest.success == statusRequiest) {
if (response["status"] == "success") {
// data.addAll(response['data']);
Get.offAllNamed(AppRoutes.navbar);
// print(response);
} else {
Get.defaultDialog(title: "99".tr, middleText: "110".tr);
statusRequiest = StatusRequiest.faliure;
}
}
update();
// Get.offAllNamed(AppRoutes.verviyCodeSignUp);
} else {
if (kDebugMode) {
print('wrong');
}
}
}
#override
toSignUp() {
Get.toNamed(AppRoutes.signUp);
}
#override
toForgetPassword() {
Get.toNamed(AppRoutes.forgetPassword);
}
#override
void onInit() {
// phone = TextEditingController();
password = TextEditingController();
username = TextEditingController();
super.onInit();
}
#override
void dispose() {
// phone.dispose();
username.dispose();
password.dispose();
super.dispose();
}
}
2- json :
class UserModel {
String? userId;
String? userName;
String? userImage;
String? userCreate;
UserModel({
this.userId,
this.userName,
this.userImage,
this.userCreate,
});
UserModel.fromJson(Map<String, dynamic> json) {
userId = json['user_id'];
userName = json['user_name'];
userImage = json['user_image'];
userCreate = json['user_create'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['user_id'] = userId;
data['user_name'] = userName;
data['user_image'] = userImage;
data['user_create'] = userCreate;
return data;
}
}
3-
class LoginData {
Crud crud;
LoginData(this.crud);
postdata(String username, String password) async {
var response = await crud.postData(AppLink.login, {
"username": username,
// "phone": phone,
"password": password,
});
return response.fold((l) => l, (r) => r);
}
}
4-profile controller
abstract class ProfileController extends GetxController {
initialData();
getdata();
// goToItems(List categories, int selectedCat, String categoryid);
// goToDetails(List items, int selecteditems, String itemsid);
}
class ProfileControllerImp extends ProfileController {
MyServices myServices = Get.find();
String? username;
// String? email;
String? id;
ProfileData homeData = ProfileData(Get.find());
List users=[];
late StatusRequiest statusRequiest;
#override
initialData() {
username = myServices.sharedPreferences.getString("username");
id = myServices.sharedPreferences.getString("id");
}
#override
void onInit() {
getdata();
initialData();
super.onInit();
}
#override
getdata() async {
statusRequiest = StatusRequiest.loading;
var response = await homeData.getData(); //getData from "Data folder" not "controller folder"
statusRequiest = handlingData(response);
if (StatusRequiest.success == statusRequiest) {
// SharedPreferences sharedPreferences = SharedPreferences;
if (response['status'] == "success") {
users.addAll(response['users']);
print(users);
// items.addAll(response['items']);
if (kDebugMode) {
print(response);
}
} else {
statusRequiest = StatusRequiest.faliure;
}
}
update();
}
}
5-profile page
class Profile extends GetView<ProfileControllerImp> {
final UserModel? usermodel;
const Profile({super.key, this.usermodel});
#override
Widget build(BuildContext context) {
Get.put(ProfileControllerImp());
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
// automaticallyImplyLeading: false,
title: Text(
// '${usermodel?.userName}',
'118'.tr,
style: const TextStyle(color: Colors.black),
),
// actions: [],
centerTitle: true,
elevation: 0,
),
body: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount:2,
itemBuilder: (context, index) {
return SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
child: Text(controller.username!),
anyone can help plz
in php website and html i used to use $_session['username'] but here in flutter i dont know how to save in seesion , if you can solve this problem in same way plz help and thnx
use your user model, as i can see you areastoraging the data of the user in a class called User so you can use this same class to acces that data in the profile controller in the line
users.addAll(response['users']);
you create a list of user so you have to acces to the index of the list that contains the data of the user and instead of $_sesion["username"] you can use .username for example if you wanna show the data of the first user of the list you can do this:
users[0].name;
and thats equivalent to: $_sesion["username"]

Pagination new data Isn't added to the ListView.builder at the end instead refreshed replacing previous data

I have an API from where I am get 10 data entries on each hit. What I want to do is to add these entries at the end of the ListView.builder List. At the moment it refreshes the ListView.builder instead of populating more entries.
Code for the whole screen is quite longer so I trimmed down only for one type of entry.
class PaginatedScreen extends StatefulWidget {
const PaginatedScreen({Key? key}) : super(key: key);
#override
State<PaginatedScreen> createState() => _PaginatedScreenState();
}
class _PaginatedScreenState extends State<PaginatedScreen> {
final scrollController = ScrollController();
int offSetCurrent = 0;
bool hasMore = true;
bool isLoading = false;
#override
void initState(){
super.initState();
scrollController.addListener(() {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
setState(() {
getDiscussion(offSetCurrent);
print('initial offSet: $offSetCurrent');
offSetCurrent = offSetCurrent + 10;
print('new offset $offSetCurrent');
});
}
});
}
#override
void dispose(){
scrollController.dispose();
super.dispose();
}
Future<PaginatedModel> getDiscussion(offSetCurrent) async {
// if (isLoading) return;
// isLoading = true;
SharedPreferences sp = await SharedPreferences.getInstance();
var parameters = {
'RangeFrom':offSetCurrent.toString(),
'AccessToken':sp.getString('AccessToken'),
'DiscussionID':'22848',
};
var urlfetch = Uri(
scheme: "https",
host: baseUrl,
path: discussionAPIPath,
queryParameters: parameters,
);
try {
http.Response response = await http.get(Uri.parse(urlfetch.toString()));
var data = jsonDecode(response.body.toString());
var snapshot = PaginatedModel.fromJson(data);
var itemsTotal = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages!.length;
if (itemsTotal < 10){
setState((){
hasMore = false;
});
}
print(data);
print('Discussion API HIT with offset $offSetCurrent');
// isLoading = false;
if (response.statusCode == 200) {
return PaginatedModel.fromJson(data);
}
else {
print(response.statusCode);
return PaginatedModel.fromJson(data);
}
}
catch (e) {
print(e.toString());
rethrow;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: getDiscussion(offSetCurrent),
builder: (dCtx, dSnapshot){
if (dSnapshot.hasData){
var itemCount = dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages!.length;
return ListView.builder(
controller: scrollController,
shrinkWrap: true,
itemCount: itemCount + 1,
itemBuilder: (context, idx){
if (idx < itemCount){
return Column(
children: List<Widget>.generate(dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.length, (i) => Container(
margin: const EdgeInsets.all(10),
child: Column(
children: [
Html(data: dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages![idx].message.toString()),
],
),
))
);
}
else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(
child: hasMore? CircularProgressIndicator() : Text('No More Data'),
),
);
}
}
);
}
else {
return const Center(
child: CircularProgressIndicator(),
);
}
}
),
);
}
}
Here is my Model:
class PaginatedModel {
PaginatedModel({
List<CourseLessonDetailsData>? courseLessonDetailsData,}){
_courseLessonDetailsData = courseLessonDetailsData;
}
PaginatedModel.fromJson(dynamic json) {
if (json['CourseLessonDetailsData'] != null) {
_courseLessonDetailsData = [];
json['CourseLessonDetailsData'].forEach((v) {
_courseLessonDetailsData?.add(CourseLessonDetailsData.fromJson(v));
});
}
}
List<CourseLessonDetailsData>? _courseLessonDetailsData;
PaginatedModel copyWith({ List<CourseLessonDetailsData>? courseLessonDetailsData,
}) => PaginatedModel( courseLessonDetailsData: courseLessonDetailsData ?? _courseLessonDetailsData,
);
List<CourseLessonDetailsData>? get courseLessonDetailsData => _courseLessonDetailsData;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (_courseLessonDetailsData != null) {
map['CourseLessonDetailsData'] = _courseLessonDetailsData?.map((v) => v.toJson()).toList();
}
return map;
}
}
class CourseLessonDetailsData {
CourseLessonDetailsData({
String? requestStatus,
List<DiscussionDetails>? discussionDetails,}){
_requestStatus = requestStatus;
_discussionDetails = discussionDetails;
}
CourseLessonDetailsData.fromJson(dynamic json) {
_requestStatus = json['request_status'];
if (json['DiscussionDetails'] != null) {
_discussionDetails = [];
json['DiscussionDetails'].forEach((v) {
_discussionDetails?.add(DiscussionDetails.fromJson(v));
});
}
}
String? _requestStatus;
List<DiscussionDetails>? _discussionDetails;
CourseLessonDetailsData copyWith({ String? requestStatus,
List<DiscussionDetails>? discussionDetails,
}) => CourseLessonDetailsData( requestStatus: requestStatus ?? _requestStatus,
discussionDetails: discussionDetails ?? _discussionDetails,
);
String? get requestStatus => _requestStatus;
List<DiscussionDetails>? get discussionDetails => _discussionDetails;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['request_status'] = _requestStatus;
if (_discussionDetails != null) {
map['DiscussionDetails'] = _discussionDetails?.map((v) => v.toJson()).toList();
}
return map;
}
}
class DiscussionDetails {
DiscussionDetails({
List<DiscussionsMessages>? discussionsMessages,}){
_discussionsMessages = discussionsMessages;
}
DiscussionDetails.fromJson(dynamic json) {
if (json['DiscussionsMessages'] != null) {
_discussionsMessages = [];
json['DiscussionsMessages'].forEach((v) {
_discussionsMessages?.add(DiscussionsMessages.fromJson(v));
});
}
}
List<DiscussionsMessages>? _discussionsMessages;
DiscussionDetails copyWith({ List<DiscussionsMessages>? discussionsMessages,
}) => DiscussionDetails( discussionsMessages: discussionsMessages ?? _discussionsMessages,
);
List<DiscussionsMessages>? get discussionsMessages => _discussionsMessages;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (_discussionsMessages != null) {
map['DiscussionsMessages'] = _discussionsMessages?.map((v) => v.toJson()).toList();
}
return map;
}
}
class DiscussionsMessages {
DiscussionsMessages({
String? messageID,
String? id,
String? studentName,
String? message,
String? replyMessage,
List<OtherStudentMessages>? otherStudentMessages,}){
_messageID = messageID;
_id = id;
_studentName = studentName;
_message = message;
_replyMessage = replyMessage;
_otherStudentMessages = otherStudentMessages;
}
DiscussionsMessages.fromJson(dynamic json) {
_messageID = json['MessageID'];
_id = json['ID'];
_studentName = json['StudentName'];
_message = json['Message'];
_replyMessage = json['ReplyMessage'];
if (json['OtherStudentMessages'] != null) {
_otherStudentMessages = [];
json['OtherStudentMessages'].forEach((v) {
_otherStudentMessages?.add(OtherStudentMessages.fromJson(v));
});
}
}
String? _messageID;
String? _id;
String? _studentName;
String? _message;
String? _replyMessage;
List<OtherStudentMessages>? _otherStudentMessages;
DiscussionsMessages copyWith({ String? messageID,
String? id,
String? studentName,
String? message,
String? replyMessage,
List<OtherStudentMessages>? otherStudentMessages,
}) => DiscussionsMessages( messageID: messageID ?? _messageID,
id: id ?? _id,
studentName: studentName ?? _studentName,
message: message ?? _message,
replyMessage: replyMessage ?? _replyMessage,
otherStudentMessages: otherStudentMessages ?? _otherStudentMessages,
);
String? get messageID => _messageID;
String? get id => _id;
String? get studentName => _studentName;
String? get message => _message;
String? get replyMessage => _replyMessage;
List<OtherStudentMessages>? get otherStudentMessages => _otherStudentMessages;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['MessageID'] = _messageID;
map['ID'] = _id;
map['StudentName'] = _studentName;
map['Message'] = _message;
map['ReplyMessage'] = _replyMessage;
if (_otherStudentMessages != null) {
map['OtherStudentMessages'] = _otherStudentMessages?.map((v) => v.toJson()).toList();
}
return map;
}
}
class OtherStudentMessages {
OtherStudentMessages({
String? replyID,
String? repliedBy,
String? replyMessage,}){
_replyID = replyID;
_repliedBy = repliedBy;
_replyMessage = replyMessage;
}
OtherStudentMessages.fromJson(dynamic json) {
_replyID = json['ReplyID'];
_repliedBy = json['RepliedBy'];
_replyMessage = json['ReplyMessage'];
}
String? _replyID;
String? _repliedBy;
String? _replyMessage;
OtherStudentMessages copyWith({ String? replyID,
String? repliedBy,
String? replyMessage,
}) => OtherStudentMessages( replyID: replyID ?? _replyID,
repliedBy: repliedBy ?? _repliedBy,
replyMessage: replyMessage ?? _replyMessage,
);
String? get replyID => _replyID;
String? get repliedBy => _repliedBy;
String? get replyMessage => _replyMessage;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['ReplyID'] = _replyID;
map['RepliedBy'] = _repliedBy;
map['ReplyMessage'] = _replyMessage;
return map;
}
}
And here is the JSON response from the API. (with one entry)
{
"CourseLessonDetailsData": [
{
"request_status": "Successful",
"DiscussionDetails": [
{
"DiscussionsMessages": [
{
"MessageID": "301",
"ID": "48",
"StudentName": "Person Name",
"Message": "This is from Message",
"ReplyMessage": "This is a Reply Message",
"OtherStudentMessages": [
{
"ReplyID":"939",
"RepliedBy":"Reply Person",
"ReplyMessage":"resubmit means you can submit again if you want until the deadline is over"
}
]
}
]
}
]
}
]
}
I edited your code but could not test it out, please check it if it works.
You need to make a list variable that will hold all your paginated data. Then call your future and then after result is not null, add it to your list and change your loading status to false. After, use scrolls and scrollListener reaches max extent, change offset and call your api again and after result is not null, again add to your list.
class PaginatedScreen extends StatefulWidget {
const PaginatedScreen({Key? key}) : super(key: key);
#override
State<PaginatedScreen> createState() => _PaginatedScreenState();
}
class _PaginatedScreenState extends State<PaginatedScreen> {
final scrollController = ScrollController();
int offSetCurrent = 0;
bool hasMore = true;
bool isLoading = true;
List<PaginatedModel> listOfData = <PaginatedModel>[];
#override
void initState() async {
super.initState();
var result = await getDiscussion(offSetCurrent);
if (result != null) {
setState(() {
listOfData.add(result);
isLoading = false;
});
}
scrollController.addListener(() async {
if (scrollController.position.maxScrollExtent == scrollController.offset) {
offSetCurrent = offSetCurrent + 10;
setState(() {
isLoading = true;
});
var result = await getDiscussion(offSetCurrent);
setState(() {
listOfData.add(result!);
isLoading = false;
});
}
});
}
#override
void dispose(){
scrollController.dispose();
super.dispose();
}
Future<PaginatedModel?> getDiscussion(offSetCurrent) async {
// if (isLoading) return;
// isLoading = true;
SharedPreferences sp = await SharedPreferences.getInstance();
var parameters = {
'RangeFrom':offSetCurrent.toString(),
'AccessToken':sp.getString('AccessToken'),
'DiscussionID':'22848',
};
var urlfetch = Uri(
scheme: "https",
host: baseUrl,
path: discussionAPIPath,
queryParameters: parameters,
);
try {
http.Response response = await http.get(Uri.parse(urlfetch.toString()));
var data = jsonDecode(response.body.toString());
var snapshot = PaginatedModel.fromJson(data);
var itemsTotal = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages!.length;
if (itemsTotal < 10){
setState((){
hasMore = false;
});
}
print(data);
print('Discussion API HIT with offset $offSetCurrent');
// isLoading = false;
if (response.statusCode == 200) {
return PaginatedModel.fromJson(data);
}
else {
print(response.statusCode);
return null;
}
}
catch (e) {
print(e.toString());
rethrow;
// return null;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isLoading
? CircularProgressIndicator()
: ListView.builder(
itemCount: listOfData.length,
itemBuilder: (context, index) {
//do your widget building here
// do whatever you did in your future builder here
return Text(listOfData[index].courseLessonDetailsData![0].requestStatus!);
},
),
// body: FutureBuilder(
// future: getDiscussion(offSetCurrent),
// builder: (dCtx, dSnapshot){
// if (dSnapshot.hasData){
// var itemCount = dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages!.length;
// return ListView.builder(
// controller: scrollController,
// shrinkWrap: true,
// itemCount: itemCount + 1,
// itemBuilder: (context, idx){
// if (idx < itemCount){
// return Column(
// children: List<Widget>.generate(dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.length, (i) => Container(
// margin: const EdgeInsets.all(10),
// child: Column(
// children: [
// Html(data: dSnapshot.data!.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages![idx].message.toString()),
// ],
// ),
// ))
// );
// }
// else {
// return Padding(
// padding: EdgeInsets.symmetric(vertical: 32),
// child: Center(
// child: hasMore? CircularProgressIndicator() : Text('No More Data'),
// ),
// );
// }
// }
// );
// }
// else {
// return const Center(
// child: CircularProgressIndicator(),
// );
// }
// }
// ),
);
}
}

Retrieve Data from Realtime Database in Flutter

I want to retrieve data from a realtime database for a flutter application. My data is built like this.
I need to loop through this data to display it on the application (ecommerce app, obviously). I have tried and failed in many ways. Currently when trying to get the data I see "Instance of '_Future'" as the message.
class Cart extends StatefulWidget {
Cart({Key? key}) : super(key: key);
#override
State<Cart> createState() => _CartState();
}
class _CartState extends State<Cart> {
DatabaseReference ref = FirebaseDatabase.instance.ref();
Object? products;
List productList = [];
String displayText = 'Results go here!';
snapshot() async {
final snapshot = await ref.child('Products').get();
productList = [];
if (snapshot.exists) {
productList.add(snapshot.value);
products = (snapshot.value);
print(snapshot);
print(snapshot.value);
} else {
print('No Data Available');
}
}
#override
void initState() {
super.initState();
snapshot();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const PreferredSize(
preferredSize: Size.fromHeight(60), child: MyAppBar()),
body: Column(
children: [
ElevatedButton(
onPressed: () async {
// await ref.set({"name": "Tyler"});
snapshot();
},
child: Text("Add Data"),
),
Text("${snapshot()}", style: TextStyle(color: Colors.white))
],
)
);
}
}
I also have this data class built from other posts I have seen. I have to admit, I am not entirely sure how to use it.
import 'dart:convert';
class ProductData {
final int productID;
final String productCategory;
final String productDesc;
final String productName;
final String productPrice;
final String productSize;
final bool productInStock;
final String productImage1;
final String productGender;
final String productImage2;
ProductData(
{required this.productID,
required this.productCategory,
required this.productDesc,
required this.productName,
required this.productPrice,
required this.productSize,
required this.productInStock,
required this.productImage1,
required this.productGender,
required this.productImage2});
ProductData copyWith(
{int? productID,
String? productCategory,
String? productDesc,
String? productName,
String? productPrice,
String? productSize,
bool? productInStock,
String? productImage1,
String? productGender,
String? productImage2}) {
return ProductData(
productID: productID ?? this.productID,
productCategory: productCategory ?? this.productCategory,
productDesc: productDesc ?? this.productDesc,
productName: productName ?? this.productName,
productPrice: productPrice ?? this.productPrice,
productSize: productSize ?? this.productSize,
productInStock: productInStock ?? this.productInStock,
productImage1: productImage1 ?? this.productImage1,
productGender: productGender ?? this.productGender,
productImage2: productImage2 ?? this.productImage2,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'productID': productID,
'productCategory': productCategory,
'productDesc': productDesc,
'productName': productName,
'productPrice': productPrice,
'productSize': productSize,
'productInStock': productInStock,
'productImage1': productImage1,
'productGender': productGender,
'productImage2': productImage2,
};
}
factory ProductData.fromMap(Map<String, dynamic> map) {
return ProductData(
productID: map['productID'] as int,
productCategory: map['productCategory'] as String,
productDesc: map['productDesc'] as String,
productName: map['productName'] as String,
productPrice: map['productPrice'] as String,
productSize: map['productSize'] as String,
productInStock: map['productInStock'] as bool,
productImage1: map['productImage1'] as String,
productGender: map['productGender'] as String,
productImage2: map['productImage2'] as String,
);
}
String toJson() => json.encode(toMap());
factory ProductData.fromJson(String source) =>
ProductData.fromMap(json.decode(source) as Map<String, dynamic>);
#override
String toString() {
return 'ProductData(productID: $productID, productCategory: $productCategory, productDesc: $productDesc, productName: $productName, productPrice: $productPrice, productSize: $productSize, productInStock: $productInStock, productImage11: $productImage1, productGender: $productGender, productImage2: $productImage2)';
}
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProductData &&
other.productID == productID &&
other.productCategory == productCategory &&
other.productDesc == productDesc &&
other.productName == productName &&
other.productPrice == productPrice &&
other.productSize == productSize &&
other.productInStock == productInStock &&
other.productImage1 == productImage1 &&
other.productGender == productGender &&
other.productImage2 == productImage2;
}
#override
int get hashCode {
return productID.hashCode ^
productCategory.hashCode ^
productDesc.hashCode ^
productName.hashCode ^
productPrice.hashCode ^
productSize.hashCode ^
productInStock.hashCode ^
productImage1.hashCode ^
productGender.hashCode ^
productImage2.hashCode;
}
}
Since the data is loaded from Firebase asynchronously, its get() method returns a Future. That's also why you had to declare your snapshot() function as async, which means that you also return a Future.
On its own the rendering code doesn't know anything about Futures, so it renders it by calling its toString() method, which leads to the output you see:
Instance of '_Future'
What you want instead is to wait for the future to resolve, which is just a fancy way of saying that you want to wait for the data to load. An easy way to do that is to use a FutureBuilder, which handles the asynchronous nature of a Future and all possible states it can be in.
That'd look something like:
snapshot() async {
final snapshot = await ref.child('Products').get();
productList = [];
if (snapshot.exists) {
productList.add(snapshot.value);
products = (snapshot.value);
} else {
print('No Data Available');
}
return productList;
}
body: Column(
children: [
ElevatedButton(
onPressed: () async {
snapshot();
},
child: Text("Add Data"),
),
FutureBuilder(
future: snapshot(),
builder: (BuildContext context, AsyncSnapshot asyncSnapshot) {
if (snapshot.hasData) {
var productList = asyncSnapshot.data! as List;
return Text(productList.length.toString());
} else if (snapshot.hasError) {
return Text('Error: ${asyncSnapshot.error}');
} else {
return CircularProgressIndicator(),
}
}
)
],
)

fetch data by using flutter http request and load more data on scroll down the screen

i fetch data from server using flutter http request and load more data when user scroll to bottom of screen. i receive this error "Unhandled Exception: type 'List' is not a subtype of type 'Product'". Please help, i struggle all day without success.
model.dart file
class Product {
final int id;
final String accountName,
callNumber,
whatsappNumber,
businessLocation,
caption;
final List<Images> productPhoto;
Product({
this.id,
this.accountName,
this.callNumber,
this.whatsappNumber,
this.businessLocation,
this.caption,
this.productPhoto,
});
// this is static method
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
accountName: json['account_name'],
callNumber: json['call_number'],
whatsappNumber:
json['whatsapp_number'] != null ? json['whatsapp_number'] : null,
businessLocation: json['business_location'],
caption: json['caption'],
productPhoto:
(json['post_photos'] as List).map((i) => Images.fromJson(i)).toList(),
);
}
}
class Images {
final String filename;
Images({this.filename});
factory Images.fromJson(Map<String, dynamic> json) {
return Images(
filename: json['filename'],
);
}
}
explore.dart file (i import models.dart to this file)
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:windowshoppi/models/global.dart';
import 'package:windowshoppi/models/product.dart';
import 'package:http/http.dart' as http;
class Explore extends StatefulWidget {
#override
_ExploreState createState() => _ExploreState();
}
class _ExploreState extends State<Explore> {
ScrollController _scrollController = ScrollController();
List<Product> data;
String nextUrl;
#override
void initState() {
// TODO: implement initState
super.initState();
this.fetchProduct(http.Client(), ALL_PRODUCT_URL);
_scrollController.addListener(() {
// print(_scrollController.position.pixels);
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
if (nextUrl != null) {
this.fetchProduct(http.Client(), nextUrl);
}
// print(nextUrl);
}
});
}
Future<List<Product>> fetchProduct(http.Client client, url) async {
final response = await client.get(url);
if (response.statusCode == 200) {
Map<String, dynamic> mapResponse = json.decode(response.body);
nextUrl = mapResponse['next'];
if (mapResponse["count"] != "") {
final products = mapResponse["results"].cast<Map<String, dynamic>>();
final listOfProducts = await products.map<Product>((json) {
return Product.fromJson(json);
}).toList();
// return listOfProducts;
setState(() {
data.add(listOfProducts);
});
} else {
return [];
}
} else {
throw Exception('failed to load data from internet');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('http get'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: Container(
height: 200,
color: Colors.blue,
child: Text(data[index].caption),
),
);
},
),
);
}
}
Have a look at this part of the code.
final listOfProducts = await products.map<Product>((json) {
return Product.fromJson(json);
}).toList();
In the .map() method you are casting it to type < Product >. So judging by the error you have mentioned, "Unhandled Exception: type 'List' is not a subtype of type Product"
I think the json data being returned contains a List, instead of the product fields. I would highly recommend you to once check the json data being returned, and double-check if you are targeting the correct JSON tree nodes.
Let me know if this solved the issue.

Can't access values from redux state event hough I have an instance of the class

I have some state that is added during some middleware. This state is used to build ListTiles for a ListView. I cannot access the properties of this instance when I map over the instance.
I can see the info in the debugger: https://imgur.com/a/YTpjBou
But I cannot access the property because it returns null. I am unsure if this is because the future has not completed by the time it renders or what.
Here is the build for the home_widget
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:nasp_portal_app/model/model.dart';
import 'main_drawer.dart';
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Image.asset('lib/images/Logo.png', height: 35),
),
drawer: DrawerOnly(),
body: StoreConnector<AppState, _ViewModel>(
converter: (Store<AppState> store) => _ViewModel.create(store),
builder: (BuildContext context, _ViewModel viewModel) => Column(
children: <Widget>[Expanded(child: ItemListWidget(viewModel))],
),
),
);
}
}
class ItemListWidget extends StatelessWidget {
final _ViewModel model;
ItemListWidget(this.model);
#override
Widget build(BuildContext context) {
return ListView(
children: model.tournaments.map((Tournament tournament) {
return ListTile(
title: Text(tournament.tournName ?? 'Test'),
leading: IconButton(
icon: Icon(Icons.home),
onPressed: () => print('go to tourney'),
));
}).toList(),
);
}
}
class _ViewModel {
final List<Tournament> tournaments;
_ViewModel({this.tournaments});
factory _ViewModel.create(Store<AppState> store) {
print(store.state.tournaments.length);
return _ViewModel(tournaments: store.state.tournaments);
}
}
Here is the class definition of a Tournament
class Tournament {
final String tournName;
final String tournState;
final String tournCity;
final double distanceMiles;
final int startDate;
final int endDate;
final int tID;
Tournament({
#required this.tournName,
#required this.tournState,
#required this.tournCity,
#required this.distanceMiles,
#required this.startDate,
#required this.endDate,
#required this.tID,
});
Tournament copyWith({
String tournName,
String tournState,
String tournCity,
double distanceMiles,
int startDate,
int endDate,
int tID,
}) {
return Tournament(
tournName: tournName ?? this.tournName,
tournState: tournState ?? this.tournState,
tournCity: tournCity ?? this.tournCity,
distanceMiles: distanceMiles ?? this.distanceMiles,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
tID: tID ?? this.tID,
);
}
}
This is my redux middleware handling the async task
class NearTournamentsMiddleware extends MiddlewareClass<AppState> {
#override
void call(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is NearTournamentsAction) {
checkNearTournaments(next);
}
next(action);
}
void checkNearTournaments(NextDispatcher next) async {
final tournaments = await _tournamentsInRange();
for (final tournament in tournaments) {
next(AddTournamentsAction(
tournament['TournName'],
tournament['TID'],
tournament['TournState'],
tournament['TournCity'],
tournament['Distance_Miles'],
tournament['Start_Date'],
tournament['End_Date']));
}
}
_tournamentsInRange() async {
Map currentLocation = <String, double>{};
var location = Location();
try {
currentLocation = await location.getLocation();
final response = await _checkLocalTournaments(
currentLocation["latitude"], currentLocation["longitude"]);
final decoded = jsonDecode(response.body);
return decoded;
} on PlatformException {
currentLocation = null;
}
}
Future<http.Response> _checkLocalTournaments(lat, lng) async {
var url = 'https://napi.com';
var body = json.encode({
'miles': '-1', // -1 for test api
'lat': lat,
'lng': lng
});
Map<String, String> headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(url, body: body, headers: headers);
return response;
}
}
These are my current reducers
import 'package:nasp_portal_app/model/model.dart';
import 'package:nasp_portal_app/redux/actions.dart';
AppState appStateReducer(AppState state, action) {
return AppState(tournaments: tournamentReducer(state.tournaments, action));
}
List<Tournament> tournamentReducer(List<Tournament> state, action) {
if (action is AddTournamentsAction) {
return []
..addAll(state)
..add(Tournament(
tournName: action.tournName,
tournState: action.tournState,
tournCity: action.tournCity,
distanceMiles: action.distanceMiles,
startDate: action.startDate,
endDate: action.endDate,
tID: action.tID));
}
return state;
}
How can I properly access the values in the map in my screenshot? I know I have an instanced based on the debugger but cannot get its properties.
My issue was with the redux action that I was using called AddTournamentsAction
I was not using this to refer to the class variables in its constructor like so:
class AddTournamentsAction {
final String tournName;
final String tournState;
final String tournCity;
final double distanceMiles;
final int startDate;
final int endDate;
final int tID;
AddTournamentsAction(
tournName,
tournState,
tournCity,
distanceMiles,
startDate,
endDate,
tID,
);
}
To fix this I simply had to add the this keyword:
class AddTournamentsAction {
final String tournName;
final String tournState;
final String tournCity;
final double distanceMiles;
final int startDate;
final int endDate;
final int tID;
AddTournamentsAction(
this.tournName,
this.tournState,
this.tournCity,
this.distanceMiles,
this.startDate,
this.endDate,
this.tID,
);
}