flutter_block: BlockProvider vs. RepositoryProvider - flutter

I want some clarification on BlocProvider and RepositoryProvider. From official API reference, they provide the same functionality - they provide an instance of an object to its descendant widgets.
However, judging by the names, I guess BlocProvider should be used for Bloc objects only, and RepositoryProvider for everything else. Is this correct?

RepositoryProvider acts like Repository pattern
It's Data Provider provide data to Bloc, so Bloc do not need to know data come from cloud or sqflite or ...
And do merge/filter, see official example below
In the following LoginForm example
Repository is part of Bloc and you can use _userRepository.login
class LoginFormBloc extends FormBloc<String, String> {
final emailField = TextFieldBloc(validators: [Validators.email]);
final passwordField = TextFieldBloc();
final UserRepository _userRepository;
LoginFormBloc(this._userRepository);
#override
List<FieldBloc> get fieldBlocs => [emailField, passwordField];
#override
Stream<FormBlocState<String, String>> onSubmitting() async* {
try {
_userRepository.login(
email: emailField.value,
password: passwordField.value,
);
yield currentState.toSuccess();
} catch (e) {
yield currentState.toFailure();
}
}
}
You can inject your Repository to Bloc
code snippet
class LoginForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<LoginFormBloc>(
builder: (context) =>
LoginFormBloc(RepositoryProvider.of<UserRepository>(context)),
child: Builder(
builder: (context) {
final formBloc = BlocProvider.of<LoginFormBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Simple login')),
body: FormBlocListener<LoginFormBloc, String, String>(
onSubmitting: (context, state) => LoadingDialog.show(context),
onSuccess: (context, state) {
LoadingDialog.hide(context);
Navigator.of(context).pushReplacementNamed('success');
},
onFailure: (context, state) {
LoadingDialog.hide(context);
Notifications.showSnackBarWithError(
context, state.failureResponse);
},
you can reference https://flutterawesome.com/create-beautiful-forms-in-flutter/
for official example https://bloclibrary.dev/#/architecture
The bloc layer can depend on one or more repositories to retrieve data needed to build up the application state.
class Repository {
final DataProviderA dataProviderA;
final DataProviderB dataProviderB;
Future<Data> getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}
class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
final Repository repository;
Stream mapEventToState(event) async* {
if (event is AppStarted) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
yield Success(data);
} catch (error) {
yield Failure(error);
}
}
}
}

Simple Answer
Same as BlocProvider.
BlockProvider provides the bloc to its children through BlockProvider.of(context). In most cases, developers create a new BlocProvider and that gets available to the rest of the subtree.
Now come to RepositoryProvider
RepositoryProvider provides the repository to its children through RepositoryProvider.of(context). When developers create a new RepositoryProvider and that gets available to the rest of the subtree.

Related

Flutter - RiverPod Newbie. How to watch StateNotifier in my view

I have a Recipe Repository that get recipes from FireStore
class RecipeRepository {
Future<List<Recipe>> readAll() async {
final snap = await _recipeRef.get();
return snap.docs.map((doc) => doc.data()).toList();
}
}
Here I'm returning the Repository as a Provider
final recipeRepositoryProvider =
Provider<RecipeRepository>((ref) => RecipeRepository());
Here I have a Class that I want to use to control the state of the UI
final recipeAsyncController =
StateNotifierProvider<RecipeAsyncNotifier, AsyncValue<List<Recipe>>>(
(ref) => RecipeAsyncNotifier(ref.read));
class RecipeAsyncNotifier extends StateNotifier<AsyncValue<List<Recipe>>> {
RecipeAsyncNotifier(this._read) : super(const AsyncLoading()) {
init();
}
final Reader _read;
init() async {
final recipes = await _read(recipeRepositoryProvider).readAll();
state = AsyncData(recipes);
}
}
As you can see I'm wrapping the recipeRepositoryProvider on a read.
In my UI I want to View the recipe list
return Consumer(
builder: (context, watch, child) {
return watch(recipeAsyncController).when();
},
);
The problem is I'm getting the following error.
When trying to access the when async call.
https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/Consumer-class.html
the second parameter in builder function is actually a ref object.
return Consumer(
builder: (context, ref, child) {
return ref.watch(recipeAsyncController).when();
},
);

Flutter set state not updating my UI with new data

I have a ListView.builder widget wrapped inside a RefreshIndicator and then a FutureBuilder. Refreshing does not update my list, I have to close the app and open it again but the refresh code does the same as my FutureBuilder.
Please see my code below, when I read it I expect the widget tree to definitely update.
#override
void initState() {
super.initState();
taskListFuture= TaskService().getTasks();
}
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(builder: (context, taskData, child) {
return FutureBuilder(
future: taskListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
taskData.tasks = (snapshot.data as ApiResponseModel).responseBody;
return RefreshIndicator(
onRefresh: () async {
var responseModel = await TaskService().getTasks();
setState(() {
taskData.tasks = responseModel.responseBody;
});
},
child: ListView.builder(
...
...
Let me know if more code is required, thanks in advance!
Points
I am using a StatefulWidget
Task data is a class that extends ChangeNotifier
When I debug the refresh I can see the new data in the list, but the UI does not update
getTasks()
Future<ApiResponseModel> getTasks() async {
try {
var _sharedPreferences = await SharedPreferences.getInstance();
var userId = _sharedPreferences.getString(PreferencesModel.userId);
var response = await http.get(
Uri.parse("$apiBaseUrl/$_controllerRoute?userId=$userId"),
headers: await authorizeHttpRequest(),
);
var jsonTaskDtos = jsonDecode(response.body);
var taskDtos= List<TaskDto>.from(
jsonTaskDtos.map((jsonTaskDto) => TaskDto.fromJson(jsonTaskDto)));
return ApiResponseModel(
responseBody: taskDtos,
isSuccessStatusCode: isSuccessStatusCode(response.statusCode));
} catch (e) {
return null;
}
}
The issue here seems to be that you are updating a property that is not part of your StatefulWidget state.
setState(() {
taskData.tasks = responseModel.responseBody;
});
That sets a property part of TaskData.
My suggestion is to only use the Consumer and refactor TaskService so it controls a list of TaskData or similar. Something like:
Provider
class TaskService extends ChangeNotifier {
List<TaskData> _data;
load() async {
this.data = await _fetchData();
}
List<TaskData> get data => _data;
set data(List<TaskData> data) {
_data = data;
notifyListeners();
}
}
Widget
class MyTaskList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<TaskService>(builder: (context, service, child) {
return RefreshIndicator(
onRefresh: () {
service.getTasks();
},
child: ListView.builder(
itemCount: service.data.length,
itemBuilder: (BuildContext context, int index) {
return MyTaskItem(data:service.data[index]);
},
),
);
});
}
}
and make sure to call notifyListeners() in the service.getTasks() method to make the Consumer rebuild
I think (someone will correct me if I'm wrong) the problem is that you are using the FutureBuilder, once it's built, you need to refresh to whole widget for the FutureBuilder to listen to changes. I can suggest a StreamBuilder that listens to any changes provided from the data model/api/any kind of stream of data. Or better yet, you can use some sort of state management like Provider and use Consumer from the Provider package that notifies the widget of any changes that may occurred.

Should I use final in models with equatable and flutter_bloc to distinguish 2 states?

I'm creating an app where you login and go to a page where you have a list of your restaurants, you have also a form where you can add a new restaurant.
This part works.
The problem is that when i click add the restaurant is added in firestore correctly, but the list doesn't refresh. I usually yield 2 states, a LoadingState and a LoadedRestaurantsListState, but with the last version of flutter_bloc this trick doesn't work, seems like just the last state yielded is received, but the previous was LoadedRestaurantsListState, so they are equals and the blocbuilder ignores the second one. So I've to use the equatable's props to distinguish the 2 states, but in the equatable documentation is written: "Note: Equatable is designed to only work with immutable objects so all member variables must be final".
So I've to make all the model's fields final, but if I do it how can i modify just one o two fields when I need it to?
What is the best practice?
If someone has examples, or videos, etc it would be very appreciated.
Thanks in advance
Without props
FirebaseBloc.dart
Stream<FirebaseState> mapEventToState(
FirebaseEvent event,
) async* {
print("event firebase ${event.runtimeType.toString()}");
if (event is CreateRestaurantFirebaseEvent) {
yield LoadingState();
await _databaseService.createRestaurant(event.restaurant, event.user);
List<Restaurant> restaurantsList = await _databaseService
.loadRestaurantsList(event.user.restaurantsIDsList);
yield LoadedRestaurantsListState(restaurantsList);
}
if (event is LoadRestaurantsListEvent) {
List<Restaurant> restaurantsList =
await _databaseService.loadRestaurantsList(event.restaurantsIDs);
yield LoadedRestaurantsListState(restaurantsList);
}
FirebaseState.dart
class LoadingState extends FirebaseState {
#override
List<Object> get props => [];
}
class LoadedRestaurantsListState extends FirebaseState {
List<Restaurant> restaurantsList;
LoadedRestaurantsListState(this.restaurantsList);
#override
List<Object> get props => [];
}
view.dart
class RestaurantSelectionScreen extends StatefulWidget {
final User user;
RestaurantSelectionScreen({
#required this.user,
});
#override
_RestaurantSelectionScreenState createState() =>
_RestaurantSelectionScreenState();
}
class _RestaurantSelectionScreenState extends State<RestaurantSelectionScreen> {
FirebaseBloc _firebaseBloc;
#override
void initState() {
super.initState();
_firebaseBloc = FirebaseBloc();
_firebaseBloc.add(LoadRestaurantsListEvent(widget.user.restaurantsIDsList));
}
#override
Widget build(BuildContext context) {
return BlocProvider<FirebaseBloc>(
create: (context) => _firebaseBloc,
child: Scaffold(
body: SingleChildScrollView(
child: Center(
child: BlocBuilder(
cubit: _firebaseBloc,
builder: (context, state) {
print("state ${state.runtimeType.toString()}");
if (state is InitialFirebaseState) {
return CircularProgressIndicator();
} else if (state is LoadedRestaurantsListState) {
return buildUI(state);
} else if (state is LoadingState) {
return CircularProgressIndicator();
} else {
return _CreateRestaurantFormWidget(widget.user);
}
},
),
),
),
),
);
}

Flutter: How to fetch data from api only once while using FutureBuilder?

How can I fetch data only once while using FutureBuilder to show a loading indicator while fetching?
The problem is that every time the user opens the screen it will re-fetch the data even if I set the future in initState().
I want to fetch the data only the first time the user opens the screen then I will use the saved fetched data.
should I just use a stateful widget with a loading variable and set it in setState()?
I'm using Provider package
Future<void> fetchData() async {
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
and my screen widget:
class _MyScreenState extends State<MyScreen> {
Future<void> fetchData;
#override
void initState() {
fetchData =
Provider.of<Data>(context, listen: false).fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchData,
builder: (ctx, snapshot) =>
snapshot.connectionState == ConnectionState.done
? Consumer<Data>(
builder: (context, data, child) => Text(data.fetchedData)): Center(
child: CircularProgressIndicator(),
),
);
}
}
If you want to fetch the data only once even if the widget rebuilds, you would have to make a model for that. Here is how you can make one:
class MyModel{
String value;
Future<String> fetchData() async {
if(value==null){
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
value=(YourReturnedString)
}
}
return value;
}
}
Don't forget to place MyModel as a Provider. In your FutureBuilder:
#override
Widget build(context) {
final myModel=Provider.of<MyModel>(context)
return FutureBuilder<String>(
future: myModel.fetchData(),
builder: (context, snapshot) {
// ...
}
);
}
A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<String> _future;
#override
void initState() {
_future = callAsyncFetch();
super.initState();
}
#override
Widget build(context) {
return FutureBuilder<String>(
future: _future,
builder: (context, snapshot) {
// ...
}
);
}
}
Or you can simply use a FutureProvider instead of the StatefulWidget above:
class MyWidget extends StatelessWidget {
// Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
#override
Widget build(BuildContext context) {
// print('building widget');
return FutureProvider<String>(
create: (_) {
// print('calling future');
return callAsyncFetch();
},
child: Consumer<String>(
builder: (_, value, __) => Text(value ?? 'Loading...'),
),
);
}
}
You can implement provider and pass data among its child.
Refer this example for fetching the data once and using it throughout its child.
As Aashutosh Poudel suggested, you could use an external object to maintain your state,
FOR OTHERS COMING HERE!
To manage state for large applications, the stateful widgets management becomes a bit painful. Hence you have to use an external state object that is shall be your single source of truth.
State management in flutter is done by the following libraries | services:
i. Provider: Well, i have personally played with this a little bit, even did something with it. I could suggest this for beginners.
ii. GetX: That one library that can do everything, its a good one and is recommended for novice || noob.
iii. Redux: For anyone coming from the react and angular world to flutter, this is a very handy library. I personally love this library, plus when you give it additional plugins, you are just superman
iv. Bloc: Best for data that is in streams. in other words, best for reactive programming approach....
Anyways, that was a lot given your question. Hope i helped

Is there a way for multiple FutureBuilders to use the same future from a ChangeNotifier?

I have a class (that extends ChangeNotifier - Provider package) that has a function that returns a Future. What I'm trying to do is have it so that multiple futureBuilders in my UI code can receive values from this function but without having to call that function once per FutureBuilder.
However, I the function itself gets run again and again with every FutureBuilder I use. I know there must be a way to expose the Future itself through the Provider package but I can't seem to figure out how.
Here is the class that extends ChangeNotifier and has the future in it:
class ApiService extends ChangeNotifier {
CurrencyTicker _data;
CurrencyTicker get getdata => _data;
set setdata(CurrencyTicker data) {
_data = data;
}
Future<CurrencyTicker> fetchBaseData() async {
final response =
await http.get(API_URL_HERE); // url removed for stackoverflow
if (response.statusCode == 200) {
print('1 call logged');
setdata = CurrencyTicker.fromJson(json.decode(response.body));
return CurrencyTicker.fromJson(json.decode(response.body));
} else {
throw Exception('Request failed: ' + response.statusCode.toString());
}
}
}
Here is the UI code (it's just a FutureBuilder):
class MyBody extends StatelessWidget {
const MyBody({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final provider = Provider.of<ApiService>(context);
return Center(
child: FutureBuilder(
future: provider.fetchBaseData(),
initialData: CurrencyTicker().price,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(snapshot.data.company),
],
);
} else {
return LinearProgressIndicator();
}
},
),
);
}
}
I haven't included the MultiProvider thats at the "top level" in the widget tree since I don't see why I'd have to. I haven't included the Model for the CurrencyTicker class. I can provide both of these if necessary.
Would appreciate any input at all here
You don't want to do the HTTP call directly inside the build method of your consumers.
Instead of exposing a method on your ChangeNotifier, you should expose a property:
class MyNotifier with ChangeNotifier {
Future<Foo> foo;
}
That foo is a variable that stores the result of your last fetchData call.
Depending on your needs, you can then:
call fetchData in the constructor, if immediately needed
class MyNotifier with ChangeNotifier {
MyNotifier() {
foo = fetchData();
}
Future<Foo> foo;
}
lazy load it using a custom getter:
class MyNotifier with ChangeNotifier {
Future<Foo> _foo;
Future<Foo> get foo => _foo ??= fetchData();
}