Flutter: Provider and how to update records from the DB in the background - flutter

I am new to Flutter and I have this simple use case: in my Cloud Firestore DB I have a list of JSON representing events. I want to show them through my Flutter app in a ListView.
My requirements is that the ListView doesn't refresh in real-time but only when a pull-on refresh (implemented using RefreshIndicator) is done by the user or when the app resumes from background
I tried to implement this in 2 ways (I am using provider package for state management):
Using StreamProvider to create a stream of records from the DB. This continuosly updates the list view (basically the widget changes while the user is looking at it and I don't want this)
Using a ChangeNotifierProvider that refers to a EventManager class which holds a List<Event>. This class has a pull method which updates its internal state. I call this method when the user does the pull-on refresh (in the onRefresh callback of RefreshIndicator).
Option 2 seems to work well however I do not know how to implement the refresh when the app resumes from background. As I said I am using provider (and therefore StatelessWidget) and apparently there is no way to bind to these events when using StatelessWidgets
Do you have any suggestions and best practices for this use case?

You need to access Flutters lifecycle methods and fire a callback when the app resumes.
You can add a stateful widget with WidgetsBindingObserver and put that somewhere in the scope of your Provider, but as a parent of whatever widget you use to display the info.
Or you can make your PullToRefresh widget stateful and do the same thing.
class LifeCycleWidget extends StatefulWidget {
#override
_LifeCycleWidgetState createState() => _LifeCycleWidgetState();
}
class _LifeCycleWidgetState extends State<LifeCycleWidget>
with WidgetsBindingObserver {
AppLifecycleState _appLifecycleState;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
refreshOnResume();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_appLifecycleState = state;
});
refreshOnResume();
}
void refreshOnResume() {
if (_appLifecycleState == AppLifecycleState.resumed) {
print('resumed');
// your refresh method here
}
}
#override
Widget build(BuildContext context) {
return HomePage();
}
}
Add the following to your main method if it's not there already.
WidgetsFlutterBinding.ensureInitialized();
Another way to do it without adding a stateful widget would be with GetX. You can still keep all your Provider stuff but only use the SuperController which provides lifecycle methods. This I can't test because I don't have your Provider code but you can probably get away with creating the class below and initializing the controller somewhere within the scope of the relevant Provider widget with
Get.put(LifeCycleController());
Then call the function in the onResumed override and you can use Get.context if you need context.
class LifeCycleController extends SuperController {
#override
void onDetached() {
debugPrint('on detached');
}
#override
void onInactive() {
debugPrint('on inactive');
}
#override
void onPaused() {
debugPrint('on pause');
}
#override
void onResumed() {
// your refresh function here. Access context with Get.context
debugPrint('on resume');
}
}

Related

Flutter: How to trigger a bloc event in the controller?

