Slide 1

Slide 1 text

Replace API to Firebase Database - The view from client side Daiki Matsudate / @d_date 2017/11/03 / Serverlessconf Tokyo 2017

Slide 2

Slide 2 text

Daiki Matsudate iOS Mobile App Engineer @d_date

Slide 3

Slide 3 text

Tokyo 2018/3/1 - 3

Slide 4

Slide 4 text

Firebase

Slide 5

Slide 5 text

Firebase

Slide 6

Slide 6 text

Cloud Messaging

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

31 Oct. at Amsterdam #FirebaseSummit

Slide 10

Slide 10 text

• Prediction • A/B Testing • Integrate Crashlytics into Firebase #FirebaseSummit

Slide 11

Slide 11 text

#FirebaseSummit

Slide 12

Slide 12 text

Agenda • Firebase Realtime Database • Feature • Introduce to Mobile App • Architecture • Testing • ???

Slide 13

Slide 13 text

Firebase Database

Slide 14

Slide 14 text

• NoSQL cloud database • Realtime Data Sync with JSON Tree • Available data on offline Firebase Database

Slide 15

Slide 15 text

Firebase Database

Slide 16

Slide 16 text

Replace API to Realtime Database Case Study

Slide 17

Slide 17 text

Replace API to Realtime Database API Client (Native App) Server

Slide 18

Slide 18 text

Client (Native App) Server Firebase Database Replace API to Realtime Database

Slide 19

Slide 19 text

Replace API to Realtime Database • use func observeSingleEvent(:_) in FirebaseDatabase

Slide 20

Slide 20 text

Replace API to Realtime Database • Reduce building API man-hours • No more performance tunings • Easy to handle data in offline Purpose

Slide 21

Slide 21 text

Client (Native App) Server Firebase Database Replace API to Realtime Database

Slide 22

Slide 22 text

Client (Native App) Firebase Database Replace API to Realtime Database

Slide 23

Slide 23 text

NoSQL

Slide 24

Slide 24 text

NoSQL = Denormalization

Slide 25

Slide 25 text

Denormalization in NoSQL { "users": { "user1": { "name": "Alice" }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } }

Slide 26

Slide 26 text

Denormalization in NoSQL { "users": { "user1": { "name": "Alice" }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } }

Slide 27

Slide 27 text

Denormalization in NoSQL { "users": { "user1": { "name": "Alice" }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } } Join data in client

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

JOIN data in client in Swift self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })

Slide 30

Slide 30 text

Not Swifty!! self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) “Stringly“ typed Pyramid of Death

Slide 31

Slide 31 text

Easy to replace from API? self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) Need reference of database

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

To make friend with NoSQL in Swift

Slide 34

Slide 34 text

To make friend with NoSQL in Swift • Re-Design Architecture • Introduce Reactive Programming

Slide 35

Slide 35 text

Re-Design Architecture

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

!Architecture → Ideal State "Ideal State → Architecture

Slide 42

Slide 42 text

Ideal State • Data from Firebase ≠ Data in app -> Need translation • Easy to replace from API to Firebase -> I/F is same as API, then want to replace DAO

Slide 43

Slide 43 text

Replaceable Data Access Object Entity Data Access to API Data Access Interface (Repository) Server

Slide 44

Slide 44 text

Replaceable Data Access Object Entity Data Access to Firebase Data Access Interface (Repository)

Slide 45

Slide 45 text

Translation Entity Data Access Interface Data Access to Firebase Interactor Model View

Slide 46

Slide 46 text

Requirement for Architecture Entity Data Access Interface Data Access to Firebase Interactor Model View

Slide 47

Slide 47 text

What is this architecture? Entity Data Access Interface Data Access to Firebase Interactor Model Presenter View

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

How to resolve Pyramid of Death self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) Pyramid of Death

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

How to resolve Pyramid of Death self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })

Slide 52

Slide 52 text

