Nell’articolo shared preferences in depth, ho spiegato come utilizzare la shared preferences in Flutter. In questo articolo voglio invece mostrarvi una classe che ho ideato per semplificare l’utilizzo. Ho creato questa classe per l’esigenza di dover memorizzare i dati degli utenti. Ad esempio l’ho usata in Send to Kindle per memorizzare una lista di mail. L’ho usata anche per memorizzare la lista dei token e dati d’acquisto dell’utente. In generale questa struttura può essere utilizzata laddove serve lavorare con una lista di stringhe. Nell’articolo precedente, ho già mostrato come fare a memorizzare una lista di stringhe in local cache. Questa classe però rende l’utilizzo ancora più semplici e aggiunge dei metodi che ci permettono di lavorare in maniera più rapida. Come prima cosa, ho creato il metodo per salvare una lista il local cache nel seguente modo: class CashedUserData { static Future<void> _writes = Future.value(); //add list by key reference in local cache static Future<void> addlist(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doAddList(kPrefKey,stringList)); return _writes; } } static Future<void> _doAddList(String kPrefKey, List<String> list) async { List<String> cached = await load(kPrefKey); cached.addAll(list); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } Successivamente ho creato un altro metodo per recuperare la lista, inoltre ho messo qui i controlli del caso se empty o null: static Future<List<String>> load(String kPrefKey) async { return (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; } Man mano che la usavo ho avuto la necessità di dover aggiungere altri metodi. Metodi per: Cancellare la lista. Inserire una stringa per volta. Fare l’override della lista. Verificare se un elemento esiste. Prendere il primo elemento della lista. Salvare un elemento in top alla lista. In questo modo la classe alla fine è diventata così: class CashedUserData { static Future<void> _writes = Future.value(); static Future<void> overrideListData(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doOverrideListData(kPrefKey,stringList)); return _writes; } static Future<void> addlist(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doAddList(kPrefKey,stringList)); return _writes; } static Future<void> save(String kPrefKey, String id) async{ if(await CashedUserData.exist(kPrefKey, id)) return; _writes = _writes.then((void _) => _doSave(kPrefKey,id)); return _writes; } static Future<void> saveAsFirst(String kPrefKey, String id) async{ List<String> actuaList = await load(kPrefKey); if(actuaList!=null && actuaList.length == 0 ) return save(kPrefKey,id); if(actuaList.contains(id)) actuaList.remove(id); if(actuaList.length>1) actuaList.removeAt(0); List<String> newlist = new List(); newlist.add(id); newlist.addAll(actuaList); _writes = _writes.then((void _) => _doOverrideListData(kPrefKey,newlist)); return _writes; } static Future<void> delete(String kPrefKey, String id) { _writes = _writes.then((void _) => _doDelete(kPrefKey,id)); return _writes; } static Future<String> getFirst(String kPrefKey) async { List<String> dataList = (await SharedPreferences.getInstance()).getStringList(kPrefKey); return (dataList!=null && dataList.length>0) ? dataList[0] : ""; } static Future<List<String>> load(String kPrefKey) async { return (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; } static Future<bool> exist(String kPrefKey, String id) async { List<String> userDataList = (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; return (userDataList!=null && userDataList.length > 0) ? userDataList.contains(id) : false; } static Future<void> _doSave(String kPrefKey, String id) async { List<String> cached = await load(kPrefKey); cached.add(id); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doDelete(String kPrefKey, String id) async { List<String> cached = await load(kPrefKey); cached.remove(id); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doAddList(String kPrefKey, List<String> list) async { List<String> cached = await load(kPrefKey); cached.addAll(list); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doOverrideListData(String kPrefKey, List<String> list) async { await (await SharedPreferences.getInstance()).setStringList(kPrefKey, list); } static update(String kPrefKey, String currentItem, String kindlemail) async{ List<String> actuaList = await load(kPrefKey); if(actuaList!=null && actuaList.length > 0 ) { try{actuaList[actuaList.indexOf(currentItem)] = kindlemail;} catch (e) {} } await overrideListData(kPrefKey, actuaList); } } Facendo così ho semplificato molto e reso più semplice i punti di chiamata. Inoltre se dovesse cambiare il plugin shared preference utilizzato, c’è un unico posto dove dover intervenire.
In the article shared preferences in depth, I explained how to use shared preferences in Flutter. In this article I want to show you a class that I designed to simplify its use. I created this class out of the need to store user data. For example, I used it in Send to Kindle to store a mail list. I also used it to store the user’s token list and to purchase data. Overall, this structure can be used where there is the need of working with a list of strings. In my previous article, I have already shown how to store a list of strings in local cache. This class, however, makes it even easier to use and adds methods that allow us to work faster. First, I created the method to save a local cache list as follows: class CashedUserData { static Future<void> _writes = Future.value(); //add list by key reference in local cache static Future<void> addlist(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doAddList(kPrefKey,stringList)); return _writes; } } static Future<void> _doAddList(String kPrefKey, List<String> list) async { List<String> cached = await load(kPrefKey); cached.addAll(list); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } Then I created another method to retrieve the list, I have also put the appropriate checks here, if empty or null: static Future<List<String>> load(String kPrefKey) async { return (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; } As I used it, I needed to add more methods. Methods to: Clear the list. Enter one string at a time Override the list Check if an element exists Take the first item on the list. Save an item at the top of the list. In this way the class eventually became like this: class CashedUserData { static Future<void> _writes = Future.value(); static Future<void> overrideListData(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doOverrideListData(kPrefKey,stringList)); return _writes; } static Future<void> addlist(String kPrefKey, List<String> stringList) async{ _writes = _writes.then((void _) => _doAddList(kPrefKey,stringList)); return _writes; } static Future<void> save(String kPrefKey, String id) async{ if(await CashedUserData.exist(kPrefKey, id)) return; _writes = _writes.then((void _) => _doSave(kPrefKey,id)); return _writes; } static Future<void> saveAsFirst(String kPrefKey, String id) async{ List<String> actuaList = await load(kPrefKey); if(actuaList!=null && actuaList.length == 0 ) return save(kPrefKey,id); if(actuaList.contains(id)) actuaList.remove(id); if(actuaList.length>1) actuaList.removeAt(0); List<String> newlist = new List(); newlist.add(id); newlist.addAll(actuaList); _writes = _writes.then((void _) => _doOverrideListData(kPrefKey,newlist)); return _writes; } static Future<void> delete(String kPrefKey, String id) { _writes = _writes.then((void _) => _doDelete(kPrefKey,id)); return _writes; } static Future<String> getFirst(String kPrefKey) async { List<String> dataList = (await SharedPreferences.getInstance()).getStringList(kPrefKey); return (dataList!=null && dataList.length>0) ? dataList[0] : ""; } static Future<List<String>> load(String kPrefKey) async { return (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; } static Future<bool> exist(String kPrefKey, String id) async { List<String> userDataList = (await SharedPreferences.getInstance()).getStringList(kPrefKey) ?? []; return (userDataList!=null && userDataList.length > 0) ? userDataList.contains(id) : false; } static Future<void> _doSave(String kPrefKey, String id) async { List<String> cached = await load(kPrefKey); cached.add(id); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doDelete(String kPrefKey, String id) async { List<String> cached = await load(kPrefKey); cached.remove(id); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doAddList(String kPrefKey, List<String> list) async { List<String> cached = await load(kPrefKey); cached.addAll(list); await (await SharedPreferences.getInstance()).setStringList(kPrefKey, cached); } static Future<void> _doOverrideListData(String kPrefKey, List<String> list) async { await (await SharedPreferences.getInstance()).setStringList(kPrefKey, list); } static update(String kPrefKey, String currentItem, String kindlemail) async{ List<String> actuaList = await load(kPrefKey); if(actuaList!=null && actuaList.length > 0 ) { try{actuaList[actuaList.indexOf(currentItem)] = kindlemail;} catch (e) {} } await overrideListData(kPrefKey, actuaList); } } By doing this I simplified the call points. Furthermore, if the plugin shared preference used changed, there would only be one place where to take action.
Prerequisiti Questo articolo richiede una discreta conoscenza dei seguenti argomenti che potete approfondire ai seguenti link: Firebase Remote Config Flutter Shared Preferences Come mai vi parlo di questi due plugin che sembrerebbero non avere niente a che fare l’uno con l’altro? Con questa guida, vi illustrerò come trarre beneficio da questi due plugin e come risparmiare usando i servizi gratuiti offerti da Firebase! :) Nell’app Italiano con Eli, creata per il canale Italiano con Eli, ho creato un progetto su Firebase, in modo da poter utilizzare il remote config e altre potenzialità che Firebase mette a disposizione. L’app si occupa di mostrare una lista video da canale youtube, se interessati potete scaricare il codice sorgente su github, potete utilizzarlo con qualsiasi canale youtube. Per rendere l’app Italiano con Eli più originale ho pensato di aggiungere ai video delle domande sugli argomenti trattati nei video. Avevo quindi bisogno di un posto dove salvare i dati delle domande. Con Firebase, la cosa più logica e anche più diretta sarebbe quella di usare Realtime Database oppure Firestore. Io ho pensato di utilizzare remote config! Sì, avete capito bene! Remote config! Il primo pensiero che vi sta saltando in mente in questo momento sarà “questo è pazzo!” e il secondo “ma perchè?” condivido pienamente con voi il vostro primo pensiero… c’è un pò di pazzia nella scelta adottata. Per quanto riguarda il perché invece, posso rispondere come segue: non avevo molto tempo a disposizione remote config è gratis e non ha limitazione di utilizzo si può dare accesso al servizio a una persona esterna le altre scelte mi avrebbero portato a dover costruire anche un’interfaccia back end per il censimento delle domande Conoscevo già remote config e sarei stato più rapido nell’implementazione Ma come funziona un servizio non nato per questo lavoro? Funziona benissimo, devo dire che mi ha sorpreso la reattività che dimostra l’app e il sistema che ho messo in piedi. Per farlo funzionare così bene però ho dovuto mettere assieme i due plugin nel modo che vi spiego di seguito. All’interno della console di remote config vado a inserire un nuovo parametro per ogni video. Nel campo nome inserisco l’id del video di youtube e all’interno del parametro un json contenente le domande e le risposte così strutturato: { "Playlist": "PLsrqydfBIVzy9IMGwSQieTVeSWC7cRCnN", "Questions": [ { "question": "How do you say how much is it??: ", "answare": [ "Quanto costa?", "Quanto costano? ", "Quanto costi?" ] }, { "question": "How do you say how much are they?: ", "answare": [ "Quanto costano?", "Quanto costate?", "entrambe/both" ] }, { "question": "Choose the correct option", "answare": [ "Quanto costa questo computer? 300 Euro", "quanto computer ti costa? 300 Euro", "Quanto costano questo computer?" ] }, { "question": "Quanto costa? is", "answare": [ "singolare", "plurale" ] } ] } Inoltre firebase mette a disposizione un inserimento facilitato che effettua anche la validazione del json, questo ci permette di editare più facilmente e non commettere errori. Nell’app Flutter ho poi costruito l’oggetto Quiz che conterrà l’id di youtube, una lista di Domande e la Domanda conterrà una lista di Risposte. Con vari metodi utili di gestione e costruttore per deserializzazione dei dei dati Json come segue: class Answer { final String answerText; final bool isCorrect; Answer(this.answerText, this.isCorrect); } class Question { final String question; final List<Answer> answers; Question(this.question, this.answers); } class Quiz { List<Question> _questions; int _currentIndex = -1; int _score = 0; String _videoId; Quiz.fromJson(String videoId, String jsonInput){ _videoId = videoId; _questions = new List<Question>(); var jsonQuestions = json.decode(jsonInput); jsonQuestions['Questions'].forEach((quest) { List<Answer> answers = new List<Answer>(); bool iscorrect=true; quest['answare'].forEach((ans) { //mettere la prima a true answers.add( new Answer(ans, iscorrect) ); iscorrect=false; }); answers.shuffle(); _questions.add( new Question( quest['question'], answers) ); }); } Quiz(this._questions) { _questions.shuffle(); } List<Question> get questions => _questions; int get length => _questions.length; int get questionNumber => _currentIndex + 1; int get score => _score; Question get nextQuestion { _currentIndex++; if (_currentIndex >= length) return null; return _questions[_currentIndex]; } Question get prevQuestion { _currentIndex--; if (_currentIndex < 0) return null; return _questions[_currentIndex]; } String get correctAnsware { return _questions[_currentIndex].answers.where((x) => x.isCorrect == true).first.answerText; } void answer(bool isCorrect) { if (isCorrect) _score++; } saveScore() async { SharedPreferences preferences = await SharedPreferences.getInstance(); preferences.setInt("OK"+_videoId, this.score); preferences.setInt("KO"+_videoId, this.length - this.score ); //preferences.setString(_videoId, "{'questions':'"+ this.length.toString() +"', 'score':'"+ this.score.toString() +"'}"); } } Per prima cosa, l’App recupera la lista dei video tramite le API youtube, successivamente recupera il relativo json da remote config utilizzando l’id del video e lo passa al costruttore dell’oggetto che lo deserializza ed il gioco è fatto! Una volta ultimata l’app mi sono accorto però che youtube mette a disposizione un numero limitato di chiamate API giornaliere e oltretutto ho pensato che fosse inutile che l’App, ogni volta che viene aperta, debba richiamare le API Youtube. I video oltretutto non cambiano tanto in quanto questo canale produce un nuovo video a settimana. Per risolvere il problema delle chiamate API Youtube e rendere il tutto più reattivo ho deciso di salvare tutto in una lista di stringhe json in cache utilizzando proprio la Shared Preferences. Infine ho aggiunto un parametro nella remote config contente una data. In questo modo ho fatto un meccanismo che mi permette di aggiornare i contenuti nei dispositivi solo se la data è più recente rispetto a quella memorizzata in cache del singolo dispositivo stesso: class YoutubeData { RemoteConfig remoteConfig; String apikey; List<YoutubeDto> allYoutubeVideoList = []; List<YoutubeDto> getHomeList() { return List<YoutubeDto>.from(allYoutubeVideoList.where((item) => item.kind == "video" && item.id != item.channelId )); } List<YoutubeDto> allCategory() { return List<YoutubeDto>.from(allYoutubeVideoList.where((item) => item.kind == "playlist" && item.id != item.channelId )); } List<YoutubeDto> getCategoryVideoList(String plylistID) { return List<YoutubeDto>.from(allYoutubeVideoList.where((item) => item.kind == "video" && item.id != item.channelId && item.playlist == plylistID )); } YoutubeData (this.apikey, this.remoteConfig); checkAndLoad() async { SharedPreferences preferences = await SharedPreferences.getInstance(); List<String> videoIDList = preferences.getStringList("VideoIdList"); DateTime lastUpdateDate,updateDate; String date = remoteConfig.getString("UpdateDatetime"); updateDate = DateTime.parse(date); if(preferences.getString("LastUpdateDatetime")!=null && preferences.getString("LastUpdateDatetime").isNotEmpty) lastUpdateDate = DateTime.parse(preferences.getString("LastUpdateDatetime")); else lastUpdateDate = DateTime(2010); if( (videoIDList==null || videoIDList.length==0) || updateDate.isAfter(lastUpdateDate)) { await loadYtDataAndStoreInSharedPref(); preferences.setString("LastUpdateDatetime", DateTime.now().toString() ); } await loadLocalDataAndAddQuestion(); } loadYtDataAndStoreInSharedPref() async { YoutubeAPI ytApi = new YoutubeAPI(apikey, maxResults: 50); List<YT_API> ytResult = await ytApi.channel( remoteConfig.getString('ChannelId') ); //salviamo il conteggio dei video in shared pref SharedPreferences preferences = await SharedPreferences.getInstance(); preferences.setInt("TotVideo", ytResult.length ); //salviamo json youtube video in shared pref e lista video YoutubeDto youtubedto = new YoutubeDto(); List<String> videoIdList = new List<String>(); for(var result in ytResult) { videoIdList.add( result.id); String json = jsonEncode( youtubedto.yApitoJson(result) ); preferences.setString( result.id, json ); } preferences.setStringList("VideoIdList", videoIdList ); } //aggiungiamo le domande agli oggetti youtube precedentemente salvati loadLocalDataAndAddQuestion() async { SharedPreferences preferences = await SharedPreferences.getInstance(); List<String> videoIDList = preferences.getStringList("VideoIdList"); allYoutubeVideoList = new List<YoutubeDto>(); int cntQuestion=0; for(String videoID in videoIDList) { String jsonYoutube = preferences.getString(videoID); Map userMap = jsonDecode(jsonYoutube); YoutubeDto youtubeDao = new YoutubeDto.fromJson(userMap); youtubeDao.questionJson = remoteConfig.getString( "_" + videoID.replaceAll("-", "_trattino_") ); if( youtubeDao.questionJson != null && youtubeDao.questionJson.isNotEmpty ) { cntQuestion = cntQuestion + 'question'.allMatches(youtubeDao.questionJson).length; youtubeDao.playlist = json.decode( youtubeDao.questionJson)['Playlist']; } allYoutubeVideoList.add(youtubeDao); } preferences.setInt("TotQuestion", cntQuestion ); } } Qui di seguito potete vedere un esempio di come funziona:
Prerequisites This article requires a good knowledge of the following topics that you can study at the following links: Firebase Remote Config Flutter Shared Preferences Why are we talking about these two plugins that would see having nothing in common? i With this guide I will show you how to benefit from this two plugins and how to save money using the free services offered by Firebase! :) In the talian with Eli app, created for the youtube channel Italian with Eli, I created a project on Firebase, so you can use the remote config and other potential that Firebase makes available for you. The app takes care of showing a video list from the youtube channel, if interested you can download the source code on github, you can use it with any youtube channel. To make the Italian with Eli app more original I thought of adding questions to the videos on the topics covered in the videos. So I needed a place to save the question data. With Firebase, the most logical and also most direct thing would be to use Realtime Database or Firestore. I thought of using remote config! Yes you got it right remote config! You will be thinking “this guy is crazy” and “why is he doing that?” I fully share with you this thought… there is a bit of madness in the choice made. The reason why I thought of using remote config is the following: I didn’t have much time available remote config is free and has no limitation of use you can give access to the service to an external person the other choices would have led me to have to also build a back end interface to allow the census of the questions I had already used remote config and I would have been faster in the implementation. But how does a service not born with this scope work? It works very well indeed, I must say that I was surprised by the responsiveness that shows the app and the system that I put up. To make it work so well, however, I had to put the two plugins together in the way that I explain below. Inside the remote config console I’m going to insert a new parameter for each video. In the name field I insert the id of the youtube video and inside the parameter a json containing the questions and answers structured as follows: { "Playlist": "PLsrqydfBIVzy9IMGwSQieTVeSWC7cRCnN", " Questions ": [ { " question ":" How do you say how much is it ??: ", " answare ": [ " How much does it cost? ", " How much does it cost? ", " How much does it cost? " ] }, { "question": "How do you say how much are they ?:", "answare": [ "How much do they cost?", "How much do you cost?", "both / both" ] }, { "question" : "Choose the correct option", "answare": [ "How much does this computer cost? 300 Euros", "How much computer does it cost? 300 Euros", "How much does this computer cost?" ] }, { "question": "How much does it cost? is", "answare": [ "singular", "plural" ] } ] } In addition, firebase provides an easy insertion that also performs json validation, this allows us to edit more easily and not to make mistakes. In the Flutter app I then built the Quiz object which contains the youtube id, a list of Questions and the Question will contain a list of Answers. With various useful management and constructor methods for deserializing Json data as follows: class Answer { final String answerText; final bool isCorrect; Answer (this.answerText, this.isCorrect); } class Question { final String question; final List <Answer> answers; Question (this.question, this.answers); } class Quiz { List <Question> _questions; int _currentIndex = -1; int _score = 0; String _videoId; Quiz.fromJson (String videoId, String jsonInput) { _videoId = videoId; _questions = new List <Question> (); var jsonQuestions = json.decode (jsonInput); jsonQuestions ['Questions']. forEach ((quest) { List <Answer> answers = new List <Answer> (); bool iscorrect = true; quest ['answare']. forEach ((ans) { // put the first to true answers.add (new Answer (ans, iscorrect)); iscorrect = false; }); answers.shuffle (); _questions.add (new Question (quest ['question'], answers)); }); } Quiz (this._questions) { _questions.shuffle (); } List <Question> get questions => _questions; int get length => _questions.length; int get questionNumber => _currentIndex + 1; int get score => _score; Question get nextQuestion { _currentIndex ++; if (_currentIndex> = length) return null; return _questions [_currentIndex]; } Question get prevQuestion { _currentIndex--; if (_currentIndex <0) return null; return _questions [_currentIndex]; } String get correctAnsware { return _questions [_currentIndex] .answers.where ((x) => x.isCorrect == true) .first.answerText; } void answer (bool isCorrect) { if (isCorrect) _score ++; } saveScore () async { SharedPreferences preferences = await SharedPreferences.getInstance (); preferences.setInt ("OK" + _ videoId, this.score); preferences.setInt ("KO" + _ videoId, this.length - this.score); //preferences.setString(_videoId, "{'questions': '" + this.length.toString () + "', 'score': '" + this.score.toString () + "'}"); } } The app first retrieves the list of videos via the youtube API, then retrieves the relative json from remote config using the video id and passes it to the constructor of the object that deserialize and you’re done. Once the app was completed, however, I realized that youtube provides a limited number of daily API calls. Moreover I thought it was useless every time I opened the app that it had to call the Youtube API. The videos, moreover, do not change as much as the channel makes a new video a week. To solve the problem of Youtube API calls and make everything more responsive, I decided to save everything in a list of cached json strings using the Shared Preferences. Finally I added a parameter in the remote config containing a date. In this way I made a mechanism that allows me to update the contents on the devices only if the date is more recent than the one stored in the cache of the single device itself: class YoutubeData { RemoteConfig remoteConfig; String apikey; List <YoutubeDto> allYoutubeVideoList = []; List <YoutubeDto> getHomeList () { return List <YoutubeDto> .from (allYoutubeVideoList.where ((item) => item.kind == "video" && item.id! = Item.channelId)); } List <YoutubeDto> allCategory () { return List <YoutubeDto> .from (allYoutubeVideoList.where ((item) => item.kind == "playlist" && item.id! = Item.channelId)); } List <YoutubeDto> getCategoryVideoList (String plylistID) { return List <YoutubeDto> .from (allYoutubeVideoList.where ((item) => item.kind == "video" && item.id! = Item.channelId && item.playlist == plylistID)); } YoutubeData (this.apikey, this.remoteConfig); checkAndLoad () async { SharedPreferences preferences = await SharedPreferences.getInstance (); List <String> videoIDList = preferences.getStringList ("VideoIdList"); DateTime lastUpdateDate, updateDate; String date = remoteConfig.getString ("UpdateDatetime"); updateDate = DateTime.parse (date); if (preferences.getString ("LastUpdateDatetime")! = null && preferences.getString ("LastUpdateDatetime"). isNotEmpty) lastUpdateDate = DateTime.parse (preferences.getString ("LastUpdateDatetime")); else lastUpdateDate = DateTime (2010); if ((videoIDList == null || videoIDList.length == 0) || updateDate.isAfter (lastUpdateDate)) { await loadYtDataAndStoreInSharedPref (); preferences.setString ("LastUpdateDatetime", DateTime.now (). toString ()); } await loadLocalDataAndAddQuestion (); } loadYtDataAndStoreInSharedPref () async { YoutubeAPI ytApi = new YoutubeAPI (apikey, maxResults: 50); List <YT_API> ytResult = await ytApi.channel (remoteConfig.getString ('ChannelId')); // save the video count in shared pref SharedPreferences preferences = await SharedPreferences.getInstance (); preferences.setInt ("TotVideo", ytResult.length); // save json youtube video in shared pref andvideo list YoutubeDtoyoutubedto = new YoutubeDto (); List <String> videoIdList = new List <String> (); for (var result in ytResult) { videoIdList.add (result.id); String json = jsonEncode (youtubedto.yApitoJson (result)); preferences.setString (result.id, json); } preferences.setStringList ("VideoIdList", videoIdList); } // add the questions to the previously saved youtube objects loadLocalDataAndAddQuestion () async { SharedPreferences preferences = await SharedPreferences.getInstance (); List <String> videoIDList = preferences.getStringList ("VideoIdList"); allYoutubeVideoList = new List <YoutubeDto> (); int cntQuestion = 0; for (String videoID in videoIDList) { String jsonYoutube = preferences.getString (videoID); Map userMap = jsonDecode (jsonYoutube); YoutubeDto youtubeDao = new YoutubeDto.fromJson (userMap); youtubeDao.questionJson = remoteConfig.getString ("_" + videoID.replaceAll ("-", "_trattino_")); if (youtubeDao.questionJson! = null && youtubeDao.questionJson.isNotEmpty) { cntQuestion = cntQuestion + 'question'.allMatches (youtubeDao.questionJson) .length; youtubeDao.playlist = json.decode (youtubeDao.questionJson) ['Playlist']; } allYoutubeVideoList.add (youtubeDao); } preferences.setInt ("TotQuestion", cntQuestion); } } Below you can see an example of how it works:
Il nome scelto da Google questa volta è molto comprensivo ed esaustivo. Firebase remote config è una configurazione salvata in remoto, in questo caso su Google Cloud. La configurazione è accessibile in modifica nella console di Firebase, alla voce di menù Firebase remote config che si trova in fondo, molto in fondo, nell’ultima sezione secondo me un pò nascosta. Prerequisiti: creare un account su Firebase utilizzando il nostro account google creare un app, io ho usato flutter collegare la nostra app al codice sorgente con la chiave json Facendo questo potremmo utilizzare tutti i servizi Firebase, ci sono tanti tutorial online e Firebase stesso mette a disposizione wizard con step per guidarvi alla corretta implementazione di Firebase con la vostra app.Possiamo usare Firebase Remote Config per memorizzare tutto quello che la nostra app necessità come parametri di configurazioni. Per prima cosa, dobbiamo individuare e mettere in configurazione ciò che pensiamo possa cambiare nel tempo, in questo modo avremo delle configurazioni nell’app modificabili in tempo reale da remoto in modo da non dover rieffettuare un nuovo deploy dell’app stessa. Un esempio può essere il caso in cui stiamo utilizzando un Api per recuperare dei dati, l’url base delle api potrebbe cambiare nel tempo. Un altro esempio più concreto che utilizzo io nell’app send to kindle è un parametro che contiene il numero di azioni prima di mostrare una pubblicità. In questo modo se gli utenti si lamentano per le troppe pubblicità posso cambiarlo in tempo reale con il solo cambio di un parametro da interfaccia web a costo zero. Il sistema di Firebase funziona davvero alla grande, c’è dietro un sistema push-lazy che utilizza i dati internet per recuperare le configurazioni solo se davvero abbiamo cambiato qualcosa lato server e quindi abbiamo pubblicato una modifica ad uno o più parametri. L’aggiornamento dei parametri avviene in real time e quindi la modifica sarà subito disponibile nell’app. I parametri possono essere anche associati a delle condizioni. Ad esempio un parametro può essere utilizzato solo per gli utenti di una certa età di un determinato paese o lingua a un determinato orario. Figata no? Vantaggi Controllo da remoto istantaneo, semplice e centralizzato. Modifica App in tempo reale. Diminuisce la necessità di dover effettuare un nuovo deploy. i parametri possono essere combinati con delle condizioni. Come lo uso io nelle mie app? Di seguito voglio mostrarvi un esempio di implementazione. In questo modo ho centralizzato la parte di inizializzazione e creazione dei parametri in modo che siano più accessibili. Ho deciso di utilizzare un singleton in modo da poterlo usare in tutte le view e in tutte le classi per cui ne ho bisogno, implementato nel seguente modo: import 'package:firebase_remote_config/firebase_remote_config.dart'; class Configuration { int _link_sent_before_ads; int _max_send_limit_size = 24000000; int get linkSentBeforeAds => _link_sent_before_ads; int get maxSendLimitSize => _max_send_limit_size; int get maxSendLimitSizeMb => (_max_send_limit_size/1000000).round(); static Configuration get instance => _instance; static final Configuration _instance = Configuration._privateConstructor(); Configuration._privateConstructor() { //initializeRemoteConfig(); } loadConfig(RemoteConfig _remoteConfig) async { _link_sent_before_ads = _remoteConfig.getString('link_sent_before_ads').isEmpty ? _link_sent_before_ads : int.tryParse(_remoteConfig.getString('link_sent_before_ads')); _max_send_limit_size = _remoteConfig.getString('max_send_limit_size').isEmpty ? _max_send_limit_size : int.tryParse(_remoteConfig.getString('max_send_limit_size')); } initializeRemoteConfig() async{ RemoteConfig remoteConfig = await RemoteConfig.instance; try { //set some default params in case of crash await remoteConfig.setConfigSettings(RemoteConfigSettings(debugMode: false)); await remoteConfig.setDefaults(<String, dynamic>{ 'service_api_conversion_1': 'https://ebook-converter.app/calibre/ebook-convert', }); // Using default duration to force fetching from remote server. await remoteConfig.fetch(expiration: const Duration(seconds: 0)); await remoteConfig.activateFetched(); } on FetchThrottledException catch (exception) { // Fetch throttled. print(exception); } catch (exception) { print('Unable to fetch remote config. Cached or default values will be used'); } await this.loadConfig(remoteConfig); } } potrà essere infine utilizzato molto semplicemente nel seguente modo: //call this just first time in app initialization await Configuration.instance.initializeRemoteConfig(); Print(Configuration.instance.maxSendLimitSize);
What is shared_preferences where, when and how much to use it? The shared_preference is a plugin that allows you to save data app dedicated memory, the folder that will contain this data is named in Android with the package name of your app, e.g. com.companyname.appname. The data that we can store in this space with this plugin are data of these types: String Integer Bool List It should be noted that these parameters are not persistent, they remain in the area dedicated to the app therefore are eliminated when the app is uninstalled or if the app cache data is removed. So, in general it is right to store non-essential preference data so that it is not a tragedy if they have been lost. A classic example can be the app theme color light or dark or the order of menu title. How to use? Saving: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.setString("theme", "dark"); _preferences.setInt("countaccess", 2); Recovery: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.getString("theme"); _preferences.getInt("countaccess"); At first we might think that it is of little use since in a string we cannot store a lot of information. In fact, if we think about it in a string, we can also store a lot but really a lot by using a json, below an example: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.setString("{"name":"Andrea","surname":"Luciano","age":31,"sex":"M","SurnameCouldBeName":true}"); In this example I put SurnameCouldBeName as a field because my last name is also a first name. All those who don’t know me always ask me “Luciano is your name or surname?” And every time I would like to do something like that: We can therefore create a collection of people using a list of strings instead of the single string as follows: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); List<String> users = new List<String>(); users.add("{"name":"Andrea","surname":"Luciano","age":31,"sex":"M","SurnameCouldBeName":true}"); users.add("{"name":"elisa","surname":"luciano","age":32,"sex":"F","SurnameCouldBeName":false}"); _preferences.setStringList("users", users); This could finally be mapped into an object in order to access the data simply and finally show it to the interface: Class UserDto() { String name, surname, age, sex, SurnameCouldBeName; UserDto.fromJson(Map<String, dynamic> json) : name= json[name], surname= json[surname], age= json[age], sex= json[sex], SurnameCouldBeName = json[SurnameCouldBeName]; } SharedPreferences _preferences = await SharedPreferences.getInstance(); List<UserDto> UserObj = new List<UserDto>; List<String> users = _preferences.getStringList("users"); for(String userjson in users) { Map userMap = jsonDecode(userjson); UserDto = new UserDto.fromJson(userMap); print(UserDto.name); UserObj.add(UserDto); } Think about the possible benefits and applications of this. It can be used to store data returned by an API. In this way, the application would be more efficient and also can work offline without therefore having to use network data for recovering data from the server.
Che cos’è la shared_preferences dove, quando e quanto utilizzarla? La shared_preference è un plugin che vi permette di salvare dei dati in memoria dedicata all’app, la cartella che li conterrà prende il nome in android con il nome del pacchetto della vostra app es com.nomeazienda.nomeapp. I dati che possiamo memorizzare all’interno di questa memoria con questo plugin sono dei seguenti tipi: string intero bool List E’ da notare che questi parametri non sono persistenti, rimangono nell’area dedicata all’app e quindi vengono eliminate nel momento in cui l’app viene disinstallata oppure se vengono rimossi i dati della cache dell’app. Quindi in genere è doveroso memorizzare dati di preferenze non indispensabili in modo che non sia una tragedia se dovessero essere persi. Un esempio classico può essere come voglio il tema light o dark oppure l’ordine degli oggetti nel menu. Come si usa? Salvataggio: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.setString("theme", "dark"); _preferences.setInt("countaccess", 2); Recupero: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.getString("theme"); _preferences.getInt("countaccess"); In un primo momento potremmo pensare che è una cosa che serve a poco in quanto in una stringa non possiamo memorizzare tante informazioni. In realtà, se ci pensiamo bene, in una stringa possiamo anche memorizzare molto ma davvero molto semplicemente utilizzando un json, di seguito esempio: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); _preferences.setString("{"name":"Andrea","surname":"Luciano","age":31,"sex":"M","SurnameCouldBeName":true}"); In questo esempio ho messo come campo SurnameCouldBeName in quanto il mio cognome è anche un nome. Tutti quelli che non mi conoscono mi chiedono sempre “Luciano è il nome o il cognome?” E ogni volta vorrei fare qualcosa del genere: Possiamo quindi creare una collezione di persone utilizzando, al posto della singola stringa, una lista di stringhe come segue: import 'package:shared_preferences/shared_preferences.dart'; SharedPreferences _preferences = await SharedPreferences.getInstance(); List<String> users = new List<String>(); users.add("{"name":"Andrea","surname":"Luciano","age":31,"sex":"M","SurnameCouldBeName":true}"); users.add("{"name":"elisa","surname":"luciano","age":32,"sex":"F","SurnameCouldBeName":false}"); _preferences.setStringList("users", users); Questo infine potrebbe essere mappato in un oggetto in modo da accedere ai dati più semplicemente ed infine mostrarli nell’interfaccia: Class UserDto() { String name, surname, age, sex, SurnameCouldBeName; UserDto.fromJson(Map<String, dynamic> json) : name= json[name], surname= json[surname], age= json[age], sex= json[sex], SurnameCouldBeName = json[SurnameCouldBeName]; } SharedPreferences _preferences = await SharedPreferences.getInstance(); List<UserDto> UserObj = new List<UserDto>; List<String> users = _preferences.getStringList("users"); for(String userjson in users) { Map userMap = jsonDecode(userjson); UserDto = new UserDto.fromJson(userMap); print(UserDto.name); UserObj.add(UserDto); } Pensate ai possibili vantaggi e alle possibili applicazioni. Può essere utilizzato per memorizzare dati restituiti da un API. Con questo approccio, l’applicazione è più efficiente e potrebbe funzionare anche offline senza quindi dover utilizzare sempre la rete dati per recuperare i dati dal server.
The name chosen by Google this time is very comprehensive and exhaustive. Firebase remote config is a remotely saved configuration, in this case on Google Cloud. This configuration is accessible to insert and update in the Firebase console, under the menu item Firebase remote config which is located at the very bottom, in the last section, in my opinion a little hidden. Prerequisites: create an account on Firebase using our google account create an app, I used flutter connect our app to the source code using json key file By doing this we could use all the Firebase services, there are many online tutorials and Firebase itself provides wizards with steps to guide you to the correct implementation of Firebase with your app. We can use Firebase Remote Config to store everything our app needs as configuration parameters. First of all, We must identify and configure what we think will change over time. In this way we will have configurations in the app that can be modified in real time remotely so as not to have to carry out a new app deployment. An example may be the case in which we are using an API to recover data, the base url may change over time. Another more down to earth-example that I use in the send to kindle app is a parameter that contains the number of actions before showing an advertisement. In this way, if users complain about too many advertisements, I can change it in real time with just changing a parameter into the web interface at no cost. The Firebase system works really well, there is a push-lazy system behind it that uses internet data to retrieve the configurations only if we have really changed something on the server side and therefore we have published a change to one or more parameters. The updating of the parameters takes place in real time so it changes immediately the behavior, state or data in app as real time. Parameters can also be associated with conditions. For example, a parameter can only be used for users of a certain age in a certain country or language at a specific time. Cool! isn’t it? Advantages Instant and simple remote control. Real-time app customization. It decreases the need for a new deployment. Parameters can be combined with conditions. How do I use it in my apps? Below I want to show you an example of implementation. In this way I centralized the initialisation and parameter creation part so that they are more accessible. I decided to use a singleton so that I can use it in all the views and all the classes I need it, implemented in the following way: import 'package:firebase_remote_config/firebase_remote_config.dart'; class Configuration { int _link_sent_before_ads; int _max_send_limit_size = 24000000; int get linkSentBeforeAds => _link_sent_before_ads; int get maxSendLimitSize => _max_send_limit_size; int get maxSendLimitSizeMb => (_max_send_limit_size/1000000).round(); static Configuration get instance => _instance; static final Configuration _instance = Configuration._privateConstructor(); Configuration._privateConstructor() { //initializeRemoteConfig(); } loadConfig(RemoteConfig _remoteConfig) async { _link_sent_before_ads = _remoteConfig.getString('link_sent_before_ads').isEmpty ? _link_sent_before_ads : int.tryParse(_remoteConfig.getString('link_sent_before_ads')); _max_send_limit_size = _remoteConfig.getString('max_send_limit_size').isEmpty ? _max_send_limit_size : int.tryParse(_remoteConfig.getString('max_send_limit_size')); } initializeRemoteConfig() async{ RemoteConfig remoteConfig = await RemoteConfig.instance; try { //set some default params in case of crash await remoteConfig.setConfigSettings(RemoteConfigSettings(debugMode: false)); await remoteConfig.setDefaults(<String, dynamic>{ 'service_api_conversion_1': 'https://ebook-converter.app/calibre/ebook-convert', }); // Using default duration to force fetching from remote server. await remoteConfig.fetch(expiration: const Duration(seconds: 0)); await remoteConfig.activateFetched(); } on FetchThrottledException catch (exception) { // Fetch throttled. print(exception); } catch (exception) { print('Unable to fetch remote config. Cached or default values will be used'); } await this.loadConfig(remoteConfig); } } It can finally be used very simply in the following way: //call this just first time in app initialization await Configuration.instance.initializeRemoteConfig(); Print(Configuration.instance.maxSendLimitSize);