Dart - processing RxCommand result before send it to RxLoader - flutter

I'm writing a Flutter app and I decided to use RxDart to pass my data and events along the managers, services and UI.
Basically I have a service which fetches data from a web service and returns it. Let's assume it returns a List of a model called ExploreEntity.
class ExploreMockService extends ExploreServiceStruct {
final String response = /** a sample json **/;
#override
Future<List<ExploreEntity>> loadExploreData(PaginationInput input) async {
await Future.delayed(new Duration(seconds: 2));
return List<ExploreEntity>.from(jsonDecode(response));
}
}
Now in my manager class I call the loadExploreData method inside a RxCommand.
class ExploreManagerImplementation extends ExploreManager {
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input) =>
sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input));
}
}
And finally I get the result by a RxLoader and pass it to a GridView if data was fetched successfully.
class ExplorePageState extends State<ExplorePage>{
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Explore"),
),
body: Column(children: <Widget>[
Expanded(
child: RxLoader<List<ExploreEntity>>(
commandResults:
sl.get<ExploreManager>().loadExploreDataCommand.results,
dataBuilder: (context, data) => ExploreGridView(data),
placeHolderBuilder: (context) => Center(
child: Center(
child: CircularProgressIndicator(),
),
),
errorBuilder: (context, error) => Center(
child: Text("Error"),
)),
)
]));
}
}
It works like a charm but when I wanted to load the data of the next page from web service and append it to the list, I couldn't find a solution to store the content of previous pages and just append the new page's contents to them, since data is passed along the RxCommand and RxLoader automatically.
When loadExploreData sends the reponse to the manager, I need to firstly append the result to a list, and then send that list as the result to RxLoader. Any suggestions?

Hmm that's a good question if this can be done just using Rx. What I would do is keeping a list of the received items in the manager. So when triggering the command to get the next page the command would first add the new data to the list and then push the whole list to the UI.
I"m curious if there is another solution.
My described approach in a rough code sample
class ExploreManagerImplementation extends ExploreManager {
List<ExploreEntity>> receivedData = <ExploreEntity>[];
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input)
async {
var newData = await sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input);
receivedData.addAll(newData);
return receivedData;
};
}
}

Related

Flutter - updating state and THEN navigating away

