Flutter 2 plugin indispensabili dalle grandi potenzialità

Prerequisiti

Questo articolo richiede una discreta conoscenza dei seguenti argomenti che potete approfondire ai seguenti link:

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:

Last modified: 16 May 2020