Contents
Chào bạn đến với Fx Studio. Bài viết này vẫn là chủ đề liên quan tới các Operators trong thế giới RxSwift. Nhóm toán tử đề cập đến lần này là Combining Operators.
Dành một chút thời gian để quay về các khái niệm cơ bản của Operators trong RxSwift. Và dành cho các bạn mới tiếp cận thì có thể tham khảo lại link sau:
Chuẩn bị
-
- Xcode 11
- Swift 5
- Playground
Vẫn là em Playground huyền thoại. Chúng ta vẫn còn dùng tới nó để demo code cho bài viết này. Bạn chỉ cần tạo mới 1 file Playground từ project mà đã cài đặt ngay từ bài đầu của series. Bạn có thể checkout code lại đây.
Combining Operators
Bạn đã đi qua rất nhiều thứ trong RxSwift, từ bài đầu tiên trong series mệt mỏi này của Fx Studio rồi. Và mình có thể tóm gọn lại như sau:
Tạo & Lọc
Giờ chúng ta sẽ đi tới phần kết hợp. Có nhiều thứ chúng ta có thể kết hợp lại với nhau. Từ mặt dữ liệu tới đối tượng hoặc các stream … Đây cũng là những toán tử mà bạn sẽ phải dùng nhiều trong việc triển khai các logic trong project.
Chúng ta sẽ đi vào chi tiết các toán được sử dụng phổ biến với chức năng Combining.
1. Prefixing and concatenating
1.1. startWith
Đôi lúc chúng ta cần thêm một/nhiều phần tử trước khi Observable bắt đầu emit dữ liệu đi. Để làm gì, thì tuỳ ý bạn. Có khi đơn giản chỉ là muốn nó luôn có 1 giá trị nào đó lúc đầu tiên thôi. Toán tử này là startWith
Tham số truyền vào chính là giá trị mà có cùng kiểu giá trị với phần tử của Observable phát đi.
Ví dụ đoạn code sau:
let bag = DisposeBag() Observable.of("B", "C", "D", "E") .startWith("A") .subscribe(onNext: { value in print(value) }) .disposed(by: bag)
Bạn sẽ thấy, với Observable phát ra các giá trị bắt đầu từ B
. Thì để chèn thêm 1 phần tử ở trước ta dùng toán tử startWith
và thêm A
vào. Kết quả ra như sau:
A B C D E
Quá EZ!
1.2. Observable.concat
Trước tiên thì bạn hãy ví dụ cho toán tử mới này.
let bag = DisposeBag() let first = Observable.of(1, 2, 3) let second = Observable.of(4, 5, 6) let observable = Observable.concat([first, second]) observable .subscribe(onNext: { value in print(value) }) .disposed(by: bag)
Bạn sẽ nhận ra là Observable được tạo ra từ toán tử concat
, thì có 1 tham số là 1 array Observable. Thử chạy kết quả xem như thế nào.
1 2 3 4 5 6
Tới đây thì bạn cũng hiểu ra rồi. concat
sẽ nối các phần tử của nhiều sequence observable lại với nhau. Lại EZ nữa rồi!
1.3. concat
Bạn xem qua sơ đồ sau về toán tử Observable.concat
.
Toán tử ở phần trên là một phương thức tạo Observable từ việc nối nhiều Observable lại với nhau. Còn đây là toán tử concat
dùng với 1 đối tượng Observable. Nó sẽ nối các phần tử của một đối tượng Observable khác vào.
Phương pháp này giúp bạn có thêm 1 lúc nhiều phần tử, khắc phục nhược điểm của phần 1.
Code ví dụ như sau:
let bag = DisposeBag() let first = Observable.of("A", "B", "C") let second = Observable.of("D", "E", "F") let observable = first.concat(second) observable .subscribe(onNext: { value in print(value) }) .disposed(by: bag)
Ta dùng first.concat
với tham số là second
. Kết quả in ra như sau:
A B C D E F
Cũng khá là đơn giản phải không nào. Đặc điểm của toán tử này là sử dụng cho 1 đối tượng Observable.
Chú ý: Toán tử
concat
này sẽ đợi Observable gốc hoàn thành. Thì sau đó Observable thứ 2 sẽ tiếp tục được nối vào.
1.4. concatMap
Lại là một toán tử concat đến từ nhóm toán tử Combining Operators. Và chúng ta xem sơ đồ mô tả nó như thế nào.
Khi có từ map
trong tên của 1 toán tử, thì bạn cũng sẽ rằng sẽ có sự biến đổi về kiểu dữ liệu ở đây. Toán tử này đảm bảo việc các chuỗi được đóng lại trước khi chuỗi tiếp theo được đăng kí vào. Đảm vảo thứ tự trình tự.
Ở hình trên thì mô tả như sau:
- Hình tròn là 1 Observable
- Hình thoi là giá trị được phát ra cho Subscriber
- Luật trong toán tử là cứ 1 Observable sẽ phát ra 2 dữ liệu hình thoi
Còn sau đây là code ví dụ:
let bag = DisposeBag() let cities = [ "Mien Bac" : Observable.of("Ha Noi", "Hai Phong"), "Mien Trung" : Observable.of("Hue", "Da Nang"), "Mien Nam" : Observable.of("Ho Chi Minh", "Can Tho")] let observable = Observable.of("Mien Bac", "Mien Trung", "Mien Nam") .concatMap { name in cities[name] ?? .empty() } observable .subscribe(onNext: { (value) in print(value) }) .disposed(by: bag)
Trong đó
cities
là một Dictionary với key là String và value là 1 Observable- Tạo ra 1 Observable với kiểu dữ liệu cho phần tử là
String
- Dùng
concatMap
để biến đổi từString
thànhString
. Tuy nhiên có sự can thiệp là nối các chuỗi thuộc value của Dictionary trên lại với nhau
Kết quả ra như sau:
Ha Noi Hai Phong Hue Da Nang Ho Chi Minh Can Tho
Cũng khá là không đơn giản phải không nào. Nói chung tụi này sẽ xoay vòng với nhau thôi. Chúng ta tiếp tục với nhóm tiếp theo.
2. Merging
Toán tử đầu tiên chính là merge
. Cái tên cũng nói lên tất cả rồi. Và đặc điểm của toán tử này như sau:
merge
sẽ tạo ra 1 Observable mới, khi một Observable có cácelement
kiểu là Observable- Observable của merge sẽ kết thúc khi tất cả đều kết thúc
- Nó không quan tâm tới thứ tự các Observable được thêm vào. Nên nếu có bất cứ phần tử nào từ các Observable đó phát đi thì Subscriber cũng đều nhận được
Code ví dụ như sau:
let bag = DisposeBag() let chu = PublishSubject<String>() let so = PublishSubject<String>() let source = Observable.of(chu.asObserver(), so.asObserver()) let observable = source.merge() observable .subscribe(onNext: { (value) in print(value) }) .disposed(by: bag) chu.onNext("Một") so.onNext("1") chu.onNext("Hai") so.onNext("2") chu.onNext("Ba") so.onCompleted() so.onNext("3") chu.onNext("Bốn") chu.onCompleted()
Trong đó:
chu
&so
là 2 Subject, chịu trách nhiệm phát đi dữ liệusource
được tạo ra từ 2 Observablechu
&so
. Nó là 1 Observableobservable
được tạo ra bằng toán tửmerge
- Vẫn subscribe như bình thường
- Việc phát dữ liệu được thực hiện xen kẻ
Kết quả như sau:
Một 1 Hai 2 Ba Bốn
Và tất nhiên bạn có thể hạn chế được số lượng các Observable được phép merge vào thông qua tham số .merge(maxConcurrent:)
3. Combining elements
Phần này là sẽ tới các toán tử kết hợp trong nhóm Combining Operators. Tên của tụi nó lèn nhèn thật!
3.1. combineLatest
Thông qua sơ đồ mô tả toán tử combineLatest
, toán tử này sẽ phát đi những giá trị là sự kết hợp của các cặp giá trị mới nhất của từng Observable. Để hình dung cụ thể, ta qua từng bước ví dụ sau đây:
- Tạo các Observable, sẽ phát dữ liệu đi. Có thể không theo thứ tự.
let chu = PublishSubject<String>() let so = PublishSubject<String>()
- Sử dụng
combinedLatest
với 2 Observable trên. Sau đó tiến hànhsubscribe
như bình thường.
let observable = Observable.combineLatest(chu, so) observable .subscribe(onNext: { (value) in print(value) }) .disposed(by: bag)
- Giờ chúng ta phát dữ liệu đi một cách lần lượt và xem nó hoạt động như thế nào.
chu.onNext("Một") chu.onNext("Hai") so.onNext("1") so.onNext("2") chu.onNext("Ba") so.onNext("3") chu.onNext("Bốn") so.onNext("4") so.onNext("5") so.onNext("6")
Kết quả sẽ khác dự đoán của bạn một chút.
("Hai", "1") ("Hai", "2") ("Ba", "2") ("Ba", "3") ("Bốn", "3") ("Bốn", "4") ("Bốn", "5") ("Bốn", "6")
Kết quả là sự kết hợp dữ liệu từ 2 dữ liệu được Observable chữ và số phát đi. Bạn sẽ thấy là không có ("Một", "1")
. Vì lúc đó Observable so
chưa phát ra gì cả. Khi so
phát ra phần tử đầu tiên thì sẽ kết hợp với phần tử mới nhất của chu
, đó là Hai
.
Áp dụng tương tự cho các phần tử tiếp theo. Còn với complete
thì như thế nào?
chu.onNext("Một") chu.onNext("Hai") so.onNext("1") so.onNext("2") chu.onNext("Ba") so.onNext("3") // completed chu.onCompleted() chu.onNext("Bốn") so.onNext("4") so.onNext("5") so.onNext("6") // completed so.onCompleted()
Kết quả thực thi như sau:
("Hai", "1") ("Hai", "2") ("Ba", "2") ("Ba", "3") ("Ba", "4") ("Ba", "5") ("Ba", "6")
Vẫn không ảnh hưởng gì tới sự hoạt động của toán tử này. Nó sẽ vẫn lấy dữ liệu cuối cùng trước khi phát đi .onCompleted
để kết hợp với các phần tử khác.
3.2. combineLatest(_:_:resultSelector:)
Tiếp tục toán tự combineLatest
trong nhóm toán tử Combining Operators. Kết quả thực thi ở trên nhìn khá là xấu xí. Tuy nhiên, bạn có thể biến đổi nó như các toán tử map
bằng cách sử dụng thêm tham số resultSelector
và cung cấp 1 closure để biến đổi chúng.
Ta edit lại ví dụ trên một chút.
let observable = Observable.combineLatest(chu, so) { chu, so in "\(chu) : \(so)" }
Vì nếu không có tham số này, thì giá trị của toán tử là 1 Tuple
kết hợp đơn giản mà thôi. Còn lại ta sử dụng tham số phụ này thì ta có quyền biến đổi thành kiểu dữ liệu mà ta mong muốn.
Kết quả như sau:
Hai : 1 Hai : 2 Ba : 2 Ba : 3 Ba : 4 Ba : 5 Ba : 6
Và đây là công thức chung cho bạn, nếu sau này muốn áp dụng nó. Bạn phải thêm filter
để tăng tính đảm bảo.
let observable = Observable .combineLatest(left, right) { ($0, $1) } .filter { !$0.0.isEmpty }
Thêm 1 ví dụ nữa để thấy sự tiện lợi của toán tử này:
let choice: Observable<DateFormatter.Style> = Observable.of(.short, .long) let dates = Observable.of(Date()) let observable = Observable.combineLatest(choice, dates) { format, when -> String in let formatter = DateFormatter() formatter.dateStyle = format return formatter.string(from: when) } _ = observable.subscribe(onNext: { value in print(value) })
Với 1 giá trị Date
bạn có thể lựa chọn kiểu format dữ liệu để in ra một cách tiện lợi nhất. Chúng ta không cần gọi hàm đi gọi hàm lại hay là for các kiểu nữa. Còn kết quả như sau:
8/21/20 August 21, 2020
Toán tử này có rất nhiều tuỳ chọn cho tham số của nó. Bạn hãy khám phá thêm.
3.3. zip
Nhóm toán tử Combining Operators này quá nhiều luôn. Chúng ta tiếp tục tìm hiểu về zip
. Bạn hãy xem sơ đồ mô tả sau:
Khi bạn quan tâm tới thứ tự kết hợp theo đúng thứ tự phát đi của từng Observable. Nhưng combinedLatest
không đáp ứng được thì zip
sẽ giúp bạn hoàn thành tâm nguyện này.
Tất cả mọi thứ ví dụ đều như trên. Tuy nhiên, chỉ khác là các cặp giá trị kết hợp nhau thì sẽ theo thứ tự phát của các Observable. Xem qua lại ví dụ trên thì bạn sẽ hiểu thôi.
let observable = Observable.zip(chu, so) { chu, so in "\(chu) : \(so)" }
Vẫn là ví dụ lúc nãy, bạn chỉ cần edit lại thành là zip
. Và tận hưởng kết quả nào!
Một : 1 Hai : 2 Ba : 3
Như vậy, là vừa đẹp vừa đúng rồi! Dù vậy, bạn cũng nên quản lý việc onCompleted
của từng Observable trong đó, để đảm bảo dữ liệu như bạn mong muốn.
4. Trigger
Trigger sẽ được sử dụng, khi bạn muốn tạo ra 1 điều kiện nào đó từ Observable để một Observable khác được phép hoạt động.
4.1. withLatestFrom
Bắt đầu bằng ví dụ sau:
(Cái hình là mình mượn tạm để minh hoạ cho code ở đưới đây.)
let button = PublishSubject<Void>() let textField = PublishSubject<String>() let observable = button.withLatestFrom(textField) _ = observable .subscribe(onNext: { value in print(value) }) textField.onNext("Đa") textField.onNext("Đà Na") textField.onNext("Đà Nẵng") button.onNext(()) button.onNext(())
Trong đó:
button
là một subject. VớiVoid
thì chỉ phát ra sự kiện, chứ không có giá trị nào từonNext
textField
là một subject, phát ra cácString
observable
là sự kết hợp củabutton
vớitextField
thông qua toán tửwithLatestFrom
- Mỗi lần
button
phát đi tín hiệu, thì kết quả sẽ nhận được là phần tử mới nhất từtextField
Qua ví dụ trên cũng mô tả cho bạn thấy sự hoạt động của toán tử withLatestFrom
rồi. Kết quả thực thi code như sau:
Đà Nẵng Đà Nẵng
4.2. sample
Chúng ta thay withLatestFrom
bằng sample
trong ví dụ trên.
let observable = textField.sample(button)
Kết quả sẽ cho ra 1 mà thôi.
Với sample
là tương tự như withFromLatest
. Nhưng nó chỉ phát khi Observable button
phát ra.
Chú ý:
withLatestFrom
thì tham số là dữ liệu của một Observable khácsample
thì tham số là trigger từ một Observable khác
Chúng nó rất dễ nhầm lẫn, vì vậy bạn cần cẩn thận hơn khi sử dụng.
5. Switches
Các toán tử mới từ Combining Operators này, sẽ cho phép bạn tạo ra một Observable từ nhiều Observable khác. Mà bạn có thể quyết định được việc dữ liệu từ nguồn nào để các subscriber có thể nhận được.
5.1. amb
Đây là một toán tử khá là mơ hồ, cũng như cái tên của nó là ambiguity
. Với các đặc tính sau:
- Nó sẽ tạo ra một Observable để giải quyết vấn đề quyết định nhận dữ liệu từ nguồn nào
- Trong khi cả 2 nguồn đều có thể phát dữ liệu. Thì nguồn nào phát trước, thì nó sẽ nhận dữ liệu từ nguồn đó.
- Nguồn phát sau sẽ bị âm thầm ngắt kết nối
Xem qua code demo cho thông não nè:
let bag = DisposeBag() let chu = PublishSubject<String>() let so = PublishSubject<String>() let observable = chu.amb(so) observable .subscribe(onNext: { (value) in print(value) }) .disposed(by: bag) so.onNext("1") so.onNext("2") so.onNext("3") chu.onNext("Một") chu.onNext("Hai") chu.onNext("Ba") so.onNext("4") so.onNext("5") so.onNext("6") chu.onNext("Bốn") chu.onNext("Năm") chu.onNext("Sáu")
Kết quả thực thi ra như sau:
1 2 3 4 5 6
Vì so
đã phát trước, nên các dữ liệu từ chu
sẽ không nhận được. Nếu bạn cho thêm chu
phát onNext
trước số thì sẽ thấy dữ liệu nhận được sẽ toàn là từ chu
.
5.2. switchLatest
Toán tử này tương tự như flatMapLatest
trong bài viết trước. Và để thấu hiểu nó, thì mình sẽ đi qua ví dụ từng bước sau.
Đầu tiên, bạn cần tạo ra các Observable.
let chu = PublishSubject<String>() let so = PublishSubject<String>() let dau = PublishSubject<String>() let observable = PublishSubject<Observable<String>>()
Ta có:
- 3 subject thay nhau phát dữ liệu
observable
với kiểu dữ liệu phát đi làObsevable<String>
, chính là kiểu của 3 subject trên
Tiến hành subscribe
và dùng switchLatest
như sau:
observable .switchLatest() .subscribe(onNext: { (value) in print(value) }, onCompleted: { print("completed") }) .disposed(by: bag)
Cũng khá là quen thuộc phải không nào! Giờ sang phần phát dữ liệu đi. Bạn xem tiếp.
observable.onNext(so) so.onNext("1") so.onNext("2") so.onNext("3") observable.onNext(chu) chu.onNext("Một") chu.onNext("Hai") chu.onNext("Ba") so.onNext("4") so.onNext("5") so.onNext("6") observable.onNext(dau) dau.onNext("+") dau.onNext("-") observable.onNext(chu) chu.onNext("Bốn") chu.onNext("Năm") chu.onNext("Sáu")
Bạn sẽ thấy việc observable
sẽ phát đi subject nào. Thì subscriber
trên sẽ nhận được giá trị của subject đó. Kết quả thực thi như sau:
1 2 3 Một Hai Ba + - Bốn Năm Sáu
Đối với phần này rất dễ bị nhầm lẫn nên bạn muốn hiểu hơn, thì hãy thay phiên nhau việc phát dữ liệu.
Còn để kết thúc nó, thì phải phải .dispose
subscription. Chứ không thể nào kết thúc nó được, mặc dù các subject có thể onCompleted
hết tất cả nhưng nó vẫn không kết thúc.
6. Combining elements within a sequence
Tiếp theo là các toán tử của Combining Operators, với sự kết hợp các phần tử trong cùng 1 sequence observable. Có nhiều điểm thú vị với các toán tử này.
6.1. reduce
Đây là một toán tử khá là quen thuộc trong Swift code. Chắc bạn cũng vài lần sử dụng nó trong các Array để tính toán liên quan tới tất cả các phần tử trong array đó.
Ví dụ như cộng tất cả giá trị của một array lại với nhau. Hình sau đây cũng minh hoạ cho ví dụ này.
Về ví dụ bằng code thì như sau:
let source = Observable.of(1, 2, 3, 4, 5, 6, 7, 8, 9) let observable = source.reduce(0, accumulator: +) _ = observable .subscribe(onNext: { value in print(value) })
Trong đó accumulator
là sự rút gọn toán tử +
lại. Và 0
là giá trị ban đầu được cấp phát cho để thực hiện việc này.
Hoặc bạn có thể code xịn sò hơn như sau:
let observable = source.reduce(0) { $0 + $1 }
Hoặc code nghiêm chỉnh lại một chút.
let observable = source.reduce(0) { summary, newValue in
return summary + newValue
}
Việc tích luỹ sẽ bắt đầu bằng giá trị bạn cung cấp cho nó. Khi đó nó sẽ hiểu là $0
. Tiếp theo khi nhận được 1 giá trị, thì giá trị đó là $1
. Ta thực hiện biểu thức, kết quả trả về sẽ gán lại cho $0
. Cứ như thế, vòng lặp định mệnh sẽ liếp tục cho đến hết. Tức là Observable phát đi giá trị cuối cùng được tính toán xong sau khi phát onCompleted
.
OKAY phải không nào, EZ.
6.2. scan(_:accumulator:)
Thêm một thành viên hack não nữa từ nhóm toán tử Combining Operators. Bạn xem qua sơ đồ mô tả sau:
Về cấu trúc và cách viết thì tương tự reduce
. Có chút khác biệt ở đây là, thay vì chờ Observable kết thúc và đưa ra kết quả cuối cùng. Thì scan
nó sẽ tính toán và phát đi từng kết quả tinh toán được, sau khi có dữ liệu từ Observable phát ra. Không quan tâm Observable kết thúc mới thực hiện.
Chỉ cần dùng lại ví dụ trên, thay reduce bằng scan
là bạn sẽ hiểu thôi.
let source = Observable.of(1, 2, 3, 4, 5, 6, 7, 8, 9) //let observable = source.scan(0, accumulator: +) //let observable = source.scan(0) { $0 + $1 } let observable = source.scan(0) { summary, newValue in return summary + newValue } _ = observable .subscribe(onNext: { value in print(value) })
Kết quả thực thi như sau:
1 3 6 10 15 21 28 36 45
Về bản chất thì không khác nhau mấy khi so với reduce
. Về ý nghĩa thì bạn sẽ có 1 cách tạo ra 1 for loop
xịn sò nữa. Ahihi!
Tạm kết
Mình sẽ tóm tắt lại ý nghĩa các toán tử trong nhóm Combining Operators nha.
startWith
để thêm một hoặc nhiều phần tử trước khi Observable phát ra giá trị đầu tiên.concat
dùng để nối nhiều Observables lại với nhau. Hoặc tiếp tục việc phát dữ liệu sau khi một Observable kết thúc bằng 1 Observable khác.concatMap
vừa nối vừa biến đổimerge
hợp nhất các giá trị từ nhiều Observables phát ra (không theo thứ tự) thành 1 Observable duy nhất.combineLatest
kết hợp các cặp giá trị mới nhất từ các Observables. Và không quan tâm tới thứ tự phát.zip
tương tự như combineLatest nhưng cặp giá trị kết hợp sẽ theo thứ tự phát của các Observables.withLatestFrom
là trigger với tham số là dữ liệu của một Observable khác. Nó phát và nhận giá trị từ người khác.sample
cũng là trigger, nhưng tham số là một Observable khác. Khi người khác phát thì nó sẽ phát ra dữ liệu.amb
giải quyết việc chọn Observables nào sẽ toàn quyền phát dữ liệu. Bằng cách xem Observable nào phát trước tiên, các Observable còn lại thì sẽ bị ngắt kết nối.switchLatest
thì chỉ nhận dữ liệu phát ra từ Observable cuối cùng tham gia vào.reduce
làm gọn lại tất cả các dữ liệu phát ra từ Observable bằng một luật tính toán nào đó. Khi Observable kết thúc thì sẽ nhận được kết quả. Ngoài ra, subscriber sẽ không nhận được bất kì giá trị nào khác.scan
tương tự như reduce. Nhưng subscriber sẽ nhận được kết quả ở từng bước tính toán khi Observable phát dữ liệu đi.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
You may also like:
Archives
- December 2024 (3)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)