Flutter Hot Restart - suspect it populates class instances incorrectly - class

My app is in 2 parts. It creates a widget tree and populates a Stateful Widget Config.
I then pass this to phase 2 of my app as a static. (I tried various ways).
On a clean emulator it runs fine. When I run it again using Hot Restart my buildSitePath is passed a half-baked instance that did not go thru the proper constructors.
Thus where appCfg is a static Config appCfg
that has been confirmed to be populated properly the log statement in the following will execute after
the populating and yet it will be the half-baked instance that breaks the app. This happens on Hot Restart which supposedly restarts the app from the beginning.
routes:<String,WidgetBuilder>{
"/site":(BuildContext context) {assert(log("site: $appCfg"));
return buildSitePath(context,appCfg,title);}
},
The solution was to not use a static and to use a onGenerateRoute and pass the actual instance as a parameter. Thus, this worked.
onGenerateRoute: (settings) {
log("onGenerateRoute ${settings.name}");
if (settings.name == "/site") {
Config appCfg = settings.arguments;
log("onGenerateRoute $appCfg");
if (isMaterial(contextA)) {
return MaterialPageRoute(builder: (context) => buildSitePath(contextA, appCfg, title));
} else {
return CupertinoPageRoute(builder: (context) => buildSitePath(contextA, appCfg, title));
}
}
return null;
I looked and found no understanding as to what Hot Restart is doing with classes that might explain this.
What am I missing? Thanks.

As I said in my question my suspicion is that Flutter Hot Restart sees the static class instance that is not populated and jams the class in there using a default constructor.
Even if the class was not static but a member of another class the same ailment persisted.
I record this behavior here so that others need not waste as much time as I did to understand the problem.

Related

Flutter: How to load user settings from SharedPreferences when a new version of the app introduces structural changes to the Settings class?

I am working on a flutter app which is meant for music students. The students can set the difficulty of the tasks they are shown. These settings are stored in an object of the Settings class and this object is stored in SharedPreferences. However, we frequently change the settings class in order to provide new options for the students. This causes problems when the app is loaded for the first time after the update.
Here is the part where the settings are loaded. I do not want the QuizView to load before the settings are available. Therefore, I use a FutureBuilder widget:
MaterialPageRoute(builder: (context) =>
FutureBuilder<Settings>(
// Fetching the settings from SharedPreferences. This is the asynchronous operation.
future: loadSettings("Uebungsmodus"),
builder: (BuildContext context,
AsyncSnapshot<Settings> snapshot) {
Widget child;
if (snapshot.hasData) {
// Extract the data from the snapshot.
// There might be a better solution, but at least this works for now.
Settings settings = Settings();
settings.augmentedFrequency =
snapshot.data!.augmentedFrequency;
settings.diminishedFrequency =
snapshot.data!.diminishedFrequency;
settings.majorFrequency =
snapshot.data!.majorFrequency;
settings.minorFrequency =
snapshot.data!.minorFrequency;
settings.maxKeySignatureAccidentals =
snapshot.data!
.maxKeySignatureAccidentals;
settings.snowmenOnly =
snapshot.data!.snowmenOnly;
settings.standardClefArrangement =
snapshot
.data!.standardClefArrangement;
settings.clefPreferences =
snapshot
.data!.clefPreferences;
QuizPageViewModel qpvm =
QuizPageViewModel(
settings,
TriadProvider(),
ParamGenerator());
child = QuizView(vm: qpvm);
} else if (snapshot.hasError) {
// Provide default settings in case of an error.
QuizPageViewModel qpvm =
QuizPageViewModel(
Settings(),
TriadProvider(),
ParamGenerator());
child = QuizView(vm: qpvm);
} else {
// Show a circular progress indicator as long as the settings are loading.
child = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator())
],
);
}
return child;
},
)));
The Map clefPreferences was not there before. Instead, there were two boolean variables. The problem now is that snapshot.data!.clefPreferences is null, since this part was never stored in SharedPreferences before. Since the data object is not null itself, flutter won't notice. If I try
settings.clefPreferences =
snapshot
.data!.clefPreferences ?? [some code initialising default preferences];
flutter tells me that "the left operand can't be null, so the right operand is never executed". It is null though! In this scenario, the app crashes.
I am unsure how to deal with this. On loading the App, there will be an update screen informing users about the new version, storing new settings at the same time. However, I am worried that I might miss something and users might somehow bypass this update-screen. This will completely break the app on a user's device unless I do another check somewhere. How can I still make sure no null entries are loaded? Any advice is appreciated!
Update: Everything seems to work now and this problem does not occur any more. If any of the variables from the Settings class cannot be retrieved, if (snapshot.data) simply evaluates to false immediately and the default settings are loaded. I am still wondering why the problem described above occurred in the first place. Apparently, I was able to load snapshot data which were null without Flutter admitting it. I could see in the debugger that they were null. I am unable to reproduce this scenario whatever I try. Has anyone come across this problem before?
There is another thing which I am wondering about: When I manually write corrupt data into the file FlutterSharedPreferences.xml, it seems to fix itself, meaning the changes are reverted when I trigger loading the data. I noticed that SharedPreferences uses the two variables _store and _preferenceCache. Are these responsible for this? Are there any backup copies or cached data which are used to overwrite the changes I manually write into that file. I am asking this because it might be easier to reproduce the problem described above if I could manually change FlutterSharedPreferences.xml.

