Flutter Bloc - Flutter Bloc state not updating - flutter

i'm just getting started with flutter bloc.
i try to make state but it always goes to initial. what's the solution?
BLOC
class VisiMisiBloc extends Bloc<VisiMisiEvent, VisiMisiState> {
VisiMisiBloc(this.visiMisiRepository) : super(VisiMisiInitial());
final VisiMisiRepository visiMisiRepository;
#override
Stream<VisiMisiState> mapEventToState(VisiMisiEvent event) async* {
if (event is GetVisiMisiList) {
yield* _getVisiMisi(event, state);
}
}
Stream<VisiMisiState> _getVisiMisi(VisiMisiEvent event, VisiMisiState state) async* {
yield VisiMisiLoading();
try {
ResponseModel<VisiMisiModel> response = await visiMisiRepository.getVisiMisi();
print(response);
if (response.statusCode == 0) {
int insertedId = await visiMisiRepository.insertVisiMisi(response.data);
print(insertedId);
List<VisiMisiModel> visiMisiList = await visiMisiRepository.getAllVisiMisi();
yield VisiMisiLoaded(visiMisiList);
} else {
yield VisiMisiError(response.errorMessage);
}
} on Exception catch (e) {
yield VisiMisiError(e.toString());
}
}
}
STATE
part of 'visi_misi_bloc.dart';
abstract class VisiMisiState extends Equatable {
const VisiMisiState();
}
class VisiMisiInitial extends VisiMisiState {
const VisiMisiInitial();
#override
List<Object>get props => [];
}
class VisiMisiLoading extends VisiMisiState {
const VisiMisiLoading();
#override
List<Object>get props => [];
}
class VisiMisiLoaded extends VisiMisiState {
final List<VisiMisiModel> visiMisiModel;
const VisiMisiLoaded(this.visiMisiModel);
#override
List<Object> get props => [visiMisiModel];
}
class VisiMisiError extends VisiMisiState {
final String message;
const VisiMisiError(this.message);
#override
List<Object>get props => [message];
}
EVENT
part of 'visi_misi_bloc.dart';
abstract class VisiMisiEvent extends Equatable{
const VisiMisiEvent();
}
class GetVisiMisiList extends VisiMisiEvent {
#override
List<Object> get props => [];
}
REPOSITORY
abstract class VisiMisiRepository {
Future<int> insertVisiMisi(VisiMisiModel todo);
Future<ResponseModel<VisiMisiModel>> getVisiMisi();
Future<List<VisiMisiModel>> getAllVisiMisi();
}
REPOSITORY IMPL
class VisiMisiRepositoryImpl extends VisiMisiRepository {
final NetworkInfoImpl networkInfo;
final RemoteDataSource remoteDatasource;
final VisiMisiDao dao;
VisiMisiRepositoryImpl(this.networkInfo, this.remoteDatasource, this.dao);
#override
Future<ResponseModel<VisiMisiModel>> getVisiMisi() {
return remoteDatasource.visiMisi();
}
#override
Future<int> insertVisiMisi(VisiMisiModel todo) {
return dao.upsert(todo);
}
#override
Future<List<VisiMisiModel>> getAllVisiMisi() {
return dao.getAll(userTABLE, VisiMisiModel.fromJson);
}
}
REMOTE DATASOURCE
Future<ResponseModel<VisiMisiModel>> visiMisi() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String auth_token = prefs.getString("auth_token");
try {
final response = await httpClient.get(ServiceUrl.visiMisi, auth_token);
if(response.statusCode != 200){
throw new Exception('Error getting visi misi');
}
return ResponseModel<VisiMisiModel>.fromJson(response, VisiMisiModel.fromJson);
}catch(e){
print(e);
}
}
VIEW
class VisiMisiPage extends StatefulWidget {
#override
_VisiMisiPageState createState() => _VisiMisiPageState();
}
class _VisiMisiPageState extends State<VisiMisiPage> {
VisiMisiRepository repository;
#override
void initState(){
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: AppColor.white,
appBar: AppBar(
title: Text("VISI & MISI", style: TextStyle(fontSize: 16, color: AppColor.deepCerulean),),
backgroundColor: Colors.white,
elevation: 0,
automaticallyImplyLeading: false,
brightness: Brightness.light,
leading: IconButton(
icon: new Icon(Icons.arrow_back, color: AppColor.deepCerulean,),
onPressed: () => Navigator.of(context).pop(),
),
),
body: BlocProvider<VisiMisiBloc>(
create: (_) => VisiMisiBloc(repository),
child: BlocBuilder<VisiMisiBloc, VisiMisiState>(
builder: (context, state) {
if (state is VisiMisiInitial) {
//BlocProvider.of<VisiMisiBloc>(context).add(GetVisiMisiList());
return Center(child: Text(state.toString()),);
} else if (state is VisiMisiLoading) {
return Center(child: CircularProgressIndicator(),);
} else if (state is VisiMisiLoaded) {
return SingleChildScrollView(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_visiWidget(context, state),
SizedBox(height: 20,),
_misiWidget(context),
SizedBox(height: 30,),
_footerWidget(context)
],
),
),
);
} else if (state is VisiMisiError) {
return Center(child: Text(state.message),);
}
}
)
),
)
);
}
void _onWidgetDidBuild(Function callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}
}
I get an Unhandled Exception: Unhandled error NoSuchMethodError: The method 'getVisiMisi' was called on null.
in view, the state shows in VisiMisiInitial, and doesn't want to update to VisiMisiLoading

