Dettagli implementativi clients
Nella seguente sezione verranno discussi i dettagli implementativi delle due diverse tipologie di clients realizzate: Client Desktop e Client Mobile, di questi alcuni risultano essere comuni e verranno descritti nel seguente paragrafo, mentre altri che risultano essere specifici per la diversa tipologia di Client considerato, verranno descritti in un apposita sottosezione a loro dedicata.
Socket
Per poter effettuare l’aggiornamento delle pagine dell’applicazione in real-time, a seguito della ricezione di un nuovo dato, senza dover inviare continue richieste al Server, ma ricevendo direttamente da lui i dati aggiornati, sia per il Client Desktop che per il Client Mobile si è deciso di utilizzare il meccanismo delle socket.
Questo meccanismo prevede all’interno del backend dell’applicazione la presenza di una server socket incaricata di accettare le connessioni dei clients e inviare a loro un messaggio non appena avviene un determinato evento, che nel nostro caso è rappresentato dalla rilevazione dei nuovi parametri e dall’esecuzione di una nuova operazione. Il client socket è invece posizionato all’interno di ciascun Client del sistema. Come si può vedere nel listato 1 , che mostra l’utilizzo delle socket nel Client Desktop, il client socket è incaricato di ricevere il messaggio e, una volta elaborato, aggiornare la View.
Nello specifico, è possibile osservare come avviene l’aggiornamento in real-time del valore di un determinato parametro. In questo caso, a seguito della connessione al Server, il Client si mette in attesa dell’arrivo dei messaggi da parte della socket server. Non appena il messaggio viene ricevuto dal Client, mediante il textMessageHandler
, viene convertito in formato JSON e da questo vengono poi estratte tutte le informazioni necessarie per l’aggiornamento: per prima cosa si verifica che il messaggio di aggiornamento sia relativo alla serra che l’applicazione sta monitorando, poi individuato il parametro per il quale viene notificato il nuovo valore, vengono estratti e di conseguenza aggiornati: il valore corrente, ossia quello nuovo rilevato, la data in cui questo è stato rilevato ed infine, lo storico delle rilevazioni associate ad asso. Una volta effettuato l’aggiornamento di quello che è sostanzialmente il Model, si procede, a richiamare l’aggiornamento della View passandogli i nuovi valori.
HttpClient socketClient = vertx.createHttpClient();
socketClient.webSocket(SOCKET_PORT,
HOST,
"/",
wsC -> {
WebSocket ctx = wsC.result();
if (ctx != null) {
socket = ctx;
ctx.textMessageHandler(msg -> {
JsonObject json = new JsonObject(msg);
if(json.getValue("greenhouseId").equals(this.id)) {
if (json.getValue("parameterName").equals(parameterType.getName())) {
DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy - HH:mm:ss");
this.parameter.getCurrentValue()
.setValue(
Double.valueOf(json.getValue("value").toString())
);
try {
this.parameter.getCurrentValue()
.setDate(formatter.parse(json.getString("date")));
} catch (ParseException e) {
throw new RuntimeException(e);
}
var newHistory = this.parameter.getHistory();
newHistory.remove(0);
ParameterValue newParamValue = new ParameterValueImpl(this.id,
this.parameter.getCurrentValue().getDate(),
this.parameter.getCurrentValue().getValue());
newHistory.add(newParamValue);
this.parameter.setHistory(newHistory);
this.view.updateValues(json.getValue("value").toString() + " " + unit,
this.status(),
this.parameter.getHistoryAsMap());
}
}
});
}
});
[Listato 1] Esempio client socket
Dettagli implementativi del Client Desktop
Come per le altre parti del sistema, anche per il Client Desktop si è deciso di seguire un approccio il più modulare possibile, cosicché l’eventuale aggiunta o rimozione di funzionalità sia il più semplice e meno impattante possibile.
JavaFx
Il Client realizzato, come detto precedentemente, si compone di 3 schermate: la homepage, incaricata di mostrare lo stato della serra; quella di dettaglio di un parametro, nel quale è possibile visualizzare uno snapshot dello storico delle rilevazioni di uno specifico parametro; ed infine, quella di riepilogo delle operazioni svolte, che mostra anche in questo caso uno snapshot delle operazioni effettuate sia in modalità manuale che automatica, potendole filtrare per parametro o per data.
Per l’implementazione dell’interfaccia grafica si è scelto di utilizzare la libreria JavaFX, la quale permette di gestire le parti statiche dell’applicazione attraverso la creazione di layout in formato FXML
FXML è un formato XML che permette di comporre applicazioni JavaFX, separando il codice per la gestione degli elementi dalla parte di visualizzazione dei layout. Inoltre, l’utilizzo di SceneBuilder ha facilitato la creazione delle pagine attraverso il suo ambiente grafico, fornendo una renderizzazione visiva e intuitiva.
La logica di caricamento del file FXML viene racchiusa nella classe ApplicationViewImpl
: tutti i componenti delle subView
hanno un riferimento a tale classe, specificando il file FXML associato, e possono ottenere in automatico il layout caricato.
Di fatto, il componente View rappresenta il Controller associato al layout. Il Controller può ottenere il riferimento agli elementi dell’interfaccia attraverso gli id specificati nell’FXML e mediante il caricatore, ossia FXMLLoader che cercherà di istanziarli e di renderli accessibili. Tale Controller ha il compito di inizializzare gli elementi dell’interfaccia utente e di gestirne il loro comportamento dinamico.
Dettagli implementativi del Client Mobile
In questa sezione vengono illustrati gli aspetti rilevanti per la realizzazione dell’applicazione Mobile, descrivendo le scelte e i dettagli implementativi.
Gestione e condivisione dei dati fra Fragment
Come detto in precedenza Android gestisce i cicli di vita delle Activity
e dei Fragment
in base alle azioni dell’utente oppure ad eventi fuori dal controllo dell’applicazione e quindi il sistema operativo può decidere di distruggere o ricreare il Controller UI
.
Ma se il sistema distrugge il controller dell’interfaccia utente, tutti i dati visualizzati nell’interfaccia utente verranno persi ed è necessario che la nuova Activity
recuperi nuovamente tutti i dati.
Ovviamente per dati semplici l’Activity
può effettuare il loro recupero grazie al metodo onSavedInstanceState()
, ma per dati di grandi dimensioni e dati che devono essere caricati, viene consigliato di gestire quest’aspetto con un nuovo componente ViewModel
.
Architecture Components fornisce la classe di supporto ViewModel per il controller dell’interfaccia. Gli oggetti ViewModel
vengono conservati automaticamente in modo che i dati siano disponibili alla ripresa o al passaggio dell’Activity
o Fragment
successiva.
Nel listato 2 è mostrato un esempio di creazione del ViewModel
. In particolare GreenhouseViewModel
deve estendere dalla classe base AndroidViewModel
e fornire il contesto dell’applicazione.
Il ViewModel
contiene degli oggetti particolari, contenitori di dati, chiamati LiveData
, oppure MutableLiveData
i quali sono oggetti che contengono dati che possono essere modificati. Il ViewModel
contiene anche dei Repository da cui richiedere il caricamento dei dati.
Gli oggetti LiveData
e MutableLiveData
possono essere osservati dai controller, attraverso la loro registrazione come osservatori. In tal modo, ogni volta che la lista si modifica, il controller viene notificato e quindi può aggiornare l’interfaccia.
Per fare ciò, nelle Activity o nei Fragment interessati, tramite il ViewModelProvider
è possibile richiedere l’istanza del ViewModel se è già stato creato dai precedenti controller, oppure richiedere una nuova istanza del ViewModel.
public class GreenhouseViewModelImpl extends AndroidViewModel implements GreenhouseViewModel {
private final MutableLiveData<Plant> plantLiveData;
//...
@Override
public LiveData<Plant> getPlantLiveData() {
return this.plantLiveData;
}
@Override
public void updatePlantInformation(Plant plant) {
this.plantLiveData.postValue(plant);
//...
}
[Listato 2] Esempio di creazione del ViewModel