felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.79k stars 3.39k forks source link

Need a better way to handle data backend? #124

Closed elonzh closed 5 years ago

elonzh commented 5 years ago

I'm a backend developer and a novice in app development.

State management is an interesting area in frontend development. I read many materials then I found Bloc.

Bloc is a great pattern and your package helps me a lot. I can easily handle input and output in my app, but there is still a problem.

As your examples described, I need passing repository from the root widget to every widget that has a bloc require the repository, It's quite annoying and this behavior breaks the architecture we want.

image

So, I delegate my repository implementation to an abstract Repository, The repository is hidden for widgets because all inputs and outputs are managed by the bloc. When a bloc needs a repository, just uses the Repository delegation.

Is my solution right for real production developments?

If not, is there a better way to solve this problem?

Here is the sample code:

Repository

import 'package:life_logger/models/models.dart';

abstract class Repository {
  static Repository delegate;
  factory Repository() {
    return delegate;
  }

  Future<void> init();

  DateTime currentMonth;

  Future<List<Mood>> listMoods();

  Future<List<Activity>> listActivities();

  Future<bool> hasAddedToday();

  Future<List<Entry>> listCurrentMonthEntries();

  Future<void> createEntry(Entry entry);

  Future<void> updateEntry(Entry entry);
}

Bloc

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:life_logger/models/models.dart';
import 'package:life_logger/repositories/repositories.dart';
import 'package:meta/meta.dart';

abstract class ChoiceEvent extends Equatable {
  ChoiceEvent([List props = const []]) : super(props);
}

class AppStarted extends ChoiceEvent {}

abstract class ChoiceState extends Equatable {
  ChoiceState([List props = const []]) : super(props);
}

class ChoiceEmpty extends ChoiceState {}

class ChoiceLoading extends ChoiceState {}

class ChoiceLoaded extends ChoiceState {
  List<Mood> moods;
  List<Activity> activities;
  ChoiceLoaded({@required this.moods, @required this.activities})
      : super([moods, activities]);
}

class ChoiceBloc extends Bloc<ChoiceEvent, ChoiceState> {
  @override
  ChoiceState get initialState => ChoiceEmpty();

  @override
  Stream<ChoiceState> mapEventToState(
      ChoiceState currentState, ChoiceEvent event) async* {
    if (event is AppStarted) {
      yield ChoiceLoading();
      final moods = await Repository().listMoods();
      final activities = await Repository().listActivities();
      yield ChoiceLoaded(moods: moods, activities: activities);
    }
  }
}

main.dart

void main() async {
  Repository.delegate = LocalRepository(databaseName: null);
  await Repository().init();
  runApp(App());
}
felangel commented 5 years ago

@earlzo thanks for bringing this up and opening an issue!

I would recommend using InheritedWidget or InheritedModel to expose your repositories to the sections of the widget tree that need them. If you want to use a 3rd party package, you can check out the provider package and do something like:

MultiProvider(
  providers: [
    Provider<LocalRepository>(value: LocalRepository(databaseName: null)),
    Provider<AnotherRepository>(value: AnotherRepository()),
    Provider<YetAnotherRepository>(value: YetAnotherRepository()),
  ],
  child: MaterialApp(
    ...
  ),
)

Then when creating a bloc that has a dependency on a repository, you can use the context to access the repository and inject it into the bloc like:

final ChoiceBloc _choiceBloc = ChoiceBloc(Provider.of<LocalRepository>(context));

Otherwise you can simply extend InheritedWidget/InheritedModel and implement your own RepositoryProvider.

The main advantage of doing it this way as opposed to the way you described is testability. It's much easier test your widgets and blocs with mock repositories using this approach.

Let me know if that helps!

elonzh commented 5 years ago

I will try this.

Thanks for your prompt and detailed response.

felangel commented 5 years ago

No problem! πŸ‘

Closing this for now but feel free to comment with further questions/concerns and I'll gladly reopen this. πŸ˜„

elonzh commented 5 years ago

I think this section can be documented in tutorials for beginners.

felangel commented 5 years ago

I'm still actively writing sample apps and adding to the documentation so I'll definitely be sure to keep this in mind πŸ‘

Also feel free to propose updates to the documentation and create PRs to improve it!

basketball-ico commented 5 years ago

Hello, I try to use Provider Package.

I have 1 Bloc that have a Respositoty paramenter

Want to use like this:

class _someScreenState extends State<someScreen> {
  Bloc bloc;
  @override
  void initState() {
    bloc = Bloc(Provider.of<Repository>(context));
    super.initState();
  }
  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
}

But I got his error

inheritFromWidgetOfExactType(Provider<Repository>) or inheritFromElement() was called before
_SomeScreenState.initState() completed.

I try fo fix like using the didChangeDependencies() and works... But this method are called a lot of times, and the dispose() only one, And I think I could have memory leaks but i don't know. Can i have memory leaks?

class _someScreenState extends State<someScreen> {
  Bloc bloc;

  @override
  void didChangeDependencies() {
    bloc = Bloc(Provider.of<Repository>(context));
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
}

Also I can do this

  @override
  void didChangeDependencies() {
    bloc ??= Bloc(Provider.of<UserRepository>(context));
    super.didChangeDependencies();
  }

But I don't know what is the correct way.

Thanks

felangel commented 5 years ago

Hey @basketball-ico πŸ‘‹ Can you please share a sample app with the problem you're having and I can take a look?

In general, the approach you've taken is not ideal because, like you said, didChangeDependencies is called multiple times and the bloc streams will not be disposed which will cause memory leaks.

Have you tried this?

class _someScreenState extends State<someScreen> {
  Bloc _bloc;
  @override
  void initState() {
    super.initState(); // call super.initState first
    _bloc = Bloc(Provider.of<Repository>(context));
  }
  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
}

Please let me know if that helps and if not it'd be awesome if you could share a sample application with the problem you're having πŸ‘

Thanks! πŸ˜„

basketball-ico commented 5 years ago

Hello I tried that, but doesn't work

Here is a little demo

Thanks

felangel commented 5 years ago

@basketball-ico looks like you can set listen to false when using Provider.of<Repository>(context, listen: false) to avoid re-triggering builds on changes (since the repository shouldn't change). Check this sample out.

Alternatively, in a simple example like this you can skip using Provider altogether by injecting the repository directly (sample).

Let me know if that helps! πŸ‘

basketball-ico commented 5 years ago

Thanks, It Works, I like it. πŸ‘

But I can't understand, the Repository can Change in some scenario? When i need use listen: true and when listen: false ?

Thanks πŸ€—

felangel commented 5 years ago

Awesome! Basically when you say listen: true then any time the Repository changes your widget will rebuild. Since your repository should not change over the course of the application lifecycle you should set listen to false. Hope that helps!

basketball-ico commented 5 years ago

Hey, thanks but sorry i can't understand, When you said "change", that means that the some property of Repository are changed or when you assign new Object. Thanks

For example:

class Repository{
String someProperty;
}

void main() {
  var r = Repository();

  // Change property
  r.someProperty = 'sd';

  // assign new Object
  r = Repository();
}
felangel commented 5 years ago

Hey no problem! When I said change I meant the old value is not the same instance as the new value. In your example changing the property would not trigger a rebuild but assigning a new object would since it is not longer the same instance of Repository.

Does that help?