Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to build a messenger on Android?

How to build a messenger on Android?

Andrii Rakhimov

June 01, 2019
Tweet

More Decks by Andrii Rakhimov

Other Decks in Programming

Transcript

  1. ➔ Story ➔ Transport ➔ Properties of transport and protocol

    ➔ Architecture ➔ MVP & MVI ➔ Learnings Agenda
  2. 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 ➔ ...
  3. HTTP + Simple - Reopening connection is expensive - Unnecessary

    requests to server - Slow, not real-time
  4. 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
  5. Socket.IO + Reconnects + Bidirectional + Fast + Do not

    waste connection and resources + Support web - 200+ issues - Not maintained for 2 years
  6. 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
  7. 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(); } });
  8. 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
  9. How to design a transport? ➔ Start small ➔ Determine

    your business needs ◆ Speed ◆ Security ◆ Features ◆ Web support ➔ Make it flexible ➔ Remember about battery
  10. 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
  11. /** * Input Actions */ sealed class Action { object

    LoadNextPageAction : Action() data class ErrorLoadingPageAction(val error: Throwable, val page: Int) : Action() data class PageLoadedAction(val itemsLoaded: List<GithubRepository>, val page: Int) : Action() data class StartLoadingNextPage(val page: Int) : Action() } StateMachine Actions
  12. StateMachine States sealed class State { object LoadingFirstPageState : State()

    data class ErrorLoadingFirstPageState(val errorMessage: String) : State() data class ShowContentState(val items: List<GithubRepository>, val page: Int) : State() }
  13. StateMachine Setup class PaginationStateMachine @Inject constructor(private val api: GithubApiFacade) {

    val input: Relay<Action> = PublishRelay.create() val state: Observable<State> = input .reduxStore( initialState = State.LoadingFirstPageState, sideEffects = listOf( ::loadFirstPageSideEffect, ::loadNextPageSideEffect, ::showAndHideLoadingErrorSideEffect ), reducer = ::reducer )
  14. StateMachine Side Effect /** * Load the first Page */

    private fun loadFirstPageSideEffect(actions: Observable<Action>, state: StateAccessor<State>): Observable<Action> { 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<Action> { result -> PageLoadedAction(itemsLoaded = result.items, page = nextPage) } .onErrorReturn { error -> ErrorLoadingPageAction(error, nextPage) } .startWith(StartLoadingNextPage(nextPage)) } }
  15. 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) } }
  16. 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() } }
  17. DistinctUntilChanged val state: Observable<State> = input .reduxStore( initialState = State.LoadingFirstPageState,

    sideEffects = listOf( ::loadFirstPageSideEffect, ::loadNextPageSideEffect, ::showAndHideLoadingErrorSideEffect ), reducer = ::reducer ) .distinctUntilChanged()
  18. Testing @Test fun `Empty upstream just emits initial state and

    completes`() { val upstream: Observable<String> = Observable.empty() upstream.reduxStore( "InitialState", sideEffects = emptyList() ) { state, action -> state } .test() .assertNoErrors() .assertValues("InitialState") .assertComplete() }
  19. Pagination ➔ Use one source of truth for paging DB,

    api, local storage, etc.. ➔ Use DiffUtil ➔ Use distinctUntilChanged or similar
  20. Learnings ➔ Fair estimates ➔ Use appropriate technology for the

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

    task ➔ Design for flexibility ➔ Start small and validate hypothesis early