Proper use of RxSwift for chaining requests, flatMap or something else?

Firs of whole I'm new to rxswift, so I think the answer is obvious, but at the moment I can not find a solution on my own.

I have two functions:

func downloadAllTasks() -> Observable<[Task]> func getTaskDetails(taskId: Int64) -> Observable<TaskDetails> 

First, a list of Task objects is loaded using a network request, the second is loaded with task information for an individual task (using its id)

What I want to achieve is to download all the tasks, and then for each task I want to download its details and subscribe to the event that starts when all the details of the tasks are ready.

So, I have to somehow subscribe to Observable <[TaskDetails]>, but I don't know how to do this.

  downloadAllTasks() .flatMap{ ... // flatMap? something else? } .subscribe( onNext: { details in print("tasks details: \(details.map{$0.name})") }) .addDisposableTo(disposeBag) 

// EDIT

Thanks to Sylvan Mosberger the answer: I am much closer to the solution. One problem remains. Now I have something like this:

  downloadAllTasks() .flatMap{ Observable.from($0) } .map{ $0.id } .flatMap{ [unowned self] id in self.getTaskDetails(taskId: id).catchError{ error in print("$$$ Error downloading task \(id)") return .empty() } } .do(onNext: { _ in print(" $$$ single task details downloaded") } ) .toArray() .debug("$$$ task details array debug", trimOutput: false) .subscribe({ _ in print("$$$ all tasks downloaded") }) .addDisposableTo(disposeBag) 

Output signal

 $$$ task details array debug -> subscribed $$$ single task details downloaded $$$ single task details downloaded $$$ single task details downloaded 

There are 3 tasks available, since you can download them, but for some reason, the result toArray () - ( Observable<[TaskDetails]> ) does not create "onNext" after all the details of the task are ready.

// Edit again

Ok, I'm adding a simplified version of the functions that provide observables, maybe this will help something

 func downloadAllTasks() -> Observable<Task> { return Observable.create { observer in //... network request to download tasks //... for task in tasks { observer.onNext(task) } observer.onCompleted() return Disposables.create() } } func getTaskDetails(id: Int64) -> Observable< TaskDetails > { return Observable.create { observer in //... network request to download task details //... observer.onNext(taskDetails) return Disposables.create() } } 
+5
source share
1 answer

With RxSwift, you want to use Observable whenever possible, so I recommend that you reorganize the downloadAllTasks method to return an Observable<Task> . This should be pretty trivial, just looping through the elements, instead of directly emitting an array:

 // In downloadAllTasks() -> Observable<Task> for task in receivedTasks { observable.onNext(task) } 

If this is not possible for any reason, RxSwift also has a statement:

 // Converts downloadAllTasks() -> Observable<[Task]> to Observable<Task> downloadAllTasks().flatMap{ Observable.from($0) } 

In the following code, I will use the refactored method downloadAllTasks() -> Observable<Task> , because it is a cleaner approach.

You can map to perform its tasks in order to get its identifier (if your Task type has the id: Int64 property) and flatMap using the downloadAllTasks function to get an Observable<TaskDetails> :

 let details : Observable<TaskDetails> = downloadAllTasks() .map{ $0.id } .flatMap(getTaskDetails) 

Then you can use the toArray() operator to collect the entire sequence and emit an event containing all the elements in the array:

 let allDetails : Observable<[TaskDetails]> = details.toArray() 

In short, without type annotations and task sharing (so you won’t download them only once):

 let tasks = downloadAllTasks().share() let allDetails = tasks .map{ $0.id } .flatMap(getTaskDetails) .toArray() 

EDIT: Note that this Observable will be erroneous if any of the part loads encounters an error. I'm not quite sure the best way to prevent this, but this works:

 let allDetails = tasks .map{ $0.id } .flatMap{ id in getTaskDetails(id: id).catchError{ error in print("Error downloading task \(id)") return .empty() } } .toArray() 

EDIT2: it will not work if your getTaskDetails returns an observable that never completes. The following is a simple reference implementation of getTaskDetails (with String instead of TaskDetails ) using JSONPlaceholder :

 func getTaskDetails(id: Int64) -> Observable<String> { let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")! return Observable.create{ observer in let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { observer.onError(error) } else if let data = data, let result = String(data: data, encoding: .utf8) { observer.onNext(result) observer.onCompleted() } else { observer.onError("Couldn't get data") } } task.resume() return Disposables.create{ task.cancel() } } } 
+8
source

Source: https://habr.com/ru/post/1260668/


All Articles