Press enter to see results or esc to cancel.

Handling Multiple Datasources Using Repository Pattern

Building an iOS client-server based application is easy. You can do it in 3 steps:
(1) Place an Alamofire pod dependencies into your project then
(2) call the API from UIViewController and
(3) show its results on your View.

For example, we want to create simple To-do list app and we have an api endpoint to request the Todos, api.awesomeserver.io/api/todos It will then return a JSON Object.

{
  "todos": [
    {
      "is_completed": "0",
      "tempId": "710BBEC4-8A7B-486F-9329-412DFD5FB738",
      "title": "coding",
      "id": "SJcbh6mtb"
    },
    {
      "is_completed": "0",
      "tempId": "FAF6D1D3-A8B6-4E46-B25F-484B783EDD99",
      "title": "goto futsal",
      "id": "SyXOu_utb"
    }
  ]
}

Now, from the UIViewController’s side, import the Alamofire dependencies and then call it by using Alamofire.request() after the view has loaded.

import Alamofire

class TodosViewController:UIViewController {
  func viewDidLoad(){
    Alamofire.request(url, method: .get).responseJSON { response in      
            if response.result.isSuccess {
                var todos:[Todo] = []
                let data = JSON(rawValue: response.value!)
                data?.forEach({ (index, obj) in
                    let id = obj["id"].stringValue
                    let tempId = obj["tempId"].stringValue
                    let title = obj["title"].stringValue
                    let isCompleted = obj["is_completed"].stringValue == "1"
                    todos.append(Todo(id: id, title: title, isCompleted: isCompleted, tempId: tempId))
                })
                displayToTableView(todos)
            }else{
                displayTheErrorPopup(message:"failed")
            }
        }
  }
}

Done! Now the application is ready for use.

Of course your requirements may not be as simple as that. Perhaps you have a login page or a page to show the to-do details and other features so you have to navigate around from a page to other pages. Are you going to let the user always make a request to the server when opening a particular page? Some users may not like it because they have to keep on waiting after each action is made.

Of course your requirements may not be as simple as that. Perhaps you have a login page or a page to show the to-do details and other features so you have to navigate around from a page to other pages. Are you going to let the user always make a request to the server when opening a particular page? Some users may not like it because they have to keep on waiting after each action is made.

Caching in the memory is the solution. You can hold the todos item in a variable and it will be available while your application is running. Even though the user may switch pages frequently, it has significant performance improvement by displaying todos from cached data first. You can put a refresh button or pull-to-refresh feature to make a new server request, or even make the synchronisation smarter by detecting some event trigger to fetch the data from servers.

Inefficient Server Requests

Let’s discuss this in more detail. Since your data is rarely being changed, there is actually no need to make a request to server when users open the app. The data set can be temporarily stored in a local storage and you can validate it later by introducing a synchronisation mechanism.

The inefficient requests issue can be solved by using a combination of cached data, local storage and remote storage. But the next challenge is how to manage three data sources without violating the S.O.L.I.D. principle. It is good to use solid principle to measure if our code base is modular or not. In Qiscus, we often discuss and share something related with architecture and code optimization. We need to deliver products quickly with high quality.

Your UIViewController (View) doesn’t need to know the details

The main responsibility of the View’s part is managing the View’s state itself. For example, what will happen when View has already finished loading or what will happen when View appears or disappears? It doesn’t need to know the process of making request to server. It shouldn’t place dependence on Alamofire. Hence, we need to change the ViewController and wrap its process with a new class.

class TodosViewController:UIViewController{
 class viewDidLoad{
   let dataManager = DataManager()
   let items:[Todo] = dataManager.getTodos()
   displayToTableView(items)
 }
}

The data manager is an encapsulation class that decides when to use cached data or to obtain the data from server. In the next discussion, I will use CacheRepository term to represent cached data and RemoteRepository term to represent an API request. Both of them should be using the same methods such as getTodos() to getting all collections item, getTodo() for getting single item, updateTodo() for changing the specific data and addTodo() to insert a new item. Unfortunately, both have totally different logical implementations so we have to make them separately (make it match to single responsibility principle).

 

Also read: “How To Create Simple Custom Chat Rooms Using Qiscus Chat SDK

 

Getting started: Make an abstraction using Protocol

“Software entities (classes, modules, functions, etc.) should be opened for extension, but closed for modification.” – The open closed principle.

If you want to create a class that is easy to maintain, it must have two important characteristics:

  • Open for extension: You should be able to extend or change the behaviours of a class without effort.
  • Closed for modification: You must extend a class without changing the implementation.

You can achieve these characteristics, thanks to the abstraction.

Let us start by creating a contract for Repository domain. It has four main functionality getTodos() for getting all todo items, getTodo() for getting single todo item, addTodo() to put new Todo item and updateTodo() to replace the todo with new value. We also need to create a CachedData abstraction to hold the collections of todo in memory.

protocol Repository{
    func getTodos()->[Todo]
    func getTodo(id:Int) -> Todo
    func addTodo(todo: Todo) -> Todo
    func updateTodo(todo: Todo) -> Todo
}
protocol CachedData{
    var todosInCache:[Todo] { get set }
}

//...here is the Todo class
class Todo{
    var tempId: String
    var id : Int
    var title: String
    var isCompleted: Bool
    
    init(id:Int, title:String, isCompleted:Bool) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.tempId = id == -1 ? UUID() : nil
    }
}

The abstraction will be useful when we create an implementation class.

class RemoteRepository: Repository{
    private var todos:[Todo] = []    
    func getTodos() -> [Todo] {
     // put the logic here for making request to remote server
    }
    func getTodo(id: Int) -> Todo {}
    func addTodo(todo: Todo) -> Todo {}
    func updateTodo(todo: Todo) -> Todo {}
}
class CacheRepository: Repository, CachedData{
    var todosInCache:[Todo] = []
    func getTodos() -> [Todo] {}    
    func getTodo(id: Int) -> Todo {}
    func addTodo(todo: Todo) -> Todo{}
    func updateTodo(todo: Todo) -> Todo {}
}

The repositories above match those with Single Responsibility Principle which between of them have different implementation. It means that whenever you change the implementation inside, RemoteRepository will not have an effect on CacheRepository. The RemoteRepository doesn’t know CacheRepository and vice versa.

Lets create one more class to encapsulate the logic whether the todo is taken from cache memory or remote server. In order to be usable, each repository have to initialise inside init().

class DataManager{
    var cacheRepository: Repository&CachedData
    var remoteRepository: Repository
    
    init(){
      cacheRepository = CacheRepository()
      remoteRepository = RemoteRepository()
    }
}

Put the getTodos() function to trigger both repositories to do their task. Remember, the logic for deciding which repository should be used is only available here.

 class DataManager{
    var cacheRepository: Repository&CachedData
    var remoteRepository: Repository
    
    init(){
      cacheRepository = CacheRepository()
      remoteRepository = RemoteRepository()
    }
    
    func getTodos()->[Todos]{
       if cacheRepository.todosInCache.isEmpty{
          cacheRepository.todosInCache = remoteRepository.getTodos()
       }
       return cacheRepository.todos.value
    }
}

You can try this tutorial on Playground by downloading the full source code from gist on github. For sample code implementation, you can check it from repository here and run the unit testing.

Comments

Leave a Comment

Show Buttons
Hide Buttons