RxJava

2014-06-16 18 views
11

में एपीआई अपवादों को संभालना मैं वर्तमान में आरएक्सजेवा के आसपास अपने सिर को लपेटने की कोशिश कर रहा हूं, लेकिन मुझे एक सुंदर तरीके से सेवा कॉल अपवादों को संभालने में थोड़ी परेशानी हो रही है।RxJava

असल में, मेरे पास एक (रेट्रोफिट) सेवा है जो Observable<ServiceResponse> देता है। ServiceResponse इसलिए की तरह परिभाषित किया गया है:

public class ServiceResponse { 
    private int status; 
    private String message; 
    private JsonElement data; 

    public JsonElement getData() { 
     return data; 
    } 

    public int getStatus() { 
     return status; 
    } 

    public String getMessage() { 
     return message; 
    } 
} 

अब मैं क्या चाहते हैं एक List<Account> डेटा JsonElement क्षेत्र के भीतर निहित है कि सामान्य प्रतिक्रिया (मुझे लगता है आप परवाह नहीं है Account वस्तु कैसा दिखता मैप करने के लिए है, इसलिए मैं जीता इसके साथ पोस्ट प्रदूषित नहीं करें)। निम्नलिखित कोड सफलता मामले के लिए वास्तव में अच्छी तरह से काम करता है, लेकिन मैं अपने एपीआई अपवाद को संभालने के लिए एक अच्छा तरीका नहीं मिल सकता है:

service.getAccounts() 
     .subscribeOn(Schedulers.io()) 
     .observeOn(AndroidSchedulers.mainThread()) 
     .map(new Func1<ServiceResponse, AccountData>() { 
       @Override 
       public AccountData call(ServiceResponse serviceResponse) { 

        // TODO: ick. fix this. there must be a better way... 
        ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus()); 
        switch (responseType) { 
         case SUCCESS: 
          Gson gson = new GsonBuilder().create(); 
          return gson.fromJson(serviceResponse.getData(), AccountData.class); 
         case HOST_UNAVAILABLE: 
          throw new HostUnavailableException(serviceResponse.getMessage()); 
         case SUSPENDED_USER: 
          throw new SuspendedUserException(serviceResponse.getMessage()); 
         case SYSTEM_ERROR: 
         case UNKNOWN: 
         default: 
          throw new SystemErrorException(serviceResponse.getMessage()); 
        } 
       } 
     }) 
     .map(new Func1<AccountData, List<Account>>() { 
       @Override 
       public List<Account> call(AccountData accountData) { 
        Gson gson = new GsonBuilder().create(); 
        List<Account> res = new ArrayList<Account>(); 
        for (JsonElement account : accountData.getAccounts()) { 
         res.add(gson.fromJson(account, Account.class)); 
        } 
        return res; 
       } 
     }) 
     .subscribe(accountsRequest); 

वहाँ यह करने के लिए एक बेहतर तरीका है? यह काम करता है, ऑनर मेरे पर्यवेक्षक को आग लगाएगा, और मुझे जो त्रुटि आई है, उसे प्राप्त होगा, लेकिन यह निश्चित रूप से ऐसा नहीं लगता है कि मैं यह सही कर रहा हूं।

अग्रिम धन्यवाद!

संपादित करें:

मुझे स्पष्ट मैं वास्तव में क्या हासिल करना चाहते हैं करते हैं:

मैं एक वर्ग है कि यूआई से कहा जा सकता है करना चाहते हैं (उदाहरण के लिए एक गतिविधि, या टुकड़ा, या जो कुछ भी) । यही कारण है कि वर्ग तो जैसे एक पैरामीटर के रूप में एक Observer<List<Account>> ले जाएगा:

public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) { 
    ... 
} 

कि विधि एक सदस्यता कि सदस्यता रद्द कर दी हो सकता है जब यूआई/अलग है को नष्ट कर दिया/आदि लौट आते हैं।

पैरामीटरयुक्त पर्यवेक्षक खाते की सूची में उत्तीर्ण सफल प्रतिक्रियाओं के लिए अगला पर संभाल लेंगे। ऑनरर किसी भी अपवाद को संभालेगा, लेकिन किसी भी एपीआई अपवादों को भी पारित करेगा (उदाहरण के लिए यदि प्रतिक्रिया स्थिति! = 200 हम एक थ्रोबल बनाएंगे और इसे एरर पर पास करेंगे)। आदर्श रूप में मैं अपवाद को "फेंकना" नहीं चाहता हूं, मैं इसे सीधे पर्यवेक्षक को पास करना चाहता हूं। यही वह उदाहरण है जो मैं देखता हूं।

