How to create a list with values ​from state? - flutter

In the application, when I click on the button, I write the name of the group in the field and add it to the database. The name of the group is also displayed on the screen. My problem is that every time I create a new group, it is overwritten on the screen.
And I need to display the names of the groups on the screen as a list and so that it is added and not overwritten. But the implementation is such that in my state there is not a list and it is impossible to display the names of groups using map. How can I display the titles as a list so they don't get overwritten?
#immutable
abstract class NewGroupState {
const NewGroupState();
#override
List<Object?> get props => [];
}
class NewGroupInitial extends NewGroupState {
const NewGroupInitial();
}
class AddGroupState extends NewGroupState {
const AddGroupState(this.group);
final Group group;
}
body: BlocBuilder<NewGroupBloc, NewGroupState>(
builder: (context, state) {
return ListTile(
title: Text(state is AddGroupState ? state.group.groupName : ''),
);
},
),

I'm not sure I understood your question, is this what you're trying to do?
If not, can you rewrite your question?
class AddGroupState extends NewGroupState {
const AddGroupState(this.currentGroup, this.previousGroups);
final Group currentCroup;
final List<Group> previousGroups;
}
class NewGroupBloc extends Bloc<NewGroupState> {
Future<void> addGroupToDB() async {
final newGroup = Group();
try {
// add group to DB
await _groupRepository.add(newGroup);
final updatedGroups = [...state.previousGroups, newGroup];
emit(NewGroupState(newGroup, updatedGroups));
} on DBError catch (e) {
emit(NewGroupErrorState());
}
}
}
Edit: this is a BAD answer but author asked how it could be done.
class NewGroupBloc extends Bloc<NewGroupState> {
late Future<void> Function(Group group) onAddGroup;
Future<void> addGroupToDB() async {
final newGroup = Group();
try {
await _groupRepository.add(newGroup);
await onAddGroup(newGroup);
emit(NewGroupState(newGroup));
} on DBError catch (e) {
emit(NewGroupErrorState());
}
}
}
class _GroupPageState extends State<GroupPage> {
var groups = <Group>[];
NewGroupBloc bloc = // don't know how it was initialized
#override
void initState() {
bloc.onAddGroup = (newGroup) {
setState(() {
groups = [...groups, newGroup];
});
}
}
Widget build(BuildContext context) {
// The same as you already have, but using groups declared in the Widget instead of BLoC's state.
}
}

Related

list access outside provider class

