2015-02-06 6 views
6

Ich baue ein 2-Spieler-Spiel auf Android. Das Spiel funktioniert abwechselnd, so dass Spieler 1 wartet, bis Spieler 2 seine Eingabe gemacht hat und umgekehrt. Ich habe einen Webserver, auf dem ich eine API mit dem Slim Framework betreibe. Auf den Clients verwende ich Retrofit. Also auf den Clients möchte ich meinen Webserver (ich weiß, es ist nicht der beste Ansatz) alle X Sekunden abfragen, ob es eine Eingabe von Spieler 2 gab oder nicht, wenn ja, ändere die Benutzeroberfläche (das Spielbrett).Android: Abruf eines Servers mit Retrofit

Mit Retrofit umgehen Ich stieß auf RxJava. Mein Problem ist herauszufinden, ob ich RxJava benutzen muss oder nicht? Wenn ja, gibt es wirklich einfache Beispiele für Umfragen mit Nachrüstung? (Da ich nur ein paar Schlüssel/Wert-Paare aussende) Und wenn nicht wie geht es mit Nachrüstung statt?

Ich habe this Thread hier gefunden, aber es hat mir auch nicht geholfen, weil ich immer noch nicht weiß, ob ich Retrofit + RxJava überhaupt brauche, gibt es vielleicht einfachere Wege?

+0

Sie brauchen * RxJava * nicht. Aber es funktioniert gut mit Retrofit. – njzk2

+0

Wie implementiere ich die Abfrage mit Retrofit ohne RxJava? – mrpool89

Antwort

14

Lassen Sie uns sagen, dass die Schnittstelle, die Sie für Retrofit definiert eine Methode wie folgt enthält:

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

Nachrüstverfahren können in eine von drei Arten definiert werden:

1.) eine einfache synchrone ein:

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

2.) ein, die ein Callback für asynchrone Handhabung nehmen:

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

3.) und die, die einen rxjava Observable zurückgibt, siehe oben. Ich denke, wenn Sie Retrofit in Verbindung mit rxjava verwenden, ist es am sinnvollsten, diese Version zu verwenden.

Auf diese Weise können nur die beobachtbaren für eine einzige Anforderung wie diese direkt verwenden:

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 
}); 

Wenn Sie wiederholt den Server abzufragen verwenden Sie die „Impuls“ zur Verfügung stellen kann Versionen von timer() oder interval() mit:

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 
}). 

Es ist wichtig, dass ich flatMap hier statt map bin mit zu beachten - das ist, weil der Rückgabewert von loadGameState(mGameId) selbst ein 012.313..

Aber die Version, die Sie in Ihrem Update verwenden, sollten auch funktionieren:

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); 

Das heißt, wenn ReceiveGameTurn() synchron wie mein 1. definiert ist) oben, würden Sie map statt flatMap verwenden.

In beiden Fällen würde die onNext Ihrer Subscriber alle zwei Sekunden mit dem neuesten Spielstand vom Server aufgerufen werden. Sie können sie nacheinander verarbeiten, um die Emission auf einen einzelnen Artikel zu beschränken, indem Sie take(1) vor subscribe() einfügen.

jedoch in Bezug auf die erste Version: Ein einzelner Netzwerkfehler zuerst onError geliefert werden würde, und dann würde die beobachtbare stoppen noch mehr Produkte zu emittieren, Ihre Abonnenten nutzlos und ohne Eingabe Rendering (denken Sie daran, onError nur einmal aufgerufen werden kann). Um dies zu umgehen, können Sie eine der onError* Methoden von rxjava verwenden, um den Fehler auf onNext umzuleiten.

Zum Beispiel:

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 
}). 

Dies werden alle zwei Sekunden: * Verwendung Retrofit den aktuellen Spielzustand vom Server * herauszuzufiltern ungültig derjenigediejenigedasjenige * nehmen Sie die ersten gültigen * zu erhalten und die abmelden