जटिलता यह है कि मेरी रेट्रोफिट सेवा ServiceResponse ऑब्जेक्ट देता है, इसलिए मेरा पर्यवेक्षक उस पर सब्सक्राइब नहीं कर सकता है। सबसे अच्छा मैं ले कर आए हैं तो जैसे मेरी पर्यवेक्षक के चारों ओर एक ऑब्जर्वर आवरण बनाने के लिए, यह है:

@Singleton 
public class AccountsDatabase { 

    private AccountsService service; 

    private List<Account> accountsCache = null; 
    private PublishSubject<ServiceResponse> accountsRequest = null; 

    @Inject 
    public AccountsDatabase(AccountsService service) { 
     this.service = service; 
    } 

    public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) { 

     ObserverWrapper observerWrapper = new ObserverWrapper(observer); 

     if (accountsCache != null) { 
      // We have a cached value. Emit it immediately. 
      observer.onNext(accountsCache); 
     } 

     if (accountsRequest != null) { 
      // There's an in-flight network request for this section already. Join it. 
      return accountsRequest.subscribe(observerWrapper); 
     } 

     if (accountsCache != null && !forceRefresh) { 
      // We had a cached value and don't want to force a refresh on the data. Just 
      // return an empty subscription 
      observer.onCompleted(); 
      return Subscriptions.empty(); 
     } 

     accountsRequest = PublishSubject.create(); 

     accountsRequest.subscribe(new ObserverWrapper(new EndObserver<List<Account>>() { 

      @Override 
      public void onNext(List<Account> accounts) { 
       accountsCache = accounts; 
      } 

      @Override 
      public void onEnd() { 
       accountsRequest = null; 
      } 
     })); 

     Subscription subscription = accountsRequest.subscribe(observerWrapper); 

     service.getAccounts() 
       .subscribeOn(Schedulers.io()) 
       .observeOn(AndroidSchedulers.mainThread()) 
       .subscribe(accountsRequest); 

     return subscription; 
    } 

    static class ObserverWrapper implements Observer<ServiceResponse> { 

     private Observer<List<Account>> observer; 

     public ObserverWrapper(Observer<List<Account>> observer) { 
      this.observer = observer; 
     } 

     @Override 
     public void onCompleted() { 
      observer.onCompleted(); 
     } 

     @Override 
     public void onError(Throwable e) { 
      observer.onError(e); 
     } 

     @Override 
     public void onNext(ServiceResponse serviceResponse) { 
      ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus()); 
      switch (responseType) { 
       case SUCCESS: 
        Gson gson = new GsonBuilder().create(); 
        AccountData accountData = gson.fromJson(serviceResponse.getData(), AccountData.class); 
        List<Account> res = new ArrayList<>(); 
        for (JsonElement account : accountData.getAccounts()) { 
         res.add(gson.fromJson(account, Account.class)); 
        } 
        observer.onNext(res); 
        observer.onCompleted(); 
        break; 
       default: 
        observer.onError(new ApiException(serviceResponse.getMessage(), responseType)); 
        break; 
      } 
     } 
    } 
} 

मैं अभी भी है कि मैं यह सही है, हालांकि उपयोग नहीं कर रहा है। मैंने निश्चित रूप से पहले किसी ऑब्जर्वरवापर का उपयोग करके किसी और को नहीं देखा है। शायद मुझे आरएक्सजेवा का उपयोग नहीं करना चाहिए, हालांकि साउंडक्लाउड और नेटफ्लिक्स के लोगों ने वास्तव में मुझे अपनी प्रस्तुतियों में बेच दिया और मैं इसे सीखने के लिए उत्सुक हूं।

उत्तर

12

कृपया नीचे पढ़ें मैंने एक संपादन जोड़ा है।

आरएक्सजेवा के साथ एक एक्शन/फनक/पर्यवेक्षक के भीतर फेंकना बिल्कुल सही है। अपवाद आपके पर्यवेक्षक को ढांचे के आधार पर प्रचारित किया जाएगा। यदि आप केवल इरर पर कॉल करने के लिए खुद को सीमित करते हैं तो आप ऐसा करने के लिए खुद को घुमाएंगे।

ऐसा कहा जा रहा है कि एक सुझाव यह है कि इस रैपर को आसानी से हटा दें और एक सरल सत्यापन सेवा के भीतर कार्रवाई करें .getAccount ... पर्यवेक्षकों की श्रृंखला।

मैं एक मानचित्र (नया MapValidResponseToAccountList) के साथ जंजीर (नया ValidateServiceResponseOrThrow) का उपयोग करता हूं। वे साधारण वर्ग हैं जो अवलोकन श्रृंखला को थोड़ा अधिक पठनीय रखने के लिए आवश्यक कोड लागू करते हैं।

यहां आपका भार है जो मैंने सुझाए गए उपयोग का सरलीकृत किया है।

