Flutter configurations from cloud using Google cloud storage

Sergiu Rosu
4 min readMar 12, 2020

I have been working on mobile app development for quite a while now and realized that one big problem many people are facing is to update the app configuration after the app was released in the store, by configuration I mean :

  • Localization, this means all the texts used inside the app
  • Colors and font sizes
  • Icons and images
  • many more

Basically everything can be configured from the cloud

In this article I used google cloud storage to upload the json files which are storing all the information.

I used “Smart JSON editor” to create and edit the json files, it has a great featured, it contains a web server so while developing you can use those url’s directly, no need to upload the files each time.

Smart JSON editor

It’s best to use streams for the configuration files or async functions to make sure that we have the latest function, we decided to use async functions and load the file from multiple sources, to make sure that we have the latest version when starting the app.

it’s clear that we should either use streams or make sure that we wait for the file to download, which may generate problems if the internet connection is not very good.

Using is very simple, we first have to make sure that the manifest is loaded, the manifest is just a json file containing the latest version number for each file:

showHud();await _configService.getConfigSync("manifest");
await _configService.getConfigSync("colors");
await _configService.getConfigSync("localization-en");

dismissHud();

and bellow is the full code I used (it uses a mem cache mechanism to improve performance) even it used incorrectly, the correct usage will be to load and store the config in each class which is using it:

class ConfigService {
Future<String> get _docPath async {
final directory = await getApplicationDocumentsDirectory();

return directory.path;
}

Map<String, StreamController> _controllers = Map<String, StreamController>();
StorageReference _ref = FirebaseStorage().ref();
CacheService _cacheService = ServiceManager().cacheService;

//this is a magic value for the manifest file, so we know not to check the version and shit
final String _manifestKey = "manifest";
ConfigObject _manifestConfig;
get devUrl {
if (Foundation.kReleaseMode) {
return {};
} else {
if (Platform.isAndroid) {
return {
"localization-en": "http://10.0.2.2:8080/localization-en",
"colors": "http://10.0.2.2:8080/colors",
"manifest": "http://10.0.2.2:8080/manifest",
};
} else {
return {
"localization-en": "http://127.0.0.1:8080/localization-en",
"colors": "http://127.0.0.1:8080/colors",
"manifest": "http://127.0.0.1:8080/manifest",
};
}
}
}

Future<ConfigObject> getConfigSync(String manifestKey) async {
String str;
ConfigObject config;

print("cache: loading ${manifestKey} from documents");
str = await _fromDocumentsDir(manifestKey);

/// if there is no file on documents we load it from assets
if (str.length == 0) {
print("cache: loading ${manifestKey} from assets");

config = await rootBundle.loadStructuredData("assets/json/${manifestKey}.json", (astr) {
_toDocumentsDir(manifestKey, astr);
return Future.value(_parseFileContent(astr));
});
} else {
config = _parseFileContent(str);
}

print("cache: checking ${manifestKey} version with ${config.version}");

/// check the config version vs manifest
///
if (_manifestConfig != null && _manifestConfig.keys.containsKey(manifestKey)) {
int version = _manifestConfig.keys[manifestKey];
if (version > config.version) {
print("cache: loading ${manifestKey} from cloud");

str = await _fromStorage(manifestKey);
print("cache: loaded ${manifestKey} from cloud");

await _toDocumentsDir(manifestKey, str);
print("cache: saving ${manifestKey} to documents");

config = _parseFileContent(str);
}
}

if (manifestKey == _manifestKey) {
print("cache: loading ${manifestKey} from cloud");

str = await _fromStorage(manifestKey);
print("cache: loaded ${manifestKey} from cloud");

config = _parseFileContent(str);
}

if (manifestKey == _manifestKey) {
_manifestConfig = config;
}

return _cacheService.cache("config_${manifestKey}", value: config);
}

Future<File> _toDocumentsDir(String fileName, String value) async {
final path = await _docPath;

File file = File('$path/$fileName.json');

return file.writeAsString(value);
}

Future<String> _fromDocumentsDir(String fileName) async {
// return "";
final path = await _docPath;

File file = File('$path/$fileName.json');

bool fileExists = await file.exists();

if (fileExists) {
String contents = await file.readAsString();
return contents;
} else {
return "";
}
}

Future<String> _fromStorage(String manifestKey) async {
String url = "";

print("try to load '$manifestKey' from cloud");

if (devUrl.containsKey(manifestKey)) {
url = devUrl[manifestKey];
} else {
StorageReference ref = _ref.child("config/${manifestKey}.json");
url = (await ref.getDownloadURL()).toString();
}

print("config url ${url}");
try {
http.Response result = await http.get(url);
print("done: ${result}");
return result.body;
} catch (e) {
// print(e);
print("error getting '$manifestKey' from cloud");

return "{\"version\": 0, \"keys\": {}}";
}
}

ConfigObject _parseFileContent(String jsonBody) {
var json;
try {
json = jsonDecode(jsonBody);
} catch (e) {
print(e);
}

return ConfigObject(json['version'], json['keys'], -1);
}
}

class ConfigObject {
int version;
Map<String, dynamic> keys;
int source; // 0 - assets, 1 - documents, 2 - cloud

ConfigObject(this.version, this.keys, this.source);
}

this code may not compile and should not be used as it is… even if this is my actual file may not work in your situation.

--

--

Sergiu Rosu

Software engineer, mainly on iOS with Swift, also working with Flutter for iOS and Android.