Flutter - availableCameras() returns empty list

This code was working just fine a while ago, but is now misbehaving for no reasons, I tried to re-install the app 2 times, but didn't worked, what might be causing it? It was giving me a list of available cameras before, but after a hot-restart, the code is constantly breaking.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras(); // returns an empty list, which it shouldn't because I'm using a real device which has two physical cameras, all dependencies are added, all permissions are allowed.
runApp(
MyApp(),
);
}
Note: The error is coming when I try to access the cameras list, but it was able to access it two hours ago, why is it returning an empty list right now?
For all of you that may have done the same error as me by copy-pasting "camera" code from pubdev,
keep in mind that the code of "camera" was made for being main page, wich means _cameras is initialised by :
List<CameraDescription> _cameras = <CameraDescription>[];
but it's the main() function role to fill it, so if you call CameraApp() from outside of the page, main will not be triggered, therefore you need to modify the class like this:
class CameraApp extends StatelessWidget {
/// Default Constructor
const CameraApp({Key? key, required this.cameras}) : super(key: key);
final List<CameraDescription> cameras;
#override
Widget build(BuildContext context) {
_cameras=cameras;
return const MaterialApp(
home: CameraExampleHome(),
);
}
}
and call it from another page with:
await availableCameras().then((value) => Navigator.push(context,
MaterialPageRoute(builder: (_) => CameraApp(cameras: value))));
Like so the cameras are properly filled. That's my working solution.
Possible amelioration: Maybe the cameras list could be filled directly in CameraApp?
This error is most probably due to the "camera" plugin's internal working or due to Android OS's security reasons or something like that. The camera package is new, so you can expect such behaviors, but there are bunch of other enhanced packages as well based on the original one.
In my case, I used "flutter_camera" and modified the source code as per my needs in order to achieve the desired UI, and it works pretty good.
Update: I found out that the error was indirectly connected to "compileSdkVersion" in my app/build.gradle being set to 33 which was required by a random flutter plugin, setting it to 29 allowed me to access my camera and successfully executed availableCameras() method too but then the plugin can't be used.

Flutter onGenerateRoutes - Unexpected Behavior