I have a stateful widget called AuthenticatingScreen where I'm trying to perform the following flow...
Output message letting the user know we are logging them in
Get user oAuth token (calls to service file)
Update the message to let the user know we are loading their details
Fetch the users details and redirect them away
The problem is that at step three, I'm rebuilding the state, which is in turn causing the build method to be fired again and calling the service again, which triggers an exception.
import 'package:flutter/material.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
// step 1) our default message
String _message = 'Please wait while we log you in...';
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
Widget build(BuildContext context) {
// step 2) get our oAuth token
AuthService.handleCallback(widget.token).then((accessCode) async {
// step 3) update our message
setState(() => _message = 'We\'re just getting your details');
// step 4) retrieve our user details and redirect away
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/home',
(Route<dynamic> route) => false,
);
});
});
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
My question being: How can I work around this / extract this logic to only fire when the widget is created, while still having access to the build context for navigation?
I've tried making the widget itself stateless and extracting the message and spinner into a separate widget, but changing the input argument alone still doesn't force a rebuild.
you can do it this way, i usually use getx & controller to achieve this.
separate the UI class & service class preferably in a controller
make the UI class statefull
call the API in onInit() method,as it called only once it will trigger the
service class
in API method when you get the result 200, initiate the UI transition
Ok, so I have figured out the solution. It seems making service calls within the build() method is a bad idea.
Moving my service calls into a void function which can then be called within the initState() method seems to be the way to go.
import 'package:flutter/material.dart';
import 'package:testing/screens/home.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
/// the default message to display to the user.
String _message = 'Please wait while we log you in...';
void _authenticateUser(String token) {
AuthService.handleCallback(widget.token).then((accessCode) async {
// we've got the users token, now we need to fetch the user details
setState(() => _message = 'We\'re just getting your details');
// after fetching the user details, push them to the home screen
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
HomeScreen.name,
(Route<dynamic> route) => false,
);
});
});
}
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
void initState() {
super.initState();
_authenticateUser(widget.token);
}
#override
Widget build(BuildContext context) {
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
And this way when the build() method is called again for the rebuild, very little details have to be redrawn.

Pagination with Flutter, Ferry, Graphql?

I have graphql hooked up, and the collection in question has about 2000 documents. What I want to do is simply load in 25 at a time. If I perform the query to fetch the full list it takes 5-10 seconds, so what I need is for it to only ask for 25 at a time.
I have it working so it will load up X amount of documents and display them in a list. What I cannot figure out is how to get pagination working. Ive read the docs 100 times, but I cannot make sense of it. Here is the pagination doc for ferry https://ferrygraphql.com/docs/pagination
I am fairly new to flutter and dart so any help here would be appriecited. Thank you.
This is my code. Currently this just displays the first 25 documents and nothing else.
class DisplayPartnerOrganistions extends StatefulWidget {
#override
_DisplayPartnerOrganistionsState createState() =>
_DisplayPartnerOrganistionsState();
}
class _DisplayPartnerOrganistionsState
extends State<DisplayPartnerOrganistions> {
var offset = 0;
final client = GetIt.I<Client>();
late GFetchPartnerOrganisationsReq fetchPartnerOrganisationsReq;
#override
void initState() {
super.initState();
fetchPartnerOrganisationsReq = GFetchPartnerOrganisationsReq(
(b) => b
..vars.offset = offset
..vars.limit = 25,
);
}
Widget _buildList(partners) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, item) {
return _buildRow(partners[item]);
});
}
Widget _buildRow(partnerOrganisation) {
return ListTile(
title: Text(partnerOrganisation.toString()),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Partner Organisations'),
),
body: Operation<GFetchPartnerOrganisationsData,
GFetchPartnerOrganisationsVars>(
client: client,
operationRequest: fetchPartnerOrganisationsReq,
builder: (context, response, error) {
if (response!.loading) {
return const Center(child: CircularProgressIndicator());
}
final partners = response.data?.partnerOrganizations;
return _buildList(partners);
}),
);
}
}
I been trying different things for about 12 hours but nothing is making any sense. Am I using the wrong widgets or something?
You will need to create a requestId so ferry considers the different request for the different pages the same one:
final fetchPartnerOrganisationsReq = GFetchPartnerOrganisationsReq((req) =>
req
..requestId = 'myRequestId'
..vars.offset = 0
..vars.limit = 25,
);
If you don't do this, ferry creates an id from your request type and the variables of the request, which will be different for the different pages (offset won't be the same).
Then when you need to load more data, you can rebuild the request with different variables (offset). Rebuilding it will allow you to keep the same requestId you specified in the first place.
final newRequest = fetchPartnerOrganisationsReq.rebuild((request) =>
// `requestId` is still `'myRequestId'`
request..vars.offset = fetchPartnerOrganisationsReq.vars.offset + 25 // <- You update the offset here
request..updateResult = (previous, result) {
// This tells ferry what to do when you get the result from the new query.
// Since it is using the same id as the initial one (with `offset = 0`), by default it will override the result.
// Here we can tell ferry how to keep both.
// `previous` is what you already received in the previous query (the previous page).
// `result` is the new page you get.
if (previous == null) return result; // If we didn't have a previous page yet, we just use what we got with the new query.
return previous!.rebuild((previous) =>
previous.partnerOrganizations.addAll(result!.partnerOrganizations), // We concatenate the lists of the previous pages and the new one.
);
},
);
When do you create a new request ?
It can be when you reach the end of a list. You can know that by using a ScrollController() with a listener. You can know you reached the end of the list with _scrollController.position.extentAfter == 0).

Flutter_bloc: Is there a cleaner way to access the state of a flutter Cubit than a redundant if/else statement?

