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.
I have a question. In my project, I have some class with singleton.
DioMethod (some api configs),
Repository (Connect ApiProvider and BLoc class),
ApiProvider (call api => get data => pass to Repository),
Some BloC classes such as MoviesBloc, MovieDetailBloc,..i created a BLoC class for each Screen.
and init all at $initGetIt the same.
_i1.GetIt $initGetIt(_i1.GetIt get,
{String? environment, _i2.EnvironmentFilter? environmentFilter}) {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
gh.singleton<_i3.DioConfig>(_i3.DioConfig());
gh.singleton<_i4.DioMethod>(_i4.DioMethod(get<_i3.DioConfig>()));
gh.singleton<_i5.MovieApiProvider>(
_i5.MovieApiProvider(get<_i4.DioMethod>()));
gh.singleton<_i6.Repository>(_i6.Repository());
gh.lazySingleton<_i7.MovieDetailBloc>(
() => _i7.MovieDetailBloc(get<_i6.Repository>()),
dispose: (i) => i.dispose());
gh.lazySingleton<_i8.MoviesBloc>(() => _i8.MoviesBloc(get<_i6.Repository>()),
dispose: (i) => i.dispose());
return get;
}
I think If I scale my project, I can get some problem.
This function will be too large and we have too much singleton still live in app.
We need to init a lot of Object when opening the app => Can delay app?
How to create a singleton only alive when access to the screen and destroyed after pop this screen.
Thank you for your anwser!
This function will be too large and we have too much singleton still live in app.
I have hundreds of GetIt singletons and I do not see any problems.
We need to init a lot of Object when opening the app => Can delay app?
If you are worried about number of Objects - do not need to worry. When Flutter builds a frame (you know, it builds 60 frames per second), it creates a huge widget tree contains thousands of widgets and each widget contains dozens of objects. So hundreds of objects are really a very small number. Dart's Garbage Collector is designed for the many-object case.
How to create a singleton only alive when access to the screen and destroyed after pop this screen.
class MyPageOne extends StatefulWidget {
const MyPageOne({Key? key}) : super(key: key);
#override
_MyPageOneState createState() => _MyPageOneState();
}
class _MyPageOneState extends State<MyPageOne> {
#override
void initState() {
super.initState();
GetIt.I.registerSingleton<MyClass>(MyClass());
}
#override
void dispose() {
GetIt.I.unregister<MyClass>();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ...your page...;
}
}
This is just an example and you can, for example, encapsulate it into a widget.
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');
}
}
I am new to flutter and when I want to call my context in InitState it throws an error :
which is about
BuildContext.inheritFromWidgetOfExactType
but then I use didChangeDependencies and it works correctly.
Now I have 2 question:
1- Why calling our context in initState does not work but it works when calling from didChangeDependencies ?
(because as I read in official doc This method is also called immediately after [initState],
and both of them will be called before build method. )
2- Why do we have access to our context outside of build method ( because there we have build(BuildContext context) and we can use our context but in didChangeDependencies we don't have anything like didChangeDependencies(BuildContext context) , so from where can we call context to use it) ?
Context of a state is available to us from the moment the State loads its dependencies.
At the time build is called, context is available to us and is passed as an argument.
Now moving on,
initstate is called before the state loads its dependencies and for that reason no context is available and you get an error for that if you use context in initstate.
However, didChangeDependencies is called just a few moments after the state loads its dependencies and context is available at this moment so here you can use context.
However both of them are called before build is called.
The only difference is that one is called before the state loads its dependencies and the other is called a few moments after the state loads its dependencies.
I've found a significant difference between initState and didChangeDependencies:
initState is called only once for a widget.
didChangeDependencies may be called multiple times per widget lifecycle (in my case it was called when the keyboard appears / disappears)
initState() Called when new Widget is inserted into the tree.
The framework will call this method exactly once for each [State] object
it creates. This will be called once so perform work which required to be performed only once, but remember context can't be used here, as widget state gets loaded only initState() work is done.
Syntax:
#override
void initState() {
debugPrint('initState()');
super.initState();
}
didChangeDependencies() Called when a dependency of this [State] object changes.
So, exactly How it gets called? as by the above definition, it looks like it will be called after state changes but how we come to know the state is changed?
Example:
The below example uses the Provider state management mechanism to update the child widget from the parent widget. The Provider has an attribute called updateShouldNotify which decides whether to state is changed or not. If it's returning true then only didChangeDependencies gets called in ChildWidget class.
updateShouldNotify is returning true by default internally, as it knows the state got changed. Then Why we need updateShouldNotify? it's need because if someone wants to update the state on a specific condition,
Eg: if UI required to show only even values then we can add a condition like
updateShouldNotify: (oldValue, newValue) => newValue % 2 == 0,
Code Snippet:
class ParentWidget extends StatefulWidget {
ParentWidget({Key key, this.title}) : super(key: key);
final String title;
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Life Cycle'),
),
body: Provider.value(
value: _counter,
updateShouldNotify: (oldValue, newValue) => true,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Press Fab button to increase counter:',
),
ChildWidget()
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class ChildWidget extends StatefulWidget {
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
void initState() {
print('initState(), counter = $_counter');
super.initState();
}
#override
void didChangeDependencies() {
_counter = Provider.of<int>(context);
print('didChangeDependencies(), counter = $_counter');
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
print('build(), counter = $_counter');
return Text(
'$_counter',
);
}
}
Output Logs:
I/flutter ( 3779): didChangeDependencies(), counter = 1
I/flutter ( 3779): build(), counter = 1
For detail explanation:
https://medium.com/#jitsm555/differentiate-between-didchangedependencies-and-initstate-f98a8ae43164?sk=47b8dda310f307865d8d3873966a9f4f
According to initState documentation
You cannot use BuildContext.inheritFromWidgetOfExactType from this method. However, didChangeDependencies will be called immediately following this method, and BuildContext.inheritFromWidgetOfExactType can be used there.
So you need to use BuildContext.inheritFromWidgetOfExactType in didChangeDependencies.
Every Widget has its own context. That is why you have access to context outside build method.
Regarding build(BuildContext context), build method accepts context from the parent widget. It means this parameter BuildContext context is not current widget's context but its parent's context.
The notion of context being mysteriously accessible outside build() was the one that bugged me. I think clarifying this subtle point supplements explanations given in other answers about the first question.
How is context accessible from outside the build() method?
The confusion stems from the (wrong) assumption that context needed to be passed to State.build() at all. Note that the State class already has a context property and according to the documentation, it is redundantly provided to build() here, so that its signature matches that of a WidgetBuilder. However, this is not the same build() method as that of a StatelessWidget.
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an InheritedWidget that later changed, the framework would call this method to notify this object about the change.
This method is also called immediately after initState. It is safe to call BuildContext.dependOnInheritedWidgetOfExactType from this method.
In fact Subclasses rarely override this method because the framework always calls build after a dependency changes. Some subclasses do override this method because they need to do some expensive work (e.g., network fetches) when their dependencies change, and that work would be too expensive to do for every build.
This is a supplemental answer showing what the OP described.
The State class of a StatefulWidget has a context property. This build context is first available in didChangeDependencies. Trying to use context in initState will cause an error.
class HomeWidget extends StatefulWidget {
const HomeWidget({Key key}) : super(key: key);
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
#override
void initState() {
print('initState');
// print(Theme.of(context)); // ERROR!
super.initState();
}
#override
void didChangeDependencies() {
print('didChangeDependencies');
print(Theme.of(context)); // OK
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
print('build');
print(Theme.of(context)); // OK
return Container();
}
}
Running that gives the print statements in the following order:
initState
didChangeDependencies
ThemeData#93b06
build
ThemeData#93b06
See also Working with didChangeDependencies() in Flutter
you can still use context in initState() method, its hack buts works, all you need to do is sought of delay whatever you will need to execute that has context in it like so:
#override
void initState() {
Future.delayed(Duration.zero).then((_) {
// you code with context here
});
super.initState();
}
I am working on app which requires me to fetch data from the server (in my case a simple firebase real time database) and store the fetched data in map before the build method is called since i will be needing to use the map in the build method.
Here I have tried to recreate the error using a test code.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Test extends StatefulWidget {
#override
Map<String, dynamic> livefeed;
State<StatefulWidget> createState() {
// TODO: implement createState
return TestState();
}
}
class TestState extends State<Test> {
#override
void initState() {
http
.get('HERE GOES THE URL FOR THE DATABASE ,CANT DISCLOSE')
.then((http.Response response) {
widget.livefeed = json.decode(response.body);
print(widget.livefeed);
print(widget.livefeed.length);
});
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.livefeed.toString()),
);
}
}
The Console print :
{-Lh7FJV_7FIXgEUUS3Is: {skill: Andriod dev, target: Just Entered The Lab.,
username: test}, -Lh7RKgw3K2ZuKjrBZd2: {skill: Andriod dev, target: Just
Entered The Lab., username: test}}
I/flutter ( 7078): 2
Yet my UI shows null when i try to display the map on the screen
inside the build method.
I am aware that initState() method is called before the Build() method yet initialization fails, the debug console confirms that data is being fetched yet it fails to be printed on the screen implying build is being called before the values are initialized .
You need to put this inside a setState method
widget.livefeed = json.decode(response.body);
And ofcourse you need to handle what to show while the widget.livefeed is null. As you said even initState() method is called before the Build() method, http.get() is an async function. You can't assure that it will finish before build method is called BECAUSE IT IS AN ASYNC FUNCTION.
You can handle it like this
Text(widget.livefeed!=null?widget.livefeed.toString():'some default text like loading...'),