Slide 1

Slide 1 text

How to build a messenger for Android? Rakhimov Andrii Lead Android Engineer at Lalafo [email protected]

Slide 2

Slide 2 text

➔ Story ➔ Transport ➔ Properties of transport and protocol ➔ Architecture ➔ MVP & MVI ➔ Learnings Agenda

Slide 3

Slide 3 text

Story Story

Slide 4

Slide 4 text

Story

Slide 5

Slide 5 text

Features ➔ Real time send/delivery 1:1, 1:n ➔ Image/File sending ➔ Delivery status ➔ Typing ➔ Online status ➔ Pagination ➔ Offline pushes ➔ Delete chat ➔ Blacklist user ➔ Different types of chats ➔ Search ➔ Chat with Lalafo ➔ Automate response ➔ ...

Slide 6

Slide 6 text

Variety of market solutions

Slide 7

Slide 7 text

Telegram Codebase 546 743 LOC

Slide 8

Slide 8 text

Transport

Slide 9

Slide 9 text

HTTP

Slide 10

Slide 10 text

HTTP

Slide 11

Slide 11 text

HTTP + Simple - Reopening connection is expensive - Unnecessary requests to server - Slow, not real-time

Slide 12

Slide 12 text

HTTP + Firebase Cloud Messaging

Slide 13

Slide 13 text

HTTP + Firebase Cloud Messaging + Simple + Works well for a basic need + Close to real-time + Emulation of bidirectional communication - Reopening connection is expensive - Still unnecessary requests to server - Still slow

Slide 14

Slide 14 text

Socket solutions

Slide 15

Slide 15 text

Socket solutions

Slide 16

Slide 16 text

Messaging protocols

Slide 17

Slide 17 text

Socket.IO + Reconnects + Bidirectional + Fast + Do not waste connection and resources + Support web - 200+ issues - Not maintained for 2 years

Slide 18

Slide 18 text

Acknowledgment

Slide 19

Slide 19 text

Duplicate messages

Slide 20

Slide 20 text

Duplicate messages

Slide 21

Slide 21 text

Time sync and message order

Slide 22

Slide 22 text

Time sync and message order ➔ Validate if time is differed widely, and send appropriate time from backend ➔ Store diff on client ➔ Send and show messages accordingly

Slide 23

Slide 23 text

Battery concerns

Slide 24

Slide 24 text