I'm experiencing a very weird behaviour out of onGenerateRoutes. So, I've this app in which a user is able to see the home screen if :
He signs in using the correct OTP for a phone number that's already registered. (case - '/checkUserDetails')
After he/she registers on the app. (case - '/verifyRegistration')
If he's already registered and reopening the app. (case - '/')
Now, to achieve this, I've created a Routes object which looks something like this:
Route routes(RouteSettings settings) {
switch (settings.name) {
case '/':
{
//Show OnboardingScreen or LoginScreen or HomeScreen based on user's app state
}
case '/checkUserDetails':
// Executed when user has submitted the received OTP
{
//Do something and return RegisterScreen or HomeScreen based on data received from the backend
}
case '/verifyRegistration':
//Executed when the user has submitted the registration form
{
//Verify the registration and send user to HomeScreen or ErrorScreen
}
}
Now, what happening is that If my code is on (let's say) - /verifyRegistration or even /checkUserDetails, before executing return HomeScreen(), the code will automatically go to case '/' of the switch statement and start executing the code from within there.
Now, this is a problem because my bloc class executes a certain set of functions before HomeScreen is rendered. Calls to those functions are mentioned inside all three of these routes (as user can go to the HomePage from either of them based on the 3 points I've mentioned above).
Since, my code is unexpectedly going to the case '/' (even when settings.name is /verifyUser or something else) It's making duplicate function calls which is resulting in unexpected data. Duplicate because those functions are being executed from within the expected route and the unexpected one as well (case '/')
Any idea how can I stop this from happening ?? Do you think that the switch statement has something to do with it ? I hope I was able to explain properly.
Thanks in advance!

Stateful widget consumer widget builds twice and hence calls initState twice. How to stop this from happening?

So for my project, I am using Riverpod, and I am creating the home page of my app.
The tree looks like this ->
CommunityView(Stateful Widget)
WillPopScope
ProviderScope
Consumer
Scaffold
...and so on
Now inside the build method of the CommunityView Widget,
final params = _VSControllerParams(
selectedFamily: widget.selectedFamily,
);
print('familiesStatus: rebuild main');
return WillPopScope(
onWillPop: onWillPop,
child: ProviderScope(
overrides: [_paramsProvider.overrideWithValue(params)],
child: Consumer(
builder: (context, watch, child) {
print('familiesStatus: rebuild consumer');
final state = watch(_vsProvider(params));
final stateController = watch(_vsProvider(params).notifier);
The rebuild main print happens only once, while the rebuild consumer print happens twice. Previously home page fetched data and then shows data fetched or error depending on network response. Now I use an enum to get loading state, i.e. ApiStatus.loading is the initial state, then ApiStatus.success or failure depending on response. Now what I have changed that is causing this issue is -> I have added a function call in initstate that fetches local saved cache and loads data from that. So what happens is ApiStatus.loading->ApiStatus.success(Cache fetched)->ApiStatus.loading(somehow whole widget rebuild)->ApiStatus.success(cache fetched again)->ApiStatus.success(Data fetched from internet). So I am not sure why it is rebuilding first time when cache is fetched.
Now to fix this, I first tried to find any reason in the cache method call that is causing this rebuild, but was not able to find any even with heavy debugging. Then I thought to create a global parameter which is something like this
bool fetchDataFromCache = true;
and then inside initState, I call fetch Cache method something like this
if(fetchDataFromCache){
fetchCache();
fetchDataFromCache = false;
}
But this caused the ApiStatus loading parameter to change like this ->ApiStatus.loading(initial build)->ApiStatus.success(Cache fetched)->ApiStatus.loading(somehow whole widget rebuild)->ApiStatus.success(Data fetched from internet). So widget is still rebuilding because I have set ApiStatus.loading only in initState, and nowhere else. So it is still glitching out.
Demo video - https://youtu.be/1EzYfCRiwk0

How does the listen: false work in Flutter Providers?

I just started learning Flutter, and I'm going through the Udemy course "Flutter & Dart - The Complete Guide". In that course, there is a section about building a shopping app, which uses providers. In one instance of that app, where the user swipes to delete a product from the Cart page (or screen/route) with the help of the Dismissible widget, he uses a function inside the provider class, which takes a product ID, to delete the item from the cart.
Here is the thing that I don't understand. The Dismissible widget is connected to the provider via this code in the onDismissed property (which fires after the swipe):
Provider.of<Cart>(context, listen: false).removeItem(productId);
And it all works just fine like this. But if you remove the listen parameter (hence turning it into it's default state which is true), then the Dismiss animation still takes place, but the removeItem() method doesn't work, and the cart still stays the same.
Why does this happen?
When we use listen: false we are telling to not rebuild the widget after we remove an item, but they know that we are removing an item so we don't need to listen any value here it's just doing the action of removing
I'll refer to the method Provider.of(context, listen: true) as x throughout this answer.
x is expected to be used ONLY for properties of a Widget that is
expected to change; and
can be rebuilt. For example:
SizedBox(
width: Provider.of<MyLogic>(context, listen: true).width,
)
When used this way, x will ONLY be called when the context owner is being built/rebuilt.
To ensure that it is being used properly, x performs a sanity check every time it is called, making sure that the owner of the context you passed is actually being built/rebuilt. When you call x from within your onPressed or whatever method it is you call it from, x sees that the context owner is not in the "build" phase, and throws this error.
There are a few more details to this, but you don't actually need to know more about it (especially you're just beginning) unless you want to contribute to the package, in which case you should read their documentation.
Side note: you can now use context.watch() and context.read() instead of Provider.of().