2015-02-06 15 views
6

Sto costruendo un gioco per 2 giocatori su Android. Il gioco funziona in senso orario, quindi il giocatore 1 attende fino a quando il giocatore 2 ha fatto il suo ingresso e viceversa. Ho un server web in cui eseguo un'API con il framework Slim. Io uso Retrofit sui clienti. Quindi, sui client vorrei fare il polling del mio server web (so che non è l'approccio migliore) ogni X secondi per verificare se ci fosse un input dal player 2 o no, se sì cambia l'interfaccia utente (il gameboard).Android: Polling su server con Retrofit

Gestire il retrofit Mi sono imbattuto in RxJava. Il mio problema è capire se devo usare RxJava o no? Se sì, ci sono esempi davvero semplici per il polling con retrofit? (Dal momento che invio solo un paio di coppie chiave/valore) E se non come farlo con il retrofit, invece?

Ho trovato il thread this qui ma non ha aiutato anche me perché non so ancora se ho bisogno di Retrofit + RxJava a tutti, ci sono forse modi più semplici?

+0

non * necessario * RxJava. Ma funziona bene con il retrofit. – njzk2

+0

Quindi, come implemento il polling con Retrofit senza RxJava? – mrpool89

risposta

14

Diciamo che l'interfaccia definito per retrofit contiene un metodo come questo:

public Observable<GameState> loadGameState(@Query("id") String gameId); 

metodi retrofit possono essere definite in uno dei tre modi:

1.) una semplice sincrono uno:

public GameState loadGameState(@Query("id") String gameId); 

2.) uno che fa una Callback per la gestione asincrona:

public void loadGameState(@Query("id") String gameId, Callback<GameState> callback); 

3.) e quello che restituisce un rxjava Observable, vedere sopra. Penso che se si utilizzerà Retrofit in combinazione con rxjava è più sensato usare questa versione.

In questo modo si potrebbe utilizzare osservabile per una singola richiesta direttamente in questo modo:

mApiService.loadGameState(mGameId) 
.observeOn(AndroidSchedulers.mainThread()) 
.subscribe(new Subscriber<GameState>() { 

    @Override 
    public void onNext(GameState gameState) { 
     // use the current game state here 
    } 

    // onError and onCompleted are also here 
}); 

Se si vuole interrogare ripetutamente il server utilizzando è possibile fornire il "pulse" utilizzando versioni di timer() o interval():

Observable.timer(0, 2000, TimeUnit.MILLISECONDS) 
.flatMap(mApiService.loadGameState(mGameId)) 
.observeOn(AndroidSchedulers.mainThread()) 
.subscribe(new Subscriber<GameState>() { 

    @Override 
    public void onNext(GameState gameState) { 
     // use the current game state here 
    } 

    // onError and onCompleted are also here 
}). 

E 'importante notare che sto usando flatMap qui invece di map - Questo perché il valore di ritorno di loadGameState(mGameId) è di per sé un 012.313..

Ma la versione che si sta utilizzando nel vostro aggiornamento dovrebbe funzionare anche:

Observable.interval(2, TimeUnit.SECONDS, Schedulers.io()) 
.map(tick -> Api.ReceiveGameTurn()) 
.doOnError(err -> Log.e("Polling", "Error retrieving messages" + err)) 
.retry() 
.observeOn(AndroidSchedulers.mainThread()) 
.subscribe(sub); 

Cioè, se ReceiveGameTurn() è definito in modo sincrono come il mio 1.) di cui sopra, si usa al posto di mapflatMap.

In entrambi i casi il onNext del tuo Subscriber verrebbe chiamato ogni due secondi con l'ultimo stato di gioco dal server. È possibile elaborarli uno dopo l'altro limitando l'emissione a un singolo articolo inserendo take(1) prima dello subscribe().

Tuttavia, per quanto riguarda la prima versione: Un unico errore di rete sarebbe stato prima consegnato a onError e quindi osservabile avrebbe smesso di emissione di eventuali altri elementi, rendendo il vostro abbonato inutile e senza input (ricordate, onError può essere chiamato solo una volta). Per ovviare a questo problema, è possibile utilizzare uno qualsiasi dei metodi onError* di rxjava per "reindirizzare" l'errore su onNext.

