/* * Copyright 2016 Hannes Dorfmann. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.hannesdorfmann.mosby3.sample.mvi.view.home; import android.support.v4.util.Pair; import com.hannesdorfmann.mosby3.mvi.MviBasePresenter; import com.hannesdorfmann.mosby3.sample.mvi.businesslogic.feed.HomeFeedLoader; import com.hannesdorfmann.mosby3.sample.mvi.businesslogic.model.AdditionalItemsLoadable; import com.hannesdorfmann.mosby3.sample.mvi.businesslogic.model.FeedItem; import com.hannesdorfmann.mosby3.sample.mvi.businesslogic.model.SectionHeader; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.List; import timber.log.Timber; /** * @author Hannes Dorfmann */ public class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> { private final HomeFeedLoader feedLoader; public HomePresenter(HomeFeedLoader feedLoader) { this.feedLoader = feedLoader; } @Override protected void bindIntents() { // // In a real app this code would rather be moved to an Interactor // Observable<PartialStateChanges> loadFirstPage = intent(HomeView::loadFirstPageIntent).doOnNext( ignored -> Timber.d("intent: load first page")) .flatMap(ignored -> feedLoader.loadFirstPage() .map(items -> (PartialStateChanges) new PartialStateChanges.FirstPageLoaded(items)) .startWith(new PartialStateChanges.FirstPageLoading()) .onErrorReturn(PartialStateChanges.FirstPageError::new) .subscribeOn(Schedulers.io())); Observable<PartialStateChanges> nextPage = intent(HomeView::loadNextPageIntent).doOnNext(ignored -> Timber.d("intent: load next page")) .flatMap(ignored -> feedLoader.loadNextPage() .map(items -> (PartialStateChanges) new PartialStateChanges.NextPageLoaded(items)) .startWith(new PartialStateChanges.NextPageLoading()) .onErrorReturn(PartialStateChanges.NexPageLoadingError::new) .subscribeOn(Schedulers.io())); Observable<PartialStateChanges> pullToRefresh = intent(HomeView::pullToRefreshIntent).doOnNext( ignored -> Timber.d("intent: pull to refresh")) .flatMap(ignored -> feedLoader.loadNewestPage() .subscribeOn(Schedulers.io()) .map(items -> (PartialStateChanges) new PartialStateChanges.PullToRefreshLoaded(items)) .startWith(new PartialStateChanges.PullToRefreshLoading()) .onErrorReturn(PartialStateChanges.PullToRefeshLoadingError::new)); Observable<PartialStateChanges> loadMoreFromGroup = intent(HomeView::loadAllProductsFromCategoryIntent).doOnNext( categoryName -> Timber.d("intent: load more from category %s", categoryName)) .flatMap(categoryName -> feedLoader.loadProductsOfCategory(categoryName) .subscribeOn(Schedulers.io()) .map( products -> (PartialStateChanges) new PartialStateChanges.ProductsOfCategoryLoaded( categoryName, products)) .startWith(new PartialStateChanges.ProductsOfCategoryLoading(categoryName)) .onErrorReturn( error -> new PartialStateChanges.ProductsOfCategoryLoadingError(categoryName, error))); Observable<PartialStateChanges> allIntentsObservable = Observable.merge(loadFirstPage, nextPage, pullToRefresh, loadMoreFromGroup) .observeOn(AndroidSchedulers.mainThread()); HomeViewState initialState = new HomeViewState.Builder().firstPageLoading(true).build(); subscribeViewState( allIntentsObservable.scan(initialState, this::viewStateReducer).distinctUntilChanged(), HomeView::render); } private HomeViewState viewStateReducer(HomeViewState previousState, PartialStateChanges partialChanges) { if (partialChanges instanceof PartialStateChanges.FirstPageLoading) { return previousState.builder().firstPageLoading(true).firstPageError(null).build(); } if (partialChanges instanceof PartialStateChanges.FirstPageError) { return previousState.builder() .firstPageLoading(false) .firstPageError(((PartialStateChanges.FirstPageError) partialChanges).getError()) .build(); } if (partialChanges instanceof PartialStateChanges.FirstPageLoaded) { return previousState.builder() .firstPageLoading(false) .firstPageError(null) .data(((PartialStateChanges.FirstPageLoaded) partialChanges).getData()) .build(); } if (partialChanges instanceof PartialStateChanges.NextPageLoading) { return previousState.builder().nextPageLoading(true).nextPageError(null).build(); } if (partialChanges instanceof PartialStateChanges.NexPageLoadingError) { return previousState.builder() .nextPageLoading(false) .nextPageError(((PartialStateChanges.NexPageLoadingError) partialChanges).getError()) .build(); } if (partialChanges instanceof PartialStateChanges.NextPageLoaded) { List<FeedItem> data = new ArrayList<>(previousState.getData().size() + ((PartialStateChanges.NextPageLoaded) partialChanges).getData().size()); data.addAll(previousState.getData()); data.addAll(((PartialStateChanges.NextPageLoaded) partialChanges).getData()); return previousState.builder().nextPageLoading(false).nextPageError(null).data(data).build(); } if (partialChanges instanceof PartialStateChanges.PullToRefreshLoading) { return previousState.builder().pullToRefreshLoading(true).pullToRefreshError(null).build(); } if (partialChanges instanceof PartialStateChanges.PullToRefeshLoadingError) { return previousState.builder() .pullToRefreshLoading(false) .pullToRefreshError( ((PartialStateChanges.PullToRefeshLoadingError) partialChanges).getError()) .build(); } if (partialChanges instanceof PartialStateChanges.PullToRefreshLoaded) { List<FeedItem> data = new ArrayList<>(previousState.getData().size() + ((PartialStateChanges.PullToRefreshLoaded) partialChanges).getData().size()); data.addAll(((PartialStateChanges.PullToRefreshLoaded) partialChanges).getData()); data.addAll(previousState.getData()); return previousState.builder() .pullToRefreshLoading(false) .pullToRefreshError(null) .data(data) .build(); } if (partialChanges instanceof PartialStateChanges.ProductsOfCategoryLoading) { Pair<Integer, AdditionalItemsLoadable> found = findAdditionalItems( ((PartialStateChanges.ProductsOfCategoryLoading) partialChanges).getCategoryName(), previousState.getData()); AdditionalItemsLoadable foundItem = found.second; AdditionalItemsLoadable toInsert = new AdditionalItemsLoadable(foundItem.getMoreItemsAvailableCount(), foundItem.getCategoryName(), true, null); List<FeedItem> data = new ArrayList<>(previousState.getData().size()); data.addAll(previousState.getData()); data.set(found.first, toInsert); return previousState.builder().data(data).build(); } if (partialChanges instanceof PartialStateChanges.ProductsOfCategoryLoadingError) { Pair<Integer, AdditionalItemsLoadable> found = findAdditionalItems( ((PartialStateChanges.ProductsOfCategoryLoadingError) partialChanges).getCategoryName(), previousState.getData()); AdditionalItemsLoadable foundItem = found.second; AdditionalItemsLoadable toInsert = new AdditionalItemsLoadable(foundItem.getMoreItemsAvailableCount(), foundItem.getCategoryName(), false, ((PartialStateChanges.ProductsOfCategoryLoadingError) partialChanges).getError()); List<FeedItem> data = new ArrayList<>(previousState.getData().size()); data.addAll(previousState.getData()); data.set(found.first, toInsert); return previousState.builder().data(data).build(); } if (partialChanges instanceof PartialStateChanges.ProductsOfCategoryLoaded) { Pair<Integer, AdditionalItemsLoadable> found = findAdditionalItems( ((PartialStateChanges.ProductsOfCategoryLoaded) partialChanges).getCategoryName(), previousState.getData()); List<FeedItem> data = new ArrayList<>(previousState.getData().size() + ((PartialStateChanges.ProductsOfCategoryLoaded) partialChanges).getData().size()); data.addAll(previousState.getData()); // Search for the section header int sectionHeaderIndex = -1; for (int i = found.first; i >= 0; i--) { FeedItem item = previousState.getData().get(i); if (item instanceof SectionHeader && ((SectionHeader) item).getName() .equals( ((PartialStateChanges.ProductsOfCategoryLoaded) partialChanges).getCategoryName())) { sectionHeaderIndex = i; break; } // Remove all items of that category. The new list of products will be added afterwards data.remove(i); } if (sectionHeaderIndex < 0) { throw new RuntimeException("Couldn't find the section header for category " + ((PartialStateChanges.ProductsOfCategoryLoaded) partialChanges).getCategoryName()); } data.addAll(sectionHeaderIndex + 1, ((PartialStateChanges.ProductsOfCategoryLoaded) partialChanges).getData()); return previousState.builder().data(data).build(); } throw new IllegalStateException("Don't know how to reduce the partial state " + partialChanges); } /** * find the {@link AdditionalItemsLoadable} for the given category name * * @param categoryName The name of the category * @param items the list of feeditems */ private Pair<Integer, AdditionalItemsLoadable> findAdditionalItems(String categoryName, List<FeedItem> items) { int size = items.size(); for (int i = 0; i < size; i++) { FeedItem item = items.get(i); if (item instanceof AdditionalItemsLoadable && ((AdditionalItemsLoadable) item).getCategoryName().equals(categoryName)) { return Pair.create(i, (AdditionalItemsLoadable) item); } } throw new RuntimeException("No " + AdditionalItemsLoadable.class.getSimpleName() + " has been found for category = " + categoryName); } }