How to dispose rxsubject correctly in flutter? - flutter

I am trying to dispose rxsubject on the bloc. But when I call the dispose method on UI part, it throws error saying :
Bad state: Cannot add new events after calling close
Here's my bloc.
class EventBloc {
final EventRepository _repository = EventRepository();
final BehaviorSubject<EventResponse> _subject =
BehaviorSubject<EventResponse>();
getEvents() async {
EventResponse response = await _repository.getEvents();
_subject.sink.add(response);
}
dispose() {
_subject?.close();
}
BehaviorSubject<EventResponse> get subject => _subject;
}
final eventBloc = EventBloc();
dispose method in UI:
void dispose() {
super.dispose();
eventBloc.dispose();
}
When I am not calling dispose method on the UI, it works. Should I not call dispose method at all? If not, how should I dispose it?
Solution
moving final eventBloc=EventBloc(); from bloc and initializing bloc in UI.
Previously, without closing the subject, my UI would retain the data and when I navigated to the events page, the data would already be displayed there until api call succeed and when it did, it would simply rebuild listview with new data. But now, every time I navigate to the events page, all the progress is lost. Streambuilder starts from the very beginning of api call and until snapshot, it shows progress indicator and then data. It is like using PublishSubject.
Also, I heard an argument about not having to dispose stream when bloc is initialized within the bloc since the widget doesn't create the subject. How much is that true? Doesn't that lead to memory leaks?

To prevent your data from getting lost, you can make use of provider.
You'll need to create your bloc a level higher than the widget that uses it, ideally at the top level of the app. Like this:
runApp(
Provider<EventBloc>(
create: (_) => EventBloc(),
dispose: (context, value) => value.dispose(),
child: MyApp(),
),
);
Then your widget can access it when it needs it. Like:
#override
build(context) {
final EventBloc eventBloc = Provider.of<EventBloc>(context);
//Rest of your code
}

Related

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.

Flutter Provider with listen false, but still get error "setState() or markNeedsBuild() called during build."

I would like to reset userProvider when networkProvider changes.
In the userProvider.reset(), I have notifyListeners.
void didChangeDependencies() async {
super.didChangeDependencies();
final NetworkProvider networkProvider = Provider.of<NetworkProvider>(context);
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.reset(); }
When it runs, it gives error "setState() or markNeedsBuild() called during build."
My question is, I have set the listen to false, why it still rebuild this widget?
When there is only a UserProvider, it has the same error:
void didChangeDependencies() async {
super.didChangeDependencies();
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.reset(); }
If my idea of usage is totally wrong, is there any suggestion to achieve the same result?
Because you mention that NetworkProvider changes, I'm guessing it's also publishing notifications in which case you'd also want this to not trigger rebuilds with the listen: false parameter. It seems like you do want those notifications though so that you can reset your UserProvider. I would suggest that you don't try and do this in a Widget's lifecycle but use a ProxyProvider to create and update your UserProvider with it being dependent on the NetworkProvider.
All right, I found the answer. It is writen in latter of their home page.
What I tried in my question is not allowed, because the state update is synchronous.
This means that some widgets may build before the mutation happens (getting an old value), while other widgets will build after the mutation is complete (getting a new value). This could cause inconsistencies in your UI and is therefore not allowed.
Instead, you should perform that mutation in a place that would affect the entire tree equally:
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {
}
To use it in build:
Future.microtask(() =>
context.read<MyNotifier>().fetchSomething(someValue);
);

How to create a dependency for ChangeNotifierProvider and make it wait to complete?

I have ChangeNotifierProvider object that uses data stored sqflite asset database which need to be loaded at the beginning as future. The problem is that ChangeNotifierProvider doesn't wait for future operation to complete. I tried to add a mechanism to make ChangeNotifierProvider wait but couldn't succeed. (tried FutureBuilder, FutureProvider, using all together etc...)
Note : FutureProvider solves waiting problem but it doesn't listen the object as ChangeNotifierProvider does. When I use them in multiprovider I had two different object instances...
All solutions that I found in StackOverflow or other sites don't give a general solution or approach for this particular problem. (or I couldn't find) I believe there must be a very basic solution/approach and decided to ask for your help. How can I implement a future to this code or how can I make ChangeNotifierProvider wait for future?
Here is my summary code;
class DataSource with ChangeNotifier {
int _myId;
List _myList;
int get myId => _myId;
List get myList => _myList;
void setMyId(int changeMyId) {
_myId = changeMyId;
notifyListeners();
}
.... same setter code for myList object.
DataSource(){initDatabase();}
Future<bool> initDatabase() {
.... fetching data from asset database. (this code works properly)
return true;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<DataSource>(
create: (context) => DataSource(),
child: MaterialApp(
home: HomePage(),
),
);
}
}
Following code and widgets has this code part (it works fine)
return Consumer<DataSource>(
builder: (context, myDataSource, child) {.......
There are multiple ways that you can achieve. The main point of it is that you should stick to reactive principle rather than trying to await the change. Say for example, you could change the state of boolean value inside the DataSource class when the ajax request changes
class DataSource extends ChangeNotifier{
bool isDone = false;
Future<bool> initDatabase(){
//Do Whatever
isDone = true;
notifyListeners();
}
}
Then you could listen to this change in the build method like so
Widget build(BuildContext ctx){
bool isDone = Provider.of<DataSource>(context).isDone;
if(isDone){
// render result
}else{
// maybe render loading
}
}

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.

Complete scoped model async call in initstate before widget builds

I have a requirement which is to make an API call when the page is opened and use the result of the API call to populate a widget. I have to cater for cases like when the call is being made, show a loader, if an error occurs, show a widget that allows the user to reload the page which calls that method again if the call is successful use the result to populate the widget.
This looks like a perfect use case for a FutureBuilder, however, should the call fail, there won't be a way to reload as FutureBuilder is executed once.
An alternative is to use a StreamBuilder where I just add to the stream the error or response, this allows me to reload the page if the call fails. The streambuidler approach works fine.
Initially, I've tried to use a ScopedModel approach by making the async API call in the initstate method, however, as expected the api call takes some time and the widget Build method is called so the page is built before the api call completes and when it's done, even if I call notifylisteners() the widget tree doesn't rebuild with the data from the API call. Is there a way to make this work using a ScopedModel approach?
DashboardService.dart
Future<string> getDashboardState() async{
try{
var response = await client.get('/getdashboardstate');
return response;
}catch(error){
return error;
}
}
DashboardModel.dart
Future<BaseDataResponse<MambuLoan>> getDashboardState() async {
try {
setState(ViewState.Busy);
var info = await _dashboardService.getDashboardState();
if (info.isSuccessful) {
return info;
} else {
setState(ViewState.Error);
return info;
}
} catch (error) {
setState(ViewState.Error);
return error;
}
}
DashboardView.dart
#override
void initState() {
// TODO: implement initState
super.initState();
getDashboard() // result of this set some data in the scopedmodel
}
Future<String> getDashboard() async{
var response = await dashboardModel.getDashboardState();
return response;
}
Widget Build(BuildContext context){
//gets data in scoped model to build widget
return Container() //actual widget here
}
UPDATE
I Was approaching this the wrong way. I ended up not needing a stateful widget but a stateless one in which I called my Future in the build method before returning a widget.