Con questo articolo voglio spiegare come poter fare una app per il proprio canale personale youtube in 10 minuti. Io ho sviluppato in questo modo l’app Italiano con Eli, creata per il canale Italiano con Eli. Mentre stavo sviluppato l’app ho deciso di generalizzare la soluzione in modo da poter essere utilizzata da chiunque per recuperare qualsiasi video dato un determinato link del corrispettivo canale Youtube. L’applicazione completa e funzionante è disponibile al seguente link github. Potete fare un Fork o un Clone, usarla così com’è oppure modificandola come meglio vi pare. Se vi piace potete anche mettere un like. Configurazione necessaria Unica cosa da fare per poterla utilizzare è configurare la connessione alle API di Youtube in modo da poter utilizzare la ricerca dei vostri video del canale. Per una corretta configurazione potete seguire i seguenti step: Accedere al Google Api Console con il vostro account Google se non lo avete, create un nuovo account. Andare sotto progetti e cliccate su NUOVO PROGETTO, Immetti il nome e aspetta che google crei il progetto. Clicca sul link delle Google API nella libreria delle API e cerca oppure seleziona le api youtube come evidenziato nella foto di seguito dopo clicca abilità e vai sotto la voce di menu credenziali Seleziona Youtube Api v3, seleziona dati pubblici e conferma Adesso Google creerà la tua chiave di accesso l’api key recuperata al punto 6 dovrà essere inserita all’interno dell’applicazione e sostituita nella pagina main.dart al posto della key come segue: static String key = ""; // ** ENTER YOUTUBE API KEY HERE ** static String channelid = "UC_8mNVpafplqHNy85No4O2g"; // ** ENTER YOUTUBE CHANNEL ID HERE ** il channel id deve fare riferimento al vostro canale youtube. Lo potete semplicemente estrarre dal link del vostro canale youtube, prendendo la parte finale del link: La configurazione è finita, avete appena creato la vostra nuova App per il vostro canale Youtube Yehaa! Qualche cenno al codice Per agevolare lo sviluppo dell’applicazione ho usato i seguenti due plugin che potete trovate nella pagina dei plugin per flutter flutter packages i plugin utilizzati sono i seguenti: youtube player Flutter youtube Api il codice dell’applicazione è molto semplice e ho usato una struttura di suddivisione pagine abbastanza chiara: Model contiene il modello dei dati recuperati dalle youtube Api. Adesso la classe fa anche qualcosa in più, c’è un metodo che recupera i dati da youtube e li inserisce in una lista di youtube data. Usa un sistema che fa il recupero dei dati solo se non sono presenti in cache usando la flutter shared preference. Questo meccanismo è molto utile in quanto le Api di Google sono a pagamento in funzione dell’utilizzo. Utilizzando la cache risparmierete drasticamente in chiamate. se non avete mai usato la cache in flutter potete leggere quest’altro mio articolo: flutter shared preferences in profondità. Pages contiene le pagine dell’APP home page category page pagina di play video UI contiene un widget per rappresentare la lista dei video. L’ho creato a parte come widget per avere a disposizione una view da poter utilizzare sia nella pagina home che category. In questo modo, potrei usarlo anche in altre pagine in futuro. Main.dart altro non fa che inizializzare la classe del model, caricare i dati di youtube e passarli alle rispettive pagine. Utilizzando questa struttura il codice è molto semplice anche da manutenere ad esempio la pagina home contiene solo il seguente codice: import 'package:flutter/material.dart'; import 'package:youtubeapichannel/Model/YoutubeData.dart'; import 'package:youtubeapichannel/UI/YoutubeVideoList.dart'; YoutubeData youtubeData; class Home extends StatefulWidget { Home(YoutubeData inyoutubeData) { youtubeData = inyoutubeData; } @override _HomeState createState() => new _HomeState(); } class _HomeState extends State<Home> { callAPI() async { setState(() {print('UI Updated'); }); } @override void initState() { super.initState(); //callAPI(); } @override Widget build(BuildContext context) { if(youtubeData!=null) return new YoutubeVideoList(youtubeData.getHomeList()); else return new Container(child:new Text("problem loading data from youtube!")); } } Quindi riceve semplicemente in input la classe contenente la lista dei video e richiama il plugin YoutubeVideoList che sta sotto la UI per rappresentare i video sotto forma di lista di video youtube. Unica pecca, è che quando l’ho realizzata non ero tanto esperto e ho deciso di passare i dati come parametro. Oggi flutter mette a disposizioni tecniche migliori per passare i dati da una view ad un altra, in effetti dovrei fare un po' di refactor, ma oggi non ne ho voglia.
With this article I want to explain how to build an app for your personal youtube channel in 10 minutes. I did this basic project to create Italiano con Eli app, created for the YouTube channel Italiano con Eli. While I was developing the app I decided to generalize the solution so that it can retrieve any video to any Youtube channel. The complete and working application is available at the following link github. You can make a Fork or a Clone or use it as it or by modifying it as you like. If you like you can also put a like to my git project. Required configuration The only thing to do to be able to use it is to configure the connection to the Youtube API so that you can use the search for your channel videos. For a correct configuration you can follow the following steps: Log in to the Google Api Console with your Google Account if you don’t have it, create a new account. Go under projects and click on NEW PROJECT, enter the name and wait for google to create project. Click on the link in the Google API library and search or select the youtube API as highlighted in the photo below after click ability and go under the credentials menu item Select Youtube Api v3, select public data and confirm Now Google will create your api key the api key recovered in step 6 will be inserted within the application and replaced the page main.dart instead of the key as follows: static String key = ""; // ** ENTER YOUTUBE API KEY HERE ** static String channelid = "UC_8mNVpafplqHNy85No4O2g"; // ** ENTER YOUTUBE CHANNEL ID HERE ** the channel id must refer to your youtube channel. You can simply extract it from the link of your youtube channel, taking the final part of the link: The configuration is finished, you have just created your App for your personal Youtube channel! Yehaa! Some references to the code To facilitate the development of the application I used the following two flutter plugins that you can find on flutter packages pages. The plugins used are the following: youtube player Flutter youtube API the application code is very simple and I have given apage division structure fairly clear: Model contains the model of data recovered from API youtube. Now the class also does something more, there is a method that also retrieves the data from youtube and inserts it in the model and some method to recover the data. There is also a system that recovers data only if it is not cached using the shared preference flutter. This mechanism is very useful as the Google API is paid according to the use. Using the cache will save you drastically in calls. if you have never used the cache in flutter you can read this mine other article: flutter shared preferences in depth. Pages contains the APP pages home page category page play video page UI contains a widget to represent the list of videos. I created it separately as a widget to create a view that can be used on both the home page and category and I could also use it in other pages in the future. **Main.dart ** just to initialize view, the model class, load the youtube data and pass it to the respective pages. Using this structure, the code is very simple to maintain, for example the home page contains only the following code: import 'package:flutter/material.dart'; import 'package:youtubeapichannel/Model/YoutubeData.dart'; import 'package:youtubeapichannel/UI/YoutubeVideoList.dart'; YoutubeData youtubeData; class Home extends StatefulWidget { Home(YoutubeData inyoutubeData) { youtubeData = inyoutubeData; } @override _HomeState createState() => new _HomeState(); } class _HomeState extends State<Home> { callAPI() async { setState(() {print('UI Updated'); }); } @override void initState() { super.initState(); //callAPI(); } @override Widget build(BuildContext context) { if(youtubeData!=null) return new YoutubeVideoList(youtubeData.getHomeList()); else return new Container(child:new Text("problem loading data from youtube!")); } } Then simply receive the class containing the list of videos as input and call the YoutubeVideoList plugin that is under the UI to represent them in the form of a list of YouTube videos. The only flaw is that when I made it I was not so expert and I decided to pass the data as a parameter. Today flutter offers better technical provisions for passing data from one view to another, in fact I should make an update and do the usual refactor.
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: