I'm developing a Flutter mobile application which uses Google APIs. In one of the screens of my application I want to let the user type in a place (city, address, ...) and call the Google Places API to generate a list of suggestions based on user input. Whenever the text input changes a new GET request is issued.
To handle user input I am using a TextEditingController and in order to have a better user experience I want to use FutureBuilder class in order to show a loading spinner when the data is not ready. This is the code:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
class Info extends StatefulWidget {
static const routeName = '/info';
#override
_InfoState createState() => _InfoState();
}
class _InfoState extends State<Info> {
final controller = TextEditingController();
#override
void initState() {
// Start listening to changes.
controller.addListener(buildPredictionList);
super.initState();
}
#override
void dispose() {
// Clean up the controller when the widget is disposed.
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Info'),
),
body: Column(
children: <Widget>[
TextField(
controller: controller,
),
Container(
height: 200,
child: buildPredictionList(),
),
],
),
);
}
Widget buildPredictionList() {
return FutureBuilder(
future: fetchPredictions, // <-- Error! fetchPredictions expects a parameter!
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
Prediction pred = snapshot.data[index];
return Card(
child: ListTile(
leading: Icon(Icons.pin_drop),
title: Text('${pred.description}'),
),
);
},
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return CircularProgressIndicator();
},
);
}
}
class Prediction {
final String placeId;
final String description;
Prediction({this.placeId, this.description});
factory Prediction.fromJson(Map<String, dynamic> json) {
return Prediction(
placeId: json['place_id'],
description: json['description'],
);
}
}
Future<List<Prediction>> fetchPredictions(String query) async {
const GOOGLE_API_KEY = '...';
final lat = 40.758058;
final lng = -73.985626;
final radius = 2000;
final lang = 'en';
var url =
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$query&key=$GOOGLE_API_KEY&location=$lat,$lng&radius=$radius&language=$lang&strictbounds=true';
final response = await http.get(url);
if (response.statusCode == 200) {
var predictionsJson = json.decode(response.body)['predictions'] as List;
List<Prediction> predictions = predictionsJson
.map((predictionJson) => Prediction.fromJson(predictionJson))
.toList();
return predictions;
} else {
throw Exception('Failed to fetch Predictions');
}
}
My async function fetchPredictions expects an argument, which is the query string used for the GET request (so the input address, city, ...). But I cannot wrap this in an anonymous function because the future argument is expecting a return type of Future.
Thanks in advance!
Related
i'm following flutter offical docs to parse JSON in the background with the rest api from themoviedb. I get this following error when trying to show the list of movies
the api link : https://api.themoviedb.org/3/trending/movie/week?api_key=$apiKey
json data
app.dart'
simplify main.dart and isolate parsing using compute
import 'dart:convert';
import 'package:flutter/material.dart';
import 'models/movie_response.dart';
import 'package:auth_request/api_key.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
Future<List<Movie>> fetchMovies(http.Client client) async {
final response = await client
.get(Uri.parse('https://api.themoviedb.org/3/trending/movie/week?api_key=$apiKey'));
// Use the compute funtion to run fetchMovies in a separate isolate
return compute(parseMovies, response.body);
}
// A function that converts a response body into a List<Movie>.
List<Movie> parseMovies(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Movie>((json) => Movie.fromJson(json)).toList();
}
class MovieApp extends StatefulWidget {
const MovieApp({super.key});
#override
MovieAppState createState() => MovieAppState();
}
class MovieAppState extends State<MovieApp> {
late Future<Movie> futureAlbum;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MovieDB List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('MovieDB List'),
),
body: Center(
child: FutureBuilder<List<Movie>>(
future: fetchMovies(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
} else if (snapshot.hasData) {
return MoviesList(movies: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
),
),
);
}
}
//
class MoviesList extends StatelessWidget {
const MoviesList({super.key, required this.movies});
final List<Movie> movies;
#override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return Image.network(movies[index].original_title);
},
);
}
}
movie.dart
Parse and convert the JSON into a list of movies with the help of map and factory
class Movie {
final String original_title;
final String overview;
final String poster_path;
final String release_date;
const Movie({
required this.original_title,
required this.overview,
required this.poster_path,
required this.release_date,
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
original_title: json['original_title'],
overview: json['overview'],
poster_path: json['poster_path'],
release_date: json['release_date'],
);
}
}
change this:
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
to
final parsed = jsonDecode(responseBody)['results'];
I'd like to restructure my code to MVVM, I'm new to flutter, How do I restructure the function above to a Service Class
import 'dart:convert';
import 'package:amaizi_test/models/UserModel.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class JsonApiPhp extends StatefulWidget {
#override
_JsonApiPhpState createState() => _JsonApiPhpState();
}
class _JsonApiPhpState extends State<JsonApiPhp> {
bool loading = true;
final String url = 'https://jsonplaceholder.typicode.com/users';
var client = http.Client();
List<UserModel> users = [];
#override
void initState(){
fetchData();
super.initState();
}
I'd like to restructure my files,
Future<void> fetchData() async {
http.Response response = await client.get(Uri.parse(url));
if(response.statusCode == 200){ // Connection Ok
List responseJson = json.decode(response.body);
responseJson.map((m) => users.add(new UserModel.fromJson(m))).toList();
setState(() {
loading = false;
});
} else {
throw('error');
}
}
and inititialize in HomePage InitState
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: loading ?
Container(
child: Center(
child: CircularProgressIndicator(),
),
) :
ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index){
return Card(
child: ListTile(
title: Text(
users[index].name,
),
),
);
},
)
),
);
}
}
// The function calls API and displays in UI
For example you can create directory "services" inside lib dir, then create inside that directory dart file with your service class.
For example:
class ServerApiService{
Future<List<User>> fetchData() async {
///your code, but without setState()
return users;
}
}
After it in initState you can create service object and call your fetchData function from service class. But instead of create service class object every time, I recommend to create service object above MaterialApp and use Provider for access service object in any widget of app.
You can use FutureBuilder widget for access data from fetchData() method.
FutureBuilder<List<User>>(
future: users,
builder: (context, snapshot) {
if (snapshot.hasData) {
var data = snapshot.data!;
return ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
User user = data[index];
return Card(
child: ListTile(
title: Text(
user.name,
),
),
)
});
}
else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
else {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
});
Where users which access FutureBuilder (future: users) is fetchedData from service object.
And also little note: better if inside initState() you will call super.initState() above other code.
Start your initState with super.initState and end your dispose with super.dispose
it take about 7 days trying make a working example for lazyload listview with provider in flutter with real world example and it's still not working because i think something is missing
As a note : the first load , works good and when i scroll it's print (scroll) but nothing happened it's still in the same page
if i try to return _todolist variable in the _onScrollUpdated it not change page correctly and after three times i see this error
E/flutter ( 7713): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: type 'String' is not a subtype of type
'List' E/flutter ( 7713): #0 TodoService.fetchTodos
(package:flutter_todo_provider/services/todo_service.dart:32:21)
json example
https://jsonformatter.org/52c83e
todos_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_todo_provider/helpers/http_exception.dart';
import 'package:provider/provider.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/services/todo_service.dart';
class TodosScreen extends StatefulWidget {
#override
_TodosScreenState createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
ScrollController _controller;
List<dynamic> _todoList;
bool _isLoading ;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Configuration.AppName),
),
body: FutureBuilder(
future: _fetchListItems(),
builder: (context, snapshot){
if(snapshot.hasData){
return _listItems(snapshot.data);
}
return _buildProgressIndicator();
}
),
);
}
_fetchListItems() async {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
return _todoList ;
}
Widget _listItems(data){
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return ListView.builder(
controller: _controller,
itemCount: data.length ,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle:Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Future<void> _onScrollUpdated() async {
print("Scroll11");
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll ) {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
// return _todoList ; if use this line i see the error
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
}
}
Widget _buildProgressIndicator() {
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: _isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
}
todo_service.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/models/todo.dart';
class TodoService with ChangeNotifier {
bool isLoading = false;
bool isFetching = false;
int currentPage = 1;
int totalRows = 10;
List<Todo> items = [];
loadNextPage() async {
await fetchTodos(currentPage);
currentPage++;
notifyListeners();
}
Future fetchTodos(int currentPage) async {
try {
//404
var options = Options(headers: {
HttpHeaders.authorizationHeader: 'Basic ${Configuration.authToken}'
});
Map<String, dynamic> qParams = {
'current_page': currentPage,
};
Response response = await Dio().get('${Configuration.ApiUrl}/todos/my_todos', options: options, queryParameters: qParams);
List<dynamic> responseBode = response.data["data"];
responseBode.forEach(( dynamic json) {
items.add(Todo.fromJson(json));
});
notifyListeners();
} on DioError catch (e) {
print("Error Message" + e.response.statusMessage);
return items=[];
}
}
}
Here is the code:
class TodoScreen extends StatefulWidget {
// Your service goes here
// (the class extending ChangeNotifier)
#override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TodoService todoService = TodoService();
ScrollController _controller;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
loadNextPage();
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Configuration.AppName'),
),
body: ChangeNotifierProvider.value(
value: todoService,
child: Consumer<TodoService>(builder: (_, ctl, __) {
if (todoService.isLoading) {
return _buildProgressIndicator();
} else {
return _listItems(todoService.items);
}
}),
),
);
}
Widget _listItems(data) {
return ListView.builder(
controller: _controller,
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle: Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: CircularProgressIndicator(),
),
);
}
Future<void> _onScrollUpdated() async {
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll) {
todoService.loadNextPage();
}
}
}
Note that i didn't make changes to your service. The notifyListeners will do all the job for us.
When you are using Provider, the idea is to keep all your data inside the controller or service (the class that extends ChangeNitifier) and just use the variables with notifyListeners to change the behavior of your screen.
The screen needs to be listening for changes, for this we use the pair ChangeNotifierProvider.value with Consumer(builder: (_, ctl, __) {}).
Use ChangeNotifierProvider in some upper level of the widget tree and use Consumer only where you need the widget to be updated. You can even use more than one Consumer, they all just need to be under ChangeNotifierProvider.
I have FutureBuilder to fetch User profil from API and code to fetch user like this :
Future<List<UserModel>> getUserByUsername({#required String username}) async {
try {
final response =
await _client.get("$_baseUrl/getUserByUsername?username=$username");
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson["status"] == "ok") {
List userList = responseJson['data'];
final result = userList
.map<UserModel>((json) => UserModel.fromJson(json))
.toList();
return result;
} else {
throw CustomError(responseJson['message']);
}
} catch (e) {
return Future.error(e.toString());
}
}
If you can see in above GIF, My FutureBuilder are inside BottomNavigationBar. Every i change the screen/page from BottomNavigationBar and come back to my FutureBuilder is always refresh !
How can i fixed it to only once to refresh ?
Home Screen
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
final username = Provider.of<SharedPreferencesFunction>(context).username;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardTime(),
FutureBuilder(
future: userApi.getUserByUsername(username: username),
builder: (BuildContext context,
AsyncSnapshot<List<UserModel>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
),
);
} else {
final user = snapshot.data[0];
return CardProfil(
imageUrl: "${userApi.baseImageUrl}/${user.fotoUser}",
detailProfil: [
Text(
user.namaUser,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(user.idDevice),
],
);
}
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
],
),
);
}
}
Shared Preferences Function
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesFunction extends ChangeNotifier {
SharedPreferencesFunction() {
initialSharedPreferences();
getUsername();
}
String _username;
String get username => _username;
void initialSharedPreferences() {
getUsername();
}
Future updateUsername(String username) async {
SharedPreferences pref = await SharedPreferences.getInstance();
await pref.setString("username", username);
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
notifyListeners();
}
Future removeUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = await pref.remove("username");
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
print(result);
notifyListeners();
}
Future getUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = pref.getString("username");
_username = result;
notifyListeners();
}
}
final sharedpref = SharedPreferencesFunction();
Update Question
I already try Initialize FutureBuilder and use initState and didChangeDependencies . But new problem is , if i initialize inside initState my profil not rebuild because Provider listen=false.
If i using didChangeDependencies my FutureBuilder still refresh every i change screen.
Something wrong ?
Using initState
Using didChangeDependencies
Initialize the Future during initState or didChangeDependencies instead.
class _HomeScreenState extends State<HomeScreen> {
Future<List<UserModel>> user;
#override
void initState() {
super.initState();
// must use listen false here
final username = Provider.of<SharedPreferencesFunction>(context, listen: false).username;
user = userApi.getUserByUsername(username: username);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final username = Provider.of<SharedPreferencesFunction>(context).username;
user = userApi.getUserByUsername(username: username);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FutureBuilder(
future: user,
builder: (context, snapshot) {
// ...
},
),
],
),
);
}
}
I faced a similar case and use AutomaticKeepAliveClientMixin on each view / page / tab bar view / widget / child to keep the page not refreshing every time I go back and forth through the tab bar.
class YourClass extends StatefulWidget {
YourClass({
Key key
}): super(key key);
#override
_YourClassState createState() => _YourClassState();
}
// Must include AutomaticKeepAliveClientMixin
class _YourClassState extends State<YourClass> with AutomaticKeepAliveClientMixin {
Future resultGetData;
void getData() {
setState(() {
resultGetData = getDataFromAPI();
});
}
// Must include
#override
bool get wantKeepAlive => true;
#override
void initState() {
getData();
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // Must include
return FutureBuilder(
future: resultGetAllByUserIdMCId,
builder: (context, snapshot) {
// ...
// Some Code
// ...
}
);
}
}
If you want to refresh the data you could use RefreshIndicator that runs the getData() function. Put this code inside FutureBuilder. The key: PageStorageKey(widget.key) will keep the scroll in the exact same place where you left of.
return RefreshIndicator(
onRefresh: () async {
getData();
},
child: ListView.separated(
key: PageStorageKey(widget.key),
itemCount: data.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 0);
},
itemBuilder: (context, index) {
return ...;
},
),
);
Use IndexedStack as the parent of tabbar.
You have to put your Future Builder in a Stateful Widget then define a
late final Future myFuture;
then you have to initialize it in the initstate so the future will be executed only one time.
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