Fetch & show item details - flutter

I'm currently learning Flutter and developing a Shopping List.
In the backend, there's a Laravel instance providing the following Endpoints:
/lists -> A list of all shopping lists
/lists/{listId}/items -> All items in the list
For the Flutter side, I've created a provider which fetches all lists from the server.
Future<void> init() async {
this.lists = await this.service.getLists();
notifyListeners();
}
This data (<List<ListModel>>) is retreived by my widget and displayed through ListView.builder. The RefreshIndicator just refreshes the provider data by run the init method again.
Widget build(BuildContext context) {
final provider = Provider.of<ShoppingListProvider>(context);
List<ListModel> lists = provider.lists;
...
child: RefreshIndicator(
onRefresh: () => provider.init(),
Now, if I click on the ListTile, I'd like to display all items. They should be fetched from the API as well, and I'd like to refresh the item list too. Unfortunately, I don't manage to get this data provided through the provider, as it requires the "listId".
Maybe someone can give me a hint on this?
Fetched from inside the widget, which works, but then I can't refresh the data. It somehow should be provided through the provider

Related

Where to put bloc providers in flutter

I have tried to use bloc in flutter for a period, but the question of where to put bloc provider confuse me.
At the beginning of learning flutter, I put nearly all the providers in the main() function by using MultipleBlocProvider.
But due to some of my blocs subscribe to streams such as firebase snapshots, I think it might cost extra resources (I am not sure but at least some memory was occupied I guess) if I do not close those subscriptions by closing the bloc.
However, if the provider is in the main(), change page or pop up would not close these blocs and the subscriptions inside.
In this scenario, I try to put some bloc providers into specific pages, so the bloc can be close and recreate when I goes in and out of this page. But there are some other questions occurs.
For example, if I have a bloc called ProductDetailsBloc which is used to control the widget of product details in product details page, its events contains an event called GetProductBySku which is used to get product from firebase and set the product inside the state(ProductDetailsState).
I want to put this bloc and its provider inside product details page and put an event trigger inside product list widget (located in product list page) and its onTap() function, where users click on the product list item (this is a thumbnail product item which is from another resources and only contains very basic info such as sku, title ) inside product list page and then navigator to product details page to view the full information of this product.
But as I mentioned before, If I put the bloc provider of ProductDetailsBloc inside product details page, I can not use GetProductBySku before entering product details page
So, I personally have two ideas for this questions,
the first one is passing the product sku through arguments to product details page and then call the GetProductBySku after the bloc has been created.
the second one is the put the ProductDetailsBloc inside the main(), so I can skip this questions, and directly use the GetProductBySku in product list widget, but this turns the problems in to the very front.
I do not know which one is better, so I would be very appreciative if someone can give some suggestions.
And back to the main questions, what is the best practice of putting bloc providers and what are the concerns if I put providers inside main().
Thanks for every one that read to here because this is a bit long and this is my first time of asking on attack overflow :3
You can find more discussion regarding this in the discord server of Bloc. More info: https://discord.com/channels/649708778631200778/649708993773699083/860604230930006016
I will copy paste the message and code here. we have been using bloc
in our app for over a year and it seems we have got an issue.
Scenario: List of Posts needs to be shown in home feed. Let's say each
post is encapsulated in PostBloc (operations on post to be performed
here). I have been using moor to reactively find if anything changes
in the data (like number of likes of post which might have trigged
from post detail page where post detail was fetched again). I want to
know if it is a wise decision to create BlocFactory which will be
providing Bloc object (here different PostBloc object) based on
Post_Id.
I have written a sample BlocFactoryProvider, please let me know your
thoughts
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
typedef CreateIfAbsent<T> = T Function();
//create bloc of <T> type
//author: https://github.com/imrhk
class BlocFactory<T extends Bloc<dynamic, dynamic>> {
Map<String, T> _cache = <String, T>{};
T createBloc({#required String id, #required CreateIfAbsent<T> createIfAbsent}) {
assert(id != null && createIfAbsent != null);
if (_cache.containsKey(id)) {
return _cache[id];
}
final value = createIfAbsent.call();
_cache[id] = value;
return value;
}
void dispose() {
_cache.values.forEach((bloc) => bloc?.close());
_cache.clear();
}
}
class BlocFactoryProvider<T extends Bloc<dynamic, dynamic>, V>
extends SingleChildStatelessWidget {
final BlocFactory<T> _blocFactory;
final Widget child;
BlocFactoryProvider({BlocFactory<T> blocFactory, this.child})
: _blocFactory = blocFactory ?? BlocFactory<T>();
#override
Widget buildWithChild(BuildContext context, Widget child) {
return InheritedProvider<BlocFactory<T>>(
create: (_) => _blocFactory,
dispose: (_, __) => _blocFactory.dispose(),
child: child,
lazy: false,
);
}
static BlocFactory<T> of<T extends Bloc<dynamic, dynamic>>(BuildContext context) {
try {
return Provider.of<BlocFactory<T>>(context, listen: false);
} on ProviderNotFoundException catch (e) {
if (e.valueType != T) rethrow;
throw FlutterError(
"""
BlocProvider.of() called with a context that does not contain a Bloc of type $T.
No ancestor could be found starting from the context that was passed to BlocProvider.of<$T>().
This can happen if the context you used comes from a widget above the BlocProvider.
The context used was: $context
""",
);
}
}
Felix's replay was
It’s hard to say without more context but I’d recommend moving the
caching to the repository layer and instead have a single PostBloc
with PostChanged event that pulls the latest details (using cache).
So he did not completely discarded the idea.
declaration: I am the author of the below code shared over discord in March'21.

StreamProvider in flutter: do not always change the UI but set when

I have a StreamProvider as follows
Stream<List<Match>> streamMatches() =>
FirebaseFirestore.instance.collection('matches').snapshots()
.map((list) => list.docs.map((doc) => Match.fromJson(doc.data(), doc.id)).toList());
StreamProvider<List<Match>>(create: (context) => streamMatches(), initialData: [])
which I use it in a ListView inside a StatelessWidget
ListView(
shrinkWrap: true,
children: _getMatchesWidget(context.watch<List<Match>>())
This is nice because any update to the DB refreshes the stream. However I would like to not have the UI showing the list view change constantly in real-time for new updates (since I believe it might be a bad UX).
I would like to use a pull-on refresh (RefreshIndicator) and update the list view only onRefresh
Of course I also would like to update the list of matches in background when the user is not visualizing the list (e.g. he paused the app).
How can I tell the StreamProvider to update only in certain cases or what other Provider should I use for it?
You can change this code below:
context.watch<List<Match>>()
to this:
context.read<List<Match>>()
.read returns the List<Match> object without listening to it while .watch makes the widget listen to changes on List<Match>
Edit:
When you want to update the List<Match> object, you can call setState which will rebuild the UI and read the latest value of the stream.
You mentioned that you want to refresh the list using the RefreshIndicator.
You can just add the setState to your onRefresh method like below:
RefreshIndicator(
onRefresh: () {
setState(() {
//This refreshes the list
});
}
)

Flutter Function returns empty list using Firebase

I'm using Flutter web and firebase realtime database. In my databse I have some questions, structured as maps, and I'm trying to display them in my app.
For each question, I create a widget, I add it inside a list and then I append the list to a Column() widget.
The Column() widget is the following (view_question.dart file):
Column(
children: Question().view(),
),
As you can see, I use the method view() from a class I created named Question().
The view() method returns a list of widgets.
Before I show you the .view() method, I need to mention that outside of the Question class I have initialized the following:
List<Widget> widgets = List();
Below you can see the .view() method from the Question class (Question.dart file):
List<Widget> view() {
Database db = database();
DatabaseReference ref = db.ref('questions/');
ref.orderByKey().once("value").then((e) {
DataSnapshot datasnapshot = e.snapshot;
datasnapshot.val().forEach((key, values) {
widgets.add(QuestionWidget(values));
print(widgets);
});
});
print(widgets);
return widgets;
I'm getting my questions' data from my database, then for each question I'm creating a QuestionWidget() widget, where I pass inside it the respective data and I add it to the widgets list. Afterwards, I print() the widgets list, so I can monitor the whole process.
As you also can see, I've added a print(widgets) just before the return statement.
This is the prints' output in my console
From that, what I understand is that the function returns an empty list and then I retrieve my data from the database and add them to the list.
So I'm asking, how can I add my data to the list, and when this process has finished, return the list and append it to the Column() widget? Should I add a delay to the return statement? What am I missing?
Thank you for your time
Data is loaded from Firebase asynchronously. By the time your return widgets runs, the widgets.add(QuestionWidget(values)) hasn't been called yet. If you check the debug output of your app, the print statements you have should show that.
For this reason you should set the widgets in the state once they're loaded. That will then trigger a repaint of the UI, which can pick them up from the state. Alternatively you can use a FutureBuilder to handle this process.
See:
How can I put retrieved data from firebase to a widget in Flutter?
Slow data loading from firebase causing "RangeError (index)"
// return type is like this
Future<List<Widget>> view() {
Database db = database();
DatabaseReference ref = db.ref('questions/');
// return this
return ref.orderByKey().once("value").then((e) {
DataSnapshot datasnapshot = e.snapshot;
datasnapshot.val().forEach((key, values) {
widgets.add(QuestionWidget(values));
});
// return widgets after added all values
return widgets;
});

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

I am losing stream data when navigating to another screen

I am new to Dart/Flutter and after "attending" a Udemy course,
everything has been going well.
Until now ;-)
As in the sample application in the Udemy course i am using the BLOC pattern.
Like this:
class App extends StatelessWidget {
Widget build(context) {
return AppBlocProvider(
child: MaterialApp(
(See "AppBlocProvider" which I later on use to get the "AppBloc")
The App as well as all the screens are StatelessWidget's.
The AppBlocProvider extends the InheritedWidget.
Like this:
class AppBlocProvider extends InheritedWidget {
final AppBloc bloc;
AppBlocProvider({Key key, Widget child})
: bloc = AppBloc(),
super(key: key, child: child);
bool updateShouldNotify(_) => true;
static AppBloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(AppBlocProvider) as AppBlocProvider).bloc;
}
}
The AppBlocProvider provides an "AppBloc" containing two further bloc's to separate the different data a bit.
Like this:
class AppBloc {
//Variables holding the continuous state
Locale appLocale;
final UserBloc userBloc;
final GroupsBloc groupsBlock;
In my application I have a "GroupSearchScreen" with just one entry field, where you can enter a fragment of a group name. When clicking a button, a REST API call is done and list of group names is returned.
As in the sample application, I put the data in a stream too.
In the sample application the data fetching and putting it in the stream is done in the bloc itself.
On the next line, the screen that uses the data, is created.
Like this:
//Collecting data and putting it in the stream
storiesBloc.fetchTopIds();
//Creating a screen ths shows a list
return NewsList();
In my case however, there are two major differences:
After collecting the data in the GroupSearchScreen, I call/create the GroupsListScreen, where the list of groups shall be shown, using regular routing.
Like this:
//Add data to stream ("changeGroupList" privides the add function of the stream!)
appBloc.groupsBlock.changeGroupList(groups);
//Call/create screen to show list of groups
Navigator.pushNamed(context, '/groups_list');
In the GroupsListScreen, that is created, I fetch the bloc.
Like this:
Widget build(context) {
final AppBloc appBloc = AppBlocProvider.of(context);
These are the routes:
Route routes(RouteSettings settings) {
switch (settings.name) {
case '/':
return createLoginScreen();
case '/search_group':
return createSearchGroupScreen();
case '/groups_list':
return createGroupsListScreen();
default:
return null;
}
}//routes
And "/groups_list" points to this function:
Route createSearchGroupScreen() {
return MaterialPageRoute(
builder: (context) {
//Do we need a DashboardScreen BLOC?
//final storiesBloc = StoriesProvider.of(context);
//storiesBloc.fetchTopIds();
return GroupSearchScreen();
}
);
}
As you can see, the "AppBlocProvider" is only used once.
(I ran into that problem too ;-)
Now to the problem:
When the GroupsListScreen starts rendering the list, the stream/snapshot has no data!
(See "if (!snapshot.hasData)" )
Widget buildList(AppBloc appBloc) {
return StreamBuilder(
stream: appBloc.groupsBlock.groups,
builder: (context, AsyncSnapshot<List<Map<String, dynamic>>>snapshot) {
if (!snapshot.hasData) {
In order to test if all data in the bloc gets lost, I tried not to put the data in the stream directly, but in a member variable (in the bloc!).
In GroupSearchScreen I put the json data in a member variable in the bloc.
Now, just before the GroupsListScreen starts rendering the list, I take the data (json) out of the bloc, put it in the stream, which still resides in the bloc, and everything works fine!
The snapshot has data...
Like this (in the GroupsListScreen):
//Add data to Stream
appBloc.groupsBlock.changeGroupList(appBloc.groupsBlock.groupSearchResult);
Why on earth is the stream "losing" its data on the way from "GroupSearchScreen" to "GroupsListScreen" when the ordinary member variable is not? Both reside in the same bloc!
At the start of the build method of the GroupsListScreen, I have a print statement.
Hence I can see that GroupsListScreen is built twice.
That should be normal, but could that be the reason for not finding data in the stream?
Is the Stream listened on twice?
Widget buildList(AppBloc appBloc) {
return StreamBuilder(
stream: appBloc.groupsBlock.groups,
I tried to explain my problem this way, not providing tons of code.
But I don't know if it's enough to give a hint where I can continue to search...
Update 16.04.2019 - SOLUTION:
I built up my first app using another app seen in a Udemy course...
The most important difference between "his" app and mine is that he creates the Widget that listens to the stream and then adds data to the stream.
I add data to the stream and then navigate to the Widget that shows the data.
Unfortunately I used an RX-Dart "PublishSubject." If you listen to that one you will get all the data put in the stream starting at that time you started listening!
An RX-Dart "BehaviorSubject" however, will also give you the last data, just before you started listening.
And that's the behavior I needed here:
Put data on stream
Create Widget and start listening
I can encourage all Flutter newbies to read both of these very good tutorials:
https://medium.com/flutter-community/reactive-programming-streams-bloc-6f0d2bd2d248
https://www.didierboelens.com/2018/12/reactive-programming---streams---bloc---practical-use-cases/
In the first one, both of the streams mentioned, are explained very well.