Flutter 2 plugin necessary with great potentiality

Prerequisites

This article requires a good knowledge of the following topics that you can study at the following links:

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:

Last modified: 16 May 2020