Ответ
zip объединяет элементы из нескольких потоков попарно, ожидая, пока в каждом появится новый элемент. Результат формируется только когда все источники испустили соответствующие элементы.
combineLatest же эмитит значение при каждом изменении любого из потоков, используя последние значения из всех источников.
Пример:
val obs1 = Observable.just("A", "B", "C")
val obs2 = Observable.just(1, 2, 3)
// zip: ("A",1), ("B",2), ("C",3)
Observable.zip(obs1, obs2) { a, b -> "$a$b" }
// combineLatest: эмитит при каждом изменении, например:
// ("A",1), ("B",1), ("B",2), ("C",2), ("C",3)
Observable.combineLatest(obs1, obs2) { a, b -> "$a$b" }
zip строго синхронизирует эмиты, combineLatest реагирует на любые изменения.