try to initialize the repository like so:
class _VisiMisiPageState extends State<VisiMisiPage> {
VisiMisiRepository repository;
#override
void initState(){
super.initState();
repository = VisiMisiRepositoryImpl( parameters here ); // add this one
}

To initialize repository, you should use RepositoryProvider.
For example, something like that
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider<UserRepository>(
create: (context) => UserRepository(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<LoginBloc>(
create: (context) => LoginBloc(context.read<UserRepository>()),
),
],
child: Widget()));
}
}
Then, it will be automatically initialized

I have analysed your code ,and I found 2 mistakes:
1st. you just created instance of your VisiMisiRepository and not initialised. and you are calling their methods that's why you getting error Unhandled error NoSuchMethodError: The method 'getVisiMisi' was called on null.
2nd. You just initialised your bloc within passing instance of repository, and haven't performed any bloc event . that's why code keep showing initial state.
You might have got your answer.
if not, take help from here it'll surely help you:
replace this:
create: (_) => VisiMisiBloc(repository),
with this:
create: (_) => VisiMisiBloc(VisiMisiRepository())..add(GetVisiMisiList()),
In your widget tree:
body: BlocProvider<VisiMisiBloc>(
create: (_) => VisiMisiBloc(VisiMisiRepository())..add(GetVisiMisiList()), //initialising bloc within repository and hit event as well.
child: BlocBuilder<VisiMisiBloc, VisiMisiState>(
builder: (context, state) {
if (state is VisiMisiInitial) {
return Center(child: Text(state.toString()),);
} else if (state is VisiMisiError) {
return Center(child: Text(state.message),);
} else if (state is VisiMisiLoaded) {
return SingleChildScrollView(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_visiWidget(context, state),
SizedBox(height: 20,),
_misiWidget(context),
SizedBox(height: 30,),
_footerWidget(context)
],
),
),
);
}
return Center(child: CircularProgressIndicator(),);
}
)
),
this answer will also resolve Unhandled error NoSuchMethodError: The method 'getVisiMisi' was called on null error.

Related

Widget is not rebuilding when notifyListeners is called

i am trying to create a login page so that when i am logged-in, the Navigationrail Widget lets me access all its destinations. When logged off i can only access two pages.
I am using Provider in login.dart to triger a widget rebuild in main.dart .
here is the code.
login.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:httptest/depand/XmlLogin.dart';
import 'package:httptest/main.dart';
void login(String userName, String passWord) async {
Response response;
Dio dio = Dio();
dio.interceptors.add(InterceptorsWrapper(
onResponse: (response, handler) {
var token = getToken.Transcribe(response.data);
LoginProvider obj = LoginProvider();
obj.providestate(true);
print(token);
print("logged in");
handler.next(response);
},
));
try {
//Http Post method
} catch (e) {
print(e);
}
}
class LoginProvider extends ChangeNotifier {
bool loginstate = false;
void providestate(bool val) {
loginstate = val;
print("loginstate changed to $loginstate");
notifyListeners();
}
}
main.dart
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
List<Widget> pages = [Page0(), Page1(), Page2(), Placeholder()];
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: ChangeNotifierProvider<LoginProvider>(
create: (context) => LoginProvider(),
child: Builder(
builder: (context) {
return Consumer<LoginProvider>(
builder: (context, provider, child) {
return NavigationRail(
extended: constraints.maxWidth >= 600,
minExtendedWidth: 200,
destinations: [
NavigationRailDestination(),
NavigationRailDestination(),
NavigationRailDestination(),
NavigationRailDestination()
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
if (provider.loginstate) {
setState(() {
selectedIndex = value;
});
print("On");
} else {
if (value == 0 || value == 3) {
setState(() {
selectedIndex = value;
});
print("OFF");
}
}
},
);
});
},
),
)),
Expanded(
child: Scaffold(
body: IndexedStack(
index: selectedIndex,
children: pages,
),
),
),
],
),
);
});
}
}
the login goes through but i Still cant access pages 1 and 2.
it prints out 'loginstate changed to True' from login.dart.
To ensure access place a provider in a widget tree something like this:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => LoginProvider()),
...
],
child: const MyApp(),
),
);
}
Then in child widgets access it using either Consumer to listen changes or Provider.of when just calling a method.
Consumer<LoginProvider>(
builder: (context, provider, child) {
...
},
)
Provider.of<LoginProvider>(context, listen: false).login(userName, passWord);
See Simple app state management for the full example.