protocol Entity: Codable {} struct UserEntity: Entity { let name: String } struct LinkEntity: Entity { let title: String let href: URL let submitted: String } struct CommentEntity: Entity { let link: String let body: String let author: String } Entity Entity Data Access Data Access Interactor Model Presenter View

Slide 53

Slide 53 text

Data Access + RxSwift extension DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } }

Slide 54

Slide 54 text

Data Access + RxSwift extension DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } } If value not exist, invoke error

Slide 55

Slide 55 text

Data Access + RxSwift extension DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } } If fail to serialize JSON, invoke error

Slide 56

Slide 56 text

Data Provider class DataProvider { static let shared = DataProvider() private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single { return ref.child("users").child(key).get() } func getLink(key: String) -> Single { return ref.child("links").child(key).get() } func getComments() -> Single { return ref.child("comments").get() } }

Slide 57

Slide 57 text

Data Provider class DataProvider { static let shared = DataProvider() private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single { return ref.child("users").child(key).get() } func getLink(key: String) -> Single { return ref.child("links").child(key).get() } func getComments() -> Single { return ref.child("comments").get() } } Singleton in Swift

Slide 58

Slide 58 text

Data Provider class DataProvider { static let shared = DataProvider() private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single { return ref.child("users").child(key).get() } func getLink(key: String) -> Single { return ref.child("links").child(key).get() } func getComments() -> Single { return ref.child("comments").get() } } Retain DatabaseReference

Slide 59

Slide 59 text

Data Provider class DataProvider { static let shared = DataProvider() private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single { return ref.child("users").child(key).get() } func getLink(key: String) -> Single { return ref.child("links").child(key).get() } func getComments() -> Single { return ref.child("comments").get() } } Get Entity from Database, then return Single

Slide 60

Slide 60 text

Data Access struct UserRepository { let dataStore: LinkDataStore func get(userId: String) -> Single { return dataStore.get(key: userId) } } struct UserDataStore { let provider = DataProvider.shared func get(key: String) -> Single { return provider.getUser(key: key) } } Entity Data Access Data Access Interactor Model Presenter View

Slide 61

Slide 61 text

Data Access struct UserRepository { let dataStore: LinkDataStore func get(userId: String) -> Single { return dataStore.get(key: userId) } } struct UserDataStore { let provider = DataProvider.shared func get(key: String) -> Single { return provider.getUser(key: key) } } I/F for data access Implementation for data access Entity Data Access Data Access Interactor Model Presenter View

Slide 62

Slide 62 text

Interactor (UseCase) struct UseCase { let commentsRepository: CommentRepository let usersRepository: UserRepository let linksRepository: LinkRepository func getComments() -> Single<[CommentModel]> { return commentsRepository.get() .map { (comment) -> [CommentModel] in let link = self.getLink(linkId: comment.link) let author = self.getUser(userId: comment.author) return Single<(LinkModel, UserModel)>.zip(link, author) { ($0, $1) }).map(translator: CommentTranslator()) } } } Entity Data Access Data Access Interactor Model Presenter View

Slide 63

Slide 63 text

Before self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })

Slide 64

Slide 64 text

Presenter class Presenter { let useCase: UseCase init(useCase: UseCase) { self.useCase = useCase } func getComments() -> Single<[CommentModel]> { return useCase.getComments() } } Entity Data Access Data Access Interactor Model Presenter View

Slide 65

Slide 65 text