I'm using flutter_bloc and have created a sessionState that gets triggered when a user has successfully authenticated and the state saves the user object (did).
I use a Cubit to change the state which then results in showing different screens in the ui. But whenever I want to access the did object of the Verified sessionState which holds the user information, I have to create an if else statement in every file to check if the sessionState is Verified and if that is true I can access the did object with state.did.
I would be interested to know if it's possible to provide this did object to all underlying widgets without passing it down manually every widget.
I would like to be able to just access the did object from the context so that I can access it everywhere below where it is provided.
My SessionState:
abstract class SessionState {}
class UnkownSessionState extends SessionState {}
class Unverified extends SessionState {}
class Verified extends SessionState {
Verified({required this.did});
final Did did;
}
The SessionCubit launches states that I use to define the global state of the app and show different screens as a result.
class SessionCubit extends Cubit<SessionState> {
SessionCubit(this.commonBackendRepo) : super(UnkownSessionState()) {
attemptGettingDid();
}
final CommonBackendRepo commonBackendRepo;
final SecureStorage secureStorage = SecureStorage();
Future<void> attemptGettingDid() async {
try {
//more logic
emit(Unverified());
} catch (e) {
emit(Unverified());
}
}
void showUnverified() => emit(Unverified());
void showSession(Did did) {
emit(Verified(did: did));
}
}
This is how I currently access the did object in every file:
BlocBuilder<SessionCubit, SessionState>(builder: (context, state) {
if (state is Verified) {
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(
state.did.username,
style: Theme.of(context).textTheme.headline5,
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: CredentialDetailsView(),
))
],
);
} else {
return const Text("Unverified");
}
});
Edit:
That's how I would imagine the optimal solution to my scenario:
Create if/else to check if state is verfied in one parent widget.
If state is verfied create a Provider as a child with the 'did' Object so that the children of this Provider don't have to access the SessionState but can just access the provided did object.
Response to Robert Sandberg's answer
I also have an AppNavigator that either start the authNavigator or sessionNavigator. So the sessionNavigator is the parent widget of all widgets that get shown only when the state is Verified. So I think this would be a great place to wrap the sessionNavigator with the Provider.value, as you explained it.
I wrapped my SessionNavigator with the Provider.value widget and provided the state object:
class AppNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<SessionCubit, SessionState>(
builder: (context, state) {
return Navigator(
pages: [
if (state is UnkownSessionState)
MaterialPage(child: StartupScreen()),
//show auth flow
if (state is Unverified)
MaterialPage(
child: BlocProvider(
create: (context) => AuthCubit(context.read<SessionCubit()),
child: AuthNavigator(),
)),
//show session flow
if (state is Verified)
MaterialPage(
child: Provider.value(
// here I can access the state.did object
value: state,
child: SessionNavigator(),
))
],
onPopPage: (route, result) => route.didPop(result),
);
},
);
}
}
For testing I tried to watch the provided value inside of my Home screen which is a child of the SessionNavigator:
...
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
//
final sessionState = context.watch<SessionState>();
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
floating: true,
expandedHeight: 60.0,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
title: Text(
// I can't access state.did because value is of type SessionState
sessionState.did,
style: Theme.of(context).textTheme.headline6,
),
titlePadding:
const EdgeInsetsDirectional.only(start: 20, bottom: 16)),
)
],
);
}
}
But because the type of my value is SessionState I can't access the did object of the underlying Verified state. I though about providing the state.did object but I don't know how I would watch for that type.
I also got the error that my home screen couldn't find the Provider in the build context. Could that happen because my Home screen has a Navigator as parent?
This is a visualization of my app architecture:
I use this pattern sometimes, and it sounds like my solution very much fits your imagined optimal solution :)
In a parent:
BlocBuilder<MyBloc, MyBlocState>(
builder: (context, state) {
return state.when(
stateA: () => SomeWidgetA(),
stateB: () => SomeWidgetB(),
stateC: (myValue) {
return Provider.value(
value: myValue,
child: SomeWidgetC(),
);
}),
}
)
... and then in SomeWidgetC() and all it's children I do the following to get hold of myValue (where it is of type MyValueType)
final myValue = context.watch<MyValueType>();
If you want, you could also use regular InheritedWidget instead of using the Provider package but you get a lot of nifty features with Provider.
EDIT
Response to your further questions.
In your case, if you provide the entire state as you do in AppNavigator, then you should look for Verified state in SessionNavigator as such:
final sessionState = context.watch<Verified>();
Or, if you only need the Did object, then just provide that, so in AppNavigator:
if (state is Verified)
MaterialPage(
child: Provider.value(
value: state.did,
child: SessionNavigator(),
))
And then in SessionNavigator:
final did = context.watch<Did>();
Edit 3:
It might be that your problems arises with nested Navigators. Have a look at this SO answer for some guidance.
If you continue struggle, I suggest that you open new SO questions for the specific problems, as this is starting to get a bit wide :) I think that the original question is answered, even though you haven't reached a final solution.
You can use something like (state as Verified).did.username, but I would still recommend using the if/else pattern since you ensure that you are using the correct state.
In my personal projects, I use the Freezed package (https://pub.dev/packages/freezed) for such cases since there are some useful methods to cover this. For instance, by using freezed, your issue could be resolved as:
return state.maybeWhen(
verified: (did) => CustomScrollView(...),
orElse: () => const Text("Unverified"),
)

Riverpode and Hooks in flutter, how to implement the StateNotifier for async calls?

I am new to flutter and recently I decided to look into providers to be able to reuse some sqlite DB calls I have using FutureBuilder as suggested in this other question.
After reading about it I started using riverpod_hooks but I got stuck.
Currently I have a provider to get the data from sqlite:
final bloodPressureProvider = FutureProvider((_) => _findAllBloodPressures());
Future<List<BloodPressure>> _findAllBloodPressures() async {
var _bloodPressure = BloodPressure.forSearchOnly();
var result = await _bloodPressure.findAll();
return result;
}
Which I can successfully use to render a chart and display a list:
class BloodPressureList extends HookWidget {
#override
Widget build(BuildContext context) {
final bpList = useProvider(bloodPressureProvider);
return Scaffold(
drawer: NutriDrawer(context).drawer(),
appBar: AppBar(
title: Text('Blood Pressure History'),
),
body: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(0, 25, 15, 5),
child: bpList.when(
data: (bloodPressureList) => _buildLineChart(bloodPressureList),
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('Ooooopsss error'),
),
),
Expanded(
child: bpList.when(
data: (bloodPressureList) => _getSlidableListView(bloodPressureList, context),
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('Ooopsss error'),
),
),
],
), ...
The issue I currently have is to make the notification to the widgets whenever a new element is added to the DB which is done in another screen/widget, before using providers I would use setState but now I understand I should implement possibly a StateNotifier but I am missing the proper way to do it integrated with the asynchronous call returned from the DB used in the provider fragment above.
Any pointers in the right direction is greatly appreciated.
Update with solution: Based on the accepted answer below I used context.refresh and it did exactly what I was looking for:
var result = Navigator.push(context, MaterialPageRoute(builder: (context) {
return BloodPressureDetail(title, bloodPressure);
}));
result.then((_) => context.refresh(bloodPressureProvider));
So when navigating back from the edit/add screen the provider context is refreshed and the state of the chart and list get updated automatically.
By default FutureProvider cache the result of the future so it's only fetched the first time it's accessed.
If you want to get a new value each time you go to the screen use FutureProvider.autoDispose.
If you want to manually refresh the provider you can use context.refresh(bloodPressureProvider) with FutureProvider.
And if you don't want to access you db for fetching values but still want to have screens synchronized you can implement a stateNotifier which will represent your db state
final dbState =
StateNotifierProvider<dbState>((ProviderReference ref) {
final dbState = DbState();
dbState.init();
return dbState;
});
class dbState extends StateNotifier<AsyncValue<List<String>>> {
/// when the constructor is called the initial state is loading
dbState() : super(AsyncLoading<List<String>>());
/// fetch the values from the database and set the state to AsyncData
///(could be good to add a try catch and set the sate to an error if one
/// occurs)
void init()async{
state = AsyncLoading<List<String>>();
final fetchedValues = await fetchValues();
state = AsyncData<List<String>>(fetchedValues);
}
}

Slow data loading from firebase causing "RangeError (index)"

Here i am trying to load image link from firebase into flutter app then passing it into image.network(). the data loading is taking time to load data so it is giving me following error: RangeError (index): Invalid value: Valid value range is empty: 1 but after 5 second the data is loaded successfully and printed in console but not displayed on screen. here is code to print data
class _DataFetching extends State<MyHomePage> {
List<eyes> allCriminals=eyes.allcriminals();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("fetch"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Image.network(allCriminals[1].link
),
]// This trailing comma makes auto-formatting nicer for build methods.
)));
}
}
and function to load data from firebase is
static List<eyes> allcriminals() {
var allEyes = new List<eyes>();
Future<Query> queryUsers() async{
return await FirebaseDatabase.instance
.reference()
.child('Facial Parts/Eyes');
}
queryUsers().then((query) {
query.once().then((snapshot) {
var result = snapshot.value.values as Iterable;
for (var item in result) {
print(result);
allEyes.add(new eyes(criminal_id: item["criminal_id"],
eye_size: item["eyesize"],
link: item["link"]));
print(item['link']);
}
});
});
return allEyes;
}
is there any method that can help me to load all data and after that it should go to Widget build()?
Data is loaded from Firebase asynchronously. By the time your return allEyes runs, the .then() from the database hasn't run yet. And you can't return something now that hasn't been loaded yet.
That's why you'll have to return a Future<List<eyes>>, which is a promise to have a list of eyes at some point in the future.
static Future<List<eyes>> allcriminals() {
var query = FirebaseDatabase.instance.reference().child('Facial Parts/Eyes');
return query.once().then((snapshot) {
var allEyes = new List<eyes>();
var result = snapshot.value.values as Iterable;
for (var item in result) {
allEyes.add(new eyes(criminal_id: item["criminal_id"],
eye_size: item["eyesize"],
link: item["link"]));
}
return allEyes;
});
}
Then you can use that in your render method by either using a FutureBuilder or by directly calling setState().