[SOLVED] RxSwift | Best way to make consecutive network requests?

Issue

I’ve been practicing RxSwift recently, but I’m running into a problem in making network requests.

The question is how can I make consecutive network requests .

For example, in Github api, I should use https://api.github.com/user/starred/{\owner}/{\repository_name} to check if the user starred the repository or not.

It should be sent after I received the data requested but I’m having a hard time to implement this.

Here’s what I’ve tried so far:

import RxSwift

// Struct used to encode response
struct RepositoryResponse: Codable  {
    let items: [Item]
    
    enum CodingKeys: String, CodingKey {
        case items
    }
    
    struct Item: Codable {
        let fullName: String
        
        enum CodingKeys: String, CodingKey {
            case fullName = "full_name"
        }
    }
}

// Actual data for further use
struct Repository {
    let item: RepositoryResponse.Item
    
    var fullName: String {
        return item.fullName
    }
    
    var isStarred: Bool
    
    init(_ item: RepositoryData, isStarred: Bool) {
        self.item = item
        self.isStarred = isStarred
    }
}

// Url components
var baseUrl = URLComponents(string: "https://api.github.com/search/repositories")   // base url
let query = URLQueryItem(name: "q", value: "flutter")    // Keyword. flutter for this time.
let sort = URLQueryItem(name: "sort", value: "stars")   // Sort by stars
let order = URLQueryItem(name: "order", value: "desc")  // Desc order
baseUrl?.queryItems = [query, sort, order]


// Observable expected to return Observable<[Repository]>
Observable<URL>.of((baseUrl?.url)!)
    .map { URLRequest(url: $0) }
    .flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
        return URLSession.shared.rx.response(request: request)
    }
    .filter { response, data in
        return 200..<300 ~= response.statusCode
    }
    .map { _, data -> [RepositoryResponse.Item] in
        let decoder = JSONDecoder()
        if let decoded = try? decoder.decode(RepositoryResponse.self, from: data) {
            return decoded.items
        } else {
            print("ERROR: decoding")
            return [RepositoryResponse.Item]()
        }
    }
    .map { items -> [Repository] in
        let repos = items.map { item -> Repository in
            var isStarred: Bool?
            
            /// I want to initialize Repository with isStarred value
            /// What should I do in here?
            
            return Repository(item, isStarred: isStarred)
        }
        
        return repos
    }

What I planned to do is getting repositories by Github search api and then checking if the user has starred each repository.

So I made Repository struct which has two variables containing the name of repository and star status each.

A problem occurs right here. To initialize the Repository struct, I should get star status.

I’ve tried a completion way, but it seems return before completion returns value.

    private func isRepoStarred(name: String, completion: @escaping (Bool) -> Void) {
        let isStarredCheckerUrl = URL(string: "https://api.github.com/user/starred/\(name)")!
        URLSession.shared.dataTask(with: isStarredCheckerUrl) { _, response, _ in
            guard let response = response as? HTTPURLResponse else {
                return
            }
            
            let code = response.statusCode
            if code == 404 {
                return completion(false)
            } else if code == 204 {
                return completion(true)
            } else {
                return completion(false)
            }
        }
    }

Another way I’ve tried is making Single observable but don’t know how to use this exactly.

func isRepoStarredObs(name: String) -> Single<Bool> {
        return Single<Bool>.create { observer in
            let isStarredCheckerUrl = URL(string: "https://api.github.com/user/starred/\(name)")!
            let task = URLSession.shared.dataTask(with: isStarredCheckerUrl) { _, response, _ in
                guard let response = response as? HTTPURLResponse else {
                    return
                }
                
                let code = response.statusCode
                if code == 404 {
                    observer(.success(false))
                } else if code == 204 {
                    observer(.success(true))
                } else {
                    observer(.failure(NSError(domain: "Invalid response", code: code)))
                }
            }
            task.resume()
            
            return Disposables.create { task.cancel() }
        }
    }

If you have any ideas, please let me know. Thanks.

Solution

This gets the starred status:

func isRepoStarred(name: String) -> Observable<Bool> {
    URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.github.com/user/starred/\(name)")!))
        .map { data in
            var result = false
            // find out if repo is starred here and return true or false.
            return result
        }
}

and this is your search.

func searchRepositories() -> Observable<RepositoryResponse> {
    var baseUrl = URLComponents(string: "https://api.github.com/search/repositories")   // base url
    let query = URLQueryItem(name: "q", value: "flutter")    // Keyword. flutter for this time.
    let sort = URLQueryItem(name: "sort", value: "stars")   // Sort by stars
    let order = URLQueryItem(name: "order", value: "desc")  // Desc order
    baseUrl?.queryItems = [query, sort, order]
    
    return URLSession.shared.rx.data(request: URLRequest(url: baseUrl!.url!))
        .map { data in
            try JSONDecoder().decode(RepositoryResponse.self, from: data)
        }
}

That’s all you need to make requests.

To combine them you would do this:

let repositories = searchRepositories()
    .flatMap {
        Observable.zip($0.items.map { item in
            isRepoStarred(name: item.fullName).map { Repository(item, isStarred: $0) }
        })
    }

In general, it’s best to reduce the amount of code inside a flatMap as much as possible. Here’s a version that breaks the code up a bit better. This version might also be a bit easier to understand what’s going on.

let repositories = searchRepositories()
    .map { $0.items }

let starreds = repositories
    .flatMap { items in
        Observable.zip(items.map { isRepoStarred(name: $0.fullName) })
    }

let repos = Observable.zip(repositories, starreds) { items, starreds in
    zip(items, starreds)
        .map { Repository($0, isStarred: $1) }
}

Answered By – Daniel T.

Answer Checked By – Dawn Plyler (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.