Flutter Getx builder not updating UI - flutter

I'm trying to use a GetX Builder in combination with a bool in the controller to display a loading spinner while fetching data and then the data afterwards.
Printing the update to the bool shows it finishes loading and changes the variable but the UI is never updated to reflect this.
Controller
class AuthController extends GetxController {
//final RxBool isLoading = true.obs;
//var isLoading = true.obs;
final Rx<bool> isLoading = Rx<bool>(true);
setLoading(bool status) {
isLoading.value = status;
print(isLoading.value);
update();
}
fetchUserData() async {
setLoading(true);
_firebaseUser.bindStream(_auth.authStateChanges());
if (_auth.currentUser != null) {
bool profileRetrieved =
await FirestoreDB().getUserProfile(_auth.currentUser!.uid).then((_) {
return true;
});
setLoading(!profileRetrieved);
}
}
}
Profile Page
class ProfileCard extends StatelessWidget {
const ProfileCard({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<AuthController>(
init: AuthController(),
initState: (_) => AuthController().fetchUserData(),
builder: (controller) {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Container(...);
},
);
}
}
That widget is called within another as part of the wider UI. Let me know if you'd need to see that and/or anything else.
As you can see I've tried different ways of setting up the bool and none of them seem to trigger a change of UI in the builder.
Probably doing something stupid but I've tried a few different approaches and looked around for people having similar problems but not been able to crack it.
Thanks in advance.

You are using isLoading as a Rx<bool>. In that case you need to change GetBuilder into GetX. And no need to call update().
GetBuilder is for non-reactive, non-Rx, simple state management
GetX is for reactive, Rx state management

Related

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

How to display loading widget until the main widget is loaded in flutter?

I have a widget with a lot of contents like image, text and more, which make it heavy widget in flutter app, But when the app is navigated to the widget having the complex widget the app faces the jank since the widget is too large to load at an instant,
I want to show simple lite loading widget until the original widget is loaded thus removing the jank from the app and enable lazy loading of the widget,
How to achieve this in flutter?
EDIT:-
To make it clear, I am not loading any data from the Internet, and this is not causing the delay. For Loading the data from Internet we have FutureBuilder. Here my widget is itself heavy such that it takes some time to load.
How to display loading Widget while the main widget is being loaded.
First you have to create a variable to keep the state
bool isLoading = true; //this can be declared outside the class
then you can return the loading widget or any other widget according to this variable
return isLoading ?
CircularProgressIndicator() //loading widget goes here
: Scaffold() //otherwidget goes here
you can change between these two states using setState method
Once your data is loaded use the below code
setState(() {
isLoading = false;
});
Sample Code
class SampleClass extends StatefulWidget {
SampleClass({Key key}) : super(key: key);
#override
_SampleClassState createState() => _SampleClassState();
}
bool isLoading = true; // variable to check state
class _SampleClassState extends State<SampleClass> {
loadData() {
//somecode to load data
setState(() {
isLoading = false;//setting state to false after data loaded
});
}
#override
void initState() {
loadData(); //call load data on start
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: isLoading ? //check loadind status
CircularProgressIndicator() //if true
:Container(), //if false
);
}
}
This is a perfect place to use a FutureBuilder.
Widget loadingWidget = ...;
Future<Widget> buildHeavyWidget() async {
// build and return heavy widget
}
FutureBuilder(
future: buildHeavyWidget(),
builder: (context, snapshot) {
if(snapshot.hasData) {
// after the future is completed
// the heavy widget is availabe as snapshot.data
return snapshot.data;
}
return loadingWidget;
},
)
First define a bool value.
bool isLoading = false;
In your function.
yourfunction(){
setState(){
isLoading = true;
}
setState(){
isLoading = false;
}
}
In your widget.
isLoading?CircularProgressIndicator():Widget()

Widget continuously being reloaded

