Updating toggle button using Flutter and BloC - flutter

I'm using the youtube api and placed a favorites toggle button to fill and empty the icon depending on the state and saving them in ListView in a new page called 'favorites'. Everything is working fine in the new page as I can see the favorite Icon filled, but is not being refreshed/updated in real time in the current view. If I switch to a statefulWidget, I'm able to make it work using 'setstate', but then the changes are not reflected if I empty the icons from the favorites page.
There must be something wrong in my approach as I should use the Bloc state to change both, but really stuck here. Please could you take a look at my code and give me some thoughts or ideas?
Bloc File
class MasterBloc extends Bloc<MasterEvents, MasterState> {
#override
MasterState get initialState => MasterState.initialState();
#override
Stream<MasterState> mapEventToState(MasterEvents event) async* {
if (event is MasterSetTab) {
yield this.state.copyWith(currentTab: event.tab);
} else if (event is MasterAddToHistory) {
yield* _addToHistory(event);
} else if (event is MasterRemoveFromHistory) {
yield* _removeFromHistory(event);
} else if (event is MasterToggleInFavorites) {
yield* _toggleInFavorites(event);
} else if (event is MasterLogout) {
yield this.state.copyWith(history: [], currentTab: 0);
}
}
Stream<MasterState> _addToHistory(MasterAddToHistory event) async* {
final int index = this
.state
.history
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final history = List<YoutubeVideo>.from(this.state.history);
history.add(event.youtubeVideo);
yield this.state.copyWith(history: history);
}
}
Stream<MasterState> _removeFromHistory(MasterRemoveFromHistory event) async* {
final history = List<YoutubeVideo>.from(this.state.history);
history.removeAt(event.index);
yield this.state.copyWith(history: history);
}
Stream<MasterState> _toggleInFavorites(MasterToggleInFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
event.youtubeVideo.isFavorite = true;
yield this.state.copyWith(favorites: favorites);
} else {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(index);
event.youtubeVideo.isFavorite = false;
yield this.state.copyWith(favorites: favorites);
}
}
}
Bloc State
class MasterState extends Equatable {
final int currentTab;
final List<YoutubeVideo> history;
final List<YoutubeVideo> favorites;
MasterState(
{#required this.currentTab, #required this.history, this.favorites});
static MasterState initialState() =>
MasterState(currentTab: 0, history: [], favorites: []);
MasterState copyWith(
{int currentTab,
List<YoutubeVideo> history,
List<YoutubeVideo> favorites}) {
return MasterState(
currentTab: currentTab ?? this.currentTab,
history: history ?? this.history,
favorites: favorites ?? this.favorites);
}
#override
List<Object> get props => [currentTab, history, favorites];
}
BloC Events
import 'package:documentales_app/models/youtube_video.dart';
abstract class MasterEvents {}
class MasterSetTab extends MasterEvents {
final int tab;
MasterSetTab(this.tab);
}
class MasterAddToHistory extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToHistory(this.youtubeVideo);
}
class MasterRemoveFromHistory extends MasterEvents {
final int index;
MasterRemoveFromHistory(this.index);
}
class MasterToggleInFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterToggleInFavorites(this.youtubeVideo);
}
class MasterLogout extends MasterEvents {}
Favorites Tab
class FavsTab extends StatefulWidget {
#override
_FavsTabState createState() => _FavsTabState();
}
class _FavsTabState extends State<FavsTab> {
#override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<MasterBloc>(context);
return BlocBuilder<MasterBloc, MasterState>(
builder: (_, state) {
if (state.favorites.length == 0) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset('assets/icons/empty.svg',
width: 50, color: Colors.greenAccent),
SizedBox(
height: 5,
),
Text(
'No hay favoritos ..',
style: TextStyle(
color: Colors.greenAccent,
fontWeight: FontWeight.bold,
fontSize: 20),
)
],
),
);
}
return ListView.builder(
itemBuilder: (_, index) {
final YoutubeVideo item = state.favorites[index];
return YoutubeVideoItem(
item: item,
onDismissed: () {
bloc.add(MasterToggleInFavorites(item));
},
);
},
itemCount: state.favorites.length,
);
},
condition: (prevState, newState) =>
prevState.favorites.length != newState.favorites.length,
);
}
}
Home Tab where the state is called
class HomeTab extends StatefulWidget {
#override
_HomeTabState createState() => _HomeTabState();
}
class _HomeTabState extends State<HomeTab> {
AccountApi _accountApi = AccountApi();
YoutubeApi _youtubeApi = YoutubeApi(apiKey: API_KEY);
List<dynamic> _users = [];
List<PlayList> _playlists = [];
List<YoutubeVideo> _newVideos = [];
bool _isLoading = true;
#override
void initState() {
super.initState();
_load();
}
_load() async {
final users = await _accountApi.getUsers(1);
final List<PlayList> playLists =
await _youtubeApi.getPlaylists('UCCMksip5JfLMW4AJGsjTYUA');
final List<YoutubeVideo> newVideos = await _youtubeApi
.getPlaylistVideos('PLFXLg_sVKmuujWVeOmrzsM1NnDFa8uoNk');
setState(() {
_users.addAll(users);
_playlists.addAll(playLists);
_newVideos.addAll(newVideos);
_isLoading = false;
});
}
#override
Widget build(BuildContext context) {
return ListView(
children: [
_isLoading
? HomeTabShimmer()
: Column(
children: [
TopPlayLists(items: _playlists),
SizedBox(height: 10),
NewVideos(
items: _newVideos,
),
SizedBox(height: 5),
],
)
],
);
}
}
Finally the toggle button
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterToggleInFavorites(item));
},
child: CircleContainer(
child: Icon(
//Icons.playlist_add,
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),

You can update the Toggle inside a BlocBuilder - the widget rebuilds on state changes. If there's a state change that you'd like to observe in your bloc, calling something like context.read<YourBloc>().doSomething() should update the widgets inside BlocBuilder.
Using BlocListener is another approach that you can use.

Related

Flutter BlocBuilder doesn't update my List of Inputs

I'm using Flutter Bloc to keep track of state changes in my PIN and Fingerprint Authentication.
Currently i have the problem that my List of current Pin Inputs just get updated for the first item in the List. All the following inputs don't update the UI, but the List keeps on growing.
My Authentication_Bloc:
abstract class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationPinState> {
AuthenticationBloc()
: super(AuthenticationPinState(
pinNumbers: const [], pinStatus: AuthenticationPINStatus.enter));
}
class AuthenticationPinBloc extends AuthenticationBloc {
List<int> pinNumbers = [];
AuthenticationPinBloc() : super() {
on<AuthenticationPinAddEvent>((event, emit) async {
try {
pinNumbers.add(event.pinNumber);
if (pinNumbers.length < 6) {
emit(state.copyWith(
pinNumbers: pinNumbers,
pinStatus: AuthenticationPINStatus.enter));
} else if (pinNumbers.length == 6) {
emit(state.copyWith(
pinNumbers: pinNumbers,
pinStatus: AuthenticationPINStatus.equals));
} else {
emit(state.copyWith(
pinNumbers: pinNumbers,
pinStatus: AuthenticationPINStatus.unequals));
await Future.delayed(
const Duration(seconds: 2),
() => emit(AuthenticationPinState(
pinNumbers: pinNumbers,
pinStatus: AuthenticationPINStatus.enter)));
}
} catch (e) {}
});
on<AuthenticationPinEraseEvent>((event, emit) async {});
on<AuthenticationFingerprintEvent>((event, emit) {});
}
}
My Authentication_State:
enum AuthenticationPINStatus { enter, equals, unequals }
abstract class AuthenticationState extends Equatable {
#override
List<Object?> get props => [];
}
// Waiting to see if the user is authenticated
class AuthenticationUninitialized extends AuthenticationState {}
// Sucessfully authenticated
class AuthenticationAuthenticated extends AuthenticationState {}
// Not authenticated
class AuthenticationUnauthenticated extends AuthenticationState {}
class AuthenticationLoading extends AuthenticationState {}
class AuthenticationPinState extends AuthenticationState {
final List<int> pinNumbers;
final AuthenticationPINStatus pinStatus;
AuthenticationPinState({required this.pinNumbers, required this.pinStatus});
AuthenticationPinState copyWith(
{required AuthenticationPINStatus pinStatus,
required List<int> pinNumbers}) {
return AuthenticationPinState(pinNumbers: pinNumbers, pinStatus: pinStatus);
}
int getPINCount() {
return pinNumbers.length;
}
#override
List<Object?> get props => [List.from(pinNumbers), pinStatus];
}
My Authentication_Event:
abstract class AuthenticationEvent extends Equatable {
const AuthenticationEvent([List props = const []]) : super();
}
abstract class AuthenticationPinEvent extends AuthenticationEvent {
final int pinNumber;
const AuthenticationPinEvent({required this.pinNumber});
#override
List<Object?> get props => [pinNumber];
}
class AuthenticationPinAddEvent extends AuthenticationPinEvent {
const AuthenticationPinAddEvent({required int pinNumber})
: super(pinNumber: pinNumber);
#override
List<Object?> get props => [pinNumber];
}
class AuthenticationPinEraseEvent extends AuthenticationPinEvent {
const AuthenticationPinEraseEvent() : super(pinNumber: -1);
#override
List<Object?> get props => [];
}
class AuthenticationFingerprintEvent extends AuthenticationEvent {
const AuthenticationFingerprintEvent();
#override
List<Object?> get props => [];
}
My Code calling the Bloc:
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: BlocProvider(
create: (context) => AuthenticationPinBloc(),
child: BlocListener<AuthenticationPinBloc, AuthenticationPinState>(
listener: (context, state) {
if (state.pinStatus == AuthenticationPINStatus.equals) {
// Validate PIN
} else if (state.pinStatus == AuthenticationPINStatus.unequals) {
// Retry Logic
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: 120,
),
Flexible(
flex: 2,
child: Text(authenticationByPINCode,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(const TextStyle(color: Colors.blueGrey))),
),
BlocBuilder<AuthenticationPinBloc, AuthenticationPinState>(
buildWhen: (previous, current) {
return previous.pinNumbers != current.pinNumbers;
},
builder: (context, state) {
return BlocProvider<AuthenticationPinBloc>.value(
value: context.read<AuthenticationPinBloc>(),
child: Flexible(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (index) {
return PinSphere(
input: index < state.getPINCount());
}),
)),
);
},
),
const SizedBox(
height: 80,
),
const Flexible(flex: 3, child: Numpad())
],
),
),
)),
);
}

