I am building a method that the user can select a prefered profile picture to show arround the app, using provider package. I used shared_preferences to save the profile picture preferences on locally as a int value. And it worked, means I can save the profile picture to local system. But the problem is, the provider package completely became useless in this case, because I have to convert the widget to statefull and call the setState method when ever I insert a profilePicture widget inside the widget tree. And even the profilePicture widget in the HomeScreen not updating this way. I want to know how can I use the provider package for this issue instead of using statefulWidgets.
watch the Gif or video
This is the Provider class I created:
class ProfilePicProvider with ChangeNotifier {
ProfilePicPref profilePicPreferences = ProfilePicPref();
int _svgNumber = 1;
int get svgNumber => _svgNumber;
set svgNumber(int value) {
_svgNumber = value;
profilePicPreferences.setProfilePic(value);
notifyListeners();
}
void changePic(int val) {
_svgNumber = val;
profilePicPreferences.setProfilePic(val);
notifyListeners();
}
}
This is the sharedPreferences class
class ProfilePicPref {
static const PRO_PIC_STS = 'PROFILESTATUS';
setProfilePic(int svgNo) async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
profilePref.setInt(PRO_PIC_STS, svgNo);
}
Future<int> getProfilePicture() async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
return profilePref.getInt(PRO_PIC_STS) ?? 1;
}
}
This is the image selection screen and save that data to sharedPreferences class
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
This is the HomeScreen which is not updating the profile image whether it is statefull or stateless
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final proPicProvider = Provider.of<ProfilePicProvider>(context);
return Scaffold(
body:
Column(
children: [
Row(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proPicProvider.svgNumber}.svg'),
],
),
],
),
);
}
}
example:
I have to convert the widget to statefull and call setState method to get the current profile picture from sharedPreferences. You may find this screen from the GIF I provided.
class Progress extends StatefulWidget {
const Progress({Key? key}) : super(key: key);
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SizedBox(
height: 130.0,
width: 130.0,
child: SvgPicture.asset(
'assets/svg/${proProvider.svgNumber}.svg'),
),
),
);
}
}
The problem is in _SelectProfilePictureState when you create new instance of your ChangeNotifier:
ProfilePicProvider proProvider = ProfilePicProvider();. It means you are not using the provider available across the context but creating new one every time. So when the value of your provider changed it has effect only inside _SelectProfilePictureState. Instead of creating new instance you must call it always using the context:
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
// [removed] ProfilePicProvider proProvider = ProfilePicProvider();
//removed
/*void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}*/
#override
Widget build(BuildContext context) {
//use provider from the context
final proProvider = Provider.of<ProfilePicProvider>(context,listen:true);
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
If you enter the application you may want send initially selected image to your provider:
Add parameter to the constructor of ProfilePicProvider:
ProfilePicProvider(SharedPreferences prefs): _svgNumber = prefs.getInt(ProfilePicPref.PRO_PIC_STS) ?? 1;
In main.dart:
Future<void> main()async{
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers:[
ChangeNotifierProvider( create:(_) => ProfilePicProvider(prefs))
],
child: yourtopWidget
)
);
}
Related
First, 'main.dart' calls 'homepage.dart'.
And 'homepage.dart' calls several pages in the body(the code'_pages[_index]') using bottomnavigationbar and index.
Initially, index is 0 and 'Ppage1' appears by default.
In this 'Ppage1', I brought the collection 'exhibition' from firestore.
And then called the '_buildBody'(if data is not arrived,LinearProgressIndicator will be displayed) ,
and in there i made the list 'exhibitions' using 'Exhibition'. (Previously, I had made a data model called 'Exhibition' in 'model_exhibitions.dart'.)
And in the '_buildBody', I brought the class 'BoxSlider' that is using the data from firebase.
I think the data is arrived wel since the LinearProgressIndicator is not displayed.
But only the UI using the data from firestore is not displayed.
What is problem? i can't find it
homepage.dart
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _index = 0;
final _pages = [
Ppage1(),
const Page2(),
const Page3(),
const Page4(),
const Page5(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( ...
Ppage1.dart
class Ppage1 extends StatefulWidget {
#override
State<Ppage1> createState() => _Ppage1State();
}
class _Ppage1State extends State<Ppage1> {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
late Stream<QuerySnapshot> streamData;
#override
void initState() {
super.initState();
streamData = firebaseFirestore.collection('exhibition').snapshots();
}
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildBody(context, snapshot.data!.docs);
},
);
}
Widget _buildBody(BuildContext context, List<DocumentSnapshot> snapshot) {
List<Exhibition> exhibitions = snapshot.map((d) => Exhibition.fromSnapshot(d)).toList();
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
BoxSlider(exhibitions: exhibitions), ...
box_slider.dart
class BoxSlider extends StatefulWidget {
late final List<Exhibition> exhibitions;
BoxSlider({required this.exhibitions});
#override
State<BoxSlider> createState() => _BoxSliderState();
}
class _BoxSliderState extends State<BoxSlider> {
late List<Exhibition> exhibitions;
late List<Widget> posters;
...
#override
void initState() {
super.initState();
exhibitions = widget.exhibitions;
posters = exhibitions.map((m) => Image.asset(m.poster)).toList();
...
}
#override
Widget build(BuildContext context) {
return Container(
height: 440,
child: ListView(
scrollDirection: Axis.horizontal,
children: makeBoxImages(context, widget.exhibitions),
),
);
}
}
List<Widget> makeBoxImages(BuildContext context, List<Exhibition> exhibitions) {
List<Widget> results = [];
for (var i = 0; i < exhibitions.length; i++) {
results.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () {Navigator.push(
context,
MaterialPageRoute(builder: (context)=>DetailScreen(exhibition: exhibitions[i])),
);
},
child: SizedBox(
height: 350,
child: Image.network(exhibitions[i].poster),
), ...
You should change the _fetchdata method to know if the data is fetched correctly:
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
//To know if the snapshot contains errors.
if (snapshot.hasError) {
return Center(
child: Text('Some error occured: ${snapshot.error.toString()}'),
);
}
//To know if the snapshot contains valid data.
if (snapshot.hasData) {
return _buildBody(context, snapshot.data!.docs);
}
//Returns a progress indicator in case that the two conditions above are not satisfied.
return LinearProgressIndicator();
},
);
}
I am working in Riverpod Auth flow boilerplate application.
I want to use common loading screen for all async function even login and logout. Currently I have AppState provider if Appstate loading i show loading screen. it's working fine for login but i wonder it’s good way or bad way.
Can i use this loading screen for all async task in the App?
AuthWidget:
class AuthWidget extends ConsumerWidget {
const AuthWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
AppState appState = ref.watch(appStateProvider);
if(appState.isLoading){
return const Center(child: CircularProgressIndicator(color: Colors.red),);
}
return appState.isAuthenticated ? const HomePage() : const SignIn();
}
}
AppState:
class AppState {
User? user;
bool isLoading;
bool isAuthenticated;
AppState(this.user, this.isLoading, this.isAuthenticated);
}
AuthRepository:
class AuthRepository extends StateNotifier<AppState>{
AuthRepository() : super(AppState(null,false,false));
Future<void> signIn()async {
state = AppState(null,true,false);
await Future.delayed(const Duration(seconds: 3));
User user = User(userName: 'FakeUser', email: 'user#gmail.com');
AppState appState = AppState(user, false, true);
state = appState;
}
}
final appStateProvider = StateNotifierProvider<AuthRepository,AppState>((ref){
return AuthRepository();
});
To answer your question : Yes you can.
The only thing I'd change here is the content of your AppState : I'd use a LoadingState dedicated to trigger your Loader instead.
Here is how I like to manage screens with a common loader in my apps.
1 - Create a LoadingState and provide it
final loadingStateProvider = ChangeNotifierProvider((ref) => LoadingState());
class LoadingState extends ChangeNotifier {
bool isLoading = false;
void startLoader() {
if (!isLoading) {
isLoading = true;
notifyListeners();
}
}
void stopLoader() {
if (isLoading) {
isLoading = false;
notifyListeners();
}
}
}
2 - Define a base page with the "common" loader
class LoadingContainer extends ConsumerWidget {
const LoadingContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(loadingStateProvider);
return Stack(
children: [
child,
if (state.isLoading)
const Center(child: CircularProgressIndicator())
else
const SizedBox(),
],
);
}
}
3 - Implement this widget whenever I need to handle loading datas.
return Scaffold(
backgroundColor: AppColor.blue,
body: LoadingContainer(
child: ...
And then I simply have to update my loadingStateProvider and it's isLoading value from a Controller or the Widget directly
If you want a centralized/common async calls, the InheritedWidget is ideal for that, you can just add a method and call it from anywhere down stream and because the call is offloaded with async, you can attach extra arguments and add usefull functionality such as a live update instead of relying on stuff like .then(). This example might not be as simple as FDuhen's but you can mix them together if you want to not use keys
AppState now is a widget and contains trigers that rely on global keys to rebuild the correct components, here i assumed that you actualy want to have an common overlay and not a loading screen widget, if not using a Navigator would be batter
Using keys is specially good if you end up implementing something this line, <token> been just a number that references a group of widgets
key: AppState.of(ctx).rebuild_on_triger(<token>)
class App_State_Data {
GlobalKey? page_key;
bool is_logged = false;
bool loading_overlay = false;
String loading_message = '';
}
class AppState extends InheritedWidget {
final App_State_Data _state;
bool get is_logged => _state.is_logged;
bool get should_overlay => _state.loading_overlay;
String get loading_message => _state.loading_message;
void page_rebuild() {
(_state.page_key!.currentState as _Page_Base).rebuild();
}
GlobalKey get page_key {
if (_state.page_key == null) {
_state.page_key = GlobalKey();
}
return _state.page_key!;
}
void place_overlay(String msg) {
_state.loading_message = msg;
_state.loading_overlay = true;
page_rebuild();
}
void clear_overlay() {
_state.loading_message = '';
_state.loading_overlay = false;
page_rebuild();
}
Future<void> triger_login(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 2));
_state.is_logged = true;
clear_overlay();
}
Future<void> triger_logout(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 1));
_state.is_logged = false;
clear_overlay();
}
AppState({Key? key, required Widget child})
: this._state = App_State_Data(),
super(key: key, child: child);
static AppState of(BuildContext ctx) {
final AppState? ret = ctx.dependOnInheritedWidgetOfExactType<AppState>();
assert(ret != null, 'No AppState found!');
return ret!;
}
#override
bool updateShouldNotify(AppState old) => true;
}
Here i added it as the topmost element making it like a global data class with is not necessary, you can split the state content and add just the necessary to where its needed
void main() => runApp(AppState(child: App()));
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: Page_Base(
key: AppState.of(ctx).page_key,
),
),
);
}
}
class Page_Base extends StatefulWidget {
final GlobalKey key;
const Page_Base({
required this.key,
}) : super(key: key);
#override
_Page_Base createState() => _Page_Base();
}
class _Page_Base extends State<Page_Base> {
Widget build_overlay(BuildContext ctx) {
return Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: Color(0xC09E9E9E),
child: Center(
child: Text(AppState.of(ctx).loading_message),
),
),
);
}
#override
Widget build(BuildContext ctx) {
return Stack(
children: [
AppState.of(ctx).is_logged ? Page_Home() : Page_Login(),
AppState.of(ctx).should_overlay ? build_overlay(ctx) : Material(),
],
);
}
void rebuild() {
// setState() is protected and can not be called
// from outside of the this. scope
setState(() => null);
}
}
Using AppState is the best part, just because the widget does not have to call more than 1 function and it will rebuild with the correct data on complition
class Page_Login extends StatelessWidget {
const Page_Login({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_login('Login'),
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
child: Text('Page_Login'),
),
),
);
}
}
class Page_Home extends StatelessWidget {
const Page_Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_logout('Logout'),
child: Container(
width: 200,
height: 200,
color: Colors.blueAccent,
child: Text('Page_Home'),
),
),
);
}
}
Global loading indicator
If you want a centralized loading indicator to use in your whole app you could take advantage of Overlay's, which flutter already uses for dialogs, popups, bottom sheets etc. This way we don't introduce new widget in the widget tree.
If you only want to toggle between loading states you can use a StateProvider to handle the simple boolean value, else you could create a State/Change Notifier. This way you decouple your loading state from your AppState
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(const ProviderScope(child: MaterialApp(home: GlobalLoadingIndicator(child: Home()))));
// This widget should wrap your entire app, but be below MaterialApp in order to have access to the Overlay
class GlobalLoadingIndicator extends ConsumerStatefulWidget {
final Widget child;
const GlobalLoadingIndicator({required this.child, Key? key}) : super(key: key);
#override
ConsumerState createState() => _GlobalLoadingIndicatorState();
}
class _GlobalLoadingIndicatorState extends ConsumerState<GlobalLoadingIndicator> {
//We need to cache the overlay entries we are showing as part of the indicator in order to remove them when the indicator is hidden.
final List<OverlayEntry> _entries = [];
#override
Widget build(BuildContext context) {
ref.listen<bool>(loadingProvider, (previous, next) {
// We just want to make changes if the states are different
if (previous == next) return;
if (next) {
// Add a modal barrier so the user cannot interact with the app while the loading indicator is visible
_entries.add(OverlayEntry(builder: (_) => ModalBarrier(color: Colors.black12.withOpacity(.5))));
_entries.add(OverlayEntry(
builder: (_) =>const Center(
child: Card(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())))));
// Insert the overlay entries into the overlay to actually show the loading indicator
Overlay.of(context)?.insertAll(_entries);
} else {
// Remove the overlay entries from the overlay to hide the loading indicator
_entries.forEach((e) => e.remove());
// Remove the cached overlay entries from the widget state
_entries.clear();
}
});
return widget.child;
}
}
We insert the GlobalLoadingIndicator high up in the widget tree although anywhere below the MaterialApp is fine (as long as it can access the Overlay via context).
The GlobalLoadingIndicator wont create extra widgets in the widget tree, and will only manage the overlays, here I add two overlays, one is a ModalBarrier which the user from interacting with widgets behind itself. And the other the actual LoadingIndicator. You are free to not add the ModalBarrier, or make it dismissible (or even if you decide to create a more complex loadingProvider, customize it in case you need to cater different use cases).
A sample usage after you have this set up is just switching the state of the loadingProvider, most of the times you would do this programatically, but for interactiveness I'll use a Switch :
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
Future.delayed(const Duration(seconds: 4)).then((value) {
ref.read(loadingProvider.notifier).state = false;
});
},
title: const FlutterLogo(),
),
));
}
}
You can fiddle with this snippet in dartpad
Result:
Per Screen/Section loading indicator
As a side note when displaying loading states inside components of the app I recommend you to use an AnimatedSwitcher , as it fades between the widgets , super handy when dealing with screens which can change content abruptly.
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(ProviderScope(child: MaterialApp(home: Home())));
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
},
title: AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isLoading?CircularProgressIndicator():FlutterLogo()
),
),
));
}
}
I am trying to get user information from an API. For this, I created a user object. I want to call the function and store it in a user. But the problem is, that I cannot use await and wait till all the data is there.
So instead of async and await, I tried to use .then and fill userInfo with the data. But now the email value is not showing. It is showing 'loading...'.
If I use Future I cannot do user.email.
Is it better to use FutureBuilder? Or try and use Async and Await (the call takes 2.5 seconds)
Here is the code
class Addressbook extends StatefulWidget {
const Addressbook({Key? key}) : super(key: key);
#override
State<Addressbook> createState() => _AddressbookState();
}
class _AddressbookState extends State<Addressbook> {
User? userInfo;
Future<User> getUserInformation() async {
User user = await UserService().getUserById(12345);
return user;
}
#override
void initState() {
// TODO: implement initState
getUserInformation().then((response) {
userInfo = response;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Mijn adresboek'),
centerTitle: true,
elevation: 0.5,
titleTextStyle: const TextStyle(
fontSize: 21,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
body: Text(userInfo?.email ?? "loading..."),
);
}
}
What do I want to archive?
I want to display user data on the screen. In this case I want to show the email.
What is the problem?
User is not filled with data on init (probably because it is still loading to get the data).
My question
How can I solve this problem? Is async await a solution or should I use FutureBuilder? Can you give me a working sample?
Thanks for helping!
You can use futurebuilder or you can use in initstate to fetch api
#override
void initState() {
// TODO: implement initState
Future.delayed(Duration(seconds: 10), () {
setState(() {
userinfo = "response";
});
// userinfo = "response";
});
super.initState();
}
Inside the widget
Text(userinfo != null ? userinfo.toString() : "loading..."),
SAmple Code
import 'package:flutter/material.dart';
//import 'package:pucon/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String? userinfo = null;
String? userinfo2 = null;
#override
void initState() {
// TODO: implement initState
Future.delayed(Duration(seconds: 10), () {
setState(() {
userinfo = "response";
});
// userinfo = "response";
});
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: ListView(
shrinkWrap: true,
children: [
Text(""),
Text(""),
Text(userinfo != null ? userinfo.toString() : "loading..."),
Row(
children: [
FutureBuilder(
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Container(
child: Text(userinfo2.toString()), );
} else {
return SizedBox(
child: CircularProgressIndicator(),
height: 45,
width: 45,
);
// else
// return Container(
// child: CircularProgressIndicator(),
// height: 45,
// width: 45,
// );
}
},
future: _future(),
),
],
)
// InsertData(),
],
),
);
}
_future() async {
await Future.delayed(Duration(seconds: 5), () {
userinfo2 = "Completed";
// setState(() {
//
// });
// userinfo = "response";
});
}
}
Let's say I create a new screen team_screen which is the first parent of the tree.
Now for my team screen there are many widgets, some of theme have their own request, I want to show loader until every widget/request finished and ready.
I thought on 2 approaches.
All the requests are executed in team_screen with future builder and I pass the props to my widgets by demand.
Every widget with request get function that get executed in the async function in the initState function, then in my parent I make to every widget state parameter that is equal to true by the function I passed and when all is don't I stop the loader.
To sum up my problem is how to maintain a widget with many children and requests and showing one loader for entire page, making all the request on same widget? Pass isInitialize function to every widget?.
Which approach is better and if there are more approaches, I would like to hear.
Thank you for your help
Example for the second approach:
import 'package:flutter/material.dart';
import 'package:info_striker/locator.dart';
import 'package:info_striker/models/fixture/fixture.dart';
import 'package:info_striker/models/odds/bookmaker.dart';
import 'package:info_striker/models/synced-team/synced_team.dart';
import 'package:info_striker/services/fixture_service.dart';
import 'package:info_striker/utils/date_utilities.dart';
class TeamNextMatch extends StatefulWidget {
Function isInitialized;
SyncedTeam team;
TeamNextMatch({
Key? key,
required this.isInitialized,
required this.team,
}) : super(key: key);
#override
State<TeamNextMatch> createState() => _TeamNextMatchState();
}
class _TeamNextMatchState extends State<TeamNextMatch> {
Fixture? _fixture;
Bookmaker? _matchResult;
bool _isInitialized = false;
#override
void initState() {
super.initState();
init();
}
init() async {
final response = await locator<FixturesService>().getData(widget.team.id);
if (response != null) {
setState(() {
_fixture = Fixture.fromMap(response["fixture"]);
_matchResult = Bookmaker.fromMap(response["matchResultOdds"]);
});
}
widget.isInitialized(true);
}
#override
Widget build(BuildContext context) {
String? _date;
bool show = _fixture != null && _matchResult != null;
_fixture != null ? "${DateUtilities.getShortDateString(_fixture!.date)}, ${DateUtilities.getTimeString(_fixture!.date)}" : null;
return show
? Column(
children: [
Text(_fixture?.league?["name"]),
if (_date != null) Text(_date),
],
)
: const SizedBox();
}
}
You can show loader as described below -
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/data_model.dart';
import 'package:http/http.dart' as http;
class APiTest extends StatefulWidget {
const APiTest({Key? key}) : super(key: key);
#override
_APiTestState createState() => _APiTestState();
}
class _APiTestState extends State<APiTest> {
final String _url = "https://jsonplaceholder.typicode.com/todos/";
bool _isLoading = true;
final List<DataModel> _allData = [];
#override
void initState() {
super.initState();
_initData().then((value) {
setState(() {
_isLoading = false;
});
});
}
Future<void> _initData() async {
final response = await http.get(Uri.parse(_url));
final List res = jsonDecode(response.body);
res.forEach((element) {
_allData.add(DataModel.fromJson(element));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Loading Demo"),
),
body: Stack(
children: [
ListView.separated(
itemCount: _allData.length,
controller: ScrollController(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: ((context, index) {
return ListTile(
tileColor: Colors.grey[200],
title: Text(_allData[index].title!),
subtitle: Text(_allData[index].id.toString()),
);
}),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
)
],
),
);
}
}
i'm trying to pull new data from firebase cloud firestore and rebuild the widget on onPress of floating action button. i'm not sure how to rebuild the whole widget. Tried to call getList from the onPressed and setState() but still not rebulding widget evening nameList was updated.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
}),
);
}
}
class MainList extends StatefulWidget {
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
List<Test> nameList = [];
#override
void initState() {
super.initState();
getList();
}
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Once a widget is created initState isn't called again. So your listview is still reflecting the old data.
You could getList in the onPressed which would then update your nameList. You could then pass this nameList to MainList.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<Test> nameList = [];
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(nameList: nameList),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
getList();
}),
);
}
}
Your MainList widget would then look like:
class MainList extends StatefulWidget {
final List nameList;
MainList({this.nameList});
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Just calling setState() is not enough. You'll have to tell Dart what you are going to set. Sample code :
setState ( ()=> nameList = _fetchedList ) ;
In the above code, the variable nameList is assigned within setState().
In your code, you've two options.
Option 1 :
setState(() {
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
}
});
Or option 2, better way, use for loop to add in the data in another list and use setState with one line as below :
List<Test> _fetchedList ;
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
_fetchedList.add(addName);
}
setState( ()=> nameList = _fetchedList ) ;