In summary I want the "context.read().add(EnabledEvent())" to work from the controller itself or some other alternative I can use to trigger an event from the controller and update the state:
class SplashCtrl extends GetxController {
#override
void onReady() {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
Another thing I tried was this:
ExampleBloc testBloc = ExampleBloc();
testBloc.add(TestEvent());
in the controller, and it does seem to trigger the event, but the UI doesn't update with the state change. I seem to really need the Context for that to change. Any ideas?
MORE DETAILS AND CONTEXT:
I want to trigger bloc events not just in the page itself, but in the page's controller!
So I have pages in my flutter app with each view having a controller set up like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
SplashCtrl is the controller for this page. This splash view page is one of the first pages that loads it runs functions when it's added and ready (basically instantly), such as checking if the user is logged in and loading their data to the app, like this:
class SplashCtrl extends GetxController {
#override
void onReady() {
// run stuff here
}
I have been able to get away with creating lambda functions in the pages and triggering events based on what they do and toggle like this:
IconSwitchedButtonUi(
value: state.isExampleOn,
icon: Images.toggleIcon,
title: "Is Example enabled?",
onChanged: (value) {
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},
),
but now I need a bloc event to trigger and change the state a bit if a user has a certain value for one of their fields. How do I do that? How do I trigger an event from within the controller? The "context.read" doesn't work because the controller doesn't have context, or does it?
ExampleBloc testBloc = ExampleBloc(); testBloc.add(TestEvent());
This does not work because you were declaring totally new instance of bloc that wasnt connected by any BlocProvider to your widget tree, hence there is no way you could trigger this bloc events that would emit state your BlocBuilder could react to. Since there is no bloc provided to widget tree read() wont find this bloc.
In order for this work you need to:
Pass BuildContext that have some Bloc or Cubit provided by BlocProvider
void foo(BuildContext context) {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
..or listen to bloc changes directly inside controller
class SplashCtrl extends GetxController {
SplashCtrl(MyBloc bloc) {
bloc.stream.listen((event) {
// React to bloc changes here
if (event is MyBlocEvent) {
// Do something
}
}
}
}
Remember that even in this approach bloc instance given to SplashCrtl constructor must be the same that is provided by BlocProvider to a widget tree in order for this to work.
I was able to resolve this by adding a late BuildContext to my controller:
class SplashCtrl extends GetxController {
late BuildContext context;
#override
void onReady() {
// run stuff here
}
After that, in the page itself, I passed the context like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
controller.context = context;
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
Then I was able to use the context within the controller itself!!
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},

Flutter clean architecture with Bloc, RxDart and StreamBuilder, Stateless vs Stateful and dispose

I'm trying to implement a clean architecture with no dependency of the framework in the business' logic layers.
The following example is a Screen with only a Text. I make an API Rest call in the repository and add the response to a BehaviorSubject that is listened through a StreamBuilder that will update the Text. Since is an StatefulWidget I'm using the dispose method to close the BehaviorSubject's StreamController.
The example is simplified, no error/loading state handling, no dependency injection, base classes, dispose interfaces etc.
class Bloc {
final UserReposiotry _userReposiotry;
final BehaviorSubject<int> _activeUsersCount = BehaviorSubject.seeded(0);
Bloc(this._userReposiotry) {
_getActiveUsersCount();
}
void _getActiveUsersCount() async {
final response = await _userReposiotry.getActiveUsersCount();
_activeUsersCount.add(response.data);
}
ValueStream<int> get activeUsersCount => _activeUsersCount.stream;
void dispose() async {
await _activeUsersCount.drain(0);
_activeUsersCount.close();
}
}
class StatefulScreen extends StatefulWidget {
final Bloc bloc;
const StatefulScreen({Key? key, required this.bloc}) : super(key: key);
#override
State<StatefulScreen> createState() => _StatefulScreenState();
}
class _StatefulScreenState extends State<StatefulScreen> {
#override
Widget build(BuildContext context) {
final stream = widget.bloc.activeUsersCount;
return StreamBuilder<int>(
stream: stream,
initialData: stream.value,
builder: (context, snapshot) {
return Text(snapshot.data.toString());
}
);
}
#override
void dispose() {
widget.bloc.dispose();
super.dispose();
}
}
I have the following doubts regarding this approach.
StreamBuilder cancels the stream subscription automatically, but it doesn't close the StreamController. I know that you should close it if you are reading a file, but in this case, if I don't manually close it, once the StatefulScreen is no longer in the navigation stack, could it be destroyed, or it would be a memory leak?
I've seen a lot of people using StatelessWidget instead of StatefulWidget using Stream and StreamBuilder approach, if it is really needed to close the BehaviorSubject it is a problem since we don't have the dispose method, I found about the WillPopScope but it won't fire in all navigation cases and also and more important would it be more performant an approach like WillPopScope, or having an StatefulWidget wrapper (BlocProvider) inside an StatelessWidget just to do the dispose, than using an StatefulWidget directly, and if so could you point to an example of that implementation?
I'm currently choosing StatefulWidget for widgets that have animations o controllers (map, text input, pageview...) or streams that I need to close, the rest StatelessWidget, is this correct or am I missing something?
About the drain method, I'm using it because I've encountered an error navigating back while an API rest call was on progress, I found a member of the RxDart team saying it isn't really necessary to call drain so I'm confused about this too..., the error:
You cannot close the subject while items are being added from addStream
Thanks for your time.

How do you initialise a provider with data taken from shared preferences?

I am new to flutter and dart so the answer to this may be simple, or I may be going about this COMPLETELY the wrong way.
I basically want to use Provider for state management, and Shared Preferences for local storage.
I have a class called 'ResolutionsProvider' which contains a list of custom 'Resolution' objects
which are used throughout my app. It contains methods for adding and removing Resolutions and it all works fine.
The problem is that I want the data to be persistent if the user restarts the app...
I'm trying to use SharedPreferences for this so I have methods within 'ResolutionsProvider' to save data to, and load data from Shared Preferences...
I know through print calls that the data is saving to shared preferences correctly...
My (potentially wrong) thinking is that I need to achieve the following...
App is run
ResolutionsProvider is instantiated as a ChangeNotifierProvider
ResolutionsProvider.loadData() is run to populate List from SharedPreferences...
I've tried to call ResolutionsProvider.loadData() from within an initState but I get the following error...
E/flutter (30660): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: dependOnInheritedWidgetOfExactType<_InheritedProviderScope<ResolutionsProvider?>>() or dependOnInheritedElement() was called before _AppState.initState() completed.
E/flutter (30660): When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
E/flutter (30660): Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
So i guess the question is two fold...
If my approach is correct, how do I call the 'loadData' method to populate my provider on booting the app if I can't do so within initState
If my approach is incorrect, what's the right approach to achieve my basic objective?
Here's my main.dart code
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await LocalStorage.init();
runApp(
ChangeNotifierProvider(
create: (context) => ResolutionsProvider(),
child: const App(),
),
);
}
Here's the relevant code from my App.dart
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
void initState() {
loadData();
print("app initialised");
super.initState();
}
void loadData() async {
await Provider.of<ResolutionsProvider>(context).loadResolutions();
}
#override
Widget build(BuildContext context) {
return MaterialApp(blah blah blah);
Here's the relevant parts of my ResolutionProvider.dart
class ResolutionsProvider extends ChangeNotifier {
List<Resolution> resolutions = [];
// Interfacing with Shared Preferences
loadResolutions() async {
resolutions = await LocalStorage.getResolutions();
print('${resolutions.length} resolutions loaded');
// notifyListeners();
}
saveResolutions() async {
LocalStorage.saveResolutions(resolutions);
}

Flutter GetX call fetch data only once in the build methode

I have a method called getData() which used to load data from API,
and i am displaying data in a separate screen, I have one issue which whenever I navigate to this page it rebuild the whole screen and call the API again and again.
PS: I'm using getX Obx to control the UI
Question: how to call the function only when new data has added
class CategoryPageBinding extends Bindings {
#override
void dependencies() {
Get.put(CategoryPageController(), permanent: true);
}
}
class CategoryPageController extends GetxController {
#override
void onInit() {
super.onInit();
getAllCategories();
}
}
You can call the method in Controller using getX.
#override
void onInit() {
super.onInit();
getData();
}

How to reset to the initial state/value whenever I close my app using flutter_bloc

I am still a beginner when it comes to using flutter_bloc.
I have tried flutter_bloc and curious how to reset my bloc class to its initial value when I have closed the page.
my_bloc_class.dart
class sumtotal_detail_transactionsbloc extends Bloc<String, String>{
#override
String get initialState => '0';
#override
Stream<String> mapEventToState(String sumtotal_detail_transactions) async* {
yield sumtotal_detail_transactions.toString();
}
}
My widget with a BlocBuilder.
BlocBuilder<sumtotal_detail_transactionsbloc, String>(
builder: (context,sumtotal_detail_transactions) => Text(
sumtotal_detail_transactions,style: TextStyle(
fontSize: 12,
color: Colors.brown[300]
),
)
),
Whenever I close the page or navigate to the page, how can I always/automatically reset the sumtotal_detail_transactions back to its initial value?
It will break my app if the value is always kept/store as it is.
Hey 👋 I would recommend providing the bloc in the page so when the page is closed the bloc is disposed automatically by BlocProvider. No need to have a reset event, just make sure to scope blocs only to the part of the widget tree that needs it. Hope that helps!
As mentioned by the plugin author here,
I don't think it's a good idea to introduce a reset() because it directly goes against the bloc library paradigm: the only way to trigger a state change is by dispatching an event.
With that being said, you must add an event/state the will be used to trigger an initialisation event.
For example:
Add an initialisation event.
some_page_bloc_events.dart
class InitializePageEvent extends SomePageEvent {
// You could also pass on some values here, if they come from the UI/page of your app
#override
String toString() => 'InitializePageEvent';
}
Add an initialisation state.
some_page_bloc_states.dart
class InitializePageState extends SomePageState {
#override
String toString() => 'InitializePageState';
}
Next, utilise these inside your bloc class to filter incoming events and notify the UI with the appropriate states.
some_page_bloc.dart
#override SomePageState get initialState => InitializePageState();
#override
Stream<SomePageState> mapEventToState(SomePageEvent event) async* {
try {
if(event is InitializePageEvent) {
// Do whatever you like here
yield InitializePageState();
}
} catch (e) {
...
}
}
Finally, you can invoke the initialisation event wherever you deemed in necessary. In your case, it should be on the initState() method of your screen.
some_page.dart
#override
void initState() {
super.initState();
_someTransactionBloc.dispatch(InitializePageEvent());
}
Felix provided a well-written documentation for his plugin, I suggest that you go over the intro concepts how BLoC works. Please read it here.