Toogle Favorites in onDismissable event in Flutter with Bloc

I'm having some trouble completing the action related to how I should add/remove (toggle) favorites from a list(coming from an api response). Actually I'm able to add the favorite to the list and also remove it in a separated way, but I can't do it with the same button in a toggled way. In Brief, it is working but I cannot achieve the toggle action properly. Please could you take a look at the code and see if there's a good approach on how to achieve toggling without using setstate?
Button to Dismiss (delete from list)
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
onDismissed();
},
child: CircleContainer(
child: Icon(
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),
Same Button achieving adding favorite to list with Bloc
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterAddToFavorites(item));
},
child: CircleContainer(
child: Icon(
item.isFavorite
? Icons.favorite
: Icons.favorite_border,
color: Colors.white,
),
size: 35,
),
),
Item definition
class YoutbeVideo {
final String videoId, title, description, banner;
bool isFavorite;
YoutbeVideo(
{#required this.videoId,
#required this.title,
#required this.description,
#required this.banner,
this.isFavorite});
void toggleFavoriteStatus() {
isFavorite = !isFavorite;
}
BloC Code divided in 3 files
master_bloc
import 'package:bloc/bloc.dart';
import 'package:documentales_app/models/youtube_video.dart';
import 'master_events.dart';
import 'master_state.dart';
class MasterBloc extends Bloc<MasterEvents, MasterState> {
#override
MasterState get initialState => MasterState.initialState();
#override
Stream<MasterState> mapEventToState(MasterEvents event) async* {
if (event is MasterSetTab) {
yield this.state.copyWith(currentTab: event.tab);
} else if (event is MasterAddToHistory) {
yield* _addToHistory(event);
} else if (event is MasterRemoveFromHistory) {
yield* _removeFromHistory(event);
} else if (event is MasterRemoveFromFavorites) {
yield* _removeFromFavorites(event);
} else if (event is MasterLogout) {
yield this.state.copyWith(history: [], currentTab: 0);
} else if (event is MasterAddToFavorites) {
yield* _addToFavorites(event);
}
}
Stream<MasterState> _addToHistory(MasterAddToHistory event) async* {
final int index = this
.state
.history
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final history = List<YoutubeVideo>.from(this.state.history);
history.add(event.youtubeVideo);
yield this.state.copyWith(history: history);
}
}
Stream<MasterState> _addToFavorites(MasterAddToFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
yield this.state.copyWith(favorites: favorites);
}
}
Stream<MasterState> _removeFromHistory(MasterRemoveFromHistory event) async* {
final history = List<YoutubeVideo>.from(this.state.history);
history.removeAt(event.index);
yield this.state.copyWith(history: history);
}
Stream<MasterState> _removeFromFavorites(
MasterRemoveFromFavorites event) async* {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(event.index);
yield this.state.copyWith(favorites: favorites);
}
}
master state
import 'package:meta/meta.dart' show required;
import 'package:documentales_app/models/youtube_video.dart';
import 'package:equatable/equatable.dart';
class MasterState extends Equatable {
final int currentTab;
final List<YoutubeVideo> history;
final List<YoutubeVideo> favorites;
MasterState(
{#required this.currentTab, #required this.history, this.favorites});
static MasterState initialState() =>
MasterState(currentTab: 0, history: [], favorites: []);
MasterState copyWith(
{int currentTab,
List<YoutubeVideo> history,
List<YoutubeVideo> favorites}) {
return MasterState(
currentTab: currentTab ?? this.currentTab,
history: history ?? this.history,
favorites: favorites ?? this.favorites);
}
#override
List<Object> get props => [currentTab, history, favorites];
}
master events
import 'package:documentales_app/models/youtube_video.dart';
abstract class MasterEvents {}
class MasterSetTab extends MasterEvents {
final int tab;
MasterSetTab(this.tab);
}
class MasterAddToHistory extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToHistory(this.youtubeVideo);
}
class MasterAddToFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToFavorites(this.youtubeVideo);
}
class MasterRemoveFromHistory extends MasterEvents {
final int index;
MasterRemoveFromHistory(this.index);
}
class MasterRemoveFromFavorites extends MasterEvents {
final int index;
MasterRemoveFromFavorites(this.index);
}
So one way to perform a toggle is to replace Add and Remove event with just one. So you could get rid of these events:
class MasterAddToFavorites extends MasterEvents {
final YoutubeVideo youtubeVideo;
MasterAddToFavorites(this.youtubeVideo);
}
class MasterRemoveFromFavorites extends MasterEvents {
final int index;
MasterRemoveFromFavorites(this.index);
}
and replace it with:
class MasterToggleInFavorites extends MasterEvents {
final YoutubeVideo video;
MasterToggleInFavorites(video);
}
Next, inside of bloc, inside of method that handles that event, you could do something like this:
Stream<MasterState> _toggleInFavorites(MasterToggleInFavorites event) async* {
final int index = this
.state
.favorites
.indexWhere((item) => item.videoId == event.youtubeVideo.videoId);
if (index == -1) {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.add(event.youtubeVideo);
event.youtubeVideo.isFavourite = true;
yield this.state.copyWith(favorites: favorites);
} else {
final favorites = List<YoutubeVideo>.from(this.state.favorites);
favorites.removeAt(index);
event.youtubeVideo.isFavourite = false;
yield this.state.copyWith(favorites: favorites);
}
Video Class
class YoutubeVideo {
final String videoId, title, description, banner;
bool isFavorite;
YoutubeVideo(
{#required this.videoId,
#required this.title,
#required this.description,
#required this.banner,
this.isFavorite = false});
factory YoutubeVideo.fromJson(Map<String, dynamic> json,
{bool fromPlayList = false}) {
final snippet = json['snippet'];
final thumbnail =
snippet['thumbnails']['standard'] ?? snippet['thumbnails']['high'];
String videoId;
if (!fromPlayList) {
videoId = json['contentDetails']['videoId'];
} else {
videoId = snippet['resourceId']['videoId'];
}
return YoutubeVideo(
videoId: videoId,
title: snippet['title'],
description: snippet['description'],
banner: thumbnail['url']);
}
}
And here is the action onPressed but for any reason is not reflecting the color change
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 30,
onPressed: () {
masterBloc.add(MasterToggleInFavorites(item));
},
child: CircleContainer(
child: Icon(
//Icons.playlist_add,
item.isFavorite
? Icons.favorite_border
: Icons.favorite,
color: Colors.white,
),
size: 35,
),
),