Please check out this 36 seconds video for more clarity, cause it was getting too verbose explaning things : https://www.youtube.com/watch?v=W6WdQuLjrCs
My best guess
It's due to the provider.
App structure ->
Outer Page -> NoteList Page
The Outer Page code :
class OuterPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
var noteList;
final _pageOptions = [
NoteList(),
AnotherPageScreen(),
];
#override
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
var customFabButton;
if (_selectedTab == 0) {
customFabButton = FloatingActionButton(
// Password section
onPressed: () {
navigateToDetail(context, Note('', '', 2), 'Add Note');
},
child: Icon(Icons.add),
);
~~~ SNIP ~~~
The Notes Tab aka NoteList page code :
class NoteList extends StatefulWidget {
NoteList();
#override
NoteListState createState() => NoteListState();
}
class NoteListState extends State<NoteList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context).getNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context).count > 0
? NoteListScreen(_scaffoldKey)
: CircularProgressIndicator());
}
}
For full code : check here : https://github.com/LuD1161/notes_app/tree/reusable_components
Update 1 - Possible solution is FutureBuilder
I know that there's a possible solution with FutureBuilder but I think even Provider is apt for this use case.
Moreover is it an anti-pattern here ?
Also, please don't suggest another package for the same thing, if possible try limiting the solution to Provider or base libraries.
Update 2 - Not possible with FutureBuilder
FutureBuilder can't be used here because there's a delete button in the list tile and hence when the note gets deleted the note list won't get updated.
The issue is coming because of the getNotes function you are calling from build method. You are calling notifyListeners from that function. It again re-builds the widget and calls the build method again and this cycle continues.
You either need to set false to the listen property of provider, but it will break your functionality. To fix this, you have to move the getNotes call from build function to initState like following:
#override
void initState() {
super.initState();
postInit(() {
Provider.of<NotesProvider>(context).getNotes();
});
}
Implement postInit (Reference: Flutter Issue 29515):
extension StateExtension<T extends StatefulWidget> on State<T> {
Stream waitForStateLoading() async* {
while (!mounted) {
yield false;
}
yield true;
}
Future<void> postInit(VoidCallback action) async {
await for (var isLoaded in waitForStateLoading()) {}
action();
}
}
Note: Instead of writing the postInit code, you can also use after_init package for same purpose.
Several other posts discussing similar kind of issues:
How to correctly fetch APIs using Provider in Flutter
Using provider in fetching data onLoad

Text widget displaying in Reload but not in Hot Reload

I have class which will fetch the data from an API and store the result in a LIST and display the content in my text widget. When I hot reload it's displaying 'Default'. But is working fine when just reload again though I guard the text widget against the NULL.
The following is my code:
class Sample extends StatefulWidget {
#override
SampleState createState() => SampleState();
}
class SampleState extends State<Sample> {
var selected = [];
#override
void initState() {
super.initState();
callAsyncFetch();
}
callAsyncFetch() async {
var url = 'http://10.0.2.2/abc.php';
var response = await http.get(url);
var jsonData = json.decode(response.body);
print(jsonData);
for (var u in jsonData) {
if (u.substring(0, 2) == 'ABC') {
selected.add(u);
}
}
for (var u in selected) {
print(u);
}
print(selected.length); // working fine
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(children: [
selected.isEmpty
? CircularProgressIndicator()
: new Text(selected[0].toString()),
// i have tried this also but no luck
new Text(selectedStudentsIT.length!=0?selectedStudentsIT[0].toString():'Default Value'),
]),
),
);
}
}
There is simple issue is that, in hot reload it will not run initState again. you can check it by printing something in initState, while in full reload it will call initState.
i think you want to display data when you get response from api.
i think this can be easily achieved by calling setState at the end of callAsyncFetch function, so when you get data from server then it will update ui.

Canonical way to use FutureBuilder with NetworkImage

I would like to be able to get to a network image within a single microtask if the image is already loaded. However, with the current API available in NetworkImage and FutureBuilder, this does not seem to be possible.
This is how we typically wire the two:
NetworkImage imageProvider = getSomeNetworkImage(id);
Completer<ui.Image> completer = Completer<ui.Image>();
imageProvider.resolve(ImageConfiguration()).addListener(
(ImageInfo info, _) => completer.complete(info.image));
return FutureBuilder<ui.Image>(
future: completer.future,
builder: (BuildContext futureBuilderContext, AsyncSnapshot<ui.Image> snapshot) {
if (!snapshot.hasData) {
return _buildPlaceholder();
} else {
return _buildActual(context, snapshot.data, imageProvider);
}
},
);
addListener() immediately calls completer.complete() if the image is already there. However, FutureBuilder is based off of completer.future which does not complete until the next microtask. So even when the image is available, placeholder is displayed momentarily.
What is the best way to avoid this? Perhaps, imageProvider should expose a Future that prevents us from piping this through a completer?
Instead of using a FutureBuilder, I would take advantage of the syncCall argument passed to the listener of the ImageStream. This will tell you if the image resolved immediately, meaning it is already cached. Otherwise you can call setState and trigger a rebuild when it does complete.
class Example extends StatefulWidget {
const Example({Key key, this.image, this.child}): super(key: key);
final ImageProvider image;
final Widget child;
#override
State createState() => new ExampleState();
}
class ExampleState extends State<Example> {
bool _isImageLoaded = false;
#override
void initState() {
widget.image
.resolve(const ImageConfiguration)
.addListener(_handleResolve);
super.initState();
}
#override
Widget build(BuildContext context) {
// if syncCall = true, then _handleResolve will have already been called.
if (_isImageLoaded)
return new Image(widget.image);
return widget.child;
}
void _handleResolve(ImageInfo info, bool syncCall) {
_isImageLoaded = true;
if (!syncCall) {
// we didn't finished loading immediately, call setState to trigger frame
setState(() { });
}
}
}