├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── attrs.xml
│ │ │ │ ├── ids.xml
│ │ │ │ ├── strings.xml
│ │ │ │ ├── styles.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── colors.xml
│ │ │ ├── drawable
│ │ │ │ └── chip.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ ├── main_activity.xml
│ │ │ │ ├── pokedex_item_loading.xml
│ │ │ │ ├── pokemon_chip.xml
│ │ │ │ ├── pokemon_table_card.xml
│ │ │ │ ├── pokedex_item.xml
│ │ │ │ ├── pokemon_titleview.xml
│ │ │ │ ├── pokedex_item_error.xml
│ │ │ │ ├── include_error.xml
│ │ │ │ ├── pokedex_fragment.xml
│ │ │ │ └── pokemon_fragment.xml
│ │ ├── java
│ │ │ └── me
│ │ │ │ └── tatarka
│ │ │ │ └── pokemvvm
│ │ │ │ ├── viewmodel
│ │ │ │ ├── State.java
│ │ │ │ └── ErrorViewModel.java
│ │ │ │ ├── pokedex
│ │ │ │ ├── PokedexComponent.java
│ │ │ │ ├── PokedexRetainedComponent.java
│ │ │ │ ├── LoadingItemViewModel.java
│ │ │ │ ├── ErrorItemViewModel.java
│ │ │ │ ├── PokemonItemViewModel.java
│ │ │ │ ├── PagedResult.java
│ │ │ │ ├── PokedexFragment.java
│ │ │ │ ├── PokedexPager.java
│ │ │ │ └── PokedexViewModel.java
│ │ │ │ ├── pokemon
│ │ │ │ ├── PokemonComponent.java
│ │ │ │ ├── Row.java
│ │ │ │ ├── Chip.java
│ │ │ │ ├── SlideDownAndFadeOut.java
│ │ │ │ ├── BindingAdaptors.java
│ │ │ │ ├── SlideAndFadeAnimatorUtil.java
│ │ │ │ ├── AnchorAppBarSlide.java
│ │ │ │ ├── ChipLayout.java
│ │ │ │ ├── AppBarAnimator.java
│ │ │ │ ├── TableCardLayout.java
│ │ │ │ ├── PokemonFragment.java
│ │ │ │ ├── TitleView.java
│ │ │ │ └── PokemonViewModel.java
│ │ │ │ ├── log
│ │ │ │ ├── EmptyTree.java
│ │ │ │ └── LogModule.java
│ │ │ │ ├── dagger
│ │ │ │ ├── ViewScope.java
│ │ │ │ ├── RetainedScope.java
│ │ │ │ ├── SingletonComponent.java
│ │ │ │ └── Dagger.java
│ │ │ │ ├── api
│ │ │ │ ├── PokeService.java
│ │ │ │ ├── PokemonItem.java
│ │ │ │ ├── Page.java
│ │ │ │ ├── HttpUrlTypeAdapter.java
│ │ │ │ ├── CachingInterceptor.java
│ │ │ │ ├── RxPagedLoader.java
│ │ │ │ ├── ApiModule.java
│ │ │ │ └── Pokemon.java
│ │ │ │ ├── databinding
│ │ │ │ ├── RVUtils.java
│ │ │ │ ├── BindingAdapters.java
│ │ │ │ └── RecyclerViewBindingAdapters.java
│ │ │ │ ├── util
│ │ │ │ └── StringUtils.java
│ │ │ │ ├── BaseActivity.java
│ │ │ │ ├── MainActivity.java
│ │ │ │ └── BaseFragment.java
│ │ └── AndroidManifest.xml
│ ├── debug
│ │ └── res
│ │ │ └── drawable-mdpi
│ │ │ └── bulbasaur.png
│ └── test
│ │ └── java
│ │ └── me
│ │ └── tatarka
│ │ └── pokemvvm
│ │ └── pokedex
│ │ ├── PokemonViewModelTest.java
│ │ ├── PokedexViewModelTest.java
│ │ └── PokedexPagerTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── README.md
├── gradlew.bat
├── gradlew
└── LICENSE
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/
4 | .DS_Store
5 | /build
6 | /captures
7 | *.iml
8 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/debug/res/drawable-mdpi/bulbasaur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/debug/res/drawable-mdpi/bulbasaur.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evant/PokeMVVM/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/viewmodel/State.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.viewmodel;
2 |
3 | public enum State {
4 | LOADING, LOADED, ERROR
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/viewmodel/ErrorViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.viewmodel;
2 |
3 | /**
4 | * A view model that can have an error state.
5 | */
6 | public interface ErrorViewModel {
7 |
8 | void retry();
9 | }
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Sep 10 16:54:24 EDT 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/chip.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokedexComponent.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import dagger.Subcomponent;
4 | import me.tatarka.pokemvvm.dagger.ViewScope;
5 |
6 | @ViewScope
7 | @Subcomponent
8 | public interface PokedexComponent {
9 | void inject(PokedexFragment fragment);
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokedexRetainedComponent.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import dagger.Subcomponent;
4 | import me.tatarka.pokemvvm.dagger.RetainedScope;
5 |
6 | @RetainedScope
7 | @Subcomponent
8 | public interface PokedexRetainedComponent {
9 | PokedexComponent pokedex();
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/PokemonComponent.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import dagger.Subcomponent;
4 | import me.tatarka.pokemvvm.dagger.ViewScope;
5 |
6 | @ViewScope
7 | @Subcomponent
8 | public interface PokemonComponent {
9 | void inject(PokemonFragment fragment);
10 |
11 | void inject(TitleView view);
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/log/EmptyTree.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.log;
2 |
3 | import timber.log.Timber;
4 |
5 | /**
6 | * A tree that does nothing, useful for production or tests.
7 | */
8 | public class EmptyTree extends Timber.Tree {
9 | @Override
10 | protected void log(int priority, String tag, String message, Throwable t) {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PokeMVVM
3 | Error Loading Data
4 | Retry
5 |
6 | #%1$s %2$s
7 | HT: %1$d WT: %2$d
8 | Stats
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/dagger/ViewScope.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.dagger;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | import javax.inject.Scope;
7 |
8 | /**
9 | * Scoped to the same lifecycle as view. It will be recreated on configuration changes.
10 | */
11 | @Scope
12 | @Retention(RetentionPolicy.RUNTIME)
13 | public @interface ViewScope {
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/dagger/RetainedScope.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.dagger;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | import javax.inject.Scope;
7 |
8 | /**
9 | * Scoped to it's parent (activity/fragment) but retained across configuration changes.
10 | */
11 | @Scope
12 | @Retention(RetentionPolicy.RUNTIME)
13 | public @interface RetainedScope {
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/PokeService.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import retrofit2.http.GET;
4 | import retrofit2.http.Url;
5 | import rx.Single;
6 |
7 | public interface PokeService {
8 | @GET("pokemon")
9 | Single> firstPokemonPage();
10 |
11 | @GET
12 | Single> nextPokemonPage(@Url String url);
13 |
14 | @GET
15 | Single pokemon(@Url String url);
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 16dp
7 | 60dp
8 | 24dp
9 |
10 | 64dp
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokedex_item_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/databinding/RVUtils.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.databinding;
2 |
3 | import android.support.v7.widget.LinearLayoutManager;
4 | import android.support.v7.widget.RecyclerView;
5 |
6 | public class RVUtils {
7 |
8 | public static int lastVisibleItemPosition(RecyclerView recyclerView) {
9 | LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
10 | return layoutManager.findLastVisibleItemPosition();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/Row.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | public class Row {
4 |
5 | private final CharSequence name;
6 | private final CharSequence value;
7 |
8 | public Row(CharSequence name, CharSequence value) {
9 | this.name = name;
10 | this.value = value;
11 | }
12 |
13 | public CharSequence name() {
14 | return name;
15 | }
16 |
17 | public CharSequence value() {
18 | return value;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/Chip.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.support.annotation.ColorInt;
4 |
5 | public class Chip {
6 | @ColorInt
7 | private final int color;
8 | private final CharSequence text;
9 |
10 | public Chip(int color, CharSequence text) {
11 | this.color = color;
12 | this.text = text;
13 | }
14 |
15 | public int color() {
16 | return color;
17 | }
18 |
19 | public CharSequence text() {
20 | return text;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/util/StringUtils.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.util;
2 |
3 | import android.support.annotation.Nullable;
4 | import android.text.TextUtils;
5 |
6 | public class StringUtils {
7 | public static String capitalize(@Nullable String string) {
8 | if (TextUtils.isEmpty(string)) {
9 | return string;
10 | } else {
11 | char ch = string.charAt(0);
12 | return Character.isTitleCase(ch) ? string : Character.toTitleCase(ch) + string.substring(1);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/log/LogModule.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.log;
2 |
3 | import javax.inject.Singleton;
4 |
5 | import dagger.Module;
6 | import dagger.Provides;
7 | import me.tatarka.pokemvvm.BuildConfig;
8 | import timber.log.Timber;
9 |
10 | @Module
11 | public class LogModule {
12 |
13 | @Provides
14 | @Singleton
15 | public Timber.Tree providesTree() {
16 | if (BuildConfig.DEBUG) {
17 | return new Timber.DebugTree();
18 | } else {
19 | return new EmptyTree();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokemon_chip.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/LoadingItemViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import me.tatarka.bindingcollectionadapter.ItemBinding;
4 | import me.tatarka.bindingcollectionadapter.itembindings.ItemBindingModel;
5 | import me.tatarka.pokemvvm.R;
6 |
7 | /**
8 | * Represents an item that just shows a loading indicator.
9 | */
10 | public class LoadingItemViewModel implements ItemBindingModel {
11 | @Override
12 | public void onItemBind(ItemBinding itemBinding) {
13 | itemBinding.set(ItemBinding.VAR_NONE, R.layout.pokedex_item_loading);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/databinding/BindingAdapters.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.databinding;
2 |
3 | import android.databinding.BindingAdapter;
4 | import android.view.View;
5 |
6 | /**
7 | * This class is only for general binding adapters. If you have some specific to some part
8 | * of the app it should go in it's respective package.
9 | */
10 | public class BindingAdapters {
11 |
12 | @BindingAdapter("android:visibility")
13 | public static void setVisible(View view, boolean visible) {
14 | view.setVisibility(visible ? View.VISIBLE : View.GONE);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/dagger/SingletonComponent.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.dagger;
2 |
3 | import javax.inject.Singleton;
4 |
5 | import dagger.Component;
6 | import me.tatarka.pokemvvm.api.ApiModule;
7 | import me.tatarka.pokemvvm.log.LogModule;
8 | import me.tatarka.pokemvvm.pokedex.PokedexRetainedComponent;
9 | import me.tatarka.pokemvvm.pokemon.PokemonComponent;
10 |
11 | @Singleton
12 | @Component(modules = {ApiModule.class, LogModule.class})
13 | public interface SingletonComponent {
14 |
15 | PokedexRetainedComponent retainedPokedex();
16 |
17 | PokemonComponent pokemon();
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokemon_table_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/SlideDownAndFadeOut.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.animation.Animator;
4 | import android.transition.TransitionValues;
5 | import android.transition.Visibility;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | public class SlideDownAndFadeOut extends Visibility {
10 |
11 | @Override
12 | public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
13 | if (startValues == null) {
14 | return null;
15 | }
16 | return SlideAndFadeAnimatorUtil.hide(view);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /opt/android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/PokemonItem.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import android.os.Parcelable;
4 |
5 | import java.util.List;
6 |
7 | import auto.parcel.AutoParcel;
8 | import me.tatarka.gsonvalue.annotations.GsonConstructor;
9 | import okhttp3.HttpUrl;
10 |
11 | @AutoParcel
12 | public abstract class PokemonItem implements Parcelable {
13 |
14 | @GsonConstructor
15 | public static PokemonItem create(String name, HttpUrl url) {
16 | return new AutoParcel_PokemonItem(name, url);
17 | }
18 |
19 | public abstract String name();
20 |
21 | public abstract HttpUrl url();
22 |
23 | public String number() {
24 | List pathSegments = url().pathSegments();
25 | return pathSegments.get(pathSegments.size() - 2);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/Page.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | import java.util.List;
6 |
7 | import auto.parcel.AutoParcel;
8 | import me.tatarka.gsonvalue.annotations.GsonConstructor;
9 | import okhttp3.HttpUrl;
10 |
11 | @AutoParcel
12 | public abstract class Page {
13 |
14 | @GsonConstructor
15 | public static Page create(int count, @Nullable HttpUrl next, @Nullable HttpUrl previous, List results) {
16 | return new AutoParcel_Page<>(count, next, previous, results);
17 | }
18 |
19 | public abstract int count();
20 |
21 | @Nullable
22 | public abstract HttpUrl next();
23 |
24 | @Nullable
25 | public abstract HttpUrl previous();
26 |
27 | public abstract List results();
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokedex_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/ErrorItemViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import com.android.databinding.library.baseAdapters.BR;
4 |
5 | import me.tatarka.bindingcollectionadapter.ItemBinding;
6 | import me.tatarka.bindingcollectionadapter.itembindings.ItemBindingModel;
7 | import me.tatarka.pokemvvm.R;
8 |
9 | public class ErrorItemViewModel implements ItemBindingModel {
10 |
11 | private final OnRetryListener listener;
12 |
13 | public ErrorItemViewModel(OnRetryListener listener) {
14 | this.listener = listener;
15 | }
16 |
17 | public void retry() {
18 | listener.retry();
19 | }
20 |
21 | @Override
22 | public void onItemBind(ItemBinding itemBinding) {
23 | itemBinding.set(BR.item, R.layout.pokedex_item_error);
24 | }
25 |
26 | public interface OnRetryListener {
27 | void retry();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/BindingAdaptors.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.databinding.BindingAdapter;
4 | import android.view.View;
5 |
6 | public class BindingAdaptors {
7 |
8 | @BindingAdapter({"android:visibility", "slideAndFade"})
9 | public static void setVisibleAnimated(final View view, boolean visible, boolean slideAndFade) {
10 | if (!slideAndFade) {
11 | view.setVisibility(visible ? View.VISIBLE : View.GONE);
12 | return;
13 | }
14 | int currentVisibility = view.getVisibility();
15 | if (currentVisibility == (visible ? View.VISIBLE : View.GONE)) {
16 | return;
17 | }
18 | if (visible) {
19 | view.setVisibility(View.VISIBLE);
20 | SlideAndFadeAnimatorUtil.show(view).start();
21 | } else {
22 | view.setVisibility(View.GONE);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/HttpUrlTypeAdapter.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import com.google.gson.TypeAdapter;
4 | import com.google.gson.stream.JsonReader;
5 | import com.google.gson.stream.JsonToken;
6 | import com.google.gson.stream.JsonWriter;
7 |
8 | import java.io.IOException;
9 |
10 | import okhttp3.HttpUrl;
11 |
12 | public class HttpUrlTypeAdapter extends TypeAdapter {
13 | @Override
14 | public void write(JsonWriter out, HttpUrl value) throws IOException {
15 | if (value == null) {
16 | out.nullValue();
17 | } else {
18 | out.value(value.toString());
19 | }
20 | }
21 |
22 | @Override
23 | public HttpUrl read(JsonReader in) throws IOException {
24 | if (in.peek() == JsonToken.NULL) {
25 | in.nextNull();
26 | return null;
27 | } else {
28 | return HttpUrl.parse(in.nextString());
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v7.app.AppCompatActivity;
6 |
7 | import me.tatarka.retainstate.RetainState;
8 |
9 | public class BaseActivity extends AppCompatActivity implements RetainState.Provider {
10 | private RetainState retainState;
11 |
12 | @Override
13 | protected void onCreate(@Nullable Bundle savedInstanceState) {
14 | super.onCreate(savedInstanceState);
15 | retainState = new RetainState(getLastCustomNonConfigurationInstance());
16 | }
17 |
18 | @Override
19 | public Object onRetainCustomNonConfigurationInstance() {
20 | return retainState.getState();
21 | }
22 |
23 | @Override
24 | public RetainState getRetainState() {
25 | if (retainState == null) {
26 | throw new IllegalStateException("RetainState has not yet been initialized");
27 | }
28 | return retainState;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PokeMVVM
2 | A playground for MVVM style architecture on Android
3 |
4 | This architecture relies heavily on android databinding, but requires _nothing_ else not provided by the framework. You split up you app into the following components:
5 |
6 | ## Model
7 | Holds are your data and buisness logic. You shouldn't need any of the android fragmework for this.
8 |
9 | ## View
10 | Includes your layout files and any custom views you need to create. Have a very simple lifecycle of being created and destroyed on configuration changes. You use databinding to connect this to your view model.
11 |
12 | ## View Model
13 | Includes are the logic to display your models and respond to user events. These have the same lifecycle of views.
14 |
15 | ## Api/Database
16 | These obtain and store your data, often asynchrnously, but don't care about Android's lifecycle.
17 |
18 | ## Activity/Fragments
19 | Used to coordinate the above components and deal with lifecycle events. May use loaders to get data from the api into the view model. Be careful with these, as soon as you need to do anything more complex than simple coordination, move it into it's own class.
20 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/SlideAndFadeAnimatorUtil.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorSet;
5 | import android.animation.ObjectAnimator;
6 | import android.view.View;
7 |
8 | import me.tatarka.pokemvvm.R;
9 |
10 | public class SlideAndFadeAnimatorUtil {
11 |
12 | public static Animator show(View view) {
13 | int translationAmount = view.getResources().getDimensionPixelOffset(R.dimen.anim_translation);
14 | AnimatorSet set = new AnimatorSet();
15 | ObjectAnimator alpha = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1);
16 | ObjectAnimator translationY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationAmount, 0);
17 | set.playTogether(alpha, translationY);
18 | return set;
19 | }
20 |
21 | public static Animator hide(View view) {
22 | int translationAmount = view.getResources().getDimensionPixelOffset(R.dimen.anim_translation);
23 | AnimatorSet set = new AnimatorSet();
24 | ObjectAnimator alpha = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0);
25 | ObjectAnimator translationY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0, translationAmount);
26 | set.playTogether(alpha, translationY);
27 | return set;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokemon_titleview.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
13 |
19 |
20 |
26 |
27 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #424242
4 | #212121
5 | #d50000
6 | #ffffff
7 |
8 | #f44336
9 | #e91e63
10 | #ff80ab
11 | #9c27b0
12 | #673ab7
13 | #3f51b5
14 | #2196f3
15 | #00bcd4
16 | #009688
17 | #4caf50
18 | #8bc34a
19 | #cddc39
20 | #ffeb3b
21 | #ff9800
22 | #ff5722
23 | #795548
24 | #9e9e9e
25 | #607d8b
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokedex_item_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
15 |
16 |
25 |
26 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/dagger/Dagger.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.dagger;
2 |
3 | import android.content.Context;
4 |
5 | import me.tatarka.pokemvvm.R;
6 | import me.tatarka.pokemvvm.api.ApiModule;
7 | import me.tatarka.pokemvvm.pokedex.PokedexFragment;
8 | import me.tatarka.pokemvvm.pokedex.PokedexRetainedComponent;
9 | import me.tatarka.pokemvvm.pokemon.PokemonFragment;
10 | import me.tatarka.retainstate.RetainState;
11 |
12 | public class Dagger {
13 |
14 | private static SingletonComponent singletonComponent;
15 |
16 | public static SingletonComponent component(Context context) {
17 | if (singletonComponent == null) {
18 | singletonComponent = DaggerSingletonComponent.builder()
19 | .apiModule(new ApiModule(context))
20 | .build();
21 | }
22 | return singletonComponent;
23 | }
24 |
25 | public static void inject(final PokedexFragment fragment) {
26 | RetainState.from(fragment).retain(R.id.component, new RetainState.OnCreate() {
27 | @Override
28 | public PokedexRetainedComponent onCreate() {
29 | return component(fragment.getContext()).retainedPokedex();
30 | }
31 | }).pokedex().inject(fragment);
32 | }
33 |
34 | public static void inject(PokemonFragment fragment) {
35 | component(fragment.getContext()).pokemon().inject(fragment);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
13 |
14 |
15 |
21 |
22 |
29 |
30 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/MainActivity.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.os.Bundle;
5 | import android.support.v4.app.FragmentTransaction;
6 |
7 | import me.tatarka.pokemvvm.api.PokemonItem;
8 | import me.tatarka.pokemvvm.pokedex.PokedexFragment;
9 | import me.tatarka.pokemvvm.pokedex.PokemonItemViewModel;
10 | import me.tatarka.pokemvvm.pokemon.PokemonFragment;
11 |
12 | public class MainActivity extends BaseActivity implements PokemonItemViewModel.OnSelectListener {
13 |
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | DataBindingUtil.setContentView(this, R.layout.main_activity);
18 | if (savedInstanceState == null) {
19 | getSupportFragmentManager()
20 | .beginTransaction()
21 | .add(R.id.content, PokedexFragment.newInstance())
22 | .commit();
23 | }
24 | }
25 |
26 | @Override
27 | public void selectItem(PokemonItem item) {
28 | //TODO: tablet mater detail support.
29 | PokedexFragment currentFragment = (PokedexFragment) getSupportFragmentManager().findFragmentById(R.id.content);
30 | FragmentTransaction ft = getSupportFragmentManager().beginTransaction()
31 | .replace(R.id.content, PokemonFragment.newInstance(item))
32 | .addToBackStack(item.name());
33 | currentFragment.addSharedElements(ft);
34 | ft.commit();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/CachingInterceptor.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import android.content.Context;
4 | import android.net.ConnectivityManager;
5 | import android.net.NetworkInfo;
6 |
7 | import java.io.IOException;
8 |
9 | import okhttp3.CacheControl;
10 | import okhttp3.Interceptor;
11 | import okhttp3.Response;
12 |
13 | /**
14 | * The pokeapi has mostly static data but does not include cache headers. Add them manually to
15 | * reduce the number of network requests since this data doesn't often change. Also, if you don't
16 | * have a network connection we can still return old data.
17 | */
18 | public class CachingInterceptor implements Interceptor {
19 | private final ConnectivityManager cm;
20 |
21 | public CachingInterceptor(Context context) {
22 | cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
23 | }
24 |
25 | @Override
26 | public Response intercept(Chain chain) throws IOException {
27 | if (isConnected()) {
28 | Response response = chain.proceed(chain.request());
29 | return response
30 | .newBuilder()
31 | .addHeader("Cache-Control", "public,max-age=" + (24 * 3600))
32 | .build();
33 | } else {
34 | return chain.proceed(chain.request().newBuilder()
35 | .cacheControl(CacheControl.FORCE_CACHE)
36 | .build());
37 | }
38 | }
39 |
40 | private boolean isConnected() {
41 | NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
42 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokemonItemViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import android.content.res.Resources;
4 |
5 | import me.tatarka.bindingcollectionadapter.ItemBinding;
6 | import me.tatarka.bindingcollectionadapter.itembindings.ItemBindingModel;
7 | import me.tatarka.pokemvvm.BR;
8 | import me.tatarka.pokemvvm.R;
9 | import me.tatarka.pokemvvm.api.PokemonItem;
10 | import me.tatarka.pokemvvm.util.StringUtils;
11 |
12 | public class PokemonItemViewModel implements ItemBindingModel {
13 |
14 | private final PokemonItem item;
15 | private final OnSelectListener listener;
16 |
17 | public PokemonItemViewModel(PokemonItem item, OnSelectListener listener) {
18 | this.item = item;
19 | this.listener = listener;
20 | }
21 |
22 | public String name(Resources resources) {
23 | return resources.getString(R.string.pokemon_name, item.number(), StringUtils.capitalize(item.name()));
24 | }
25 |
26 | public void select() {
27 | listener.selectItem(item);
28 | }
29 |
30 | @Override
31 | public boolean equals(Object o) {
32 | if (this == o) return true;
33 | if (o == null || getClass() != o.getClass()) return false;
34 | PokemonItemViewModel that = (PokemonItemViewModel) o;
35 | return item.equals(that.item);
36 | }
37 |
38 | @Override
39 | public int hashCode() {
40 | return item.hashCode();
41 | }
42 |
43 | @Override
44 | public void onItemBind(ItemBinding itemBinding) {
45 | itemBinding.set(BR.item, R.layout.pokedex_item);
46 | }
47 |
48 | public interface OnSelectListener {
49 | void selectItem(PokemonItem item);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/BaseFragment.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v4.app.Fragment;
6 |
7 | import me.tatarka.loader.LoaderManager;
8 | import me.tatarka.retainstate.RetainState;
9 | import me.tatarka.retainstate.fragment.RetainStateFragment;
10 |
11 | public class BaseFragment extends Fragment implements RetainState.Provider {
12 | private RetainState retainState;
13 | private LoaderManager loaderManager;
14 |
15 | @Override
16 | public void onActivityCreated(@Nullable Bundle savedInstanceState) {
17 | super.onActivityCreated(savedInstanceState);
18 | retainState = RetainState.from(getHost()).retain(RetainStateFragment.getId(this), RetainState.CREATE);
19 | loaderManager = retainState.retain(R.id.loader_manager, LoaderManager.CREATE);
20 | }
21 |
22 | public LoaderManager loaderManager() {
23 | if (loaderManager == null) {
24 | throw new IllegalStateException("LoaderManager has not yet been initialized");
25 | }
26 | return loaderManager;
27 | }
28 |
29 | @Override
30 | public RetainState getRetainState() {
31 | if (retainState == null) {
32 | throw new IllegalStateException("RetainState has not yet been initialized");
33 | }
34 | return retainState;
35 | }
36 |
37 | @Override
38 | public void onStop() {
39 | super.onStop();
40 | if (loaderManager != null && isRemoving()) {
41 | loaderManager.detach();
42 | }
43 | }
44 |
45 | @Override
46 | public void onDestroy() {
47 | super.onDestroy();
48 | if (loaderManager != null) {
49 | if (getActivity().isFinishing() || isRemoving()) {
50 | RetainState.from(getHost()).remove(RetainStateFragment.getId(this));
51 | loaderManager.destroy();
52 | } else {
53 | loaderManager.detach();
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/databinding/RecyclerViewBindingAdapters.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.databinding;
2 |
3 | import android.databinding.BindingAdapter;
4 | import android.databinding.adapters.ListenerUtil;
5 | import android.support.v7.widget.RecyclerView;
6 |
7 | import me.tatarka.pokemvvm.R;
8 |
9 | /**
10 | * Binding adapters for recyclerview. These really should be provided.
11 | */
12 | public class RecyclerViewBindingAdapters {
13 |
14 | @BindingAdapter(value = {"onScrollStateChanged", "onScrolled"}, requireAll = false)
15 | public static void setOnScrollListener(RecyclerView view, final OnScrollStateChanged scrollStateChanged, final OnScrolled scrolled) {
16 | final RecyclerView.OnScrollListener newValue;
17 | if (scrollStateChanged == null && scrolled == null) {
18 | newValue = null;
19 | } else {
20 | newValue = new RecyclerView.OnScrollListener() {
21 | @Override
22 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
23 | if (scrollStateChanged != null) {
24 | scrollStateChanged.onScrollStateChanged(recyclerView, newState);
25 | }
26 | }
27 |
28 | @Override
29 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
30 | if (scrolled != null) {
31 | scrolled.onScrolled(recyclerView, dx, dy);
32 | }
33 | }
34 | };
35 | }
36 | final RecyclerView.OnScrollListener oldValue = ListenerUtil.trackListener(view, newValue, R.id.recyclerViewOnScrollListener);
37 | if (oldValue != null) {
38 | view.removeOnScrollListener(oldValue);
39 | }
40 | if (newValue != null) {
41 | view.addOnScrollListener(newValue);
42 | }
43 | }
44 |
45 | public interface OnScrollStateChanged {
46 | void onScrollStateChanged(RecyclerView recyclerView, int newState);
47 | }
48 |
49 | public interface OnScrolled {
50 | void onScrolled(RecyclerView recyclerView, int dx, int dy);
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/RxPagedLoader.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import me.tatarka.loader.Loader;
7 | import me.tatarka.pokemvvm.pokedex.PagedResult;
8 | import me.tatarka.retainstate.RetainState;
9 | import rx.Observable;
10 | import rx.Subscription;
11 | import rx.android.schedulers.AndroidSchedulers;
12 | import rx.functions.Action0;
13 | import rx.functions.Action1;
14 |
15 | public class RxPagedLoader extends Loader> {
16 |
17 | public static RetainState.OnCreate> create(final Observable> observable) {
18 | return new RetainState.OnCreate>() {
19 | @Override
20 | public RxPagedLoader onCreate() {
21 | return new RxPagedLoader<>(observable);
22 | }
23 | };
24 | }
25 |
26 | private final Observable> observable;
27 | private Subscription subscription;
28 | private List results = new ArrayList<>();
29 |
30 | public RxPagedLoader(Observable> observable) {
31 | this.observable = observable;
32 | }
33 |
34 | @Override
35 | protected void onStart(final Receiver receiver) {
36 | subscription = observable.observeOn(AndroidSchedulers.mainThread())
37 | .subscribe(new Action1>() {
38 | @Override
39 | public void call(List items) {
40 | results.addAll(items);
41 | receiver.deliverResult(PagedResult.success(results, results.size() - items.size()));
42 | }
43 | }, new Action1() {
44 | @Override
45 | public void call(Throwable error) {
46 | receiver.deliverResult(PagedResult.error(results, error));
47 | receiver.complete();
48 | }
49 | }, new Action0() {
50 | @Override
51 | public void call() {
52 | receiver.complete();
53 | }
54 | });
55 | }
56 |
57 | @Override
58 | protected void onCancel() {
59 | subscription.unsubscribe();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'com.neenbedankt.android-apt'
3 |
4 | android {
5 | compileSdkVersion 24
6 | buildToolsVersion "24.0.2"
7 |
8 | defaultConfig {
9 | applicationId "me.tatarka.pokemvvm"
10 | minSdkVersion 21
11 | targetSdkVersion 24
12 | versionCode 1
13 | versionName "1.0"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | dataBinding {
22 | enabled = true
23 | }
24 | }
25 |
26 | dependencies {
27 | compile 'com.android.support:appcompat-v7:24.2.0'
28 | compile 'com.android.support:design:24.2.0'
29 | compile 'com.android.support:recyclerview-v7:24.2.0'
30 | compile 'com.android.support:cardview-v7:24.2.0'
31 | compile 'com.android.support:gridlayout-v7:24.2.0'
32 | compile 'com.squareup.retrofit2:retrofit:2.0.0'
33 | compile 'com.squareup.retrofit2:converter-gson:2.0.0'
34 | compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
35 | compile 'io.reactivex:rxjava:1.1.1'
36 | compile 'io.reactivex:rxandroid:1.1.0'
37 | compile 'com.github.frankiesardo:auto-parcel:0.3.1'
38 | compile 'me.tatarka.bindingcollectionadapter:bindingcollectionadapter:2.0.0-beta2'
39 | compile 'me.tatarka.bindingcollectionadapter:bindingcollectionadapter-recyclerview:2.0.0-beta2'
40 | apt 'com.github.frankiesardo:auto-parcel-processor:0.3.1'
41 | compile 'me.tatarka.gsonvalue:gsonvalue:0.4'
42 | apt 'me.tatarka.gsonvalue:gsonvalue-processor:0.4'
43 | compile 'com.google.dagger:dagger:2.0'
44 | apt 'com.google.dagger:dagger-compiler:2.0'
45 | provided 'org.glassfish:javax.annotation:10.0-b28'
46 | compile 'me.tatarka.retainstate:retainstate:0.3'
47 | compile 'me.tatarka.retainstate:loader:0.3'
48 | compile 'me.tatarka.retainstate:loader-rx:0.3'
49 | compile 'me.tatarka.retainstate:fragment:0.3'
50 | compile 'com.squareup.picasso:picasso:2.5.2'
51 | compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
52 | compile 'com.jakewharton.timber:timber:4.1.1'
53 | testCompile 'junit:junit:4.12'
54 | testCompile 'org.assertj:assertj-core:1.7.1'
55 | testCompile 'org.mockito:mockito-core:2.0.44-beta'
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/test/java/me/tatarka/pokemvvm/pokedex/PokemonViewModelTest.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.junit.runners.JUnit4;
7 |
8 | import java.util.Arrays;
9 |
10 | import me.tatarka.loader.Result;
11 | import me.tatarka.pokemvvm.api.Pokemon;
12 | import me.tatarka.pokemvvm.log.EmptyTree;
13 | import me.tatarka.pokemvvm.pokemon.PokemonViewModel;
14 | import me.tatarka.pokemvvm.viewmodel.State;
15 | import okhttp3.HttpUrl;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 |
19 | @RunWith(JUnit4.class)
20 | public class PokemonViewModelTest {
21 |
22 | PokemonViewModel viewModel;
23 |
24 | @Before
25 | public void setUp() {
26 | viewModel = new PokemonViewModel(new EmptyTree());
27 | }
28 |
29 | @Test
30 | public void startsLoadingAndEmpty() {
31 | assertThat(viewModel.getState()).isEqualTo(State.LOADING);
32 | assertThat(viewModel.getPokemon()).isNull();
33 | }
34 |
35 | @Test
36 | public void setsPokemonAndFinishesLoading() {
37 | viewModel.setPokemon(Result.success(Pokemon.create(0, "bulbasuar", 7, 69,
38 | Pokemon.Sprites.create(HttpUrl.parse("http://url")),
39 | Arrays.asList(
40 | Pokemon.StatEntry.create(45, Pokemon.Stat.create("speed")),
41 | Pokemon.StatEntry.create(65, Pokemon.Stat.create("special-defense")),
42 | Pokemon.StatEntry.create(65, Pokemon.Stat.create("special-attack")),
43 | Pokemon.StatEntry.create(49, Pokemon.Stat.create("defense")),
44 | Pokemon.StatEntry.create(49, Pokemon.Stat.create("attack")),
45 | Pokemon.StatEntry.create(45, Pokemon.Stat.create("hp"))
46 | ),
47 | Arrays.asList(
48 | Pokemon.TypeEntry.create(1, Pokemon.Type.create("grass")),
49 | Pokemon.TypeEntry.create(2, Pokemon.Type.create("poison"))))));
50 | assertThat(viewModel.getState()).isEqualTo(State.LOADED);
51 | assertThat(viewModel.getPokemon()).isNotNull();
52 | }
53 |
54 | @Test
55 | public void setsErrorState() {
56 | viewModel.setPokemon(Result.error(new Exception()));
57 | assertThat(viewModel.getState()).isEqualTo(State.ERROR);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/ApiModule.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import android.content.Context;
4 |
5 | import com.google.gson.Gson;
6 | import com.google.gson.GsonBuilder;
7 | import com.jakewharton.picasso.OkHttp3Downloader;
8 | import com.squareup.picasso.Picasso;
9 |
10 | import javax.inject.Singleton;
11 |
12 | import dagger.Module;
13 | import dagger.Provides;
14 | import me.tatarka.gsonvalue.ValueTypeAdapterFactory;
15 | import okhttp3.Cache;
16 | import okhttp3.HttpUrl;
17 | import okhttp3.OkHttpClient;
18 | import retrofit2.Retrofit;
19 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
20 | import retrofit2.converter.gson.GsonConverterFactory;
21 | import rx.schedulers.Schedulers;
22 |
23 | @Module
24 | public class ApiModule {
25 | private static final int CACHE_SIZE = 10 * 1024 * 1024; // 10 MiB
26 |
27 | private Context context;
28 |
29 | public ApiModule(Context context) {
30 | this.context = context.getApplicationContext();
31 | }
32 |
33 | @Provides
34 | @Singleton
35 | public OkHttpClient providesOkHttpClient() {
36 | return new OkHttpClient.Builder()
37 | .cache(new Cache(context.getCacheDir(), CACHE_SIZE))
38 | .addInterceptor(new CachingInterceptor(context))
39 | .build();
40 | }
41 |
42 | @Provides
43 | @Singleton
44 | public Gson providesGson() {
45 | return new GsonBuilder()
46 | .registerTypeAdapter(HttpUrl.class, new HttpUrlTypeAdapter())
47 | .registerTypeAdapterFactory(new ValueTypeAdapterFactory())
48 | .create();
49 | }
50 |
51 | @Provides
52 | @Singleton
53 | public PokeService providesPokeService(OkHttpClient client, Gson gson) {
54 | Retrofit retrofit = new Retrofit.Builder()
55 | .client(client)
56 | .addCallAdapterFactory(RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io()))
57 | .addConverterFactory(GsonConverterFactory.create(gson))
58 | .baseUrl("http://pokeapi.co/api/v2/")
59 | .build();
60 | return retrofit.create(PokeService.class);
61 | }
62 |
63 | @Provides
64 | @Singleton
65 | public Picasso providesPicasso(OkHttpClient client) {
66 | return new Picasso.Builder(context)
67 | .downloader(new OkHttp3Downloader(client))
68 | .build();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/AnchorAppBarSlide.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ObjectAnimator;
5 | import android.animation.ValueAnimator;
6 | import android.support.annotation.IdRes;
7 | import android.transition.SidePropagation;
8 | import android.transition.TransitionValues;
9 | import android.transition.Visibility;
10 | import android.view.Gravity;
11 | import android.view.View;
12 | import android.view.ViewGroup;
13 |
14 | /**
15 | * Slides a view out while matching it's y position to the appbar is it animates.
16 | */
17 | public class AnchorAppBarSlide extends Visibility {
18 | private static final String PROP_APPBAR_HEIGHT = "me.tatarka.pokemvvm:AnchorAppBarSlide:height";
19 |
20 | @IdRes
21 | private final int appBarId;
22 |
23 | public AnchorAppBarSlide(@IdRes int appBarId) {
24 | this.appBarId = appBarId;
25 | SidePropagation propagation = new SidePropagation();
26 | propagation.setSide(Gravity.END);
27 | setPropagation(propagation);
28 | }
29 |
30 | @Override
31 | public void captureStartValues(TransitionValues transitionValues) {
32 | super.captureStartValues(transitionValues);
33 | View appBarLayout = transitionValues.view.getRootView().findViewById(appBarId);
34 | transitionValues.values.put(PROP_APPBAR_HEIGHT, appBarLayout.getHeight());
35 | }
36 |
37 | @Override
38 | public void captureEndValues(TransitionValues transitionValues) {
39 | super.captureEndValues(transitionValues);
40 | }
41 |
42 | @Override
43 | public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues) {
44 | if (startValues == null) {
45 | return null;
46 | }
47 | final int startHeight = (int) startValues.values.get(PROP_APPBAR_HEIGHT);
48 | final View appBarLayout = sceneRoot.findViewById(appBarId);
49 | ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0, view.getWidth());
50 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
51 | @Override
52 | public void onAnimationUpdate(ValueAnimator animation) {
53 | if (appBarLayout != null) {
54 | int currentHeight = appBarLayout.getHeight();
55 | view.setTranslationY(currentHeight - startHeight);
56 | }
57 | }
58 | });
59 | return animator;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PagedResult.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import android.support.annotation.NonNull;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | import rx.functions.Func1;
9 |
10 | /**
11 | * Represents a paged result.
12 | */
13 | public class PagedResult {
14 | private final List items;
15 | private final int newItemsStartIndex;
16 | private final Throwable error;
17 |
18 | /**
19 | * Constructs a new PagedResult with the given items in the index where new items from the last
20 | * paged result starts.
21 | */
22 | public static PagedResult success(List items, int newItemsStartIndex) {
23 | return new PagedResult<>(items, newItemsStartIndex, null);
24 | }
25 |
26 | public static PagedResult error(List items, Throwable error) {
27 | return new PagedResult<>(items, items.size(), error);
28 | }
29 |
30 | private PagedResult(@NonNull List items, int newItemsStartIndex, Throwable error) {
31 | this.items = items;
32 | this.newItemsStartIndex = newItemsStartIndex;
33 | this.error = error;
34 | }
35 |
36 | /**
37 | * Updates the given list with the contents of the paged result. If the given list is empty it
38 | * will be filled with all the results. Otherwise it is assumed that it has all but the latest
39 | * results and only those will be added.
40 | */
41 | public void consume(@NonNull List out) {
42 | consume(out, new Func1() {
43 | @Override
44 | public T call(T t) {
45 | return t;
46 | }
47 | });
48 | }
49 |
50 | /**
51 | * Updates the given list with the contents of the paged result. If the given list is empty it
52 | * will be filled with all the results. Otherwise it is assumed that it has all but the latest
53 | * results and only those will be added.
54 | */
55 | public void consume(@NonNull List out, Func1 f) {
56 | List itemsToConsume;
57 | if (out.isEmpty()) {
58 | itemsToConsume = items;
59 | } else {
60 | itemsToConsume = items.subList(newItemsStartIndex, items.size());
61 | }
62 | List newItems = new ArrayList<>(itemsToConsume.size());
63 | for (T item : itemsToConsume) {
64 | newItems.add(f.call(item));
65 | }
66 | out.addAll(newItems);
67 | }
68 |
69 | public Throwable getError() {
70 | return error;
71 | }
72 |
73 | public boolean isSuccess() {
74 | return error == null;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokedex_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
22 |
23 |
33 |
34 |
38 |
39 |
44 |
45 |
51 |
52 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/api/Pokemon.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.api;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 |
5 | import java.util.List;
6 |
7 | import auto.parcel.AutoParcel;
8 | import me.tatarka.gsonvalue.annotations.GsonConstructor;
9 | import okhttp3.HttpUrl;
10 |
11 | @AutoParcel
12 | public abstract class Pokemon {
13 |
14 | @GsonConstructor
15 | public static Pokemon create(int id, String name, int height, int weight, Sprites sprites, List stats, List types) {
16 | return new AutoParcel_Pokemon(id, name, height, weight, sprites, stats, types);
17 | }
18 |
19 | public abstract int id();
20 |
21 | public abstract String name();
22 |
23 | public abstract int height();
24 |
25 | public abstract int weight();
26 |
27 | public abstract Sprites sprites();
28 |
29 | public abstract List stats();
30 |
31 | public abstract List types();
32 |
33 | @AutoParcel
34 | public static abstract class Sprites {
35 |
36 | @GsonConstructor
37 | public static Sprites create(HttpUrl frontDefault) {
38 | return new AutoParcel_Pokemon_Sprites(frontDefault);
39 | }
40 |
41 | @SerializedName("front_default")
42 | public abstract HttpUrl frontDefault();
43 | }
44 |
45 | @AutoParcel
46 | public static abstract class TypeEntry {
47 |
48 | @GsonConstructor
49 | public static TypeEntry create(int slot, Type type) {
50 | return new AutoParcel_Pokemon_TypeEntry(slot, type);
51 | }
52 |
53 | public abstract int slot();
54 |
55 | public abstract Type type();
56 | }
57 |
58 | @AutoParcel
59 | public static abstract class Type {
60 |
61 | @GsonConstructor
62 | public static Type create(String name) {
63 | return new AutoParcel_Pokemon_Type(name);
64 | }
65 |
66 | public abstract String name();
67 | }
68 |
69 | @AutoParcel
70 | public static abstract class StatEntry {
71 |
72 | @GsonConstructor
73 | public static StatEntry create(int baseStat, Stat stat) {
74 | return new AutoParcel_Pokemon_StatEntry(baseStat, stat);
75 | }
76 |
77 | @SerializedName("base_stat")
78 | public abstract int baseStat();
79 |
80 | public abstract Stat stat();
81 | }
82 |
83 | @AutoParcel
84 | public static abstract class Stat {
85 |
86 | @GsonConstructor
87 | public static Stat create(String name) {
88 | return new AutoParcel_Pokemon_Stat(name);
89 | }
90 |
91 | public abstract String name();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/ChipLayout.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.content.Context;
4 | import android.content.res.ColorStateList;
5 | import android.graphics.PorterDuff;
6 | import android.support.v4.view.ViewCompat;
7 | import android.util.AttributeSet;
8 | import android.view.LayoutInflater;
9 | import android.view.ViewGroup;
10 | import android.view.animation.OvershootInterpolator;
11 | import android.widget.LinearLayout;
12 | import android.widget.Space;
13 | import android.widget.TextView;
14 |
15 | import java.util.Arrays;
16 | import java.util.Collection;
17 | import java.util.Iterator;
18 |
19 | import me.tatarka.pokemvvm.R;
20 |
21 | public class ChipLayout extends LinearLayout {
22 |
23 | public ChipLayout(Context context, AttributeSet attrs) {
24 | super(context, attrs);
25 | setOrientation(HORIZONTAL);
26 |
27 | if (isInEditMode()) {
28 | setChips(Arrays.asList(
29 | new Chip(getResources().getColor(R.color.green), "grass"),
30 | new Chip(getResources().getColor(R.color.purple), "poison")
31 | ));
32 | }
33 | }
34 |
35 | public void setChips(Collection chips) {
36 | removeAllViews();
37 | if (chips.isEmpty()) {
38 | return;
39 | }
40 | {
41 | Space startSpace = new Space(getContext());
42 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
43 | params.weight = 1;
44 | addView(startSpace, params);
45 | }
46 | for (Iterator iterator = chips.iterator(); iterator.hasNext(); ) {
47 | Chip chip = iterator.next();
48 | TextView chipView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.pokemon_chip, this, false);
49 | chipView.setText(chip.text());
50 | ViewCompat.setBackgroundTintList(chipView, ColorStateList.valueOf(chip.color()));
51 | ViewCompat.setBackgroundTintMode(chipView, PorterDuff.Mode.MULTIPLY);
52 | addView(chipView);
53 | if (iterator.hasNext()) {
54 | Space space = new Space(getContext());
55 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getResources().getDimensionPixelOffset(R.dimen.padding_normal), ViewGroup.LayoutParams.MATCH_PARENT);
56 | addView(space, params);
57 | }
58 | }
59 |
60 | if (ViewCompat.isLaidOut(this)) {
61 | setTranslationX(getWidth());
62 | animate().translationX(0)
63 | .setStartDelay(200)
64 | .setDuration(600)
65 | .setInterpolator(new OvershootInterpolator(0.8f));
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/AppBarAnimator.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.animation.AnimatorSet;
4 | import android.animation.ObjectAnimator;
5 | import android.animation.ValueAnimator;
6 | import android.content.Context;
7 | import android.support.design.widget.AppBarLayout;
8 | import android.support.v7.appcompat.R;
9 | import android.util.TypedValue;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 | import android.view.ViewTreeObserver;
13 | import android.view.animation.DecelerateInterpolator;
14 |
15 | public class AppBarAnimator {
16 |
17 | public static void animateIn(final AppBarLayout appBar) {
18 | TypedValue value = new TypedValue();
19 | Context context = appBar.getContext();
20 | context.getTheme().resolveAttribute(R.attr.actionBarSize, value, true);
21 | final int startSize = TypedValue.complexToDimensionPixelSize(value.data, context.getResources().getDisplayMetrics());
22 |
23 | for (int i = 0; i < appBar.getChildCount(); i++) {
24 | View child = appBar.getChildAt(i);
25 | child.setAlpha(0);
26 | }
27 |
28 | if (appBar.getHeight() == 0) {
29 | appBar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
30 | @Override
31 | public void onGlobalLayout() {
32 | appBar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
33 | int targetSize = appBar.getHeight();
34 | doAnimateIn(appBar, startSize, targetSize);
35 | }
36 | });
37 | } else {
38 | int targetSize = appBar.getHeight();
39 | doAnimateIn(appBar, startSize, targetSize);
40 | }
41 | }
42 |
43 | private static void doAnimateIn(final AppBarLayout appBar, int startSize, int targetSize) {
44 | AnimatorSet set = new AnimatorSet();
45 | ValueAnimator appBarHeight = ValueAnimator.ofInt(startSize, targetSize);
46 | appBarHeight.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
47 | @Override
48 | public void onAnimationUpdate(ValueAnimator animation) {
49 | ViewGroup.LayoutParams params = appBar.getLayoutParams();
50 | params.height = (int) animation.getAnimatedValue();
51 | appBar.requestLayout();
52 | }
53 | });
54 | set.playTogether(appBarHeight);
55 | for (int i = 0; i < appBar.getChildCount(); i++) {
56 | View child = appBar.getChildAt(i);
57 | ObjectAnimator childAlpha = ObjectAnimator.ofFloat(child, View.ALPHA, 0, 1);
58 | set.playTogether(childAlpha);
59 | }
60 | set.setDuration(300);
61 | set.setInterpolator(new DecelerateInterpolator());
62 | set.start();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/TableCardLayout.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.support.v4.widget.TextViewCompat;
6 | import android.support.v7.widget.CardView;
7 | import android.support.v7.widget.GridLayout;
8 | import android.util.AttributeSet;
9 | import android.view.LayoutInflater;
10 | import android.widget.TextView;
11 |
12 | import java.util.Arrays;
13 | import java.util.Collection;
14 |
15 | import me.tatarka.pokemvvm.R;
16 |
17 | public class TableCardLayout extends CardView {
18 |
19 | private GridLayout gridLayout;
20 | private TextView titleView;
21 |
22 | public TableCardLayout(Context context, AttributeSet attrs) {
23 | super(context, attrs);
24 | int leftRightPadding = getResources().getDimensionPixelOffset(R.dimen.padding_normal);
25 | int topBottomPadding = getResources().getDimensionPixelOffset(R.dimen.padding_three_halves);
26 | setContentPadding(leftRightPadding, topBottomPadding, leftRightPadding, topBottomPadding);
27 |
28 | gridLayout = (GridLayout) LayoutInflater.from(getContext()).inflate(R.layout.pokemon_table_card, this, false);
29 | titleView = (TextView) gridLayout.findViewById(R.id.title);
30 | addView(gridLayout);
31 |
32 | if (attrs != null) {
33 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TableCardLayout);
34 | String title = a.getString(R.styleable.TableCardLayout_android_title);
35 | if (title != null) {
36 | titleView.setText(title);
37 | }
38 | a.recycle();
39 | }
40 |
41 | if (isInEditMode()) {
42 | setData(Arrays.asList(
43 | new Row("speed", "45"),
44 | new Row("special-defense", "65"),
45 | new Row("special-attack", "65"),
46 | new Row("defense", "49"),
47 | new Row("attack", "49"),
48 | new Row("hp", "45")
49 | ));
50 | }
51 | }
52 |
53 | public void setTitle(CharSequence title) {
54 | titleView.setText(title);
55 | }
56 |
57 | public void setData(Collection rows) {
58 | gridLayout.removeViews(1, gridLayout.getChildCount() - 1);
59 | for (Row row : rows) {
60 | {
61 | TextView nameView = new TextView(getContext());
62 | TextViewCompat.setTextAppearance(nameView, android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Body1);
63 | nameView.setText(row.name());
64 | GridLayout.LayoutParams params = new GridLayout.LayoutParams();
65 | params.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f);
66 | gridLayout.addView(nameView, params);
67 | }
68 | {
69 | TextView valueView = new TextView(getContext());
70 | TextViewCompat.setTextAppearance(valueView, android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Body1);
71 | valueView.setText(row.value());
72 | gridLayout.addView(valueView);
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokedexFragment.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.support.v4.app.FragmentTransaction;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | import javax.inject.Inject;
12 |
13 | import me.tatarka.loader.Loader;
14 | import me.tatarka.pokemvvm.BaseFragment;
15 | import me.tatarka.pokemvvm.R;
16 | import me.tatarka.pokemvvm.api.PokemonItem;
17 | import me.tatarka.pokemvvm.api.RxPagedLoader;
18 | import me.tatarka.pokemvvm.dagger.Dagger;
19 | import me.tatarka.pokemvvm.databinding.PokedexFragmentBinding;
20 |
21 | /**
22 | * Displays a paginated lists of all the firstPokemonPage.
23 | */
24 | public class PokedexFragment extends BaseFragment implements Loader.Callbacks>, PokedexViewModel.Callbacks {
25 |
26 | public static PokedexFragment newInstance() {
27 | return new PokedexFragment();
28 | }
29 |
30 | @Inject
31 | PokedexPager pager;
32 | @Inject
33 | PokedexViewModel viewModel;
34 |
35 | private RxPagedLoader loader;
36 | private PokedexFragmentBinding binding;
37 |
38 | @Nullable
39 | @Override
40 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
41 | binding = DataBindingUtil.inflate(inflater, R.layout.pokedex_fragment, container, false);
42 | return binding.getRoot();
43 | }
44 |
45 | @Override
46 | public void onDestroyView() {
47 | super.onDestroyView();
48 | binding = null;
49 | }
50 |
51 | @Override
52 | public void onActivityCreated(@Nullable Bundle savedInstanceState) {
53 | super.onActivityCreated(savedInstanceState);
54 | Dagger.inject(this);
55 |
56 | viewModel.setCallbacks(this);
57 |
58 | loader = loaderManager().init(0, RxPagedLoader.create(pager.pokemon()), this);
59 | loader.start();
60 |
61 | if (loader.isRunning()) {
62 | viewModel.startLoading();
63 | }
64 |
65 | binding.setViewModel(viewModel);
66 | }
67 |
68 | @Override
69 | public void onRequestRetry() {
70 | loader.restart();
71 | }
72 |
73 | @Override
74 | public void onRequestNextPage() {
75 | pager.requestNextPage();
76 | }
77 |
78 | @Override
79 | public void onSelectItem(PokemonItem item) {
80 | ((PokemonItemViewModel.OnSelectListener) getHost()).selectItem(item);
81 | }
82 |
83 | @Override
84 | public void onLoaderStart() {
85 |
86 | }
87 |
88 | @Override
89 | public void onLoaderResult(PagedResult result) {
90 | viewModel.addItems(result);
91 | }
92 |
93 | @Override
94 | public void onLoaderComplete() {
95 | viewModel.stopLoading();
96 | }
97 |
98 | public void addSharedElements(FragmentTransaction ft) {
99 | ft.addSharedElement(binding.appBar, "app_bar")
100 | .addSharedElement(binding.toolbar, "toolbar");
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokedexPager.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | import java.util.List;
6 | import java.util.concurrent.atomic.AtomicReference;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Singleton;
10 |
11 | import me.tatarka.pokemvvm.api.Page;
12 | import me.tatarka.pokemvvm.api.PokeService;
13 | import me.tatarka.pokemvvm.api.PokemonItem;
14 | import me.tatarka.pokemvvm.dagger.RetainedScope;
15 | import okhttp3.HttpUrl;
16 | import rx.Observable;
17 | import rx.Single;
18 | import rx.functions.Action1;
19 | import rx.functions.Func1;
20 | import rx.subjects.BehaviorSubject;
21 |
22 | @RetainedScope
23 | public class PokedexPager {
24 | private PokeService api;
25 | private BehaviorSubject nextRequests = BehaviorSubject.create((HttpUrl) null);
26 | private AtomicReference nextPage = new AtomicReference<>();
27 |
28 | @Inject
29 | public PokedexPager(PokeService api) {
30 | this.api = api;
31 | }
32 |
33 | /**
34 | * Returns an observable that emits pages as they are requested. The first page will be
35 | * immediately emitted when subscribed. If there is an error, you may call this again and
36 | * re-subscribe to pick up where you left off.
37 | */
38 | public Observable> pokemon() {
39 | return Observable.concat(nextRequests.map(new Func1>>() {
40 | @Override
41 | public Observable> call(HttpUrl url) {
42 | Single> call = url == null
43 | ? api.firstPokemonPage()
44 | : api.nextPokemonPage(url.toString());
45 | return call.doOnSuccess(new Action1>() {
46 | @Override
47 | public void call(Page page) {
48 | nextPage.set(page.next());
49 | }
50 | }).toObservable();
51 | }
52 | })).takeUntil(new Func1, Boolean>() {
53 | @Override
54 | public Boolean call(Page page) {
55 | return page.next() == null;
56 | }
57 | }).map(new Func1, List>() {
58 | @Override
59 | public List call(Page page) {
60 | return page.results();
61 | }
62 | });
63 | }
64 |
65 | /**
66 | * Sets the next page, notifying any subscriber. Setting this to null will 'reset' the pager to
67 | * the first page.
68 | */
69 | public void setNextPage(@Nullable HttpUrl nextPage) {
70 | this.nextPage.set(nextPage);
71 | nextRequests.onNext(nextPage);
72 | }
73 |
74 | /**
75 | * Returns the current next page or null if there isn't one.
76 | */
77 | @Nullable
78 | public HttpUrl nextPage() {
79 | return nextPage.get();
80 | }
81 |
82 | /**
83 | * Requests the next page if there is a next page available and it hasn't already been
84 | * requested. This method only has an effect after the previous page has been loaded.
85 | */
86 | public void requestNextPage() {
87 | HttpUrl next = nextPage.getAndSet(null);
88 | if (next != null) {
89 | nextRequests.onNext(next);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/PokemonFragment.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.transition.AutoTransition;
7 | import android.transition.Scene;
8 | import android.transition.TransitionSet;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 |
13 | import javax.inject.Inject;
14 |
15 | import me.tatarka.loader.Loader;
16 | import me.tatarka.loader.Result;
17 | import me.tatarka.loader.RxLoader;
18 | import me.tatarka.pokemvvm.BaseFragment;
19 | import me.tatarka.pokemvvm.R;
20 | import me.tatarka.pokemvvm.api.PokeService;
21 | import me.tatarka.pokemvvm.api.Pokemon;
22 | import me.tatarka.pokemvvm.api.PokemonItem;
23 | import me.tatarka.pokemvvm.dagger.Dagger;
24 | import me.tatarka.pokemvvm.databinding.PokemonFragmentBinding;
25 |
26 | public class PokemonFragment extends BaseFragment implements Loader.Callbacks>, PokemonViewModel.Callbacks {
27 |
28 | private static final String ARG_ITEM = "item";
29 |
30 | public static PokemonFragment newInstance(PokemonItem item) {
31 | PokemonFragment fragment = new PokemonFragment();
32 | Bundle args = new Bundle();
33 | args.putParcelable(ARG_ITEM, item);
34 | fragment.setArguments(args);
35 | return fragment;
36 | }
37 |
38 | @Inject
39 | PokeService api;
40 | @Inject
41 | PokemonViewModel viewModel;
42 |
43 | private RxLoader loader;
44 | private PokemonFragmentBinding binding;
45 |
46 | @Override
47 | public void onCreate(@Nullable Bundle savedInstanceState) {
48 | super.onCreate(savedInstanceState);
49 | setSharedElementEnterTransition(new AutoTransition());
50 | // Because we are loading things async, these are really just for exit animations.
51 | setEnterTransition(new TransitionSet()
52 | .addTransition(new AnchorAppBarSlide(R.id.app_bar).addTarget(R.id.types))
53 | .addTransition(new SlideDownAndFadeOut().addTarget(R.id.content)));
54 | }
55 |
56 | @Nullable
57 | @Override
58 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
59 | binding = DataBindingUtil.inflate(inflater, R.layout.pokemon_fragment, container, false);
60 | return binding.getRoot();
61 | }
62 |
63 | @Override
64 | public void onDestroyView() {
65 | super.onDestroyView();
66 | binding = null;
67 | }
68 |
69 | @Override
70 | public void onActivityCreated(@Nullable Bundle savedInstanceState) {
71 | super.onActivityCreated(savedInstanceState);
72 | Dagger.inject(this);
73 |
74 | PokemonItem item = getArguments().getParcelable(ARG_ITEM);
75 | if (item == null) {
76 | throw new IllegalArgumentException("PokemonFragment missing item");
77 | }
78 | viewModel.setPokemonItem(item);
79 | viewModel.setCallbacks(this);
80 |
81 | loader = loaderManager().init(0, RxLoader.create(api.pokemon(item.url().toString()).toObservable()), this);
82 | loader.start();
83 |
84 | binding.setViewModel(viewModel);
85 | }
86 |
87 | @Override
88 | public void onLoaderStart() {
89 | viewModel.startLoading();
90 | }
91 |
92 | @Override
93 | public void onLoaderResult(Result result) {
94 | viewModel.setPokemon(result);
95 | }
96 |
97 | @Override
98 | public void onLoaderComplete() {
99 |
100 | }
101 |
102 | @Override
103 | public void onRequestRetry() {
104 | loader.restart();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pokemon_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
17 |
18 |
22 |
23 |
30 |
31 |
36 |
37 |
40 |
41 |
46 |
47 |
48 |
49 |
53 |
54 |
59 |
60 |
61 |
67 |
68 |
76 |
77 |
78 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokedex/PokedexViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import android.databinding.BaseObservable;
4 | import android.databinding.Bindable;
5 | import android.databinding.ObservableArrayList;
6 | import android.databinding.ObservableList;
7 | import android.support.annotation.VisibleForTesting;
8 |
9 | import javax.inject.Inject;
10 |
11 | import me.tatarka.bindingcollectionadapter.ItemBinding;
12 | import me.tatarka.bindingcollectionadapter.collections.MergeObservableList;
13 | import me.tatarka.bindingcollectionadapter.itembindings.ItemBindingModel;
14 | import me.tatarka.bindingcollectionadapter.itembindings.OnItemBindModel;
15 | import me.tatarka.pokemvvm.BR;
16 | import me.tatarka.pokemvvm.api.PokemonItem;
17 | import me.tatarka.pokemvvm.dagger.ViewScope;
18 | import me.tatarka.pokemvvm.viewmodel.ErrorViewModel;
19 | import me.tatarka.pokemvvm.viewmodel.State;
20 | import rx.functions.Func1;
21 | import timber.log.Timber;
22 |
23 | @ViewScope
24 | public class PokedexViewModel extends BaseObservable implements ErrorViewModel, ErrorItemViewModel.OnRetryListener, PokemonItemViewModel.OnSelectListener {
25 |
26 | private final Timber.Tree log;
27 | @VisibleForTesting
28 | final LoadingItemViewModel loading;
29 | @VisibleForTesting
30 | final ErrorItemViewModel error;
31 |
32 | private State state;
33 | private ObservableList pokemonItems = new ObservableArrayList<>();
34 | private MergeObservableList items = new MergeObservableList()
35 | .insertList(pokemonItems);
36 | private Callbacks callbacks;
37 |
38 | @Inject
39 | public PokedexViewModel(Timber.Tree log) {
40 | this.log = log;
41 | loading = new LoadingItemViewModel();
42 | error = new ErrorItemViewModel(this);
43 | }
44 |
45 | public void setCallbacks(Callbacks callbacks) {
46 | this.callbacks = callbacks;
47 | }
48 |
49 | public ObservableList items() {
50 | return items;
51 | }
52 |
53 | @Bindable
54 | public State getState() {
55 | return state;
56 | }
57 |
58 | public void startLoading() {
59 | state = State.LOADING;
60 | notifyPropertyChanged(BR.state);
61 | if (!pokemonItems.isEmpty()) {
62 | items.insertItem(loading);
63 | }
64 | }
65 |
66 | public void stopLoading() {
67 | if (state != State.LOADING) {
68 | return;
69 | }
70 | state = State.LOADED;
71 | items.removeItem(loading);
72 | }
73 |
74 | @Override
75 | public void retry() {
76 | items.removeItem(error);
77 | startLoading();
78 | callbacks.onRequestRetry();
79 | }
80 |
81 | @Override
82 | public void selectItem(PokemonItem item) {
83 | callbacks.onSelectItem(item);
84 | }
85 |
86 | public void addItems(PagedResult result) {
87 | boolean needsToAddLoading = pokemonItems.isEmpty();
88 | result.consume(pokemonItems, new Func1() {
89 | @Override
90 | public PokemonItemViewModel call(PokemonItem pokemonItem) {
91 | return new PokemonItemViewModel(pokemonItem, PokedexViewModel.this);
92 | }
93 | });
94 | if (result.isSuccess()) {
95 | if (state == State.LOADING && needsToAddLoading) {
96 | this.items.insertItem(loading);
97 | }
98 | notifyPropertyChanged(BR.state);
99 | } else {
100 | this.items.removeItem(loading);
101 | if (!pokemonItems.isEmpty()) {
102 | this.items.insertItem(error);
103 | }
104 | state = State.ERROR;
105 | notifyPropertyChanged(BR.state);
106 | Throwable error = result.getError();
107 | log.e(error, error.getMessage());
108 | }
109 | }
110 |
111 | public void onScrolled(int lastVisiblePosition) {
112 | if (state == State.LOADING && lastVisiblePosition >= items.size() - 1) {
113 | callbacks.onRequestNextPage();
114 | }
115 | }
116 |
117 | public final ItemBinding itemBinding = ItemBinding.of(new OnItemBindModel<>());
118 |
119 | public interface Callbacks {
120 | void onRequestRetry();
121 |
122 | void onRequestNextPage();
123 |
124 | void onSelectItem(PokemonItem item);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/TitleView.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.animation.ValueAnimator;
4 | import android.content.Context;
5 | import android.support.annotation.Nullable;
6 | import android.support.v4.view.ViewCompat;
7 | import android.util.AttributeSet;
8 | import android.view.ViewGroup;
9 | import android.view.ViewTreeObserver;
10 | import android.view.animation.AccelerateDecelerateInterpolator;
11 | import android.widget.FrameLayout;
12 | import android.widget.ImageView;
13 | import android.widget.TextView;
14 |
15 | import com.squareup.picasso.Picasso;
16 |
17 | import javax.inject.Inject;
18 |
19 | import me.tatarka.pokemvvm.R;
20 | import me.tatarka.pokemvvm.dagger.Dagger;
21 | import okhttp3.HttpUrl;
22 |
23 | /**
24 | * Shows a title and a left image, animating the title over to make room for the left image when
25 | * it's added.
26 | */
27 | public class TitleView extends FrameLayout {
28 |
29 | @Inject
30 | Picasso picasso;
31 | private ViewGroup titleGroup;
32 | private TextView titleView;
33 | private TextView subtitleView;
34 | private ImageView spriteView;
35 | private int leftPadding;
36 |
37 | public TitleView(Context context, AttributeSet attrs) {
38 | super(context, attrs);
39 | if (!isInEditMode()) {
40 | Dagger.component(getContext()).pokemon().inject(this);
41 | }
42 |
43 | inflate(getContext(), R.layout.pokemon_titleview, this);
44 | titleGroup = (ViewGroup) findViewById(R.id.title_group);
45 | titleView = (TextView) findViewById(R.id.title);
46 | subtitleView = (TextView) findViewById(R.id.subtitle);
47 | spriteView = (ImageView) findViewById(R.id.sprite);
48 | leftPadding = getResources().getDimensionPixelOffset(R.dimen.padding_normal);
49 |
50 | if (isInEditMode()) {
51 | titleGroup.setPadding((int) (getResources().getDisplayMetrics().density * 96), 0, 0, 0);
52 | titleView.setText("#1 Bulbasaur");
53 | subtitleView.setText("HT: 7 WT: 69");
54 | spriteView.setImageResource(R.drawable.bulbasaur);
55 | } else {
56 | titleGroup.setTranslationX(leftPadding);
57 | }
58 | }
59 |
60 | public void setTitle(@Nullable CharSequence title) {
61 | titleView.setText(title);
62 | }
63 |
64 | public void setSubtitle(@Nullable CharSequence subtitle) {
65 | subtitleView.setText(subtitle);
66 | if (ViewCompat.isLaidOut(this)) {
67 | subtitleView.setAlpha(0);
68 | subtitleView.animate()
69 | .alpha(1)
70 | .setDuration(300);
71 | }
72 | }
73 |
74 | public void setImage(@Nullable final HttpUrl image) {
75 | if (image == null) {
76 | spriteView.setImageBitmap(null);
77 | if (ViewCompat.isLaidOut(this)) {
78 | titleGroup.animate()
79 | .translationX(leftPadding)
80 | .setDuration(300)
81 | .setInterpolator(new AccelerateDecelerateInterpolator());
82 | } else {
83 | titleGroup.setTranslationX(leftPadding);
84 | }
85 | } else {
86 | if (ViewCompat.isLaidOut(this)) {
87 | titleGroup.animate()
88 | .translationX(spriteView.getWidth())
89 | .setDuration(300)
90 | .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
91 | boolean hasLoaded = false;
92 |
93 | @Override
94 | public void onAnimationUpdate(ValueAnimator animation) {
95 | if (!hasLoaded && animation.getAnimatedFraction() >= 0.5) {
96 | hasLoaded = true;
97 | picasso.load(image.toString())
98 | .into(spriteView);
99 | }
100 | }
101 | })
102 | .setInterpolator(new AccelerateDecelerateInterpolator());
103 | } else {
104 | picasso.load(image.toString())
105 | .into(spriteView);
106 | getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
107 | @Override
108 | public boolean onPreDraw() {
109 | getViewTreeObserver().removeOnPreDrawListener(this);
110 | titleGroup.setTranslationX(spriteView.getWidth());
111 | return false;
112 | }
113 | });
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/test/java/me/tatarka/pokemvvm/pokedex/PokedexViewModelTest.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import java.util.Arrays;
7 | import java.util.Collections;
8 | import java.util.List;
9 |
10 | import me.tatarka.loader.Result;
11 | import me.tatarka.pokemvvm.api.PokemonItem;
12 | import me.tatarka.pokemvvm.log.EmptyTree;
13 | import me.tatarka.pokemvvm.viewmodel.State;
14 | import okhttp3.HttpUrl;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.mockito.Mockito.mock;
18 | import static org.mockito.Mockito.verify;
19 |
20 | public class PokedexViewModelTest {
21 |
22 | PokedexViewModel viewModel;
23 |
24 | @Before
25 | public void setUp() {
26 | viewModel = new PokedexViewModel(new EmptyTree());
27 | }
28 |
29 | @Test
30 | public void startsEmpty() {
31 | assertThat(viewModel.items()).isEmpty();
32 | }
33 |
34 | @Test
35 | public void loadingWhileEmptyStaysEmpty() {
36 | viewModel.startLoading();
37 | assertThat(viewModel.items()).isEmpty();
38 | }
39 |
40 | @Test
41 | public void addsItemAsViewModel() {
42 | viewModel.addItems(PagedResult.success(Arrays.asList(
43 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
44 | ), 0));
45 |
46 | assertThat(viewModel.items()).containsExactly(
47 | new PokemonItemViewModel(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")), viewModel)
48 | );
49 | }
50 |
51 | @Test
52 | public void loadingWithItemsAddsLoadingToEnd() {
53 | viewModel.startLoading();
54 | viewModel.addItems(PagedResult.success(Arrays.asList(
55 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
56 | ), 0));
57 |
58 | assertThat(viewModel.items()).containsExactly(
59 | new PokemonItemViewModel(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")), viewModel),
60 | viewModel.loading
61 | );
62 | }
63 |
64 | @Test
65 | public void stopLoadingRemovesBottomLoadingIndicator() {
66 | viewModel.startLoading();
67 | viewModel.addItems(PagedResult.success(Arrays.asList(
68 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
69 | ), 0));
70 | viewModel.stopLoading();
71 |
72 | assertThat(viewModel.items()).containsExactly(
73 | new PokemonItemViewModel(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")), viewModel)
74 | );
75 | }
76 |
77 | @Test
78 | public void scrollingToLastItemRequestsNextPage() {
79 | PokedexViewModel.Callbacks callbacks = mock(PokedexViewModel.Callbacks.class);
80 | viewModel.setCallbacks(callbacks);
81 | viewModel.startLoading();
82 | viewModel.addItems(PagedResult.success(Arrays.asList(
83 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
84 | ), 0));
85 | viewModel.onScrolled(1);
86 |
87 | verify(callbacks).onRequestNextPage();
88 | }
89 |
90 | @Test
91 | public void errorWhenEmptyIsEmptyWithErrorState() {
92 | viewModel.startLoading();
93 | viewModel.addItems(PagedResult.error(Collections.emptyList(), new Exception()));
94 | viewModel.stopLoading();
95 |
96 | assertThat(viewModel.items()).isEmpty();
97 | assertThat(viewModel.getState()).isEqualTo(State.ERROR);
98 | }
99 |
100 | @Test
101 | public void errorWithItemsHasErrorItemAndErrorState() {
102 | viewModel.startLoading();
103 | List items = Arrays.asList(
104 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
105 | );
106 | viewModel.addItems(PagedResult.success(items, 0));
107 | viewModel.addItems(PagedResult.error(items, new Exception()));
108 | viewModel.stopLoading();
109 |
110 | assertThat(viewModel.items()).containsExactly(
111 | new PokemonItemViewModel(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")), viewModel),
112 | viewModel.error
113 | );
114 | assertThat(viewModel.getState()).isEqualTo(State.ERROR);
115 | }
116 |
117 | @Test
118 | public void retryErrorWithItemsReplacesErrorWithLoading() {
119 | PokedexViewModel.Callbacks callbacks = mock(PokedexViewModel.Callbacks.class);
120 | viewModel.setCallbacks(callbacks);
121 | viewModel.startLoading();
122 | List items = Arrays.asList(
123 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
124 | );
125 | viewModel.addItems(PagedResult.success(items, 0));
126 | viewModel.addItems(PagedResult.error(items, new Exception()));
127 | viewModel.stopLoading();
128 | viewModel.retry();
129 |
130 | verify(callbacks).onRequestRetry();
131 | assertThat(viewModel.items()).containsExactly(
132 | new PokemonItemViewModel(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")), viewModel),
133 | viewModel.loading
134 | );
135 | }
136 | }
--------------------------------------------------------------------------------
/app/src/test/java/me/tatarka/pokemvvm/pokedex/PokedexPagerTest.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokedex;
2 |
3 | import org.junit.Test;
4 |
5 | import java.io.IOException;
6 | import java.util.Arrays;
7 | import java.util.List;
8 |
9 | import me.tatarka.pokemvvm.api.Page;
10 | import me.tatarka.pokemvvm.api.PokeService;
11 | import me.tatarka.pokemvvm.api.PokemonItem;
12 | import okhttp3.HttpUrl;
13 | import rx.Single;
14 | import rx.observers.TestSubscriber;
15 |
16 | import static org.mockito.Matchers.eq;
17 | import static org.mockito.Mockito.mock;
18 | import static org.mockito.Mockito.when;
19 |
20 | public class PokedexPagerTest {
21 |
22 | @Test
23 | public void immediatelyReturnsFirstPage() throws Exception {
24 | PokeService service = mock(PokeService.class);
25 | when(service.firstPokemonPage()).thenReturn(Single.just(Page.create(0, null, null, Arrays.asList(
26 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
27 | ))));
28 | PokedexPager pager = new PokedexPager(service);
29 | TestSubscriber> subscriber = TestSubscriber.create();
30 | pager.pokemon().subscribe(subscriber);
31 |
32 | subscriber.assertReceivedOnNext(Arrays.asList(
33 | Arrays.asList(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")))
34 | ));
35 | subscriber.assertCompleted();
36 | }
37 |
38 | @Test
39 | public void returnsNextPageWhenAsked() throws Exception {
40 | PokeService service = mock(PokeService.class);
41 | when(service.firstPokemonPage()).thenReturn(Single.just(Page.create(0, HttpUrl.parse("http://2/"), null, Arrays.asList(
42 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
43 | ))));
44 | when(service.nextPokemonPage(eq("http://2/"))).thenReturn(Single.just(Page.create(0, null, HttpUrl.parse("http://1/"), Arrays.asList(
45 | PokemonItem.create("ivysaur", HttpUrl.parse("http://url"))
46 | ))));
47 | PokedexPager pager = new PokedexPager(service);
48 | TestSubscriber> subscriber = TestSubscriber.create();
49 | pager.pokemon().subscribe(subscriber);
50 | pager.requestNextPage();
51 |
52 | subscriber.assertReceivedOnNext(Arrays.asList(
53 | Arrays.asList(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))),
54 | Arrays.asList(PokemonItem.create("ivysaur", HttpUrl.parse("http://url")))
55 | ));
56 | subscriber.assertCompleted();
57 | }
58 |
59 | @Test
60 | public void onFailureAResubscriptionContinuesWhereItLeftOff() {
61 | PokeService service = mock(PokeService.class);
62 | when(service.firstPokemonPage()).thenReturn(Single.just(Page.create(0, HttpUrl.parse("http://2/"), null, Arrays.asList(
63 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
64 | ))));
65 | when(service.nextPokemonPage(eq("http://2/"))).thenReturn(Single.>error(new IOException()));
66 | PokedexPager pager = new PokedexPager(service);
67 | TestSubscriber> subscriber = TestSubscriber.create();
68 | pager.pokemon().subscribe(subscriber);
69 | pager.requestNextPage();
70 |
71 | subscriber.assertReceivedOnNext(Arrays.asList(
72 | Arrays.asList(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")))
73 | ));
74 | subscriber.assertError(IOException.class);
75 |
76 | when(service.nextPokemonPage(eq("http://2/"))).thenReturn(Single.just(Page.create(0, null, HttpUrl.parse("http://1/"), Arrays.asList(
77 | PokemonItem.create("ivysaur", HttpUrl.parse("http://url"))
78 | ))));
79 | subscriber = TestSubscriber.create();
80 | pager.pokemon().subscribe(subscriber);
81 |
82 | subscriber.assertReceivedOnNext(Arrays.asList(
83 | Arrays.asList(PokemonItem.create("ivysaur", HttpUrl.parse("http://url")))
84 | ));
85 | subscriber.assertCompleted();
86 | }
87 |
88 | @Test
89 | public void settingNextPageToNulLResetsToFirstPage() {
90 | PokeService service = mock(PokeService.class);
91 | when(service.firstPokemonPage()).thenReturn(Single.just(Page.create(0, HttpUrl.parse("http://2/"), null, Arrays.asList(
92 | PokemonItem.create("bulbasaur", HttpUrl.parse("http://url"))
93 | ))));
94 | when(service.nextPokemonPage(eq("http://2/"))).thenReturn(Single.just(Page.create(0, null, HttpUrl.parse("http://1/"), Arrays.asList(
95 | PokemonItem.create("ivysaur", HttpUrl.parse("http://url"))
96 | ))));
97 | PokedexPager pager = new PokedexPager(service);
98 | TestSubscriber> subscriber = TestSubscriber.create();
99 | pager.pokemon().subscribe(subscriber);
100 | pager.requestNextPage();
101 | pager.setNextPage(null);
102 | subscriber = TestSubscriber.create();
103 | pager.pokemon().subscribe(subscriber);
104 |
105 | subscriber.assertReceivedOnNext(Arrays.asList(
106 | Arrays.asList(PokemonItem.create("bulbasaur", HttpUrl.parse("http://url")))
107 | ));
108 | }
109 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/me/tatarka/pokemvvm/pokemon/PokemonViewModel.java:
--------------------------------------------------------------------------------
1 | package me.tatarka.pokemvvm.pokemon;
2 |
3 | import android.content.res.Resources;
4 | import android.databinding.BaseObservable;
5 | import android.databinding.Bindable;
6 | import android.support.annotation.Nullable;
7 | import android.support.v4.util.SimpleArrayMap;
8 |
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.Comparator;
12 | import java.util.List;
13 |
14 | import javax.inject.Inject;
15 |
16 | import me.tatarka.loader.Result;
17 | import me.tatarka.pokemvvm.BR;
18 | import me.tatarka.pokemvvm.R;
19 | import me.tatarka.pokemvvm.api.Pokemon;
20 | import me.tatarka.pokemvvm.api.PokemonItem;
21 | import me.tatarka.pokemvvm.dagger.ViewScope;
22 | import me.tatarka.pokemvvm.util.StringUtils;
23 | import me.tatarka.pokemvvm.viewmodel.ErrorViewModel;
24 | import me.tatarka.pokemvvm.viewmodel.State;
25 | import timber.log.Timber;
26 |
27 | @ViewScope
28 | public class PokemonViewModel extends BaseObservable implements ErrorViewModel {
29 |
30 | private static final SimpleArrayMap TYPE_COLOR_MAP = new SimpleArrayMap<>();
31 |
32 | static {
33 | TYPE_COLOR_MAP.put("bug", R.color.light_green);
34 | TYPE_COLOR_MAP.put("dark", R.color.brown);
35 | TYPE_COLOR_MAP.put("dragon", R.color.deep_purple);
36 | TYPE_COLOR_MAP.put("electirc", R.color.yellow);
37 | TYPE_COLOR_MAP.put("fairy", R.color.pink_accent);
38 | TYPE_COLOR_MAP.put("fighting", R.color.deep_orange);
39 | TYPE_COLOR_MAP.put("fire", R.color.red);
40 | TYPE_COLOR_MAP.put("flying", R.color.teal);
41 | TYPE_COLOR_MAP.put("ghost", R.color.indigo);
42 | TYPE_COLOR_MAP.put("grass", R.color.green);
43 | TYPE_COLOR_MAP.put("ground", R.color.lime);
44 | TYPE_COLOR_MAP.put("ice", R.color.cyan);
45 | TYPE_COLOR_MAP.put("normal", R.color.grey);
46 | TYPE_COLOR_MAP.put("poison", R.color.purple);
47 | TYPE_COLOR_MAP.put("psychic", R.color.pink);
48 | TYPE_COLOR_MAP.put("rock", R.color.orange);
49 | TYPE_COLOR_MAP.put("steel", R.color.blue_grey);
50 | TYPE_COLOR_MAP.put("water", R.color.blue);
51 | }
52 |
53 | private final Timber.Tree log;
54 | private PokemonItem item;
55 | private Pokemon pokemon;
56 | private State state = State.LOADING;
57 | private Callbacks callbacks;
58 |
59 | @Inject
60 | public PokemonViewModel(Timber.Tree log) {
61 | this.log = log;
62 | }
63 |
64 | public void setPokemonItem(PokemonItem item) {
65 | this.item = item;
66 | }
67 |
68 | public void setCallbacks(Callbacks callbacks) {
69 | this.callbacks = callbacks;
70 | }
71 |
72 | public void setPokemon(Result result) {
73 | if (result.isSuccess()) {
74 | this.pokemon = result.getSuccess();
75 | state = State.LOADED;
76 | notifyPropertyChanged(BR.pokemon);
77 | notifyPropertyChanged(BR.state);
78 | } else {
79 | state = State.ERROR;
80 | notifyPropertyChanged(BR.state);
81 | Throwable error = result.getError();
82 | log.e(error, error.getMessage());
83 | }
84 | }
85 |
86 | public String name(Resources resources) {
87 | return resources.getString(R.string.pokemon_name, item.number(), StringUtils.capitalize(item.name()));
88 | }
89 |
90 | @Nullable
91 | @Bindable
92 | public Pokemon getPokemon() {
93 | return pokemon;
94 | }
95 |
96 | @Bindable
97 | public State getState() {
98 | return state;
99 | }
100 |
101 | @Nullable
102 | public CharSequence heightWeight(Resources resources, @Nullable Pokemon pokemon) {
103 | return pokemon == null
104 | ? null
105 | : resources.getString(R.string.pokemon_height_weight, pokemon.height(), pokemon.weight());
106 | }
107 |
108 | public List chips(Resources resources, @Nullable Pokemon pokemon) {
109 | if (pokemon == null) {
110 | return Collections.emptyList();
111 | }
112 | List types = new ArrayList<>(pokemon.types());
113 | Collections.sort(types, new Comparator() {
114 | @Override
115 | public int compare(Pokemon.TypeEntry lhs, Pokemon.TypeEntry rhs) {
116 | return Integer.compare(lhs.slot(), rhs.slot());
117 | }
118 | });
119 | List chips = new ArrayList<>(types.size());
120 | for (Pokemon.TypeEntry entry : types) {
121 | Pokemon.Type type = entry.type();
122 | Integer colorRes = TYPE_COLOR_MAP.get(type.name());
123 | int color = resources.getColor(colorRes != null ? colorRes : R.color.grey);
124 | chips.add(new Chip(color, type.name()));
125 | }
126 | return chips;
127 | }
128 |
129 | public List statData(@Nullable Pokemon pokemon) {
130 | if (pokemon == null) {
131 | return Collections.emptyList();
132 | }
133 | List stats = pokemon.stats();
134 | List rows = new ArrayList<>(stats.size());
135 | for (Pokemon.StatEntry entry : stats) {
136 | Pokemon.Stat stat = entry.stat();
137 | rows.add(new Row(stat.name(), Integer.toString(entry.baseStat())));
138 | }
139 | return rows;
140 | }
141 |
142 | public void startLoading() {
143 | state = State.LOADING;
144 | notifyPropertyChanged(BR.state);
145 | }
146 |
147 | @Override
148 | public void retry() {
149 | startLoading();
150 | callbacks.onRequestRetry();
151 | }
152 |
153 | public interface Callbacks {
154 | void onRequestRetry();
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------