Ad esempio:

Observable.timer(0, 2000, TimeUnit.MILLISECONDS) 
.flatMap(new Func1<Long, Observable<GameState>>(){ 

    @Override 
    public Observable<GameState> call(Long tick) { 
     return mApiService.loadGameState(mGameId) 
     .doOnError(err -> Log.e("Polling", "Error retrieving messages" + err)) 
     .onErrorResumeNext(new Func1<Throwable, Observable<GameState>(){ 
      @Override 
      public Observable<GameState> call(Throwable throwable) { 
       return Observable.emtpy()); 
      } 
     }); 
    } 
}) 
.filter(/* check if it is a valid new game state */) 
.take(1) 
.observeOn(AndroidSchedulers.mainThread()) 
.subscribe(new Subscriber<GameState>() { 

    @Override 
    public void onNext(GameState gameState) { 
     // use the current game state here 
    } 

    // onError and onCompleted are also here 
}). 

questo sarà ogni due secondi: * uso Retrofit per ottenere lo stato di gioco in corso dal server * filtrare quelli non validi * prendere la prima valida una * e la Annullamento

in caso di errore: * stamperà un messaggio di errore nel doOnNext * e altrimenti ignorare l'errore: onErrorResumeNext sarà "consumare" il onError -Event (ad es. il tuo numero onError non verrà chiamato) e lo sostituirà con nulla (Observable.empty()).

E, per quanto riguarda la seconda versione: In caso di un errore di rete retry sarebbe sottoscrivere nuovamente all'intervallo immediatamente - e dal momento che interval emette il primo numero intero immediatamente al momento della sottoscrizione della domanda successiva sarebbe stata inviata immediatamente, anche - e non dopo 3 secondi come probabilmente si vuole ...

Nota finale: inoltre, se il tuo stato di gioco è abbastanza grande, si potrebbe anche prima solo interrogare il server per chiedere se un nuovo stato è disponibile e solo in caso di risposta positiva ricaricare il nuovo stato di gioco.

Se avete bisogno di esempi più elaborati, si prega di chiedere.

UPDATE: ho riscritto parti di questo post e aggiunto ulteriori informazioni in mezzo.

UPDATE 2: ho aggiunto un esempio completo di trattamento effettuato con onErrorResumeNext errore.

+0

Ho appena aggiunto un altro esempio di gestione degli errori. –

+0

Dovrò dare un'occhiata alla gestione degli errori in seguito. Ma non usiamo 'doOnError' per la gestione degli errori? – mrpool89

+0

Dipende da cosa intendi per "gestione". 'doOnError' fa semplicemente _does_ qualcosa quando viene passato un errore nella linea di tubo Observable. Ma non è davvero inteso, penso, come il luogo per risolvere l'errore. Per impostazione predefinita, gli errori possono essere gestiti in "onError", ma anche questo termina completamente. Ci sono altre opzioni per reagire agli errori, che mantengono intatto l'abbonamento: emetti qualcos'altro invece ('onErrorResumeNext'),' riprova', ecc. –

1

Grazie, alla fine ho fatto in modo simile basato su post a cui mi sono riferito nella mia domanda. Ecco il mio codice per la società:

Subscriber sub = new Subscriber<Long>() { 
     @Override 
     public void onNext(Long _EmittedNumber) 
     { 
      GameTurn Turn = Api.ReceiveGameTurn(mGameInfo.GetGameID(), mGameInfo.GetPlayerOneID()); 
      Log.d("Polling", "onNext: GameID - " + Turn.GetGameID()); 
     } 

     @Override 
     public void onCompleted() { 
      Log.d("Polling", "Completed!"); 
     } 

     @Override 
     public void onError(Throwable e) { 
      Log.d("Polling", "Error: " + e); 
     } 
    }; 

    Observable.interval(3, TimeUnit.SECONDS, Schedulers.io()) 
      // .map(tick -> Api.ReceiveGameTurn()) 
      // .doOnError(err -> Log.e("Polling", "Error retrieving messages" + err)) 
      .retry() 
      .subscribe(sub); 