My problem is that when I access list under consumer and show into text widget it shows data. but when I use same list in initstate it shows null even when I use same list before return in consumer it shows null.
This is view:
class FilterView extends StatefulWidget {
const FilterView({super.key});
#override
State<FilterView> createState() => _FilterViewState();
}
class _FilterViewState extends State<FilterView> {
CategoryViewModel categoryViewModel = CategoryViewModel();
List<Categories>? categories = [];
#override
void initState() {
// categoryViewModel.fetchCategoryyListApi();
context.read<CategoryViewModel>().fetchCategoryyListApi();
// getList();
print("In filter initstate");
print(
"categoty List :${context.read<CategoryViewModel>().categoriess.length}");
getList();
super.initState();
}
getList() {
Provider.of<CategoryViewModel>(context, listen: false)
.categoriess
.map((category) {
return checkBoxes.add(CheckBoxSettings(title: category.title!));
});
print("checkBoxxxxxxxxxx: ${checkBoxes.length}");
}
**This is categoryviewModel:**
class CategoryViewModel extends ChangeNotifier {
List<Categories> categoriess = [];
final _myRepo = CategoryRepository();
Future<void> fetchCategoryyListApi() async {
_myRepo.fetchCategoryList().then((value) {
categoriess = value.categories!;
notifyListeners();
print(categoriess);
}).onError((error, stackTrace) {
print(error.toString());
});
}
}

Best practice on how to write/update data from a Flutter provider

I'm fairly new to Flutter providers. I use Riverpod.
I have a Future provider that provide some data from a JSON file - in the future it will be from a API response.
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/pokemon.dart';
final pokemonProvider = FutureProvider<List<Pokemon>>((ref) async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
return List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
});
I subscribe to with ref.watch in ConsumerState widgets, e.g.:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final AsyncValue<List<Pokemon>> pokemons =
ref.watch(pokemonProvider);
return pokemons.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (pokemons) {
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
},
);
}
}
But in that case, what is the best practice to write/update data to the JSON file/API?
It seems providers are used for reading/providing data, not updating it, so I'm confused.
Should the same provider pokemonProvider be used for that? If yes, what is the FutureProvider method that should be used and how to call it? If not, what is the best practice?
I am new to riverpod too but I'll try to explain the approach we took.
The examples with FutureProviders calling to apis are a little bit misleading for me, because the provider only offers the content for a single api call, not access to the entire api.
To solve that, we found the Repository Pattern to be very useful. We use the provider to export a class containing the complete api (or a mock one for test purposes), and we control the state (a different object containing the different situations) to manage the responses and updates.
Your example would be something like this:
First we define our state object:
enum PokemonListStatus { none, error, loaded }
class PokemonListState {
final String? error;
final List<Pokemon> pokemons;
final PokemonListStatus status;
const PokemonListState.loaded(this.pokemons)
: error = null,
status = PokemonListStatus.loaded,
super();
const PokemonListState.error(this.error)
: pokemons = const [],
status = PokemonListStatus.error,
super();
const PokemonListState.initial()
: pokemons = const [],
error = null,
status = PokemonListStatus.none,
super();
}
Now our provider and repository class (abstract is optional, but let's take that approach so you can keep the example for testing):
final pokemonRepositoryProvider =
StateNotifierProvider<PokemonRepository, PokemonListState>((ref) {
final pokemonRepository = JsonPokemonRepository(); // Or ApiRepository
pokemonRepository.getAllPokemon();
return pokemonRepository;
});
///
/// Define abstract class. Useful for testing
///
abstract class PokemonRepository extends StateNotifier<PokemonListState> {
PokemonRepository()
: super(const PokemonListState.initial());
Future<void> getAllPokemon();
Future<void> addPokemon(Pokemon pk);
}
And the implementation for each repository:
///
/// Class to manage pokemon api
///
class ApiPokemonRepository extends PokemonRepository {
ApiPokemonRepository() : super();
Future<void> getAllPokemon() async {
try {
// ... calls to API for retrieving pokemon
// updates cached list with recently obtained data and call watchers.
state = PokemonListState.loaded( ... );
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
Future<void> addPokemon(Pokemon pk) async {
try {
// ... calls to API for adding pokemon
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
}
and
///
/// Class to manage pokemon local json
///
class JsonPokemonRepository extends PokemonRepository {
JsonPokemonRepository() : super();
Future<void> getAllPokemon() async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
// updates cached list with recently obtained data and call watchers.
final pokemons = List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
state = PokemonListState.loaded(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {
// ... and write json to disk for example
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
}
}
Then in build, your widget with a few changes:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final statePokemons =
ref.watch(pokemonRepositoryProvider);
if (statePokemons.status == PokemonListStatus.error) {
return Text('Error: ${statePokemons.error}');
} else if (statePokemons.status == PokemonListStatus.none) {
return const CircularProgressIndicator();
} else {
final pokemons = statePokemons.pokemons;
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
}
}
}
Not sure if this is the best approach but it is working for us so far.
you can try it like this:
class Pokemon {
Pokemon(this.name);
final String name;
}
final pokemonProvider =
StateNotifierProvider<PokemonRepository, AsyncValue<List<Pokemon>>>(
(ref) => PokemonRepository(ref.read));
class PokemonRepository extends StateNotifier<AsyncValue<List<Pokemon>>> {
PokemonRepository(this._reader) : super(const AsyncValue.loading()) {
_init();
}
final Reader _reader;
Future<void> _init() async {
final List<Pokemon> pokemons;
try {
pokemons = await getApiPokemons();
} catch (e, s) {
state = AsyncValue.error(e, stackTrace: s);
return;
}
state = AsyncValue.data(pokemons);
}
Future<void> getAllPokemon() async {
state = const AsyncValue.loading();
/// do something...
state = AsyncValue.data(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {}
Future<void> updatePokemon(Pokemon pk) async {}
Future<void> deletePokemon(Pokemon pk) async {}
}

Flutter - Providers and Future calls, how to share the same instance?

I'm learning Flutter and there is something I cannot grasp my head around.
I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination),
it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.
My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.
Example:
Let's take a contact list, that loads 10 contacts at a time:
class ContactsBody extends StatefulWidget {
#override
_ContactsBodyState createState() => _ContactsBodyState();
}
class _ContactsBodyState extends State<ContactsBody> {
static const _pageSize = 10;
final PagingController<int, Contact> pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
pagingController.appendLastPage(newItems.contacts);
} else {
final nextPageKey = pageKey + 1;
pagingController.appendPage(newItems.contacts, nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
#override
Widget build(BuildContext context) {
return ContactsList(pagingController);
}
#override
void dispose() {
pagingController.dispose();
super.dispose();
}
So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:
Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
final response = await http.get(.....);
if (response.statusCode == 200) {
return Contacts.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load contacts');
}
}
And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.
Now my Provider class:
class Contacts extends ChangeNotifier {
List<Contact> _contacts = <Contact>[];
Contacts();
factory Contacts.fromJson(final Map<String, dynamic> json) {
final Contacts contacts = Contacts();
if (json['data'] != null) {
json['data'].forEach((contact) {
contacts.add(Contact.fromJson(contact));
});
}
return contacts;
}
void add(final Contact contact) {
this._contacts.add(contact);
this.notifyListeners();
}
The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),
How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?
Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere
EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.
I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191
There are a few confusing things in your code :
When to create instances of your objects (ContactsService, Contacts)
Provider usage
(Accessing the list of the pagingController ?)
Parsing a JSON / using a factory method
The repository results in the following :
When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites.
If you click on a Contact, it becomes a favorite and the yellow container is also updated.
I commented the repository to explain you each part.
Note: the Contacts class in your code became ContactProvider in mine.
The ContactsService class to make the API call :
class ContactsService {
static Future<List<Contact>> fetchContactsPaged(
int pageKey, int pageSize) async {
// Here, you should get your data from your API
// final response = await http.get(.....);
// if (response.statusCode == 200) {
// return Contacts.fromJson(jsonDecode(response.body));
// } else {
// throw Exception('Failed to load contacts');
// }
// I didn't do the backend part, so here is an example
// with what I understand you get from your API:
var responseBody =
"{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
Map<String, dynamic> decoded = json.decode(responseBody);
List<dynamic> contactsDynamic = decoded["data"];
List<Contact> listOfContacts =
contactsDynamic.map((c) => Contact.fromJson(c)).toList();
// you can return listOfContacts, for this example, I will add
// more Contacts for the Pagination plugin since my json only has 2 contacts
for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
listOfContacts.add(Contact(name: "Name $i"));
}
return listOfContacts;
}
}
Usage of Provider :
Consumer<ContactProvider>(
builder: (_, foo, __) => Container(
child: Text(
"${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
color: Colors.amber,
)),
Expanded(child: ContactsBody())
]),
)
Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :
Future<void> _fetchPage(int pageKey) async {
try {
// Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
final newItems =
await ContactsService.fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
// Important : we add the contacts to our provider so we can get
// them in other parts of our app
context.read<ContactProvider>().addContacts(newItems);
} catch (error) {
print(error);
_pagingController.error = error;
}
}
ContactItem widget, in which we update the favorite statuts and notify the listeners :
class ContactItem extends StatefulWidget {
final Contact contact;
ContactItem({this.contact});
#override
_ContactItemState createState() => _ContactItemState();
}
class _ContactItemState extends State<ContactItem> {
#override
Widget build(BuildContext context) {
return InkWell(
child: Padding(child: Row(children: [
Expanded(child: Text(widget.contact.name)),
if (widget.contact.isFavorite) Icon(Icons.favorite)
]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
onTap: () {
// the below code updates the item
// BUT others parts of our app won't get updated because
// we are not notifying the listeners of our ContactProvider !
setState(() {
widget.contact.isFavorite = !widget.contact.isFavorite;
});
// To update other parts, we need to use the provider
context.read<ContactProvider>().notifyContactUpdated(widget.contact);
});
}
}
And the ContactProvider :
class ContactProvider extends ChangeNotifier {
final List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;
void addContacts(List<Contact> newContacts) {
_contacts.addAll(newContacts);
notifyListeners();
}
void notifyContactUpdated(Contact contact) {
// You might want to update the contact in your database,
// send it to your backend, etc...
// Here we don't have these so we just notify our listeners :
notifyListeners();
}
}

Cubit not rebuilding when emiting a new state

Whenever I call the toggleLocked event, the BlocBuilder does not rebuild the widget.
I have looked around a lot on the internet and found this explanation: https://stackoverflow.com/a/60869187/3290471
I think that somewhere I incorrectly use the equatable package which results in the fact that the BlocBuilder thinks nothing has changed (while is has).
I have read the FAQ from the Bloc libray and the three provided solution (props for equatable / not reusing the same state / using fromList) seem to not fix the problem.
My Cubit:
class LockCubit extends Cubit<LockState> {
LockCubit({#required this.repository})
: assert(repository != null),
super(LockInitial());
final LocksRepository repository;
Future<void> fetch() async {
try {
final locks = await repository.fetchLocks();
emit(LocksDisplayed().copyWith(locks));
} on Exception {
emit(LockError());
}
}
Future<void> toggleLocked(int id) async {
try {
final locks = await repository.toggleLocked(id);
emit(LocksDisplayed().copyWith(List.from(locks)));
} on Exception {
emit(LockError());
}
}
}
My states:
abstract class LockState extends Equatable {
const LockState();
#override
List<Object> get props => [];
}
class LockInitial extends LockState {
#override
String toString() => 'LocksUninitialized';
}
class LockError extends LockState {
#override
String toString() => 'LockError';
}
class LocksDisplayed extends LockState {
final List<Lock> locks;
const LocksDisplayed([this.locks = const []]);
LocksDisplayed copyWith(locks) => LocksDisplayed(locks ?? this.locks);
#override
List<Object> get props => [locks];
#override
String toString() => 'LocksDisplayed { locks: $locks }';
}
My model:
class Lock extends Equatable {
Lock({this.id, this.name, this.locked, this.displayed});
final int id;
final String name;
final bool locked;
final bool displayed;
#override
String toString() =>
'Lock { id: $id name: $name locked: $locked displayed: $displayed }';
Lock copyWith({id, name, locked, displayed}) => Lock(
id: id ?? this.id,
name: name ?? this.name,
locked: locked ?? this.locked,
displayed: displayed ?? this.displayed);
#override
List<Object> get props => [id, name, locked, displayed];
}
My repositotory:
class LocksRepository {
List<Lock> locks = [];
Future<List<Lock>> fetchLocks() async {
// This is a temporary implementation
// In the future the data should be fetched from a provider
locks = [
new Lock(
id: 0,
name: 'Voordeur',
locked: false,
),
new Lock(
id: 1,
name: 'Achterdeur',
locked: false,
)
];
return locks;
}
Future<List<Lock>> toggleLocked(int id) async {
// This is a temporary implementation
// In the future a request to change a lock should be made and then the specific lock should be retrieved back and edited.
locks[id] = locks[id].copyWith(locked: !locks[id].locked);
return locks;
}
}
I am changing a state with the following trigger:
context.read<LockCubit>().toggleLocked(focusedIndex);
I am using BlocBuilder like this to build the state:
BlocBuilder<LockCubit, LockState>(builder: (context, state) {
print('State Changed');
if (state is LockInitial) {
return Text('lockInitial');
}
if (state is LocksDisplayed) {
return Swiper(
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
Text(state.locks[index].name),
Text(state.locks[index].locked.toString())
],
);
},
onIndexChanged: onIndexChanged,
loop: true,
itemCount: state.locks.length);
}
if (state is LockError) {
return Text('lockError');
}
return Container();
});
All help would be very appreciated.
Can you check BlocProvider ? I got the same problem. If this bloc inside materialApp, you must pass BlocProvider.value not create in widget.
I am a bit confused, if this could work. But with a bloc you would use an event not a cubit (even though events are based on cubits).
So first of all I would use the standard pattern:
state
event
bloc with mapEventToState
Then, what I also do not see in your code, if you toggle your lock it would look like this in pseudo code
if (event is toggleLock) {
yield lockInProgress();
toggleLock();
yield locksDisplayed;
}
This way your state always changes from locksDisplayed to lockInProgress to locksDisplayed - just as you read in your link above

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);
}
},
),
),
),
),
);
}