Battery concerns ProcessLifecycleOwner.get() .getLifecycle() .addObserver(new LifecycleObserver() { @OnLifecycleEvent(Lifecycle.Event.ON_START) public void onEnterForeground() { messengerClient.connect(); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void onEnterBackground() { messengerClient.disconnect(); } });

Slide 25

Slide 25 text

Battery concerns ➔ Don’t keep socket connection open, after user leaves the app ➔ Don’t use foreground Service to keep connection open ➔ Use Firebase Cloud Messaging to notify the app about new messages

Slide 26

Slide 26 text

How to design a transport? ➔ Start small ➔ Determine your business needs ◆ Speed ◆ Security ◆ Features ◆ Web support ➔ Make it flexible ➔ Remember about battery

Slide 27

Slide 27 text

Architecture

Slide 28

Slide 28 text

Architecture ➔ Extensible ➔ Maintainable ➔ Reusable ➔ Composable ➔ Testable

Slide 29

Slide 29 text

Clean Architecture Dependency rule

Slide 30

Slide 30 text

Architecture

Slide 31

Slide 31 text

Why eventBus?

Slide 32

Slide 32 text

MVP & MVI

Slide 33

Slide 33 text

MVP & MVI

Slide 34

Slide 34 text

MVP & MVI

Slide 35

Slide 35 text

MVP & MVI

Slide 36

Slide 36 text

MVP issues ➔ Many events can trigger same UI changes as result broken UI ➔ Scales badly on huge and complex screens, presenters polluted with logic ➔ Testability

Slide 37

Slide 37 text

MVP & MVI

Slide 38

Slide 38 text

MVVM & MVI

Slide 39

Slide 39 text

MVP & MVI

Slide 40

Slide 40 text

/** * Input Actions */ sealed class Action { object LoadNextPageAction : Action() data class ErrorLoadingPageAction(val error: Throwable, val page: Int) : Action() data class PageLoadedAction(val itemsLoaded: List, val page: Int) : Action() data class StartLoadingNextPage(val page: Int) : Action() } StateMachine Actions

Slide 41

Slide 41 text

StateMachine States sealed class State { object LoadingFirstPageState : State() data class ErrorLoadingFirstPageState(val errorMessage: String) : State() data class ShowContentState(val items: List, val page: Int) : State() }

Slide 42

Slide 42 text

StateMachine Setup class PaginationStateMachine @Inject constructor(private val api: GithubApiFacade) { val input: Relay = PublishRelay.create() val state: Observable = input .reduxStore( initialState = State.LoadingFirstPageState, sideEffects = listOf( ::loadFirstPageSideEffect, ::loadNextPageSideEffect, ::showAndHideLoadingErrorSideEffect ), reducer = ::reducer )

Slide 43

Slide 43 text

StateMachine Side Effect /** * Load the first Page */ private fun loadFirstPageSideEffect(actions: Observable, state: StateAccessor): Observable { return actions.ofType(Action.LoadFirstPageAction::class.java) .filter { state() !is ContainsItems } // If first page has already been loaded, do nothing .switchMap { val state = state() val nextPage = (if (state is ContainsItems) state.page else 0) + 1 api.loadNextPage(nextPage) .subscribeOn(Schedulers.io()) .toObservable() .map { result -> PageLoadedAction(itemsLoaded = result.items, page = nextPage) } .onErrorReturn { error -> ErrorLoadingPageAction(error, nextPage) } .startWith(StartLoadingNextPage(nextPage)) } }

Slide 44

Slide 44 text

StateMachine Reducer /** * The state reducer. * Takes Actions and the current state to calculate the new state. */ private fun reducer(state: State, action: Action): State { return when (action) { is StartLoadingNextPage -> State.LoadingFirstPageState is PageLoadedAction -> State.ShowContentState(items = action.items, page = action.page) is ErrorLoadingPageAction -> State.ErrorLoadingFirstPageState(action.error.localizedMessage) } }

Slide 45

Slide 45 text

Displaying state open fun render(state: PaginationStateMachine.State) = when (state) { PaginationStateMachine.State.LoadingFirstPageState -> { recyclerView.gone() loading.visible() error.gone() } is PaginationStateMachine.State.ShowContentState -> { showRecyclerView(items = state.items, showLoadingNext = false) } is PaginationStateMachine.State.ErrorLoadingFirstPageState -> { recyclerView.gone() loading.gone() error.visible() snackBar?.dismiss() } }

Slide 46

Slide 46 text

DistinctUntilChanged val state: Observable = input .reduxStore( initialState = State.LoadingFirstPageState, sideEffects = listOf( ::loadFirstPageSideEffect, ::loadNextPageSideEffect, ::showAndHideLoadingErrorSideEffect ), reducer = ::reducer ) .distinctUntilChanged()

Slide 47

Slide 47 text

Testing @Test fun `Empty upstream just emits initial state and completes`() { val upstream: Observable = Observable.empty() upstream.reduxStore( "InitialState", sideEffects = emptyList() ) { state, action -> state } .test() .assertNoErrors() .assertValues("InitialState") .assertComplete() }

Slide 48

Slide 48 text

Pagination ➔ Use one source of truth for paging DB, api, local storage, etc.. ➔ Use DiffUtil ➔ Use distinctUntilChanged or similar

Slide 49

Slide 49 text

Architecture ➔ Extensiblish ➔ Maintainable ➔ Reusable ➔ Testable ➔ Composablish

Slide 50

Slide 50 text

Learnings ➔ Fair estimates

Slide 51

Slide 51 text

Fair estimates

Slide 52

Slide 52 text

Learnings ➔ Fair estimates ➔ Use appropriate technology for the task

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

Learnings ➔ Fair estimates ➔ Use appropriate technology for the task ➔ Design for flexibility

Slide 55

Slide 55 text

Learnings ➔ Fair estimates ➔ Use appropriate technology for the task ➔ Design for flexibility ➔ Start small and validate hypothesis early

Slide 56

Slide 56 text

Cost of change

Slide 57

Slide 57 text

Learnings ➔ Fair estimates ➔ Use appropriate technology for the task ➔ Design for flexibility ➔ Start small and validate hypothesis early

Slide 58

Slide 58 text

Thank you! We are hiring! Rakhimov Andrii Lead Android Engineer at Lalafo [email protected] https://github.com/ar-g