Android: Polling a server with Retrofit

后端 未结 2 1968
萌比男神i
萌比男神i 2021-02-04 17:09

I\'m building a 2 Player game on Android. The game works turnwise, so player 1 waits until player 2 made his input and vice versa. I have a webserver where I run an API with the

相关标签:
2条回答
  • 2021-02-04 17:46

    Thank you, I finally made it in a similar way based the post I referred to in my question. Here's my code for now:

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

    The problem now is that I need to terminate emitting when I get a positive answer (a GameTurn). I read about the takeUntil method where I would need to pass another Observable which would emit something once which would trigger the termination of my polling. But I'm not sure how to implement this. According to your solution, your API method returns an Observable like it is shown on the Retrofit website. Maybe this is the solution? So how would it work?

    UPDATE: I considered @david.miholas advices and tried his suggestion with retry and filter. Below you can find the code for the game initialization. The polling should work identically: Player1 starts a new game -> polls for opponent, Player2 joins the game -> server sends to Player1 opponent's ID -> polling terminated.

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

    The emission is correct, however I get this log message on every emit:

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

    Apperently doOnError is triggered on every emit. Normally I would get some Retrofit debug logs on every emit which means that mApiService.SearchForOpponent won't get called. What do I do wrong?

    0 讨论(0)
  • 2021-02-04 18:03

    Let's say the interface you defined for Retrofit contains a method like this:

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

    Retrofit methods can be defined in one of three ways:

    1.) a simple synchronous one:

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

    2.) one that take a Callback for asynchronous handling:

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

    3.) and the one that returns an rxjava Observable, see above. I think if you are going to use Retrofit in conjunction with rxjava it makes the most sense to use this version.

    That way you could just use the Observable for a single request directly like this:

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

    If you want to repeatedly poll the server using you can provide the "pulse" using versions of timer() or 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
    }).
    

    It is important to note that I am using flatMap here instead of map - that's because the return value of loadGameState(mGameId) is itself an Observable.

    But the version you are using in your update should work too:

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

    That is, if ReceiveGameTurn() is defined synchronously like my 1.) above, you would use map instead of flatMap.

    In both cases the onNext of your Subscriber would be called every two seconds with the latest game state from the server. You can process them one after another of limit the emission to a single item by inserting take(1) before subscribe().

    However, regarding the first version: A single network error would be first delivered to onError and then the Observable would stop emitting any more items, rendering your Subscriber useless and without input (remember, onError can only be called once). To work around this you could use any of the onError* methods of rxjava to "redirect" the failure to onNext.

    For example:

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

    This will every two seconds: * use Retrofit to get the current game state from the server * filter out invalid ones * take the first valid one * and the unsubscribe

    In case of an error: * it will print an error message in doOnNext * and otherwise ignore the error: onErrorResumeNext will "consume" the onError-Event (i.e. your Subscriber's onError will not be called) and replaces it with nothing (Observable.empty()).

    And, regarding the second version: In case of a network error retry would resubscribe to the interval immediately - and since interval emits the first Integer immediately upon subscription the next request would be sent immediately, too - and not after 3 seconds as you probably want...

    Final note: Also, if your game state is quite large, you could also first just poll the server to ask whether a new state is available and only in case of a positive answer reload the new game state.

    If you need more elaborate examples, please ask.

    UPDATE: I've rewritten parts of this post and added more information in between.

    UPDATE 2: I've added a full example of error handling with onErrorResumeNext.

    0 讨论(0)
提交回复
热议问题