How to prevent multiple network calls from a flutter bloc? - flutter

I am building an app using flutter and I'm using the flutter_bloc package for managing state. I have a simple Avatar widget used for displaying for displaying profile photo of a user.
/// A widget for displaying a user profile avatar.
class UserAvatar extends StatelessWidget {
/// The unique identifier for a particular user.
final int userId;
/// The size of the avatar.
final double radius;
UserAvatar({required this.userId, this.radius = 30}) {}
#override
Widget build(BuildContext context) {
context.read<UserInfoBloc>().add(UserInfoEvent.getUserInfo(userId));
return BlocBuilder<UserInfoBloc, UserInfoState>(
builder: (context, state) {
return state.when(initial: () {
return Text('Initial');
}, loading: () {
return Text('Loading');
}, loaded: (user) {
print(user);
return Container(
child: Avatar(
shape: AvatarShape.circle(radius),
loader: Center(
child: ClipOval(
child: Skeleton(
height: radius * 2,
width: radius * 2,
),
),
),
useCache: true,
sources: [NetworkSource(user.avatarUrl ?? '')],
name: user.firstName!.trim(),
onTap: () {
//TODO implement Navigation to profile page
},
),
);
}, error: () {
return Text('error');
});
},
);
}
}
My problem is that the widget will be used multiple times (when displaying a feed and there are contents by the same user). I initially just have the id of the user, then I try to make a network call and try to get the user. I've implemented some form of caching in my repository:
#LazySingleton(as: UserInfoRepository)
class UserInfoRepositoryImpl extends UserInfoRepository {
final GetUserInfoRemoteDataSource remoteDataSource;
final GetUserInfoLocalDataSource localDataSource;
UserInfoRepositoryImpl(
{required this.remoteDataSource, required this.localDataSource});
#override
Future<Either<Failure, User>> getUserInfo(int id) async {
try {
final existsInCache = localDataSource.containsUserModel(id);
if (existsInCache) {
return right(localDataSource.getUserModel(id));
} else {
final result = await remoteDataSource.getUserInfo(id);
localDataSource.cacheUserModel(result);
return right(result);
}
} on ServerExceptions catch (e) {
return left(e.when(() => Failure(),
requestCancelled: () => Failure.requestCancelled(),
unauthorisedRequest: () => Failure.unauthorisedRequest(),
badRequest: (e) => Failure.badRequest(e),
notFound: () => Failure.notFound(),
internalServerError: () => Failure.internalServerError(),
receiveTimeout: () => Failure.receiveTimeout(),
sendTimeout: () => Failure.sendTimeout(),
noInternetConnection: () => Failure.noInternetConnection()));
} on CacheException {
return left(Failure.cacheFailure());
}
}
}
Of course, I used the injectable package for dealing with my dependencies, and I've used the #LazySingleton annotation on the repository. But unfortunately, if I try to display two avatars of the same user, two separate network calls will be made. Of course, I don't want that. How can I solve this problem?

Related

Flutter awesome notifications how to fix StateError (Bad state: Stream has already been listened to.)

I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}

Flutter - Show only one Dialog at the time

