I have code in the initState that goes something like this (video_player package):
#override void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final navBar = Provider.of<NavBar>(context, listen: false);
navBar.urls.forEach((element) {
navBar.videoControllers.add(
VideoPlayerController.network(
navBar.urls[videoCount],
videoPlayerOptions: VideoPlayerOptions(
allowBackgroundPlayback: false,
),
)..initialize().then((value) => navBar.initialized = true)
);
}
navBar.notify(); // This is a method that calls notifyListeners() within the NavBar class (with ChangeNotifier like how a Provider class does).
navBar.videoControllers[0].play();
navBar.notify();
}
}
The behaviour of this code is such that the video starts playing, I can hear the audio as well, but the page has not been loaded as I see no UI elements but a white screen.
Once I just do a hot reload, all the UI elements are there and I can see the video playing as well.
Where am I going wrong here?
Maybe you are having a problem with async methods...
Try to move this block:
navBar.notify(); // This is a method that calls notifyListeners() within the NavBar class (with ChangeNotifier like how a Provider class does).
navBar.videoControllers[0].play();
navBar.notify();
Inside the initialize response, something like this:
..initialize().then((value) {
navBar.initialized = true
navBar.notify(); // This is a method that calls notifyListeners() within the NavBar class (with ChangeNotifier like how a Provider class does).
navBar.videoControllers[0].play();
navBar.notify();
})
In this way you will only start the video after it is added to the videoControllers and it`s properly initialized.
The final code:
#override void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final navBar = Provider.of<NavBar>(context, listen: false);
navBar.urls.asMap().forEach((index, element) {
navBar.videoControllers.add(
VideoPlayerController.network(
navBar.urls[videoCount],
videoPlayerOptions: VideoPlayerOptions(
allowBackgroundPlayback: false,
),
)..initialize().then((value) {
navBar.initialized = true)
// To execute this block just when it is the first item.
if (index == 0) {
navBar.notify();
navBar.videoControllers[0].play();
navBar.notify();
}
}
);
}
}
}
The video_player package works as such that it will not display the video correctly if you display the VideoPlayer before it is initialized in your Widget tree. So just like their tutorial in the flutter packages website (which I sadly overlooked at that part), you need to check for the initialization before displaying it. If you don't, only a hot reload can work.
children:
(navBar.videoControllers[0].value.isInitialized) ?
navBar.scrollingVideos :
[
Container(),
],
Note: scrollingVideos is a List<Widget> that contains the video players.
Related
I have a chat app, so I would like to show the last messages when we open the conversation. I have initialized a ScrollController (and attached it to my FirebaseAnimatedList) and tried this :
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.hasClients) {
print("message");
controller.jumpTo(controller.position.maxScrollExtent);
}
});
}
It doesn't work, and doesn't print "message".
You can solve the reverse property of FirebaseAnimatedList class by setting true.
Updates:
2021/06/11 After hours of debugging yesterday, I confirmed that the problem is caused by aws amplify configuration: _configureAmplify(). Because the location of the amplify server was set wrong, so _configureAmplify() takes several seconds to work... and therefore, the readPost() function did not work on initialization, as it must run after _configureAmplify()...
2021/06/10I made changes to my code according to S. M. JAHANGIR's advice, and updated the question. The issue still presists. The value of posts is not updated when called in initialization and the data only shows up after reload. (if I commented out the _controller.readPost() in UI, the value of posts is always empty.
I have this page that loads information from aws amplify with getx implemented. However, I found out the readPost() async funtion in getx controller dart file is not reading from database, when the controller instance is initialized. I have to add a _controller.readPost() in UI file to make it work. And the data only shows up after a reload of that UI page...
Getx Controller dart file:
class ReadPostController extends GetxController {
var isLoading = true.obs;
var posts = <Posty>[].obs;
#override
void onInit() {
_configureAmplify();
await readPost();
super.onInit();
// print('show post return value: $posts');
}
void _configureAmplify() {
final provider = ModelProvider();
final dataStorePlugin = AmplifyDataStore(modelProvider: provider);
AmplifyStorageS3 storage = new AmplifyStorageS3();
AmplifyAuthCognito auth = new AmplifyAuthCognito();
AmplifyAPI apiRest = AmplifyAPI();
// Amplify.addPlugin(dataStorePlugin);
Amplify..addPlugins([dataStorePlugin, storage, auth, apiRest]);
Amplify.configure(amplifyconfig);
print('Amplify configured');
}
// read all posts from databases
Future readPost() async {
try {
isLoading(true);
var result = await Amplify.DataStore.query(Posty.classType);
print('finish loading request');
result = result.sublist(1);
posts.assignAll(result);
// print(the value of posts is $posts');
} finally {
isLoading(false);
}
}
#override
void onClose() {
// called just before the Controller is deleted from memory
super.onClose();
}
}
And in the UI part:
class TabBody extends StatelessWidget {
TabBody({Key? key}) : super(key: key);
final ReadPostController _controller = Get.put(ReadPostController());
#override
Widget build(BuildContext context) {
_controller.readPost();//if commented out, _controller.post is empty
return Container(
child: Obx(
() => Text('showing:${_controller.posts[1].title}'),
));
}
}
In my understanding, the readPost() function should be called when the ReadPost_controller is initiallized. And the UI will update when the posts = <Posty>[].obs changes. Guys, what am I doing wrong here?
First, when you are calling readPost on onInit you are not awaiting. So change it to:
onInit() async{
...
await readPost();
...
}
Secondly, posts is a RxList so you need to use the assignAll method to update it.
Therefore, in your readPost method, instead of posts.value = reault you need to use posts.assignAll(result)
Calling from the UI works because readPost every time the build method is called by the Flutter framework and actually the UI shows the data from every previous call.
I think try with GetBuilder instead of Obx.
GetBuilder<ReadPostController>(
builder: (value) => Text('showing:${value.posts[1].title}'),
)
and also use update(). in readPost() method.
I hope I understand how didChangeAppLifecycleState worked correctly.
I have page A and page B . When I click the back device button from page B ( Navigator.of(context).pop(); ), I expect didChangeAppLifecycleState in pageA will get called, but it doesn't.
PageA
class _ABCState extends State<ABCrList> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
....
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
print(...);
});
}else{
print(state.toString());
}
}
....
This is the initState in pageA. The function used to call backend service.
#override
void initState() {
super.initState();
_bloc.getList(context); // return list and populate to ListView
});
}
The way you're thinking it is Android's way where onResume works, but in Flutter, things don't happen this way.
Generally, this gets called when the system puts the app in the background or returns the app to the foreground.
There are mainly 4 states for it:
resumed: The application is visible and responding to user input.
inactive: The application is in an inactive state and is not receiving user input.
paused: The application is not currently visible to the user, not responding user input, and running in the background.
detached: The application is still hosted on a flutter engine but is detached from any host views.
Edit:
When you're navigating to PageB from PageA, use something like:
Navigator.pushNamed(context, "/pageB").then((flag) {
if (flag) {
// you're back from PageB, perform your function here
setState(() {}); // you may need to call this if you want to update UI
}
});
And from PageB, you'll can use
Navigator.pop(context, true);
I'm currently trying Provider as a state management solution, and I understand that it can't be used inside the initState function.
All examples that I've seen call a method inside a derived ChangeNotifier class upon user action (user clicks a button, for example), but what if I need to call a method when initialising my state?
Motivation:
Creating a screen which loads assets (async) and shows progress
An example for the ChangeNotifier class (can't call add from initState):
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
}
You can call such methods from the constructor of your ChangeNotifier:
class MyNotifier with ChangeNotifier {
MyNotifier() {
someMethod();
}
void someMethod() {
// TODO: do something
}
}
Change your code to this
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) async {
// Loading Assets maybe async process with its network call, etc.
_progress += dProgress;
notifyListeners();
}
ProgressData() {
add();
}
}
In initState all the of(context) things don't work correctly, because the widget is not fully wired up with every thing in initState.
You can use this code:
Provider.of<ProgressData>(context, listen: false).add(progress)
Or this code:
Future.delayed(Duration.zero).then(_){
Provider.of<ProgressData>(context).add(progress)
}):
So an AssetLoader class which reports on its progress will look something like this, I guess:
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
ProgressData() {
_loadFake();
}
Future<void> _loadFake() async {
await _delayed(true, Duration(seconds: 1));
_add(1.0);
await _delayed(true, Duration(seconds: 2));
_add(2.0);
await _delayed(true, Duration(seconds: 3));
_add(3.0);
}
// progress
double get progress => _progress;
// add
void _add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
// _delayed
Future<dynamic> _delayed(dynamic returnVal, Duration duration) {
return Future.delayed(duration, () => returnVal);
}
}
As Fateme said:
the widget is not fully wired up with everything in initState
Also, you can use something like this in your initState
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Provider.of<ProgressData>(context, listen: false).add(5);
});
I think it's more standard!
Be aware that you should use the correct context! I mean the context of the Builder!
The problem here lies with the fact that context does not exist yet in initState as extensively explained by the other answers. It doesn't exist because it hasn't yet been made a part of the widget tree.
Calling a method
If you're not assigning any state and only calling a method then initState would be the best place to get this done.
// The key here is the listen: false
Provider.of<MyProvider>(context, listen: false).mymethod();
The code above is allowed by Flutter because it doesn't have to listen for anything. In short, it's a one off. Use it where you only want to do something instead of read/listen to something.
Listening to changes
Alternatively, if you need to listen to changes from Provider then the use of didChangeDependencies would be the best place to do so as context would exist here as in the docs.
This method is also called immediately after initState.
int? myState;
#override
void didChangeDependencies() {
// No listen: false
myState = Provider.of<MyProvider>(context).data;
super.didChangeDependencies();
}
If you've never used didChangeDependencies before, what it does is get called whenever updateShouldNotify() returns true. This in turn lets any widgets that requested an inherited widget in build() respond as needed.
I'd usually use this method in a FutureBuilder to prevent reloading data when data already exists in Provider after switching screens. This way I can just check Provider for myState and skip the preloader (if any) entirely.
Hope this helps.
I am having a problem where my stream builder is only firing once.
I am trying to configure my bottomNavigationBar to be of a different colour based on the theme selected by the user.
To do this, I have a page whereby the user can decide whether to use the light theme or dark theme. This is saved into the device while shared preferences and then using async, i will stream the current value into my bottomNavigationBar.
The problem occurs when i use a stream builder to create two if statement. Stating that if the value returned from the stream is 0, i will show a "light mode" bottom navigation bar. Else if its 1, i will show a dark theme.
All is well when i run the program for the first time. However upon navigation into the settings page and changing the user preference, the stream builder will not load again. Here are some snapshots of my code
I have tried removing the dispose method whereby the stream will close. However that didn't solve the problem.
The Stream Builder
class mainPagev2 extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _mainPageV2();
}
}
class _mainPageV2 extends State<mainPagev2>
with SingleTickerProviderStateMixin {
// TabController _tabController;
StreamController<int> streamController = new StreamController.broadcast();
#override
void initState() {
super.initState();
// _tabController = TabController(vsync: this, length: _pageList.length);
Stream<int> stream = new Stream.fromFuture(readCurrentTheme());
streamController.addStream(stream);
}
#override
void dispose() {
// _tabController.dispose();
super.dispose();
}
String currentColor = "#ab3334";
#override
Widget build(BuildContext context) {
// TODO: implement build
return StreamBuilder(
stream: streamController.stream,
builder: (context, asyncSnapshot) {
print(asyncSnapshot.data.toString() + "WHssssAT IS THIS");
if (asyncSnapshot.hasData) {
print(asyncSnapshot.error);
if (asyncSnapshot.data == 0) {
//Return light themed Container
currentColor = "#ffffff";
return ThemeContainer(color: currentColor );
} else {
currentColor = "#101424";
//Return dark themed Container
return ThemeContainer(color: currentColor );
}
} else {
//return dark themed
return ThemeContainer(color:currentColor);
}
},
);
//
}
}
Async Code to retrieve the value stored
Future<int> readCurrentTheme() async {
final prefs = await SharedPreferences.getInstance();
final key = 'themeMode';
final value = prefs.getInt(key) ?? 0;
print('read: $value LOOK AT THISSS');
return value;
}
It is expected that the stream builder will fire whenever the value stored is changed!
I don't see in your code a way to read data from SharedPreferences when the value stored is changed. You are effectively reading it once, so the StreamBuilder is only firering once. That makes sense.
To be able to do what you want, you have to use something to tell you widget that a state has changed elsewhere in the application. There a multiple ways to achieve this and I won't make the choice for you as it would be opinion based, so you can check thing like BloC, Provider, ScopedModel, InheritedWidget