im Fall eines Fehlers: * es wird eine Fehlermeldung in doOnNext * und ansonsten ignoriert die Fehler drucken: onErrorResumeNext wird „verbrauchen“ die onError -Event (d.h. Ihre Subscriber 's onError wird nicht aufgerufen) und ersetzt sie durch nichts (Observable.empty()).

Und die zweite Version in Bezug auf: Im Falle eines Netzwerkfehlers retry dem Intervall unmittelbar resubscribe würde - und da interval die erste Integer sofort bei der Zeichnung emittiert die nächste Anforderung würde sofort auch gesendet werden - und nicht nach 3 Sekunden wie Sie wahrscheinlich wollen ...

Schlussbemerkung: Auch wenn Ihr Spielzustand ziemlich groß ist, könnten Sie auch zuerst den Server fragen, ob ein neuer Zustand verfügbar ist und nur im Falle einer positiven Antwort den neuer Spielstatus

Wenn Sie ausführlichere Beispiele benötigen, fragen Sie bitte.

AKTUALISIEREN: Ich habe Teile dieses Beitrags umgeschrieben und dazwischen weitere Informationen hinzugefügt.

UPDATE 2: Ich habe ein vollständiges Beispiel der Fehlerbehandlung mit onErrorResumeNext hinzugefügt.

+0

Ich habe gerade ein weiteres Beispiel für die Fehlerbehandlung hinzugefügt. –

+0

Ich werde später auf die Fehlerbehandlung schauen müssen. Aber benutzen wir 'doOnError' nicht zur Fehlerbehandlung? – mrpool89

+0

Das hängt davon ab, was Sie unter "Handhabung" verstehen. 'doOnError' ist einfach _etwas_, wenn ein Fehler in der Observable-Pipe-Zeile übergeben wird. Aber es ist nicht wirklich, denke ich, als der Ort, um den Fehler zu beheben. Standardmäßig können Fehler in 'onError' behandelt werden - das beendet aber auch das Observable vollständig. Es gibt andere Möglichkeiten, auf Fehler zu reagieren, die das Abonnement intakt halten: stattdessen etwas anderes ausgeben ('onErrorResumeNext'),' retry', etc. –

1

Vielen Dank, ich habe es endlich in ähnlicher Weise basierend auf der post ich in meiner Frage erwähnt. Hier ist mein Code für jetzt:

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); 

Das Problem ist jetzt, dass ich emittierende zu beenden, wenn ich eine positive Antwort bekommen (a GameTurn). Ich lese über die takeUntil Methode, wo ich eine andere Observable übergeben müsste, die etwas einmal ausstrahlen würde, was die Beendigung meiner Abfrage auslösen würde. Aber ich bin mir nicht sicher, wie ich das umsetzen soll. Gemäß Ihrer Lösung gibt Ihre API-Methode eine Observable zurück, so wie sie auf der Retrofit-Website angezeigt wird. Vielleicht ist das die Lösung? Wie würde es funktionieren?

UPDATE: Ich dachte @ david.miholas Ratschläge und versuchte seinen Vorschlag mit Wiederholung und Filter. Im Folgenden finden Sie den Code für die Initialisierung des Spiels. Das Polling sollte identisch funktionieren: Spieler1 startet ein neues Spiel -> fragt nach Gegner, Spieler2 tritt dem Spiel bei -> Server sendet an Spieler1 Gegenspieler-ID -> Polling beendet.

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); 

Die Emission ist richtig, jedoch erhalte ich diese Lognachricht auf jeden emittieren:

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

Apperently doOnError wird auf jeden ausgelöst emittieren. Normalerweise würde ich einige Retrofit-Debug-Logs für jeden emit bekommen, was bedeutet, dass mApiService.SearchForOpponent nicht aufgerufen wird. Was mache ich falsch?

+0

Vielleicht könntest du genauer erklären, was genau du erreichen willst: Zuerst dachte ich Sie wollten den Server alle zwei Sekunden für die gesamte Laufzeit Ihres Programms abfragen, aber jetzt sagen Sie, dass Sie die Ausgabe von Elementen beenden möchten, nachdem Sie eine erfolgreiche Antwort vom Server erhalten haben ... Sie könnten natürlich einfach ein '' einfügen nimm (1) 'vor dem' subscribe', um die Emission auf einen Gegenstand zu beschränken, aber du könntest auch einen 'retryWhen' sehen - vielleicht liegt das näher an dem, was du eigentlich willst ... –

+0

Es ist ein rundenbasiertes Spiel , so wartet ein Spieler den Server auf Änderungen. Wenn der andere Spieler eine Eingabe setzt, sendet er seinen Zug, der in einer Datenbank gespeichert ist. Der erste Spieler erhält dann ein positives MySQL-Ergebnis mit den Rundeninformationen, die benötigt werden, um die Benutzeroberfläche zu aktualisieren und die Abfrage zu beenden, da die Informationen empfangen wurden. Das offizielle Rx-Wiki sagt: _ "Wenn eine Quelle Observable einen Fehler ausgibt, übergeben Sie diesen Fehler an ein anderes Observable, um festzustellen, ob die Quelle neu zu abonnieren" _ was mir nicht hilft atm – mrpool89

+0

Und die Version, die Sie veröffentlicht haben, was Sie wollen außer dass es nicht nach einem ausgegebenen Gegenstand aufhörte? In diesem Fall kann man 'take (1)' einfach zwischen 'retry()' und 'subscribe()' setzen - auf diese Weise würde "take" nur einen Gegenstand "durchlassen" und sich dann abmelden und damit die Generierung stoppen tickt in 'Intervall'. Ich denke jedoch im Fall von z.B. Ein Netzwerkfehler "retry" würde sofort zum "Intervall" zurückkehren - und da "interval" den ersten Integer sofort nach dem Abonnement ausgibt, wird die nächste Anfrage sofort gesendet - und nicht nach 3 Sekunden, wie Sie wahrscheinlich wollen ... –