Flutter BLoC state managment

I decided to study the architecture of BloC. And something really worries me. I was making a login page and noticed that after calling the Emit () function, the TextField () became empty. If I understand correctly, BlocBuilder has rebuilt the entire widget. Is there a way to rebuild only the part that has changed with a single BlocBuilder? Or something smaller.
UI:
P.S:
there is a function generate Form () It returns
Column (
children: [
TextField (),
TextField (),
],
);
class MainAuth extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CustomScaffold(
resizeToAvoidBottomInset: true,
body: SafeArea(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthMain) {
return mayBe(
regForm: generateForm(context.read<AuthBloc>().loginForm,
authType.login, context),
loginForm: generateForm(context.read<AuthBloc>().regForm,
authType.register, context),
login: state.type == authType.login,
);
} else {
return Container();
}
},
),
),
);
}
}
class mayBe extends StatelessWidget {
final Widget regForm;
final Widget loginForm;
final bool login;
mayBe({required this.regForm, required this.loginForm, required this.login});
#override
Widget build(BuildContext context) {
return ListenableProvider(
create: (c) {},
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Column(
children: [
ChangedAnimationBlock(
index: login ? 0 : 1,
children: [
Builder(builder: (c) => loginForm),
regForm,
],
),
],
),
],
),
),
),
],
),
);
}
}
Auth Cubit:
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:proj/brain/base_functions/forms_to_form.dart';
import 'package:proj/data/repositories/main_api_repository.dart';
part 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> {
final MainApiRepository mainApiRepository;
authType curType = authType.register;
Map responseRegBody = {};
Map responseLoginBody = {};
AuthCubit({required this.mainApiRepository}) : super(AuthInitial()) {
start();
}
Future<void> start() async {
await getForms();
emit(
AuthMain(),
);
}
Future<void> getForms() async {
await getLoginForm();
await getRegisterForm();
}
void changeType() {
var currentState = state;
if (currentState is AuthMain) {
authType oldAuthType = curType;
authType newAuthType;
if (oldAuthType == authType.login) {
newAuthType = authType.register;
} else {
newAuthType = authType.login;
}
// curType = newAuthType;
emit(
AuthMain(type: newAuthType),
);
}
}
Map loginForm = {};
Map regForm = {};
Future<void> getLoginForm() async {
var response = await mainApiRepository.getAuthFromSettings();
loginForm = searchFormIntoForms(response);
print(response);
print(loginForm);
print("loginForm");
}
Future<void> getRegisterForm() async {
var response = await mainApiRepository.getRegistrationFormSettings();
regForm = searchFormIntoForms(response);
}
}
Auth state:
part of 'auth_cubit.dart';
enum authType {
register,
login,
}
#immutable
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthMain extends AuthState {
final authType type;
AuthMain({
this.type = authType.register,
});
}

Flutter Navigator 2.0 pages using with showGeneralDialog to handle clearing pages