Il problema ora è che ho bisogno di interrompere emettono quando ricevo una risposta positiva (un GameTurn). Ho letto del metodo takeUntil in cui avrei dovuto passare un altro Observable che avrebbe emesso qualcosa una volta che avrebbe attivato la chiusura del mio polling. Ma non sono sicuro di come implementarlo. In base alla soluzione, il metodo API restituisce un valore Observable come mostrato sul sito Web di Retrofit. Forse questa è la soluzione? Quindi come funzionerebbe?

UPDATE: consideravo @ consigli david.miholas e ha cercato il suo suggerimento con tentativi e filtro. Sotto puoi trovare il codice per l'inizializzazione del gioco. Il sondaggio dovrebbe funzionare in modo identico: Player1 inizia una nuova partita -> sondaggi per avversario, Player2 si unisce al gioco -> server invia a Player1 ID avversario -> polling terminato.

Subscriber sub = new Subscriber<String>() { 
     @Override 
     public void onNext(String _SearchOpponentResult) {} 

     @Override 
     public void onCompleted() { 
      Log.d("Polling", "Completed!"); 
     } 

     @Override 
     public void onError(Throwable e) { 
      Log.d("Polling", "Error: " + e); 
     } 
    }; 

    Observable.interval(3, TimeUnit.SECONDS, Schedulers.io()) 
      .map(tick -> mApiService.SearchForOpponent(mGameInfo.GetGameID())) 
      .doOnError(err -> Log.e("Polling", "Error retrieving messages: " + err)) 
      .retry() 
      .filter(new Func1<String, Boolean>() 
      { 
       @Override 
       public Boolean call(String _SearchOpponentResult) 
       { 
        Boolean OpponentExists; 
        if (_SearchOpponentResult != "0") 
        { 
         Log.e("Polling", "Filter " + _SearchOpponentResult); 
         OpponentExists = true; 
        } 
        else 
        { 
         OpponentExists = false; 
        } 
        return OpponentExists; 

       } 
      }) 
      .take(1) 
      .subscribe(sub); 

L'emissione è corretto, ma ottengo questo messaggio di log su ogni emettono:

E/Polling﹕ Error retrieving messages: java.lang.NullPointerException 

Apperently doOnError viene attivato su ogni emettere. Normalmente otterrei alcuni log di debug di Retrofit su ogni emit, il che significa che mApiService.SearchForOpponent non verrà chiamato. Cosa faccio di sbagliato?

+0

Forse potresti spiegare in modo più dettagliato cosa esattamente vuoi ottenere: all'inizio pensavo volevi eseguire il polling del server ogni due secondi per tutto il tempo in cui il tuo programma è in esecuzione, ma ora dici di voler interrompere l'emissione di elementi dopo aver ottenuto una risposta dal server ... Potresti ovviamente inserire un ' prendi (1) 'prima del' subscribe' per limitare l'emissione a un oggetto, ma potresti anche dare un'occhiata a 'retryWhen' - forse è più vicino a quello che vuoi realmente ... –

+0

È un gioco a turni , quindi un lettore attende/interroga il server per le modifiche. Quando l'altro giocatore imposta un input, invia il suo turno, che viene memorizzato in un database. Il primo giocatore poi registra un risultato MySQL positivo con le informazioni di svolta, necessarie per aggiornare l'interfaccia utente e interrompere il polling perché le informazioni sono state ricevute. La wiki ufficiale di Rx dice: _ "se una fonte osservabile emette un errore, passa quell'errore ad un altro osservabile per determinare se iscriversi nuovamente alla fonte" _ che non mi aiuta – mrpool89

+0

E la versione che hai postato ha fatto ciò che vuoi eccetto che non si è fermato dopo un oggetto emesso? In tal caso, puoi semplicemente mettere 'take (1)' tra 'retry()' e 'subscribe()' - in questo modo 'take' solo" lascia passare "un elemento e poi annulla l'iscrizione e quindi ferma la generazione di zecche in 'intervallo'. Tuttavia, penso in caso di es. un errore di rete 'retry' riacquisterebbe immediatamente l'intervallo '- e poiché' interval' emette immediatamente il primo numero intero al momento dell'iscrizione, anche la richiesta successiva verrà inviata immediatamente - e non dopo 3 secondi come probabilmente ... –