How I can call setState() of class A from Class B

when I click on IconButton() to delete All items from the list movies I can't see that change until I reopen the page again...
anyone know how I could fix
this my infoPage(("class B")):
class InfoPage extends StatefulWidget {
int id;
int pageId;
InfoPage(this.id,this.pageId);
#override
_InfoPageState createState() => _InfoPageState(id,pageId);
}
class _InfoPageState extends State<InfoPage> {
var db = DatabaseHelper();
String title = "";
String about = "";
String rate = "";
String date = "";
int id;
int pageId;
_InfoPageState(this.id,this.pageId);
#override
void initState() {
super.initState();
if(pageId == 1){
_getMovie();
}
}
void _getMovie() async {
Movie thisMovie = await db.getMovie(id);
setState(() {
title = thisMovie.name;
about = thisMovie.description;
rate = thisMovie.rate;
date = thisMovie.date;
});
}
_deleteMovie() async{
await db.deleteMovie(id);
Navigator.pop(context);
setState(() {
CardsListViewState(pageId).deleteAllList();
});
}
#override
Widget build(BuildContext context) {
Navigator.canPop(context);
return Scaffold(
body: Container(
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 10),
child:Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(),
),
Container(
margin: EdgeInsets.only(right: 10),
child: IconButton(
icon: Icon(Icons.delete,color: Color(0xffFC4D4D),),
onPressed: (){
_deleteMovie();
}
)
)
],
),
)
],
),
),
);
}
}
and this my CardsListView(("class A"))
class CardsListView extends StatefulWidget {
int whereComeFrom;
CardsListView(this.whereComeFrom);
#override
CardsListViewState createState() => CardsListViewState(whereComeFrom);
}
class CardsListViewState extends State<CardsListView> {
int whereComeFrom;
CardsListViewState(this.whereComeFrom);
var db = DatabaseHelper();
List mainList = [];
final List<Movie> movies = <Movie>[];
deleteAllList() async{
await db.deleteMovies();
setState(() {
movies.clear();
});
}
#override
void initState() {
super.initState();
_readUnites();
if(whereComeFrom == 1){
mainList = movies;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:
GridView.count(
crossAxisCount: 3,
addAutomaticKeepAlives: true,
childAspectRatio: (1/1.5),
children: List.generate(mainList.length, (index){
return CardUnite(mainList[index].name,mainList[index].id,whereComeFrom);
})
),
);
}
You can use a callback function from the parent class supplied to the child class.
Remember that functions are first class objects in Dart.
Just pass in a function that calls setState to the child and have the child call that function.

How to use flutter provider in a statefulWidget?

I am using flutter_provider for state management. I want to load some items on page(statefulwidget) load from Api. I am showing a loader on page start and want to show the items once they are fetched.
PlayList.dart -
class Playlist extends StatefulWidget {
#override
_PlaylistState createState() => _PlaylistState();
}
class _PlaylistState extends State<Playlist> {
var videosState;
#override
void initState() {
super.initState();
videosState = Provider.of<VideosProvider>(context);
videosState.fetchVideosList();
}
#override
Widget build(BuildContext context) {
var videos = videosState.playlist;
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
child: Container(
width: double.infinity,
height: double.infinity,
child: videos.length
? ListView.builder(
itemBuilder: (BuildContext context, index) {
return _videoListItem(context, index, videos, videosState);
},
itemCount: videos.length,
)
: Center(
child: CircularProgressIndicator(),
),
),
onRefresh: () => null,
),
);
}
}
My provider is like this -
class VideosProvider with ChangeNotifier {
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
return videos;
}
}
This gives an error of -
inheritFromWidgetOfExactType(_Provider<VideosProvider>) or inheritFromElement() was called before _PlaylistState.initState() completed.
here is the build method of the parent of playList class, wrapped in a changenotifier,
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
builder: (BuildContext context) => VideosProvider(),
child: MaterialApp(
title: "My App",
home: new Playlist(),
),
);
}
So, all the examples on flutter_provider on internet show usage of provider on a statelesswidget, where state changes occur on user interactions like a button click. None about how to use provider in a statefulWidget, and cases where data has to be updated on page load without any interaction.
I am aware of streambuilder and futurebuilder for this kind of scenarios, but want to understand how this can be done with flutter_provider. How can I use provider to call fetchVideosList in initState(on pageload)? Does this case can/should be handled with a statelessWidget?
Does this case can/should be handled with a statelessWidget?
The answer is : No, it does not
I am heavy user of StatefulWidget + Provider. I always use this pattern for displaying a Form which contains fields, that available for future edit or input.
Updated : February 9 2020
Regarding to Maks comment, I shared better way to manage provider using didChangeDependencies.
You may check to this github repository
main.dart
First Step
Initiate PlayListScreen inside ChangeNotifierProvider
class PlaylistScreenProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
create: (_) {
return VideosProvider();
},
child: PlaylistScreen(),
);
}
}
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen'),
),
body: Center(
child: RaisedButton(
child: Text("Go To StatefulWidget Screen"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
return PlaylistScreenProvider();
},
),
);
},
),
),
);
}
}
Second Step
Make PlaylistScreen as Stateful Widget to hold TextEditingContoller
and other values.
playlistScreen.dart
class PlaylistScreen extends StatefulWidget {
#override
_PlaylistScreenState createState() => _PlaylistScreenState();
}
class _PlaylistScreenState extends State<PlaylistScreen> {
List _playlistList;
String _errorMessage;
Stage _stage;
final _searchTextCtrl = TextEditingController();
#override
void dispose() {
super.dispose();
_searchTextCtrl.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final videosState = Provider.of<VideosProvider>(context);
_playlistList = videosState.playlist;
_stage = videosState.stage;
_errorMessage = videosState.errorMessage;
}
void actionSearch() {
String text = _searchTextCtrl.value.text;
Provider.of<VideosProvider>(context, listen: false)
.updateCurrentVideoId(text);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: <Widget>[
Container(
child: RaisedButton.icon(
icon: Icon(Icons.search),
label: Text("Filter"),
onPressed: () {
actionSearch();
},
),
),
Container(
child: TextField(
controller: _searchTextCtrl,
onSubmitted: (value) {
actionSearch();
},
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Please input 1 or 2',
),
),
),
Flexible(
child: _stage == Stage.DONE
? PlaylistTree(_playlistList)
: _stage == Stage.ERROR
? Center(child: Text("$_errorMessage"))
: Center(
child: CircularProgressIndicator(),
),
)
],
),
),
);
}
}
class PlaylistTree extends StatelessWidget {
PlaylistTree(this.playlistList);
final List playlistList;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: playlistList.length,
itemBuilder: (context, index) {
var data = playlistList[index];
return Container(
child: Text("${data['id']} - ${data['first_name']}"),
);
},
);
}
}
Last Step
make provider to handle Business Logic
videosProvider.dart
enum Stage { ERROR, LOADING, DONE }
class VideosProvider with ChangeNotifier {
String errorMessage = "Network Error";
Stage stage;
List _playlist;
int _currentVideoId;
VideosProvider() {
this.stage = Stage.LOADING;
initScreen();
}
void initScreen() async {
try {
await fetchVideosList();
stage = Stage.DONE;
} catch (e) {
stage = Stage.ERROR;
}
notifyListeners();
}
List get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
void validateInput(String valueText) {
if (valueText == ""){
this._currentVideoId = null;
return;
}
try {
int valueInt = int.parse(valueText);
if (valueInt == 1 || valueInt == 2){
this._currentVideoId = valueInt;
}
else {
this.errorMessage = "Use only 1 and 2";
throw 1;
}
} on FormatException catch (e) {
this.errorMessage = "Must be a number";
throw 1;
}
}
void updateCurrentVideoId(String value) async {
this.stage = Stage.LOADING;
notifyListeners();
try {
validateInput(value);
await fetchVideosList();
stage = Stage.DONE;
} on SocketException catch (e) {
this.errorMessage = "Network Error";
stage = Stage.ERROR;
} catch (e) {
stage = Stage.ERROR;
}
notifyListeners();
}
Future fetchVideosList() async {
String url;
if (_currentVideoId != null) {
url = "https://reqres.in/api/users?page=$_currentVideoId";
} else {
url = "https://reqres.in/api/users";
}
http.Response response = await http.get(url);
var videosList = json.decode(response.body)["data"];
setPlayList(videosList);
}
}
Old answer : Aug 19 2019
In my case :
form_screen.dart
class Form extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<FormProvider>(
builder: (_) {
return FormProvider(id: ...); // Passing Provider to child widget
},
child: FormWidget(), // So Provider.of<FormProvider>(context) can be read here
);
}
}
class FormWidget extends StatefulWidget {
#override
_FormWidgetState createState() => _FormWidgetState();
}
class _FormWidgetState extends State<FormWidget> {
final _formKey = GlobalKey<FormState>();
// No need to override initState like your code
#override
Widget build(BuildContext context) {
var formState = Provider.of<FormProvider>(context) // access any provided data
return Form(
key: _formKey,
child: ....
);
}
}
FormProvider as a class, need to update their latest value from API. So, initially, it will request to some URL and updates corresponding values.
form_provider.dart
class FormProvider with ChangeNotifier {
DocumentModel document;
int id;
FormProvider({#required int id}) {
this.id = id;
initFormFields(); // will perform network request
}
void initFormFields() async {
Map results = initializeDataFromApi(id: id);
try {
document = DocumentModel.fromJson(results['data']);
} catch (e) {
// Handle Exceptions
}
notifyListeners(); // triggers FormWidget to re-execute build method for second time
}
In your case :
PlayList.dart
class PlaylistScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
builder: (_) {
return VideosProvider(); // execute construct method and fetchVideosList asynchronously
},
child: Playlist(),
);
}
}
class Playlist extends StatefulWidget {
#override
_PlaylistState createState() => _PlaylistState();
}
class _PlaylistState extends State<Playlist> {
final _formKey = GlobalKey<FormState>();
#override
void initState() {
super.initState();
// We *moved* this to build method
// videosState = Provider.of<VideosProvider>(context);
// We *moved* this to constructor method in provider
// videosState.fetchVideosList();
}
#override
Widget build(BuildContext context) {
// Moved from initState
var videosState = Provider.of<VideosProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
}
}
provider.dart
class VideosProvider with ChangeNotifier {
VideosProvider() {
// *moved* from Playlist.initState()
fetchVideosList(); // will perform network request
}
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
// return videos; // no need to return
// We need to notify Playlist widget to rebuild itself for second time
notifyListeners(); // mandatory
}
}
When using Provider for state management you don't need to use StatefullWidget, so how can you call a method of the ChangeNotifier on start of the app?
You can simply do that in the constructor of the ChangeNotifier, so that when you point out VideosProvider() to the ChangeNotifierProvider Builder the constructor will get called the first time the provider constructs the VideosProvider, so:
PlayList.dart:
class Playlist extends StatelessWidget {
#override
Widget build(BuildContext context) {
final videosState = Provider.of<VideosProvider>(context);
var videos = videosState.playlist;
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
child: Container(
width: double.infinity,
height: double.infinity,
child: videos.length
? ListView.builder(
itemBuilder: (BuildContext context, index) {
return _videoListItem(context, index, videos, videosState);
},
itemCount: videos.length,
)
: Center(
child: CircularProgressIndicator(),
),
),
onRefresh: () => null,
),
);
}
}
VideosProvider.dart:
class VideosProvider with ChangeNotifier {
VideosProvider(){
fetchVideosList();
}
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
return videos;
}
}
When using a Provider you don’t need to use a StatefulWidget (as of a tutorial by the Flutter team State management
You may use the following tutorial to see how to fetch data with a provider and a
StatelessWidget: Flutter StateManagement with Provider

Bloc navigation on state change

I'm really new with flutter blocs and I having some problems with a bloc implementation, I'm trying to navigate after a state change in my splash screen widget.
After the state update to InitSuccess it should navigate to LoginScreen, but this navigation occurs many times.
I'm not able to understand what to do after the state change's to InitSuccess, after this the bloc keeps alive and calling many, many times LoginScreen.
Splash Screen
class SplashScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
SplashBloc _splashBloc;
final _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
_init();
super.initState();
}
#override
void dispose() {
_splashBloc.dispose();
super.dispose();
}
void _init() {
Future.delayed(Duration.zero, () {
checkDeviceConnection(context);
BlocSupervisor().delegate = SplashBlocDelegate();
final bool isIOS = Theme.of(context).platform == TargetPlatform.iOS;
_splashBloc = SplashBloc(
firebaseService: FirebaseService(context),
authService: AuthService(context),
devicesService: DevicesService(context),
);
_splashBloc.dispatch(SplashInitEvent(isIOS: isIOS));
});
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return BlocBuilder<SplashEvent, SplashState>(
bloc: _splashBloc,
builder: (
BuildContext context,
SplashState state,
) {
if (state is InitFailure) {
Future.delayed(Duration.zero, () {
showWarningSnackBar(_scaffoldKey, state.error);
});
}
if (state is InitSuccess) {
Future.delayed(Duration.zero, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoginScreen(),
),
);
});
}
return Scaffold(
key: _scaffoldKey,
body: Container(
decoration: appScreenGradient,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(
"assets/images/splash_screen/logo_splash.png",
width: 172.88,
height: 144.55,
fit: BoxFit.contain,
),
SizedBox(
height: 20.0,
),
LoadingSpinner(
spinnerColor: Theme.of(context).primaryColorLight,
),
],
),
),
);
},
);
}
Splash Bloc
class SplashBloc extends Bloc<SplashEvent, SplashState> {
final FirebaseService firebaseService;
final DevicesService devicesService;
final AuthService authService;
final UserPreferences _userPreferences = UserPreferences();
SplashBloc({
#required this.firebaseService,
#required this.devicesService,
#required this.authService,
});
#override
Stream<SplashEvent> transform(Stream<SplashEvent> events) {
return (events as Observable<SplashEvent>).debounce(
Duration(milliseconds: 500));
}
#override
get initialState => SplashInitial();
#override
Stream<SplashState> mapEventToState(currentState, event) async* {
if (event is SplashInitEvent) {
if (currentState is SplashInitial) {
yield InitLoading();
try {
firebaseService.togglePerformanceCollection(true);
firebaseService.firebaseCloudMessagingListeners();
String firebaseToken = await firebaseService
.getFirebaseMessagingToken();
bool isRegistered =
await _userPreferences.getIsDeviceRegistered() ?? false;
if (!isRegistered) {
final String platform = event.isIOS ? 'IOS' : 'Android';
final deviceInfo = await devicesService.getDeviceInfo(platform);
isRegistered = await devicesService.register(
deviceToken: firebaseToken,
deviceInfo: deviceInfo,
);
if (isRegistered) {
_userPreferences.setIsDeviceRegistered(true);
}
}
yield InitSuccess();
} catch (e) {
yield InitFailure(error: e.toString());
}
}
}
if (event is SplashInitialEvent) {
yield SplashInitial();
}
}
}
I found the following solution:
if (state is LoggedIn) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Navigation
});
}
I wrapped my navigation with this addPostFrame callback for delaying its appearance.