I am trying to develop a logout feature with Navigator 2.0 pages & showGeneralDialog. A dialog (created by showGeneralDialog) will handle the logout after a button in the dialog is clicked by the user and the dialog is closed. However, an error is thrown in _RouteEntry.markForComplete (the error is thrown by the assert statement).
I tried to create a dummy project with some simple code:
import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
PlatformRouteInformationProvider _platformRouteInformationProvider =
PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(location: '/'));
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
routeInformationProvider: _platformRouteInformationProvider,
);
}
}
class BookRouteInformationParser extends RouteInformationParser<RoutePath> {
#override
Future<RoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.length >= 2) {
var remaining = uri.pathSegments[1];
return RoutePath.details(int.tryParse(remaining));
} else if (uri.pathSegments.length > 0 && uri.pathSegments[0] == 'book') {
return RoutePath.home();
} else
return RoutePath.login();
}
#override
RouteInformation restoreRouteInformation(RoutePath path) {
if (path.isLogin) return RouteInformation(location: '/');
if (path.isHomePage) {
return RouteInformation(location: '/book');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<RoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Book _selectedBook;
List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
bool _showLogin = false;
RoutePath get currentConfiguration => _showLogin
? RoutePath.login()
: _selectedBook == null
? RoutePath.home()
: RoutePath.details(books.indexOf(_selectedBook));
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('LoginPage'),
child: Scaffold(
appBar: AppBar(
title: Text('Login'),
),
),
),
if (!currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
notifyListeners();
return true;
},
);
}
#override
Future<void> setNewRoutePath(RoutePath path) async {
if (path.isDetailsPage) {
_selectedBook = books[path.id];
}
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
void handleLogout() {
_showLogin = true;
_selectedBook = null;
notifyListeners();
}
}
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class RoutePath {
final bool isLogin;
final int id;
RoutePath.login()
: id = null,
isLogin = true;
RoutePath.home()
: id = null,
isLogin = false;
RoutePath.details(this.id) : isLogin = false;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
#required this.books,
#required this.onTapped,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
#required this.book,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () async {
final response = await showGeneralDialog<bool>(
context: context,
useRootNavigator: true,
barrierDismissible: false,
transitionDuration: const Duration(milliseconds: 300),
transitionBuilder: (context, animation, __, child) {
return ScaleTransition(
scale: animation,
child: child,
);
},
pageBuilder: (context, _, __) => _CustomDialog(),
);
if (response == null) return;
if (response) {
// await Future.delayed(const Duration(milliseconds: 300));
context
.findAncestorStateOfType<_BooksAppState>()
._routerDelegate
.handleLogout();
}
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
ListTile(
title: Text(book.title),
subtitle: Text(book.author,
style: Theme.of(context).textTheme.subtitle1),
),
],
],
),
),
);
}
}
class _CustomDialog extends StatelessWidget {
_CustomDialog({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Clear all pages?'),
RaisedButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop(true);
},
),
],
),
),
);
}
}
However, in this dummy project, the error is randomly thrown, sometimes it is thrown at NavigatorState.finalizeRoute (in this assert statement: assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);), and sometimes it is thrown at exactly the same as the one I mention which is _RouteEntry.markForComplete.
The workaround I can find currently is to delay until the dialog pop transition is completed (delay before context.findAncestorStateOfType<_BooksAppState>()._routerDelegate.handleLogout();).
However, I would like to know the proper fix for this instead of waiting for it to completely pop because I am unsure if there is any hidden trouble I might face.
The issue seems to be fixed on the issue ticket you've filed. No workarounds needed. Just update the Flutter SDK version to fix the issue.

Bloc Library Null

In appbar I am trying to show profile icon after logged. When app start, appbar show profile icon, but at the same time in debug console give me an error 'A build function returned null'. When open profile page and return back, still the same error 'returned null' How to solve it?
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AuthBloc(
authService: AuthService())
..add(
AppStart(),
),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
homepage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return profileIcon(context);
} else if (state is UnAuthenticated) {
return logIn(context);
}
},
),
],
),
);
}
}
bloc
#override
AuthState get initialState => AuthState();
#override
Stream<AuthState> mapEventToState(AuthEvent event) async* {
if (event is AppStart) {
try {
final user = await AuthService.getCurrentUser();
yield Authenticated(user: user);
} catch (e) {
yield UnAuthenticated();
}
}
}
icon:
Widget profileIcon(context) {
return Row(
children: <Widget>[
FlatButton.icon(
icon: Icon(),
label: Text(
'Profile',
),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => UserProfile()));
},
),
],
);
}
state:
class Authenticated extends AuthState {
final FirebaseUser user;
Authenticated({this.user});
#override
List<Object> get props => [user];
}
class UnAuthenticated extends AuthState {
#override
List<Object> get props => [];
}
I'm not really sure but my guess is you have another state for your initialState which is not handled in the BlocBuilder. Even though you add AppStart event right after providing AuthBloc which will end up with either Authenticated or UnAuthenticated state, you still need to put another else for your initialState. Even if it's not the case, try to add an else statement
appBar: AppBar(
actions: <Widget>[
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return profileIcon(context);
} else if (state is UnAuthenticated) {
return logIn(context);
} else {
return Container();
}
},
),
],
),

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