I am working on a flutter application using several dialogs for several purposes.
In our code, there are some cases where the user can open a Dialog. Inside this dialog, there are some buttons that will also open another dialog. It results with 2 dialogs on top of each other and with a very dark background screen.
What we would like to do is to only display one dialog at the time. How can we achieve that ?
Here is a simple code to illustrate our issue:
class MyScreen extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog1>(
context: context,
builder: (BuildContext context) => MyFirstDialog(),
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MyFirstDialog extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button in first dialog'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog2>(
context: context,
builder: (BuildContext context) => MySecondDialog(), // <- This will appear on top of the first dialog
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MySecondDialog extends StatelessWidget {
#override
build(BuildContext context) {
return Text('Second Dialog');
}
}
let me give you a widget for that
class MultiDialog extends StatefulWidget {
final Widget child;
const MultiDialog({Key key, this.child}) : super(key: key);
#override
_MultiDialogState createState() => _MultiDialogState();
static void addDialog(
{#required BuildContext context, #required Widget dialog}) {
assert(context != null, "the context cannot be null");
assert(dialog != null, "the dialog cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._addDialog(dialog);
}
static void remove({#required BuildContext context}) {
assert(context != null, "the context cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._remove();
}
}
class _MultiDialogState extends State<MultiDialog> {
final _allDialogs = <Widget>[];
void _addDialog<T>(Widget dialog) {
assert(dialog != null, "The dialog cannot be null");
setState(() {
_allDialogs.add(dialog);
});
}
void _remove() {
if (_allDialogs.isEmpty) {
print("No dialogs to remove");
return;
}
setState(() {
_allDialogs.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_allDialogs.isNotEmpty) _allDialogs.last,
],
);
}
}
and when you want to add a dialog just call
MultiDialog.addDialog(
context: context,
dialog: AlertDialog(),
);
call Navigator.pop to remove the dialog, if there is another dialog which you pushed exist it will be shown, you can further pop them all with results, PS:this code isn't tested, let me know in the comments if this works for you
call MultiDialog.remove(context:context) to pop the visible dialog and bring back the previous dialog,
and if you receive a error that the addDialog is called on null, its because how flutter works, after MultiDialog use a Builder to introduce a new context use it call showDialog,
PS:ABOVE CODE IS TESTED
i made a stream out of the events that cause the dialog to pop up and used rx darts exhaust map to wait for the result (i was already using rxdart)
dialogEventStream
.exhaustMap((_) => maybeShowDialog().asStream())
.listen((_) {});
Future<bool> maybeShowOfflineModeDialog() async {
final isOfflineModeEnabled = await _sharedPreferencesService.isOfflineModeEnabled();
if (!isOfflineModeEnabled) {
final isLoginOffline = await _navigationService.showDialog(NoConnectionDialog());
if (isLoginOffline == true) {
await _sharedPreferencesService.setIsOfflineModeEnabled(isOfflineModeEnabled: true);
return await _navigationService.pushReplacement(AppShellOffline.routeName) ?? true;
} else {
return false;
}
}
return false;
}
something like this

Bloc - Is it possible to yield states for a previous page in the navigation stack?

I have a BlocBuilder which handles building widgets depending on the yielded state for my dashboard page.
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
print(state);
if (state is DashboardInitial) {
return loadingList();
} else if (state is DashboardEmpty) {
return emptyList();
} else if (state is DashboardLoaded) {
return loadedList(context, state);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => AddPage()));
},
I want to be able to navigate to the add page, fill in some textfields, and then dispatch an event to my dashboard bloc, with the idea being that upon navigating back to the dashboard, my list will be updated.
class AddPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
TextEditingController titleController = TextEditingController();
TextEditingController descriptionController = TextEditingController();
return Scaffold(
appBar: AppBar(title: Text('Add')),
body: Container(
padding: EdgeInsets.all(10),
child: Column(
children: [
TextField(
controller: titleController,
),
TextField(
controller: descriptionController,
),
RaisedButton(onPressed: () {
BlocProvider.of<DashboardBloc>(context)
.add(DashboardWorryAdded('title', 'description'));
}),
],
),
),
);
}
}
When following the code using breakpoints, i am able to see that my state is yielded in the 'mapeventtostate' function, however my dashboard is never rebuilt with the new values.
Here is the code for my Bloc, events, and states. My first thought would be that Equatable was detecting the same state being returned, but upon removing Equatable, my problem is still persists.
#override
Stream<DashboardState> mapEventToState(
DashboardEvent event,
) async* {
if (event is DashboardWorryAdded) {
yield* _mapDashboardWorryAddedToState(event);
} else if (event is DashboardLoading) {
yield* _mapDashboardLoadingToState(event);
} else if (event is AppStarted) {
yield* _mapAppStartedToState(event);
}
}
Stream<DashboardState> _mapAppStartedToState(AppStarted event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList.length != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardLoadingToState(
DashboardLoading event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardWorryAddedToState(
DashboardWorryAdded event) async* {
await repo.addWorry(event.title, event.description);
List<Worry> worryList = List<Worry>();
worryList = await repo.getAllWorries();
yield DashboardLoaded(worryList: worryList);
}
}
#immutable
abstract class DashboardEvent {}
class DashboardLoading extends DashboardEvent {
DashboardLoading();
}
class DashboardWorryAdded extends DashboardEvent {
final String title, description;
DashboardWorryAdded(this.title, this.description);
}
class AppStarted extends DashboardEvent {
AppStarted();
}
#immutable
abstract class DashboardState {}
class DashboardInitial extends DashboardState {
DashboardInitial();
}
class DashboardLoaded extends DashboardState {
final List<Worry> worryList;
DashboardLoaded({this.worryList});
}
class DashboardEmpty extends DashboardState {
DashboardEmpty();
}
Instead of trying to mutate another page's state (a bit of a no-no where state management is concerned), take advantage of the fact that the push method of the navigator returns a future that completes when that page gets popped, and as a bonus, the value of the future will include the value that was given to the pop method in the other page. So you can now do something like this:
class DashboardBloc {
...
void showAddPage() async {
// Do this to retrieve the value passed to the add page's call to `pop`
final value = await Navigator.of(context).push(...);
// Do this if the add page doesn't return a value in `pop`
await Navigator.of(context).push(...);
// Either way, you can now refresh your state in response to
// the add page popping
emit(...);
}
}
Note: This works just as well for named routes too.

Reload widget in flutter

I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.

BlocProvider.of() called with a context that does not contain a Bloc of type CLASS

in flutter i just learn how can i use Bloc on applications and i want to try to implementing simple login with this feature. after implementing some class of bloc to using that on view
i get error when i try to use this code as
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
inside RaisedButton
Error:
BlocProvider.of() called with a context that does not contain a Bloc
of type LoginListingBloc.
My view :
class _HomePageState extends State<HomePage> {
LoginListingBloc _loginListingBloc;
#override
void initState() {
super.initState();
_loginListingBloc =
LoginListingBloc(loginRepository: widget.loginRepository);
}
...
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: _loginListingBloc,
child: Scaffold(
appBar: AppBar(
elevation: 5.0, title: Text('Sample Code', style: appBarTextStyle)),
body: Center(
child: RaisedButton(
child: Text(
'click here',
style: defaultButtonStyle,
),
onPressed: () {
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
}),
),
),
);
}
}
LoginListingBloc class:
class LoginListingBloc extends Bloc<LoginListingEvent, LoginListingStates> {
final LoginRepository loginRepository;
LoginListingBloc({this.loginRepository});
#override
LoginListingStates get initialState => LoginUninitializedState();
#override
Stream<LoginListingStates> mapEventToState(
LoginListingStates currentState, LoginListingEvent event) async* {
if (event is LoginEvent) {
yield LoginFetchingState();
try {
final loginInfo = await loginRepository.fetchLoginToPage(
event.loginInfoModel.username, event.loginInfoModel.password);
yield LoginFetchedState(userInfo: loginInfo);
} catch (_) {
yield LoginErrorState();
}
}
}
}
and other classes if you want to see theme
AppApiProvider class:
class AppApiProvider {
final successCode = 200;
Future<UserInfo> fetchLoginToPage(String username, String password) async {
final response = await http.get(Constants.url + "/api/v1/getPersons");
final responseString = jsonDecode(response.body);
if (response.statusCode == successCode) {
print(responseString);
return UserInfo.fromJson(responseString);
} else {
throw Exception('failed to get information');
}
}
}
LoginEvent:
class LoginEvent extends LoginListingEvent {
final LoginInfoModel loginInfoModel;
LoginEvent({#required this.loginInfoModel}) : assert(loginInfoModel != null);
}
LoginInfoModel:
class LoginInfoModel {
String username;
String password;
LoginInfoModel({this.username, this.password});
}
final testLogin = LoginInfoModel(username:'exmaple',password:'text');
No need to access loginListingBloc from context since it exists in the current class and not up the widget tree.
change:
BlocProvider.of<LoginListingBloc>(context).dispatch(LoginEvent(loginInfoModel: testLogin));
to:
_loginListingBloc.dispatch(LoginEvent(loginInfoModel: testLogin));
For all others who come here for the error message:
Make sure you always specify the types and don't omit them:
BlocProvider<YourBloc>(
create: (context) => YourBloc()
child: YourWidget()
);
and also for
BlocProvider.of<YourBloc>(context);