public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) { 
    if (accountsCache != null) { 
     // We have a cached value. Emit it immediately. 
     observer.onNext(accountsCache); 
    } 

    if (accountsRequest != null) { 
     // There's an in-flight network request for this section already. Join it. 
     return accountsRequest.subscribe(observer); 
    } 

    if (accountsCache != null && !forceRefresh) { 
     // We had a cached value and don't want to force a refresh on the data. Just 
     // return an empty subscription 
     observer.onCompleted(); 
     return Subscriptions.empty(); 
    } 

    accountsRequest = PublishSubject.create(); 
    accountsRequest.subscribe(new EndObserver<List<Account>>() { 

     @Override 
     public void onNext(List<Account> accounts) { 
      accountsCache = accounts; 
     } 

     @Override 
     public void onEnd() { 
      accountsRequest = null; 
     } 
    }); 

    Subscription subscription = accountsRequest.subscribe(observer); 

    service.getAccounts() 
      .doOnNext(new ValidateServiceResponseOrThrow()) 
      .map(new MapValidResponseToAccountList()) 
      .subscribeOn(Schedulers.io()) 
      .observeOn(AndroidSchedulers.mainThread()) 
      .subscribe(accountsRequest); 

    return subscription; 
} 

private static class ValidateResponseOrThrow implements Action1<ServiceResponse> { 
     @Override 
     public void call(ServiceResponse response) { 
      ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus()); 
      if (responseType != SUCCESS) 
       throw new ApiException(serviceResponse.getMessage(), responseType)); 
     } 
    } 

private static class MapValidResponseToAccountList implements Func1<ServiceResponse, List<Account>> { 
    @Override 
    public Message call(ServiceResponse response) { 
     // add code here to map the ServiceResponse into the List<Accounts> as you've provided already 
    } 
} 

संपादित करें: जब तक किसी को नहीं तो कहते हैं मुझे लगता है कि यह flatMap का उपयोग कर त्रुटियों वापस जाने के लिए सबसे अच्छा अभ्यास है। मैंने अतीत में अपवाद से अपवाद फेंक दिया है लेकिन मुझे विश्वास नहीं है कि यह अनुशंसित तरीका है।

यदि आप फ्लैटमैप का उपयोग करते हैं तो आपके पास क्लीनर अपवाद स्टैक होगा। यदि आप किसी एक्शन के अंदर से फेंकते हैं तो अपवाद स्टैक में वास्तव में rx.exceptions.OnErrorThrowable$OnNextValue अपवाद होगा जो आदर्श नहीं है।

मुझे इसके बजाय flatMap का उपयोग करके उपरोक्त उदाहरण प्रदर्शित करने दें।

private static class ValidateServiceResponse implements rx.functions.Func1<ServiceResponse, Observable<ServiceResponse>> { 
    @Override 
    public Observable<ServiceResponse> call(ServiceResponse response) { 
     ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus()); 
     if (responseType != SUCCESS) 
      return Observable.error(new ApiException(serviceResponse.getMessage(), responseType)); 
     return Observable.just(response); 
    } 
} 

service.getAccounts() 
    .flatMap(new ValidateServiceResponse()) 
    .map(new MapValidResponseToAccountList()) 
    .subscribeOn(Schedulers.io()) 
    .observeOn(AndroidSchedulers.mainThread()) 
    .subscribe(accountsRequest); 

जैसा कि आप देख सकते हैं कि अंतर सूक्ष्म है। ValidateServiceResponse अब के बजाय Func1 लागू करता है और हम अब throw कीवर्ड का उपयोग नहीं कर रहे हैं। हम इसके बजाय का उपयोग करते हैं। मेरा मानना ​​है कि यह अपेक्षित आरएक्स अनुबंध के साथ बेहतर फिट बैठता है।

+0

धन्यवाद त्रुटि के बारे में यह अच्छा लेख पढ़ सकते हैं, इस कोड को निश्चित रूप से अधिक पठनीय है :) –

+1

यह सिर्फ मुझे है, या नहीं एक Action1 कॉल से एक अपवाद फेंकने के लिए संभव है। यहां आपके उदाहरण में आप केवल नए कॉल कर रहे हैं लेकिन फेंक नहीं सकते हैं। तो क्या यह सिर्फ मंजिल पर नहीं छोड़ा जाएगा और आगे के बजाय अग्रेषित किया जाएगा? – schwiz

+0

@schwiz एक क्रिया के अंदर से अपवाद फेंकना संभव है। मैंने स्पष्टता के लिए लापता फेंक स्टेटमेंट जोड़ा है। लेकिन ऐसा कहा जा रहा है कि मैंने अपनी पोस्ट में एक संपादन जोड़ा है क्योंकि मैं अब इस दृष्टिकोण का उपयोग नहीं करता हूं। मैं flatMap का उपयोग करें। –