I'm new to Flutter and I'm making a To Do app. I want to be able to open a showModalBottomSheet widget when I click an ElevatedButton that belongs to another widget. Ideally it would open when the user clicks "Edit" belonging to one of the ToDo widgets.
Worst case I could probably use another showModalBottomSheet for the edit action but I'd love to be able to reuse my existing showModalBottomSheet for edits as well as new to do's since it's already in place. All I need to do is trigger it to reopen when the user selects "Edit".
Here is my code in MyApp. I can include code for NewToDo if requested but I feel like that code isn't the issue.
import 'package:flutter/material.dart';
import './todoitem.dart';
import './todolist.dart';
import 'classes/todo.dart';
import './newtodo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To Do Homie',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: const MyHomePage(title: "It's To Do's My Guy"),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String content = '';
String title = '';
int maxId = 0;
ToDo? _todo;
final titleController = TextEditingController();
final contentController = TextEditingController();
List<ToDo> _todos = [];
void _addTodo(){
final todo = ToDo (
title: title,
id: maxId,
isDone: false,
content: content
);
if (_todo != null){
setState(() {
_todos[_todos.indexOf(_todo!)] = todo;
});
} else {
setState(() {
_todos.add(todo);
});
}
setState(() {
content = '';
maxId = maxId++;
title = '';
_todo = null;
});
contentController.text = '';
titleController.text = '';
}
#override
void initState() {
super.initState();
titleController.addListener(_handleTitleChange);
contentController.addListener(_handleContentChange);
}
void _handleTitleChange() {
setState(() {
title = titleController.text;
});
}
void _handleContentChange() {
setState(() {
content = contentController.text;
});
}
void _editTodo(ToDo todoitem){
setState(() {
_todo = todoitem;
content = todoitem.content;
title = todoitem.title;
});
contentController.text = todoitem.content;
titleController.text = todoitem.title;
}
void _deleteToDo(ToDo todoitem){
setState(() {
_todos = List.from(_todos)..removeAt(_todos.indexOf(todoitem));
});
}
void _clear(){
contentController.text = '';
titleController.text = '';
setState(() {
content = '';
title = '';
_todo = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Center(
child: Container(
alignment: Alignment.topCenter,
child: ToDoList(_todos, _editTodo, _deleteToDo)
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return ValueListenableBuilder(
valueListenable: titleController,
builder: (context, _content, child) {
return NewToDo(titleController, contentController, _addTodo, _clear, _todo);
});
});
},
child: const Icon(Icons.add),
backgroundColor: Colors.deepPurple,
),
);
}
}
Try extracting the showModalBottomSheet in its own method and pass it to the onPressed like this:
floatingActionButton: FloatingActionButton(
onPressed: _triggerBottomSheet,
// then create the method
void _triggerBottomSheet(){
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return ValueListenableBuilder(
valueListenable: titleController,
builder: (context, _content, child) {
return NewToDo(titleController, contentController, _addTodo, _clear, _todo);
});
});
Related
I've followed a lot of tutorials and optimized the localization as much as possible.
The problem here is that the variables aren't properly initialized. First instance it shows errors of variables not being initialized. I then do a hot reload without changing anything. Now it rebuilds and then I navigated to the settings page to change the language and it did updated the strings.
So my main issue here is why are my variables not being initialized at first instance?
main.dart:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (context) => LocaleProvider(),
builder: (context, child) {
final provider = Provider.of<LocaleProvider>(context).locale;
return ThemeProvider(
saveThemesOnChange: true,
loadThemeOnInit: true,
child: LayoutBuilder(builder: (context, constraints) {
return OrientationBuilder(builder: (context, orientation) {
SizeConfig().init(constraints, orientation);
return MaterialApp(
theme: CustomTheme.lightTheme,
debugShowCheckedModeBanner: false,
routes: <String, WidgetBuilder>{
'/splash': (BuildContext context) =>
const SplashPortrait(),
},
locale: provider,
localizationsDelegates:
AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: ThemeConsumer(
child: orientation == Orientation.portrait
? const SplashPortrait()
: const SplashLandscape(),
),
);
});
}));
},
);
}
class SplashPortrait extends StatefulWidget {
const SplashPortrait({Key? key}) : super(key: key);
#override
State<SplashPortrait> createState() => _SplashPortraitState();
}
class _SplashPortraitState extends State<SplashPortrait>
with TickerProviderStateMixin {
late SharedPreferences sharedPreferences;
#override
void initState() {
super.initState();
init();
}
Future init() async {
sharedPreferences = await SharedPreferences.getInstance();
final rememberMe = sharedPreferences.getBool("rememberMe");
if (rememberMe != null) {
if (rememberMe) {
final profile = sharedPreferences.getString("currentUser");
if (profile == null) return;
Constants.currentProfile = Profile.fromJson(json.decode(profile));
Constants.token = sharedPreferences.getString("token")!;
Constants.headers = {
"Content-Type": 'application/json',
"x-access-token": Constants.token,
};
Future.delayed(const Duration(milliseconds: 4000), () {
Navigator.of(context)
.push(SlideRightRoute(page: const HomeScreen(fragmentIndex: 0)));
});
} else {
Future.delayed(const Duration(milliseconds: 4000), () {
Navigator.of(context)
.push(SlideRightRoute(page: const LoginScreen()));
});
}
} else {
Future.delayed(const Duration(milliseconds: 4000), () {
Navigator.of(context).push(SlideRightRoute(page: const LoginScreen()));
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: const RiveAnimation.asset(
'assets/splash.riv',
),
),
),
);
}
}
Login Page is where I am experiencing errors:
class LoginScreen extends StatefulWidget {
const LoginScreen({
Key? key,
}) : super(key: key);
#override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
late LoginBloc loginBloc;
FocusNode emailFocus = FocusNode();
FocusNode passwordFocus = FocusNode();
late TextEditingController emailController;
late TextEditingController passwordController;
late bool isPasswordVisible;
late String emailHint;
late String passwordHint;
late bool rememberMe;
#override
void initState() {
super.initState();
init();
}
init() {
emailController = TextEditingController();
passwordController = TextEditingController();
emailHint = context.loc.emailPlaceholder;
passwordHint = context.loc.passwordPlaceholder;
emailFocus.addListener(onEmailFocusChanged);
passwordFocus.addListener(onPasswordFocusChanged);
isPasswordVisible = false;
rememberMe = false;
loginBloc = LoginBloc(context: context, rememberMe: false);
}
void onEmailFocusChanged() {
if (emailFocus.hasFocus) {
setState(() {
emailHint = "";
});
} else {
setState(() {
emailHint = context.loc.emailPlaceholder;
});
}
}
void onPasswordFocusChanged() {
if (passwordFocus.hasFocus) {
setState(() {
passwordHint = "";
});
} else {
setState(() {
passwordHint = context.loc.passwordPlaceholder;
});
}
}
#override
void dispose() {
loginBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: colorWhite,
body: SafeArea(
child: WillPopScope(
child: LoginPortrait(context),
onWillPop: () => onWillPop(),
)),
);
}
onWillPop() {
SystemNavigator.pop();
}
context.loc.
extension LocalizedBuildContext on BuildContext {
AppLocalizations get loc => AppLocalizations.of(this);
}
Solutions I've tried:
1.) Future.delayed
2.) WidgetsBinding.instance?.addPostFrameCallback
3.) SchedulerBinding.instance!.addPostFrameCallback
4.) didChangeDependencies()
I'd like to ask when using the FutureBuilder to display fetched data from a remote server in a ListView. I check if the bottom of the ListView was reached using ScrollController. Everything is working well until I try to load new data and append them to the existing ListView I fetch the data add them to my Array and the in setState((){}) I update the list for the FutureBuilder this is obviously the wrong approach since then the whole FutureBuilder is rebuilt and so is the ListView. The changes however do appear all the new items are in the list as intended however it slows performance not significantly since ListView is not keeping tiles out of view active but it has a small impact on performance, but the main issue is that since ListView gets rebuilt, I'm thrown as a user to the start of this list that's because the ListView got rebuilt. Now what I would like to achieve is that the ListView doesn't get rebuilt every time I get new data. Here is the code of the whole StateFulWidget
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import '../widgets/rss_card.dart';
import '../extensions/colors.dart';
import '../extensions/rss.dart';
import '../main.dart';
import '../models/rss.dart';
class RssListView extends StatefulWidget {
final String? channel;
const RssListView.fromChannel(this.channel, {Key? key}) : super(key: key);
#override
State<RssListView> createState() => _RssListViewState();
}
class _RssListViewState extends State<RssListView>
with AutomaticKeepAliveClientMixin {
late RssListModel _rssListModel;
double _offset = 0.0;
final double _limit = 5.0;
Future<List<RssItemModel>?>? _rssFuture;
final ScrollController _scrollController = ScrollController();
Map<String, Object> _args({double? newOffset}) => {
'offset': newOffset ?? _offset,
'limit': _limit,
};
Future<bool> isConnected() async {
var conn = await Connectivity().checkConnectivity();
return (conn == ConnectivityResult.mobile ||
conn == ConnectivityResult.wifi ||
conn == ConnectivityResult.ethernet)
? true
: false;
}
Future<void> _pullRefresh() async {
_rssListModel.refresh(_args(
newOffset: 0,
));
List<RssItemModel>? refreshedRssItems = await _rssListModel.fetchData();
setState(() {
_rssFuture = Future.value(refreshedRssItems);
});
}
Future<List<RssItemModel>?> get initialize async {
await _rssListModel.initializationDone;
return _rssListModel.Items;
}
void _loadMore() async {
List<RssItemModel>? moreItems = await _rssListModel
.loadMoreWithArgs(_args(newOffset: _offset += _limit));
setState(() {
_rssFuture = Future.value(moreItems);
});
}
void _showSnackBarWithDelay({int? milliseconds}) {
Future.delayed(
Duration(milliseconds: milliseconds ?? 200),
() {
ScaffoldMessenger.of(context).showSnackBar(getDefaultSnackBar(
message: 'No Internet Connection',
));
},
);
}
void _addScrollControllerListener() {
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
(_scrollController.position.maxScrollExtent)) _loadMore();
});
}
#override
bool get wantKeepAlive => true;
#override
void initState() {
super.initState();
_rssListModel = RssListModel.fromChannel(widget.channel, _args());
isConnected().then((internet) {
if (!internet) {
_showSnackBarWithDelay();
} else {
_addScrollControllerListener();
setState(() {
_rssFuture = initialize;
});
}
});
}
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
color: Colors.white,
child: FutureBuilder<List<RssItemModel?>?>(
future: _rssFuture,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
break;
case ConnectionState.waiting:
return getLoadingWidget();
case ConnectionState.done:
{
if (!snapshot.hasData || snapshot.data!.isEmpty)
return _noDataView('No data to display');
if (snapshot.hasError)
return _noDataView("There was an error while fetching data");
return _refreshIndicator(snapshot);
}
}
return _noDataView('Unable to fetch data from server');
},
),
);
}
/// Returns a `RefreshIndicator` wrapping our `ListView`
Widget _refreshIndicator(AsyncSnapshot snapshot) => RefreshIndicator(
backgroundColor: const Color.fromARGB(255, 255, 255, 255),
triggerMode: RefreshIndicatorTriggerMode.anywhere,
color: MyColors.Red,
onRefresh: _pullRefresh,
child: _listView(snapshot),
);
/// Returns a `ListView` builder from an `AsyncSnapshot`
Widget _listView(AsyncSnapshot snapshot) => ListView.builder(
controller: _scrollController,
clipBehavior: Clip.none,
itemCount: snapshot.data!.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) => RssCard(snapshot.data![index]),
);
/// Returns a `Widget` informing of "No Data Fetched"
Widget _noDataView(String message) => Center(
child: Text(
message,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
);
}
What you need is to hold onto the state in some Listenable, such as ValueNotifier and use ValueListenableBuilder to build your ListView. I put together this demo to show you what I mean:
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
));
}
#immutable
class Person {
final String id;
Person() : id = const Uuid().v4();
}
class DataController extends ValueNotifier<Iterable<Person>> {
DataController() : super([]) {
addMoreValues();
}
void addMoreValues() {
value = value.followedBy(
Iterable.generate(
30,
(_) => Person(),
),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final ScrollController _controller;
final _generator = DataController();
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(() {
if (_controller.position.atEdge && _controller.position.pixels != 0.0) {
_generator.addMoreValues();
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: ValueListenableBuilder(
valueListenable: _generator,
builder: (context, value, child) {
final persons = value as Iterable<Person>;
return ListView.builder(
controller: _controller,
itemCount: persons.length,
itemBuilder: (context, index) {
final person = persons.elementAt(index);
return ListTile(
title: Text(person.id),
);
},
);
},
),
);
}
}
I'm working on a food delivery app I've tried to make an increment decrement system of a particular product in a list. At the start it works i.e the counter increases but a bit after the counter automatically return to 0 without any button press. I don't know why it's happening
Below is the code I'm trying
This is the class
class ItemData {
final String itemName;
final String itemPrice;
final String image;
int counter = 0;
bool isAdded = false;
ItemData({this.itemName, this.itemPrice, this.image});
}
This is the function for getting data from url
Future<List<ItemData>> _getProducts() async {
var data = await http
.get("https://orangecitycafe.in/app_configs/products_display.php");
var jsonData = json.decode(data.body);
List<ItemData> details = [];
for (var p in jsonData) {
ItemData detail = ItemData(
itemName: p["product_name"],
itemPrice: p["product_price"],
image: p["product_image"]);
details.add(detail);
}
return details;
}
This is the code for fetched products inside future builder
Widget _myCart() {
return FutureBuilder(
future: _getProfile(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data[index].itemName),
leading: Image.network("https://www.orangecitycafe.in/" +
snapshot.data[index].image),
trailing: snapshot.data[index].isAdded
? Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
setState(() {
if (snapshot.data[index].counter > 0) {
snapshot.data[index].counter--;
}
});
},
color: Colors.green,
),
Text(snapshot.data[index].counter.toString()),
IconButton(
icon: Icon(Icons.add),
color: Colors.green,
onPressed: () {
setState(() {
snapshot.data[index].counter++;
});
},
),
],
)
: RaisedButton(
onPressed: (){
setState(() {
snapshot.data[index].isAdded = true;
});
},
child: Text("Add"),
),
);
},
);
} else {
return Container();
}
},
);
}
The rest is working but only when I increase the counter it increases and after sometime it automatically returns to 0
You can copy paste run full code below
You can use the following way to use Future in FutureBuilder to avoid setState cause FutureBuilder rebuild again.
Detail reason https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
didUpdateWidget of the FutureBuilder state is being called every time a rebuild is issued. This function checks if the old future object is different from the new one, and if so, refires the FutureBuilder.
To get past this, we can call the Future somewhere other than in the build function. For example, in the initState, and save it in a member variable, and pass this variable to the FutureBuilder.
code snippet
Future<List<ItemData>> _future;
...
#override
void initState() {
_future = _getProducts();
super.initState();
}
...
Widget _myCart() {
return FutureBuilder(
future: _future,
working demo
full code
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class ItemData {
final String itemName;
final String itemPrice;
final String image;
int counter = 0;
bool isAdded = false;
ItemData({this.itemName, this.itemPrice, this.image});
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<List<ItemData>> _future;
Future<List<ItemData>> _getProducts() async {
var data = await http
.get("https://orangecitycafe.in/app_configs/products_display.php");
var jsonData = json.decode(data.body);
List<ItemData> details = [];
for (var p in jsonData) {
ItemData detail = ItemData(
itemName: p["product_name"],
itemPrice: p["product_price"],
image: p["product_image"]);
details.add(detail);
}
return details;
}
#override
void initState() {
_future = _getProducts();
super.initState();
}
Widget _myCart() {
return FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data[index].itemName),
leading: Image.network("https://www.orangecitycafe.in/" +
snapshot.data[index].image),
trailing: snapshot.data[index].isAdded
? Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
setState(() {
if (snapshot.data[index].counter > 0) {
snapshot.data[index].counter--;
}
});
},
color: Colors.green,
),
Text(snapshot.data[index].counter.toString()),
IconButton(
icon: Icon(Icons.add),
color: Colors.green,
onPressed: () {
setState(() {
snapshot.data[index].counter++;
});
},
),
],
)
: RaisedButton(
onPressed: () {
setState(() {
snapshot.data[index].isAdded = true;
});
},
child: Text("Add"),
),
);
},
);
} else {
return Container();
}
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _myCart());
}
}
I have a REST API which allows the user to update a Book model
GET /api/books.json # list of books
PUT /api/books/1.json # update the book with id=1
I have corresponding screens for these actions (an Index screen to list books; an Edit screen to edit the book details) in my flutter application. When creating the form edit a Book,
I pass a Book object to the Edit form
In the Edit form, I make a copy of the book object. I create a copy and not edit the original object to ensure that the object is not changed if the Update fails at the server
If the update is successful, I display an error message.
However, when I go back to the Index view, the book title is still the same (as this object has not changed). Also, I found that even if I make changes to the original object, instead of making a copy, the build method is not called when I go 'back'. I am wondering if there is a pattern that I can use to have this object updated across the application on successful updates.
I have the following classes
class Book {
final int id;
final String title;
Book(this.id, this.title);
static Book fromJson(json) {
return Book(
json['id'],
json['title']);
}
Map<String, dynamic> toJson() => {
'title': title
};
Future<bool> update() {
var headers = {
'Content-Type': 'application/json'
};
return http
.put(
"$HOST/api/books/${id}.json",
headers: headers,
body: jsonEncode(this.toJson()),
)
.then((response) => response.statusCode == 200);
}
}
Here is the Index view
class BooksIndex extends StatefulWidget {
static final tag = "books-index";
#override
_BooksIndexState createState() => _BooksIndexState();
}
class _BooksIndexState extends State<BooksIndex> {
final Future<http.Response> _getBooks = http.get("$HOST/api/books.json", headers: headers);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getBooks,
builder: (context, snapshot) {
if (snapshot.hasData) {
var response = snapshot.data as http.Response;
if (response.statusCode == 200) {
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
return _buildMaterialApp(ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) {
var book = books[index];
return ListTile(
title: Text(book.title),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => BooksEdit(book: book)
));
},
);
},
));
} else {
return _buildMaterialApp(Text(
"An error occured while trying to retrieve the books. Status=${response.statusCode}"));
}
} else if (snapshot.hasError) {
return _buildMaterialApp(Text(
"Could not load books. Please check your internet connection."));
} else {
return _buildMaterialApp(Text("Loading"));
}
});
}
_buildMaterialApp(widget) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Books"),
),
body: widget,
),
);
}
}
Here is the Edit form
class BooksEdit extends StatelessWidget {
final Book book;
BooksEdit({Key key, #required this.book}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Edit ${book.title}"),
),
body: Column(
children: <Widget>[
BookForm(
book: book,
)
],
),
);
}
}
class BookForm extends StatefulWidget {
Book book;
BookForm({Key key, #required this.book}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BookFormState();
}
}
class _BookFormState extends State<BookForm> {
TextEditingController _titleField;
RaisedButton _submitBtn;
bool isError = false;
String formMessage = "";
#override
Widget build(BuildContext context) {
_titleField = TextEditingController(text: widget.book.title);
_submitBtn = RaisedButton(
child: Text(
"Update",
style: Theme
.of(context)
.textTheme
.button,
),
color: Theme
.of(context)
.primaryColor,
onPressed: () {
var book = Book(
widget.book.id,
_titleField.text
);
book.update().then((success) {
if (success) {
setState(() {
isError = false;
formMessage = "Successfully updated";
widget.book = book;
});
} else {
setState(() {
isError = true;
formMessage = "Book could not be updated";
});
}
}, onError: (error) {
setState(() {
isError = true;
formMessage =
"An unexpected error occured. It has been reported to the administrator.";
});
});
},
);
var formMessageColor = isError ? Colors.red : Colors.green;
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
formMessage,
style: TextStyle(color: formMessageColor),
),
TextFormField(
controller: _titleField,
),
_submitBtn
],
),
);
}
}
Here the main file
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
'/': (context) => BooksIndex(),
};
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "BooksApp",
theme: ThemeData(primarySwatch: Colors.green),
routes: routes,
initialRoute: '/',
);
}
}
ALSO, I am new to Flutter. So, I would appreciate it if I get any feedback about any other places in my code that I can improve upon.
You can copy paste run full code below
I use fixed json string to simulate http, when update be called, only change json string
You can also reference official example https://flutter.dev/docs/cookbook/networking/fetch-data
Step 1 : You can await Navigator.push and do setState after await to refresh BooksIndex
Step 2 : Move parse json logic to getBooks
code snippet
return ListTile(
title: Text(book.title),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BooksEdit(book: book)));
setState(() {});
},
Future<List<Book>> httpGetBooks() async {
print("httpGetBooks");
var response = http.Response(jsonString, 200);
if (response.statusCode == 200) {
print("200");
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
print(books[1].title.toString());
return books;
}
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
'/': (context) => BooksIndex(),
};
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "BooksApp",
theme: ThemeData(primarySwatch: Colors.green),
routes: routes,
initialRoute: '/',
);
}
}
class BooksEdit extends StatelessWidget {
final Book book;
BooksEdit({Key key, #required this.book}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Edit ${book.title}"),
),
body: Column(
children: <Widget>[
BookForm(
book: book,
)
],
),
);
}
}
class BookForm extends StatefulWidget {
Book book;
BookForm({Key key, #required this.book}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BookFormState();
}
}
class _BookFormState extends State<BookForm> {
TextEditingController _titleField;
RaisedButton _submitBtn;
bool isError = false;
String formMessage = "";
#override
Widget build(BuildContext context) {
_titleField = TextEditingController(text: widget.book.title);
_submitBtn = RaisedButton(
child: Text(
"Update",
style: Theme.of(context).textTheme.button,
),
color: Theme.of(context).primaryColor,
onPressed: () {
var book = Book(widget.book.id, _titleField.text);
book.update().then((success) {
if (success) {
setState(() {
isError = false;
formMessage = "Successfully updated";
widget.book = book;
});
} else {
setState(() {
isError = true;
formMessage = "Book could not be updated";
});
}
}, onError: (error) {
setState(() {
isError = true;
formMessage =
"An unexpected error occured. It has been reported to the administrator.";
});
});
},
);
var formMessageColor = isError ? Colors.red : Colors.green;
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
formMessage,
style: TextStyle(color: formMessageColor),
),
TextFormField(
controller: _titleField,
),
_submitBtn
],
),
);
}
}
class BooksIndex extends StatefulWidget {
static final tag = "books-index";
#override
_BooksIndexState createState() => _BooksIndexState();
}
String jsonString = '''
[{
"id" : 1,
"title" : "t"
}
,
{
"id" : 2,
"title" : "t1"
}
]
''';
class _BooksIndexState extends State<BooksIndex> {
Future<List<Book>> httpGetBooks() async {
print("httpGetBooks");
var response = http.Response(jsonString, 200);
if (response.statusCode == 200) {
print("200");
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
print(books[1].title.toString());
return books;
}
}
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
print("build ${jsonString}");
return FutureBuilder<List<Book>>(
future: httpGetBooks(),
builder: (context, snapshot) {
if (snapshot.hasData) {
print("hasData");
return _buildMaterialApp(ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
var book = snapshot.data[index];
print(book.title);
return ListTile(
title: Text(book.title),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BooksEdit(book: book)));
setState(() {});
},
);
},
));
} else if (snapshot.hasError) {
return _buildMaterialApp(Text(
"Could not load books. Please check your internet connection."));
} else {
return _buildMaterialApp(Text("Loading"));
}
});
}
_buildMaterialApp(widget) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Books"),
),
body: widget,
),
);
}
}
class Book {
final int id;
final String title;
Book(this.id, this.title);
static Book fromJson(json) {
return Book(json['id'], json['title']);
}
Map<String, dynamic> toJson() => {'title': title};
Future<bool> update() {
print("update");
var headers = {'Content-Type': 'application/json'};
/*return http
.put(
"$HOST/api/books/${id}.json",
headers: headers,
body: jsonEncode(this.toJson()),
)
.then((response) => response.statusCode == 200);*/
jsonString = '''
[{
"id" : 1,
"title" : "t"
}
,
{
"id" : 2,
"title" : "test"
}
]
''';
return Future.value(true);
}
}
setState(() {
});
},
);
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