Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Initialising variables on startup

Tags:

flutter

I'm trying to use the values saved in the SharedPreferences to initialise several variables in my app. In Flutter, the SharedPreferences are asynchronous so it results in the variables initialising later on in the code which is creating problems with my app as some of the variables are null when the build method is called.

Here is a small test Flutter app I wrote to demonstrate this issue:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class TestingApp extends StatefulWidget {

  TestingApp() {}

  @override
  State<StatefulWidget> createState() {
// TODO: implement createState
    return new _CupertinoNavigationState();
  }
}

class _CupertinoNavigationState extends State<TestingApp> {

  int itemNo;

  @override
  void initState() {
    super.initState();
//    SharedPreferences.getInstance().then((sp) {
//      sp.setInt("itemNo", 3);
//    });
    SharedPreferences.getInstance().then((sp)  {
      print("sp " + sp.getInt("itemNo").toString());
      setState(() {
        itemNo = sp.getInt("itemNo");
      });
    });
    print("This is the item number " + itemNo.toString());
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    print("item number on build " + itemNo.toString());
    return new Text("Hello");
  }
}

This is the result in the console:

Performing full restart...
flutter: This is the item number null
flutter: item number on build null // build method being called and variable is null
Restarted app in 1 993ms.
flutter: sp 3
flutter: item number on build 3

You can see that when I tried to fetch the variable from SharedPreferences on startup, since SharedPreference is async, the itemNo is null. Then the app runs the build method and runs the build method on the itemNo = null which is resulting in crashes in the app.

Once it fetches the value from SharedPreferences, I call setState which then calls the build method again with the correct value. However, the initial call to build with the itemNo = null should not have happened.

I wish there was a synch method for SharedPreferences but it doesn't seem to exist. How do I run the app so that the variables are initialised properly in Flutter on startup?

I tried to solve this through using a synchronous method to initialise my variables by writing to a json file and then reading from it using the following short Flutter test app - to me, this appears to be overkill for saving a variable to initialise but I still gave it a try:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class TestingApp extends StatefulWidget {

  TestingApp() {}

  @override
  State<StatefulWidget> createState() {
// TODO: implement createState
    return new _CupertinoNavigationState();
  }
}

class _CupertinoNavigationState extends State<TestingApp> {

  int itemNo;
  File  jsonFile;
  String fileName = "items.json";
  Directory dir;
  bool fileExists = false;

void createFile(Map content, Directory dir, String fileName) {
//  print("Creating file for category " + dir.path);
  File file = new File(dir.path + "/" + fileName);
  file.createSync();
  fileExists = true;
  file.writeAsStringSync(json.encode(content));
}

void writeToFile(int itemNo) {
//  print("Writing to category file");
  Map itemMap = new Map();
  itemMap['item'] = itemNo;
  if (fileExists) {
    print("category file exists");
    Map jsonFileContent = json.decode(jsonFile.readAsStringSync());
    jsonFileContent.addAll(itemMap);
    jsonFile.writeAsStringSync(json.encode(itemMap));
  } else {
    print("category File does not exists");
    getApplicationDocumentsDirectory().then((Directory directory) {
      dir = directory;
      createFile(itemMap, dir, fileName);
    });

  }
}

fetchSavedItemNo() {
  //load the currency from the saved json file.
  getApplicationDocumentsDirectory().then((Directory directory) {
    dir = directory;
    jsonFile = new File(dir.path+ "/" + fileName);
    fileExists = jsonFile.existsSync();
    setState(() {
      if (fileExists)
        itemNo = json.decode(jsonFile.readAsStringSync())['item'];
      print("fetching saved itemNo " +itemNo.toString());
      if (itemNo == null) {
        itemNo = 0;
      }
    });


    return itemNo;
    //else the itemNo will just be 0
  });
}

  @override
  void initState() {
    super.initState();
    writeToFile(3);
    setState(() {
      itemNo = fetchSavedItemNo();
    });

  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    print("item number on build " + itemNo.toString());
    return new Text("Hello");
  }
}

I still have the results where the build method is being called before the variables are fully initialised which is causing app crashes.

Performing full restart...
flutter: category File does not exists
flutter: item number on build null   // the build method is called here
Restarted app in 1 894ms.
flutter: fetching saved itemNo 3
flutter: item number on build 3

How do I initialise variables in Flutter on app startup?

like image 550
Simon Avatar asked May 20 '18 17:05

Simon


Video Answer


1 Answers

As Günter Zöchbacher correctly pointed out, FutureBuilder is the way to go. In your case it would look like this:

import 'dart:async'; // you will need to add this import in order to use Future's

Future<int> fetchSavedItemNo() async { // you need to return a Future to the FutureBuilder
    dir = wait getApplicationDocumentsDirectory();
    jsonFile = new File(dir.path+ "/" + fileName);
    fileExists = jsonFile.existsSync();

    // you should also not set state because the FutureBuilder will take care of that
    if (fileExists)
        itemNo = json.decode(jsonFile.readAsStringSync())['item'];

    itemNo ??= 0; // this is a great null-aware operator, which assigns 0 if itemNo is null

    return itemNo;
}

@override
Widget build(BuildContext context) {
    return FutureBuilder<int>(
        future: fetchSavedItemNo(),
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
             if (snapshot.connectionState == ConnectionState.done) {
                 print('itemNo in FutureBuilder: ${snapshot.data}';
                 return Text('Hello');
             } else
                 return Text('Loading...');
        },
    );
}

I also changed your fetchSavedItemNo function accordingly to return a Future.

A shorter way of writing:

if (itemNo != null)
    itemNo = 0;

Is the following using a null-aware operator:

itemNo ??= 0;

Conclusion

As you can see in my code, I surrounded your Text Widget with a FutureBuilder. In Flutter you solve most problems using Widget's. I also introduced a "Loading..." Text, which can be displaced in place of the "Hello" Text while the itemNo is still being loaded.

There is no "hack" that will remove the loading time and give you access to your itemNo on startup. You either do it this, idiomatic, way or you delay your startup time.

You will need to use placeholders for loading everytime you load something because it just is not available instantaneously.

Additional

By the way, you can also just remove the "Loading..." Text and always return your "Hello" text because you will never see the "Loading..." Text in your case, it just happens way too quickly.

Another option is to elude ConnectionState and just return a Container if there is no data:

FutureBuilder<int>(
    future: fetchSavedItemNo,
    builder: (BuildContext context, AsyncSnapshot<int> snapshot) => snapshot.hasData 
        ? Text(
            'Hello, itemNo: ${snapshot.data}',
          )
        : Container(),
)

In case your UI is unafffected

You can simply execute your API logic in initState with my fetchSavedItemNo function by making initState async like this:

@override
void initState() {
    super.initState();
    fetchSavedItemNo(); // continue your work in the `fetchSavedItemNo` function
}
like image 167
creativecreatorormaybenot Avatar answered Oct 14 '22 00:10

creativecreatorormaybenot