View class ViewController: UIViewController { weak var presenter: Presenter! private let disposeBag = DisposeBag() func inject(presenter: Presenter) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() presenter.getComments() .subscribe(onSuccess: { (models) in //Handling model }, onError: { (error) in //Handling Error }).disposed(by: disposeBag) } } Entity Data Access Data Access Interactor Model Presenter View

Slide 66

Slide 66 text

View class ViewController: UIViewController { weak var presenter: Presenter! private let disposeBag = DisposeBag() func inject(presenter: Presenter) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() presenter.getComments() .subscribe(onSuccess: { (models) in //Handling model }, onError: { (error) in //Handling Error }).disposed(by: disposeBag) } } Entity Data Access Data Access Interactor Model Presenter View Focus on UI Handling

Slide 67

Slide 67 text

Side Effect

Slide 68

Slide 68 text

Side Effect Entity Data Access Interface Data Access to Firebase Interactor Model Presenter View

Slide 69

Slide 69 text

Side Effect - Easy Unit Testing Entity Data Access Interface Mock Object Interactor Model Presenter View Easy to replace

Slide 70

Slide 70 text

class DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Side Effect - Easy Unit Testing

Slide 71

Slide 71 text

class DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Side Effect - Easy Unit Testing FackObject

Slide 72

Slide 72 text

class DatabaseReference { func get() -> Single { return Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Return Entity created from Stub JSON Side Effect - Easy Unit Testing FackObject

Slide 73

Slide 73 text

Tips - Realtime update or not lazy var realtimeRef: DatabaseReference = { let ref = Database.database().reference() ref.keepSynced(true) return ref }() func getFavorite(userId: String) -> Single { return realtimeRef.child(userId).child("favorites").get() }

Slide 74

Slide 74 text

Tips - Realtime update or not lazy var realtimeRef: DatabaseReference = { let ref = Database.database().reference() ref.keepSynced(true) return ref }() func getFavorite(userId: String) -> Single { return realtimeRef.child(userId).child("favorites").get() } Reflect database update immediately

Slide 75

Slide 75 text

Pros/Cons • Reduce building API man-hours • No more performance tunings • Easy to handle data in offline Pros

Slide 76

Slide 76 text

Pros/Cons • Reduce building API man-hours • No more performance tunings • Easy to handle data in offline Pros #

Slide 77

Slide 77 text

Pros/Cons • Must have join logic in client side • Need more money $ to build in some case • Data cache system doesn’t suit for your app in some case • Sorting and Query are hard to use • Scaling requires sharding (100,000 concurrent connections & 1,000 writes/sec.) Cons

Slide 78

Slide 78 text

Pros/Cons • Must have join logic in client side • Need more money $ to build in some case • Data cache system doesn’t suit for your app in some case • Sorting and Query are hard to use • Scaling requires sharding (100,000 concurrent connections & 1,000 writes/sec.) Cons %

Slide 79

Slide 79 text

Alternatives • Realtime Database + Cloud Functions • Building API server (Old way) • ???

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

One more thing…

Slide 82

Slide 82 text

Cloud Firestore

Slide 83

Slide 83 text

• Stores data in documents organized in collections • Sending data with protobuf, which have more types • Introduced reference type, so requires less denormalization and data flattening • Indexed queries with compound sorting and filtering Cloud Firestore

Slide 84

Slide 84 text

Cloud Firestore

Slide 85

Slide 85 text

Cloud Firestore Reference type

Slide 86

Slide 86 text

What will be changed on architecture Entity Data Access Interface Data Access to Firebase Interactor Model Presenter View

Slide 87

Slide 87 text

What will be changed on architecture Entity Data Access Interface Data Access to Firebase Interactor Model Presenter View

Slide 88

Slide 88 text

Recap • Serverless architecture ( mBaaS / SaaS and etc. ) is also effective to use on native apps • API server can be replaced by Firebase Database / Cloud firestore in some case • Replaceable DAO and separate Entity and Model in app is important • With side effect, easy to test modules

Slide 89

Slide 89 text

If you have Questions… Daiki Matsudate [email protected] twitter: @d_date GitHub: @d-date

Slide 90

Slide 90 text

Enjoy! Your serverless

Slide 91

Slide 91 text

References • Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin • https://firebase.google.com/docs/firestore/rtdb-vs-firestore • https://firebase.google.com/docs/firestore/firestore-for- rtdb • https://speakerdeck.com/bpyamasinn/api-wo-firebase- realtime-database-niyi-xing-siteqi-fu-itakoto-ver2