├── .gitignore ├── .travis.yml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── glomadrian │ │ └── mvpcleanarchitecture │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── glomadrian │ │ └── mvpcleanarchitecture │ │ ├── app │ │ ├── BaseActivity.java │ │ ├── BaseFragment.java │ │ ├── BasePresenter.java │ │ ├── MVPCleanArchitectureApplication.java │ │ └── dependencyinjection │ │ │ ├── ExecutorModule.java │ │ │ ├── InteractorModule.java │ │ │ ├── PresenterModule.java │ │ │ ├── ReactiveModule.java │ │ │ ├── RepositoryModule.java │ │ │ ├── RootModule.java │ │ │ └── qualifier │ │ │ ├── ActivityContext.java │ │ │ └── ApplicationContext.java │ │ ├── domain │ │ ├── LogUtils.java │ │ ├── interactor │ │ │ ├── AbstractInteractor.java │ │ │ ├── GetMarvelCharactersLimit.java │ │ │ ├── GetMarvelCharactersLimitImp.java │ │ │ ├── GetMarvelCharactersPaginated.java │ │ │ └── GetMarvelCharactersPaginatedImp.java │ │ ├── model │ │ │ ├── MarvelCharacter.java │ │ │ └── MarvelCharacterList.java │ │ └── repository │ │ │ ├── MarvelRepository.java │ │ │ ├── ResponseMapper.java │ │ │ ├── api │ │ │ ├── ApiUtils.java │ │ │ ├── mapper │ │ │ │ └── MarvelApiResponseMapper.java │ │ │ ├── model │ │ │ │ ├── Character.java │ │ │ │ ├── CharacterDataContainer.java │ │ │ │ ├── CharacterDataWrapper.java │ │ │ │ ├── Comic.java │ │ │ │ ├── Image.java │ │ │ │ ├── Serie.java │ │ │ │ └── Story.java │ │ │ └── retrofit │ │ │ │ ├── RetrofitMarvelAPIRepository.java │ │ │ │ ├── RetrofitMarvelService.java │ │ │ │ └── interceptor │ │ │ │ └── MarvelRequestInterceptor.java │ │ │ ├── exception │ │ │ └── GetCharactersException.java │ │ │ └── mock │ │ │ └── MarvelMockRepository.java │ │ ├── executor │ │ ├── Interactor.java │ │ ├── InteractorExecutor.java │ │ ├── MainThreadExecutor.java │ │ ├── MainThreadExecutorImp.java │ │ └── ThreadExecutor.java │ │ └── ui │ │ ├── activity │ │ ├── MainActivity.java │ │ └── ModelInfoActivity.java │ │ ├── adapter │ │ └── ModelAdapter.java │ │ ├── custom │ │ └── recycler │ │ │ ├── ClickItemTouchListener.java │ │ │ └── ClickRecyclerView.java │ │ ├── fragment │ │ ├── CharacterInfoFragment.java │ │ └── CharacterListFragment.java │ │ ├── presenter │ │ ├── CharacterInfoPresenter.java │ │ ├── CharacterInfoPresenterImp.java │ │ ├── CharacterListPresenter.java │ │ ├── CharacterListPresenterImp.java │ │ └── Presenter.java │ │ ├── reactive │ │ ├── CharacterSelectedObservable.java │ │ ├── CharacterSelectedObserver.java │ │ └── Observable.java │ │ ├── view │ │ ├── CharacterListView.java │ │ ├── ModelInfoView.java │ │ ├── ModelListView.java │ │ └── View.java │ │ ├── viewholder │ │ └── AbstractRecyclerViewHolder.java │ │ └── viewmodel │ │ ├── CharacterInfoViewModel.java │ │ ├── CharacterViewModel.java │ │ ├── Model.java │ │ └── ModelInfo.java │ └── res │ ├── drawable-hdpi │ ├── ic_launcher.png │ └── spider.png │ ├── drawable-mdpi │ ├── green_circle.xml │ ├── ic_launcher.png │ └── yellow_circle.xml │ ├── drawable-xhdpi │ ├── ic_launcher.png │ └── marvel.jpg │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── drawable │ ├── blue_circle.xml │ └── line.xml │ ├── layout-land │ ├── main.xml │ └── model_info.xml │ ├── layout │ ├── character_list.xml │ ├── main.xml │ ├── model.xml │ ├── model_info.xml │ └── model_info_activity.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── screenshot_1.png ├── screenshot_2.png └── screenshot_3.png ├── settings.gradle └── sonar-project.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | 32 | ### Java ### 33 | *.class 34 | 35 | # Mobile Tools for Java (J2ME) 36 | .mtj.tmp/ 37 | 38 | # Package Files # 39 | *.jar 40 | *.war 41 | *.ear 42 | 43 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 44 | hs_err_pid* 45 | 46 | 47 | ### Gradle ### 48 | .gradle 49 | build/ 50 | 51 | # Ignore Gradle GUI config 52 | gradle-app.setting 53 | 54 | 55 | ### Intellij ### 56 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 57 | 58 | *.iml 59 | 60 | ## Directory-based project format: 61 | .idea/ 62 | # if you remove the above rule, at least ignore the following: 63 | 64 | # User-specific stuff: 65 | # .idea/workspace.xml 66 | # .idea/tasks.xml 67 | # .idea/dictionaries 68 | 69 | # Sensitive or high-churn files: 70 | # .idea/dataSources.ids 71 | # .idea/dataSources.xml 72 | # .idea/sqlDataSources.xml 73 | # .idea/dynamic.xml 74 | # .idea/uiDesigner.xml 75 | 76 | # Gradle: 77 | # .idea/gradle.xml 78 | # .idea/libraries 79 | 80 | # Mongo Explorer plugin: 81 | # .idea/mongoSettings.xml 82 | 83 | ## File-based project format: 84 | *.ipr 85 | *.iws 86 | 87 | ## Plugin-specific files: 88 | 89 | # IntelliJ 90 | out/ 91 | 92 | # mpeltonen/sbt-idea plugin 93 | .idea_modules/ 94 | 95 | # JIRA plugin 96 | atlassian-ide-plugin.xml 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | 103 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - build-tools-21.1.1 5 | - android-21 6 | 7 | # Additional components 8 | - extra-google-m2repository 9 | - extra-android-m2repository 10 | - extra-android-support 11 | 12 | script: 13 | ./gradlew build 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MVP Clean Architecture 2 | == 3 | 4 | [![Build Status](https://travis-ci.org/glomadrian/MvpCleanArchitecture.svg?branch=master)](https://travis-ci.org/glomadrian/MvpCleanArchitecture) 5 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-MVP%20Clean%20Architecture-brightgreen.svg)](https://android-arsenal.com/details/3/1210) 6 | 7 | 8 | A sample project using Clean architecture and MVP 9 | 10 | 11 | ## Motivation 12 | 13 | Auto learning and make an example of Clean Architecture with MVP are the principal motivation for this app 14 | 15 | Inspired by the project [EffectiveAndroidUI](https://github.com/pedrovgs/EffectiveAndroidUI) by @pedrovgs that I fully recommend 16 | 17 | 18 | Clean architecture 19 | ------------ 20 | 21 | The Clean architecture search to make independent our code from data sources, UI, Frameworks, etc. 22 | 23 | Make more testable the code 24 | 25 | MVP 26 | ------------ 27 | 28 | Model view presenter is one of the most used architectural pattern, in Android this pattern can be used with a few limitations, Activities and Fragments have more responsibility than a simple view, anywhere is a wonderfull pattern to be used in Android applications. 29 | 30 | Apllication structure 31 | ------------ 32 | 33 | 34 | //TODO 35 | 36 | 37 | External Libs 38 | ------------ 39 | 40 | This project use this external libs 41 | 42 | * [Retrofit ](http://square.github.io/dagger/) 43 | * [Dagger](http://square.github.io/retrofit/) 44 | * [ButterKnife](http://jakewharton.github.io/butterknife/) 45 | * [Parceler](https://github.com/johncarl81/parceler) 46 | * [Picasso](http://square.github.io/picasso/) 47 | 48 | 49 | Screenshots 50 | ------------ 51 | ![screenshot](./img/screenshot_1.png "Screenshot 1") 52 | 53 | ![screenshot](./img/screenshot_2.png "Screenshot 2") 54 | 55 | ![screenshot](./img/screenshot_3.png "Screenshot 3") 56 | 57 | 58 | Developer By 59 | ------------ 60 | 61 | 62 | Adrián García Lomas 63 | 64 | * [Twitter](https://twitter.com/glomadrian) 65 | * [LinkedIn](https://es.linkedin.com/in/glomadrian ) 66 | 67 | 68 | License 69 | ------- 70 | 71 | Copyright 2014 Adrián García Lomas 72 | 73 | Licensed under the Apache License, Version 2.0 (the "License"); 74 | you may not use this file except in compliance with the License. 75 | You may obtain a copy of the License at 76 | 77 | http://www.apache.org/licenses/LICENSE-2.0 78 | 79 | Unless required by applicable law or agreed to in writing, software 80 | distributed under the License is distributed on an "AS IS" BASIS, 81 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 82 | See the License for the specific language governing permissions and 83 | limitations under the License. 84 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.1" 6 | 7 | defaultConfig { 8 | applicationId "com.github.glomadrian.mpvcleanarchitecture" 9 | minSdkVersion 21 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | packagingOptions { 21 | exclude 'META-INF/services/javax.annotation.processing.Processor' 22 | } 23 | 24 | 25 | } 26 | repositories { 27 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 28 | } 29 | 30 | 31 | dependencies { 32 | compile fileTree(dir: 'libs', include: ['*.jar']) 33 | compile 'com.squareup.retrofit:retrofit:1.7.1' 34 | 35 | compile 'com.squareup.dagger:dagger:1.2.2' 36 | compile 'com.squareup.dagger:dagger-compiler:1.2.2' 37 | compile 'com.squareup.picasso:picasso:2.4.0' 38 | compile 'com.android.support:cardview-v7:21.0.2' 39 | compile 'com.android.support:appcompat-v7:21+' 40 | compile 'com.android.support:support-v4:21+' 41 | compile 'com.android.support:recyclerview-v7:21.0.2' 42 | compile 'com.jakewharton:butterknife:6.0.0' 43 | compile "org.parceler:parceler-api:+" 44 | provided "org.parceler:parceler:+" 45 | } 46 | -------------------------------------------------------------------------------- /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 /media/Almacen/SDK/Android/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/androidTest/java/com/github/glomadrian/mvpcleanarchitecture/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import butterknife.ButterKnife; 7 | 8 | /** 9 | * Base activity for doing the Dependency 10 | * 11 | * @author glomadrian 12 | */ 13 | public abstract class BaseActivity extends Activity { 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | injectDependencies(); 19 | injectViews(); 20 | } 21 | 22 | private void injectDependencies() { 23 | MVPCleanArchitectureApplication mvpCleanArchitectureApplication = (MVPCleanArchitectureApplication) getApplication(); 24 | mvpCleanArchitectureApplication.inject(this); 25 | } 26 | 27 | 28 | private void injectViews() { 29 | ButterKnife.inject(this); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.view.View; 7 | 8 | import butterknife.ButterKnife; 9 | 10 | /** 11 | * @author glomadrian 12 | */ 13 | public abstract class BaseFragment extends Fragment { 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | injectDependencies(); 19 | } 20 | 21 | @Override 22 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 23 | super.onViewCreated(view, savedInstanceState); 24 | injectViews(view); 25 | } 26 | 27 | private void injectDependencies() { 28 | MVPCleanArchitectureApplication mvpCleanArchitectureApplication = (MVPCleanArchitectureApplication) getActivity().getApplication(); 29 | mvpCleanArchitectureApplication.inject(this); 30 | } 31 | 32 | private void injectViews(View view) { 33 | ButterKnife.inject(this, view); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * @author glomadrian 7 | */ 8 | public class BasePresenter { 9 | 10 | public BasePresenter(Context context) { 11 | ((MVPCleanArchitectureApplication) context.getApplicationContext()).inject(this); 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/MVPCleanArchitectureApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection.RootModule; 7 | 8 | import dagger.ObjectGraph; 9 | 10 | /** 11 | * @author glomadrian 12 | */ 13 | public class MVPCleanArchitectureApplication extends Application { 14 | 15 | private ObjectGraph objectGraph; 16 | 17 | /** 18 | * Static method for get application context 19 | * 20 | * @param context 21 | * @return 22 | */ 23 | public static MVPCleanArchitectureApplication get(Context context) { 24 | return (MVPCleanArchitectureApplication) context.getApplicationContext(); 25 | } 26 | 27 | @Override 28 | public void onCreate() { 29 | super.onCreate(); 30 | objectGraph = ObjectGraph.create(new RootModule(this)); 31 | objectGraph.inject(this); 32 | } 33 | 34 | public void inject(Object object) { 35 | objectGraph.inject(object); 36 | } 37 | 38 | /** 39 | * Add a new module to the dependency graph 40 | * 41 | * @param modules 42 | */ 43 | public void addModules(Object... modules) { 44 | objectGraph.plus(modules); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/ExecutorModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.executor.InteractorExecutor; 4 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutor; 5 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutorImp; 6 | import com.github.glomadrian.mvpcleanarchitecture.executor.ThreadExecutor; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | /** 14 | * @author glomadrian 15 | */ 16 | @Module( 17 | complete = false, 18 | library = true 19 | ) 20 | public class ExecutorModule { 21 | 22 | 23 | @Provides 24 | @Singleton 25 | public InteractorExecutor provideExecutor() { 26 | return new ThreadExecutor(); 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | public MainThreadExecutor provideMainThreadExecutor() { 32 | return new MainThreadExecutorImp(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/InteractorModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersLimit; 4 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersLimitImp; 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersPaginated; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersPaginatedImp; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 8 | import com.github.glomadrian.mvpcleanarchitecture.executor.InteractorExecutor; 9 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutor; 10 | 11 | import javax.inject.Named; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | 16 | /** 17 | * @author glomadrian 18 | */ 19 | @Module( 20 | complete = false, 21 | library = true 22 | ) 23 | public class InteractorModule { 24 | 25 | 26 | @Provides 27 | public GetMarvelCharactersLimit provideGetMarvelCharactersLimit(InteractorExecutor interactorExecutor, MainThreadExecutor mainThreadExecutor, @Named("mock_api") MarvelRepository marvelRepository) { 28 | return new GetMarvelCharactersLimitImp(interactorExecutor, mainThreadExecutor, marvelRepository); 29 | } 30 | 31 | @Provides 32 | public GetMarvelCharactersPaginated provideGetMarvelCharactersPaginated(InteractorExecutor interactorExecutor, MainThreadExecutor mainThreadExecutor, @Named("mock_api") MarvelRepository marvelRepository) { 33 | return new GetMarvelCharactersPaginatedImp(interactorExecutor, mainThreadExecutor, marvelRepository); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/PresenterModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import android.content.Context; 4 | 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersLimit; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersPaginated; 7 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterInfoPresenter; 8 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterInfoPresenterImp; 9 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterListPresenter; 10 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterListPresenterImp; 11 | import com.github.glomadrian.mvpcleanarchitecture.ui.reactive.CharacterSelectedObservable; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | 16 | /** 17 | * @author glomadrian 18 | */ 19 | @Module( 20 | complete = false, 21 | library = true 22 | ) 23 | public class PresenterModule { 24 | 25 | @Provides 26 | public CharacterListPresenter provideCharacterCollectionPresenter(Context context, GetMarvelCharactersLimit getMarvelCharactersLimit, GetMarvelCharactersPaginated getMarvelCharactersPaginated, CharacterSelectedObservable characterSelectedObservable) { 27 | return new CharacterListPresenterImp(context, getMarvelCharactersLimit, getMarvelCharactersPaginated, characterSelectedObservable); 28 | } 29 | 30 | @Provides 31 | public CharacterInfoPresenter provideCharacterInfoPresenter() { 32 | return new CharacterInfoPresenterImp(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/ReactiveModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.ui.reactive.CharacterSelectedObservable; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | 10 | /** 11 | * @author glomadrian 12 | */ 13 | @Module( 14 | complete = false, 15 | library = true 16 | ) 17 | public class ReactiveModule { 18 | 19 | @Singleton 20 | @Provides 21 | CharacterSelectedObservable proviCharacterSelectedObservable() { 22 | return new CharacterSelectedObservable(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/RepositoryModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 4 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.ResponseMapper; 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.mapper.MarvelApiResponseMapper; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.retrofit.RetrofitMarvelAPIRepository; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.retrofit.interceptor.MarvelRequestInterceptor; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.mock.MarvelMockRepository; 9 | 10 | import javax.inject.Named; 11 | 12 | import dagger.Module; 13 | import dagger.Provides; 14 | import retrofit.RequestInterceptor; 15 | 16 | /** 17 | * @author glomadrian 18 | */ 19 | @Module( 20 | complete = false, 21 | library = true 22 | ) 23 | public class RepositoryModule { 24 | 25 | 26 | @Provides 27 | @Named("api_public_key") 28 | public String provideApiKey() { 29 | return "fakePublicKey"; 30 | } 31 | 32 | @Provides 33 | @Named("api_private_key") 34 | public String providePrivateKey() { 35 | return "fakePrivateKey"; 36 | } 37 | 38 | @Provides 39 | @Named("api_base_url") 40 | public String provideApiBaseUrl() { 41 | return "http://gateway.marvel.com"; 42 | } 43 | 44 | 45 | @Provides 46 | public RequestInterceptor provideRequestInterceptor(@Named("api_public_key") String publicKey, 47 | @Named("api_private_key") String privateKey) { 48 | return new MarvelRequestInterceptor(publicKey, privateKey); 49 | 50 | } 51 | 52 | @Provides 53 | public ResponseMapper provideResponseMapper() { 54 | return new MarvelApiResponseMapper(); 55 | } 56 | 57 | @Provides 58 | @Named("production_api") 59 | public MarvelRepository provideMarvelRepository(RequestInterceptor requestInterceptor, ResponseMapper responseMapper, @Named("api_base_url") String endpoint) { 60 | return new RetrofitMarvelAPIRepository(endpoint, requestInterceptor, responseMapper); 61 | } 62 | 63 | @Provides 64 | @Named("mock_api") 65 | public MarvelRepository provideMarvelRepository() { 66 | return new MarvelMockRepository(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/RootModule.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | 6 | import com.github.glomadrian.mvpcleanarchitecture.app.MVPCleanArchitectureApplication; 7 | import com.github.glomadrian.mvpcleanarchitecture.ui.activity.MainActivity; 8 | import com.github.glomadrian.mvpcleanarchitecture.ui.activity.ModelInfoActivity; 9 | import com.github.glomadrian.mvpcleanarchitecture.ui.fragment.CharacterInfoFragment; 10 | import com.github.glomadrian.mvpcleanarchitecture.ui.fragment.CharacterListFragment; 11 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterListPresenterImp; 12 | 13 | import javax.inject.Singleton; 14 | 15 | import dagger.Module; 16 | import dagger.Provides; 17 | 18 | /** 19 | * @author glomadrian 20 | */ 21 | @Module( 22 | includes = { 23 | ExecutorModule.class, 24 | InteractorModule.class, 25 | RepositoryModule.class, 26 | PresenterModule.class, 27 | ReactiveModule.class, 28 | }, 29 | injects = { 30 | MVPCleanArchitectureApplication.class, 31 | CharacterListFragment.class, 32 | CharacterInfoFragment.class, 33 | CharacterListPresenterImp.class, 34 | MainActivity.class, 35 | CharacterInfoFragment.class, 36 | ModelInfoActivity.class, 37 | MainActivity.class 38 | }, 39 | library = true 40 | ) 41 | public class RootModule { 42 | 43 | private final Context context; 44 | 45 | public RootModule(Context context) { 46 | this.context = context; 47 | } 48 | 49 | @Provides 50 | @Singleton 51 | public Context provideApplicationContext() { 52 | return context; 53 | } 54 | 55 | @Provides 56 | public LayoutInflater provideLayoutInflater() { 57 | return LayoutInflater.from(context); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/qualifier/ActivityContext.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection.qualifier; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * @author glomadrian 12 | */ 13 | @Qualifier 14 | @Documented 15 | @Retention(RUNTIME) 16 | public @interface ActivityContext { 17 | String value() default ""; 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/app/dependencyinjection/qualifier/ApplicationContext.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.app.dependencyinjection.qualifier; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * @author glomadrian 12 | */ 13 | @Qualifier 14 | @Documented 15 | @Retention(RUNTIME) 16 | public @interface ApplicationContext { 17 | String value() default ""; 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/LogUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class LogUtils { 7 | 8 | private LogUtils() { 9 | //You shall not pass 10 | } 11 | 12 | public static String generateTag(Object object) { 13 | return object.getClass().getCanonicalName(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/interactor/AbstractInteractor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.interactor; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.executor.Interactor; 4 | import com.github.glomadrian.mvpcleanarchitecture.executor.InteractorExecutor; 5 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutor; 6 | 7 | /** 8 | * @author glomadrian 9 | */ 10 | public abstract class AbstractInteractor implements Interactor { 11 | 12 | private InteractorExecutor interactorExecutor; 13 | private MainThreadExecutor mainThreadExecutor; 14 | 15 | public AbstractInteractor(InteractorExecutor interactorExecutor, MainThreadExecutor mainThreadExecutor) { 16 | this.interactorExecutor = interactorExecutor; 17 | this.mainThreadExecutor = mainThreadExecutor; 18 | } 19 | 20 | public InteractorExecutor getInteractorExecutor() { 21 | return interactorExecutor; 22 | } 23 | 24 | public void setInteractorExecutor(InteractorExecutor interactorExecutor) { 25 | this.interactorExecutor = interactorExecutor; 26 | } 27 | 28 | public MainThreadExecutor getMainThreadExecutor() { 29 | return mainThreadExecutor; 30 | } 31 | 32 | public void setMainThreadExecutor(MainThreadExecutor mainThreadExecutor) { 33 | this.mainThreadExecutor = mainThreadExecutor; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/interactor/GetMarvelCharactersLimit.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.interactor; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Interface for define this interactor 9 | * 10 | * @author glomadrian 11 | */ 12 | public interface GetMarvelCharactersLimit { 13 | 14 | void execute(final int limit, final Callback callback); 15 | 16 | interface Callback { 17 | 18 | void onMarvelCharacterList(List marvelCharacters); 19 | 20 | void onError(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/interactor/GetMarvelCharactersLimitImp.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.interactor; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception.GetCharactersException; 9 | import com.github.glomadrian.mvpcleanarchitecture.executor.InteractorExecutor; 10 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutor; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * This implementation of the interactor (case use) will use a repository (injected) to get a collection 16 | * of marvel characters 17 | * 18 | * @author glomadrian 19 | */ 20 | public class GetMarvelCharactersLimitImp extends AbstractInteractor implements GetMarvelCharactersLimit { 21 | 22 | private MarvelRepository marvelRepository; 23 | private Callback callback; 24 | private int limit; 25 | 26 | public GetMarvelCharactersLimitImp(InteractorExecutor interactorExecutor, MainThreadExecutor mainThreadExecutor, MarvelRepository marvelRepository) { 27 | super(interactorExecutor, mainThreadExecutor); 28 | this.marvelRepository = marvelRepository; 29 | } 30 | 31 | 32 | @Override 33 | public void run() { 34 | 35 | try { 36 | final List marvelCharacters = marvelRepository.getCharacterCollection(limit); 37 | 38 | getMainThreadExecutor().execute(new Runnable() { 39 | @Override 40 | public void run() { 41 | callback.onMarvelCharacterList(marvelCharacters); 42 | } 43 | }); 44 | 45 | } catch (GetCharactersException e) { 46 | Log.e(LogUtils.generateTag(this), "Error on GerMarvelCharacters interactor"); 47 | getMainThreadExecutor().execute(new Runnable() { 48 | @Override 49 | public void run() { 50 | callback.onError(); 51 | } 52 | }); 53 | } 54 | 55 | } 56 | 57 | @Override 58 | public void execute(int limit, Callback callback) { 59 | this.callback = callback; 60 | this.limit = limit; 61 | 62 | getInteractorExecutor().run(this); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/interactor/GetMarvelCharactersPaginated.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.interactor; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author glomadrian 9 | */ 10 | public interface GetMarvelCharactersPaginated { 11 | 12 | void execute(final int limit, final int offset, final Callback callback); 13 | 14 | interface Callback { 15 | 16 | void onMarvelCharacterList(List marvelCharacters); 17 | 18 | void onError(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/interactor/GetMarvelCharactersPaginatedImp.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.interactor; 2 | 3 | 4 | import android.util.Log; 5 | 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 9 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception.GetCharactersException; 10 | import com.github.glomadrian.mvpcleanarchitecture.executor.InteractorExecutor; 11 | import com.github.glomadrian.mvpcleanarchitecture.executor.MainThreadExecutor; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author glomadrian 17 | */ 18 | public class GetMarvelCharactersPaginatedImp extends AbstractInteractor implements GetMarvelCharactersPaginated { 19 | 20 | private MarvelRepository marvelRepository; 21 | private Callback callback; 22 | private int limit; 23 | private int offset; 24 | 25 | public GetMarvelCharactersPaginatedImp(InteractorExecutor interactorExecutor, MainThreadExecutor mainThreadExecutor, MarvelRepository marvelRepository) { 26 | super(interactorExecutor, mainThreadExecutor); 27 | this.marvelRepository = marvelRepository; 28 | } 29 | 30 | 31 | @Override 32 | public void run() { 33 | try { 34 | final List marvelCharacters = marvelRepository.getCharacterCollectionPaginated(limit, offset); 35 | 36 | getMainThreadExecutor().execute(new Runnable() { 37 | @Override 38 | public void run() { 39 | callback.onMarvelCharacterList(marvelCharacters); 40 | } 41 | }); 42 | } catch (GetCharactersException e) { 43 | Log.e(LogUtils.generateTag(this), "Error on GetMarvelCharactersPaginatedImp interactor"); 44 | getMainThreadExecutor().execute(new Runnable() { 45 | @Override 46 | public void run() { 47 | callback.onError(); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | @Override 54 | public void execute(int limit, int offset, Callback callback) { 55 | this.limit = limit; 56 | this.offset = offset; 57 | this.callback = callback; 58 | getInteractorExecutor().run(this); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/model/MarvelCharacter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.model; 2 | 3 | import org.parceler.Parcel; 4 | 5 | /** 6 | * Parcel is a library to manage parcelable in a easy way, for default serialization 7 | * is necessary to avoid private fields if you want to use proguard 8 | * 9 | * @author glomadrian 10 | */ 11 | @Parcel 12 | public class MarvelCharacter { 13 | 14 | int id; 15 | String name; 16 | String description; 17 | String imageURL; 18 | int comics; 19 | int stories; 20 | int series; 21 | 22 | 23 | public MarvelCharacter() { 24 | } 25 | 26 | public MarvelCharacter(int id, String name, String description, String imageURL) { 27 | this.id = id; 28 | this.name = name; 29 | this.description = description; 30 | this.imageURL = imageURL; 31 | } 32 | 33 | public int getId() { 34 | return id; 35 | } 36 | 37 | public void setId(int id) { 38 | this.id = id; 39 | } 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public String getDescription() { 50 | return description; 51 | } 52 | 53 | public void setDescription(String description) { 54 | this.description = description; 55 | } 56 | 57 | public String getImageURL() { 58 | return imageURL; 59 | } 60 | 61 | public void setImageURL(String imageURL) { 62 | this.imageURL = imageURL; 63 | } 64 | 65 | public int getComics() { 66 | return comics; 67 | } 68 | 69 | public void setComics(int comics) { 70 | this.comics = comics; 71 | } 72 | 73 | public int getStories() { 74 | return stories; 75 | } 76 | 77 | public void setStories(int stories) { 78 | this.stories = stories; 79 | } 80 | 81 | public int getSeries() { 82 | return series; 83 | } 84 | 85 | public void setSeries(int series) { 86 | this.series = series; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/model/MarvelCharacterList.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.model; 2 | 3 | import org.parceler.Parcel; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * @author glomadrian 10 | */ 11 | @Parcel 12 | public class MarvelCharacterList { 13 | 14 | public List marvelCharacters; 15 | 16 | public MarvelCharacterList() { 17 | this.marvelCharacters = new ArrayList(); 18 | } 19 | 20 | public List getMarvelCharacters() { 21 | return (List) ((ArrayList) marvelCharacters).clone(); 22 | } 23 | 24 | public void add(MarvelCharacter marvelCharacter) { 25 | this.marvelCharacters.add(marvelCharacter); 26 | } 27 | 28 | public void addAll(List marvelCharacter) { 29 | this.marvelCharacters.addAll(marvelCharacter); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/MarvelRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception.GetCharactersException; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Repository to get marvel information 10 | * 11 | * @author glomadrian 12 | */ 13 | public interface MarvelRepository { 14 | 15 | List getCharacterCollection(int limit) throws GetCharactersException; 16 | 17 | List getCharacterCollectionPaginated(int limit, int offset) throws GetCharactersException; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/ResponseMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * The domain model is different from others resources models, in each case the repository must map 9 | * the response to a basic model 10 | * 11 | * @author glomadrian 12 | */ 13 | public interface ResponseMapper { 14 | List mapResponse(T response); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/ApiUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class ApiUtils { 7 | 8 | public static final String PARAM_KEY = "apikey"; 9 | public static final String PARAM_TIMESTAMP = "ts"; 10 | public static final String PARAM_HASH = "hash"; 11 | 12 | 13 | private ApiUtils() { 14 | //you shall not pass 15 | } 16 | 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/mapper/MarvelApiResponseMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.mapper; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.ResponseMapper; 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model.Character; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model.CharacterDataWrapper; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * This custom implementation of the mapper will map the response from the Marvel api to Domain model 14 | * 15 | * @author glomadrian 16 | */ 17 | public class MarvelApiResponseMapper implements ResponseMapper { 18 | 19 | @Override 20 | public List mapResponse(CharacterDataWrapper characterDataWrapper) { 21 | 22 | List characters = characterDataWrapper.getCharacterDataContainer().getResults(); 23 | 24 | List marvelCharacters = Collections.emptyList(); 25 | 26 | if (characterDataWrapper.getCharacterDataContainer().getCount() > 0) { 27 | marvelCharacters = new ArrayList(); 28 | } 29 | 30 | for (Character character : characters) { 31 | marvelCharacters.add(createMarvelCharacterFromResponseCharacter(character)); 32 | } 33 | 34 | return marvelCharacters; 35 | } 36 | 37 | 38 | private MarvelCharacter createMarvelCharacterFromResponseCharacter(Character character) { 39 | 40 | MarvelCharacter marvelCharacter = new MarvelCharacter(); 41 | marvelCharacter.setName(character.getName()); 42 | marvelCharacter.setDescription(character.getDescription()); 43 | marvelCharacter.setId(character.getId()); 44 | marvelCharacter.setComics(character.getComics().getAvailable()); 45 | marvelCharacter.setSeries(character.getSeries().getAvailable()); 46 | marvelCharacter.setStories(character.getStories().getAvailable()); 47 | 48 | 49 | if (character.getThumbnail() != null) { 50 | marvelCharacter.setImageURL(character.getThumbnail().getPath() + "." + character.getThumbnail().getExtension()); 51 | } 52 | 53 | return marvelCharacter; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/Character.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class Character { 7 | 8 | private int id; 9 | private String name; 10 | private String description; 11 | private Image thumbnail; 12 | private Comic comics; 13 | private Serie series; 14 | private Story stories; 15 | 16 | public int getId() { 17 | return id; 18 | } 19 | 20 | public void setId(int id) { 21 | this.id = id; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | public String getDescription() { 33 | return description; 34 | } 35 | 36 | public void setDescription(String description) { 37 | this.description = description; 38 | } 39 | 40 | public Image getThumbnail() { 41 | return thumbnail; 42 | } 43 | 44 | public void setThumbnail(Image thumbnail) { 45 | this.thumbnail = thumbnail; 46 | } 47 | 48 | public Comic getComics() { 49 | return comics; 50 | } 51 | 52 | public void setComics(Comic comics) { 53 | this.comics = comics; 54 | } 55 | 56 | public Serie getSeries() { 57 | return series; 58 | } 59 | 60 | public void setSeries(Serie series) { 61 | this.series = series; 62 | } 63 | 64 | public Story getStories() { 65 | return stories; 66 | } 67 | 68 | public void setStories(Story stories) { 69 | this.stories = stories; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/CharacterDataContainer.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author glomadrian 7 | */ 8 | public class CharacterDataContainer { 9 | 10 | private int limit; 11 | private int total; 12 | private int count; 13 | private List results; 14 | 15 | public int getLimit() { 16 | return limit; 17 | } 18 | 19 | public void setLimit(int limit) { 20 | this.limit = limit; 21 | } 22 | 23 | public int getTotal() { 24 | return total; 25 | } 26 | 27 | public void setTotal(int total) { 28 | this.total = total; 29 | } 30 | 31 | public int getCount() { 32 | return count; 33 | } 34 | 35 | public void setCount(int count) { 36 | this.count = count; 37 | } 38 | 39 | public List getResults() { 40 | return results; 41 | } 42 | 43 | public void setResults(List results) { 44 | this.results = results; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/CharacterDataWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class CharacterDataWrapper { 7 | 8 | private int code; 9 | private String status; 10 | private CharacterDataContainer data; 11 | 12 | public int getCode() { 13 | return code; 14 | } 15 | 16 | public void setCode(int code) { 17 | this.code = code; 18 | } 19 | 20 | public String getStatus() { 21 | return status; 22 | } 23 | 24 | public void setStatus(String status) { 25 | this.status = status; 26 | } 27 | 28 | public CharacterDataContainer getCharacterDataContainer() { 29 | return data; 30 | } 31 | 32 | public void setCharacterDataContainer(CharacterDataContainer data) { 33 | this.data = data; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/Comic.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class Comic { 7 | 8 | private int available; 9 | 10 | 11 | public int getAvailable() { 12 | return available; 13 | } 14 | 15 | public void setAvailable(int available) { 16 | this.available = available; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/Image.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class Image { 7 | 8 | private String path; 9 | private String extension; 10 | 11 | public String getPath() { 12 | return path; 13 | } 14 | 15 | public void setPath(String path) { 16 | this.path = path; 17 | } 18 | 19 | public String getExtension() { 20 | return extension; 21 | } 22 | 23 | public void setExtension(String extension) { 24 | this.extension = extension; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/Serie.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class Serie { 7 | 8 | private int available; 9 | 10 | 11 | public int getAvailable() { 12 | return available; 13 | } 14 | 15 | public void setAvailable(int available) { 16 | this.available = available; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/model/Story.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class Story { 7 | 8 | private int available; 9 | 10 | 11 | public int getAvailable() { 12 | return available; 13 | } 14 | 15 | public void setAvailable(int available) { 16 | this.available = available; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/retrofit/RetrofitMarvelAPIRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.retrofit; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.ResponseMapper; 9 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model.CharacterDataWrapper; 10 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception.GetCharactersException; 11 | 12 | import java.util.List; 13 | 14 | import retrofit.RequestInterceptor; 15 | import retrofit.RestAdapter; 16 | import retrofit.RetrofitError; 17 | 18 | /** 19 | * Implementation of MarvelRepository using Retrofit as client, every call is synchronous 20 | * because the asynchronous is done by the concrete interactor (use case) for this repository 21 | * 22 | * @author glomadrian 23 | */ 24 | public class RetrofitMarvelAPIRepository implements MarvelRepository { 25 | 26 | private String endpoint; 27 | private RequestInterceptor requestInterceptor; 28 | private RetrofitMarvelService marvelAPI; 29 | private ResponseMapper responseMapper; 30 | 31 | public RetrofitMarvelAPIRepository(String endpoint, RequestInterceptor requestInterceptor, ResponseMapper responseMapper) { 32 | this.endpoint = endpoint; 33 | this.requestInterceptor = requestInterceptor; 34 | this.responseMapper = responseMapper; 35 | 36 | init(); 37 | } 38 | 39 | private void init() { 40 | 41 | RestAdapter restAdapter = new RestAdapter.Builder() 42 | .setEndpoint(endpoint) 43 | .setRequestInterceptor(requestInterceptor) 44 | .build(); 45 | 46 | marvelAPI = restAdapter.create(RetrofitMarvelService.class); 47 | 48 | } 49 | 50 | @Override 51 | public List getCharacterCollection(int limit) throws GetCharactersException { 52 | //The request using retrofit 53 | 54 | try { 55 | CharacterDataWrapper characterDataWrapper = marvelAPI.getCharacters(limit); 56 | //Map response from api to domain model 57 | return responseMapper.mapResponse(characterDataWrapper); 58 | } catch (RetrofitError retrofitError) { 59 | Log.e(LogUtils.generateTag(this), "Error on marvel api repository"); 60 | GetCharactersException getCharactersException = new GetCharactersException(); 61 | getCharactersException.setStackTrace(retrofitError.getStackTrace()); 62 | throw getCharactersException; 63 | } 64 | 65 | 66 | } 67 | 68 | @Override 69 | public List getCharacterCollectionPaginated(int limit, int offset) throws GetCharactersException { 70 | try { 71 | CharacterDataWrapper characterDataWrapper = marvelAPI.getCharacters(limit, offset); 72 | //Map response from api to domain model 73 | return responseMapper.mapResponse(characterDataWrapper); 74 | } catch (RetrofitError retrofitError) { 75 | Log.e(LogUtils.generateTag(this), "Error on marvel api repository"); 76 | GetCharactersException getCharactersException = new GetCharactersException(); 77 | getCharactersException.setStackTrace(retrofitError.getStackTrace()); 78 | throw getCharactersException; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/retrofit/RetrofitMarvelService.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.retrofit; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.model.CharacterDataWrapper; 4 | 5 | import retrofit.http.GET; 6 | import retrofit.http.Query; 7 | 8 | /** 9 | * @author glomadrian 10 | */ 11 | public interface RetrofitMarvelService { 12 | 13 | @GET("/v1/public/characters") 14 | CharacterDataWrapper getCharacters(@Query("limit") int limit); 15 | 16 | @GET("/v1/public/characters") 17 | CharacterDataWrapper getCharacters(@Query("limit") int limit, @Query("offset") int offset); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/api/retrofit/interceptor/MarvelRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.retrofit.interceptor; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 6 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.api.ApiUtils; 7 | 8 | import java.math.BigInteger; 9 | import java.security.MessageDigest; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.sql.Timestamp; 12 | import java.util.Date; 13 | 14 | import retrofit.RequestInterceptor; 15 | 16 | /** 17 | * Interceptor to add the auth key, and generate the hash for every request 18 | * The data is exposed in the constructor 19 | * 20 | * @author glomadrian 21 | */ 22 | public class MarvelRequestInterceptor implements RequestInterceptor { 23 | 24 | private static final int SIGNUM = 1; 25 | private static final int BYTES = 1; 26 | 27 | private String publicKey; 28 | private String privateKey; 29 | 30 | public MarvelRequestInterceptor(String publicKey, String privateKey) { 31 | this.publicKey = publicKey; 32 | this.privateKey = privateKey; 33 | } 34 | 35 | @Override 36 | public void intercept(RequestFacade request) { 37 | String timeStamp = generateTimestamp(); 38 | request.addEncodedQueryParam(ApiUtils.PARAM_TIMESTAMP, timeStamp); 39 | request.addEncodedQueryParam(ApiUtils.PARAM_KEY, publicKey); 40 | request.addEncodedQueryParam(ApiUtils.PARAM_HASH, generateMarvelHash(timeStamp, privateKey, publicKey)); 41 | } 42 | 43 | private String generateTimestamp() { 44 | Date date = new Date(); 45 | Timestamp timestamp = new Timestamp(date.getTime()); 46 | return String.valueOf(timestamp.getTime()); 47 | } 48 | 49 | 50 | private String generateMarvelHash(String timeStamp, String privateKey, String publicKey) { 51 | 52 | try { 53 | MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 54 | String marvelHash = timeStamp + privateKey + publicKey; 55 | byte[] bytes = marvelHash.getBytes(); 56 | return new BigInteger(SIGNUM, messageDigest.digest(bytes)).toString(BYTES); 57 | 58 | } catch (NoSuchAlgorithmException e) { 59 | Log.e(LogUtils.generateTag(this), " Hash type not found"); 60 | return "invalid"; 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/exception/GetCharactersException.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public class GetCharactersException extends RuntimeException { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/domain/repository/mock/MarvelMockRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.domain.repository.mock; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.MarvelRepository; 5 | import com.github.glomadrian.mvpcleanarchitecture.domain.repository.exception.GetCharactersException; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Mock repository for testing 12 | * 13 | * @author glomadrian 14 | */ 15 | public class MarvelMockRepository implements MarvelRepository { 16 | 17 | @Override 18 | public List getCharacterCollection(int limit) throws GetCharactersException { 19 | return generateMockCharacters(); 20 | } 21 | 22 | @Override 23 | public List getCharacterCollectionPaginated(int limit, int offset) throws GetCharactersException { 24 | return generateMockCharacters(); 25 | } 26 | 27 | 28 | private List generateMockCharacters() { 29 | 30 | List marvelCharacters = new ArrayList<>(); 31 | 32 | marvelCharacters.add(createMarvelCharacter("Thor", 33 | "Thor possesses a very high resistance to physical injury that approaches " + 34 | "invulnerability. Thor possesses keen senses that allow him to" + 35 | " track objects traveling faster than light and hear cries from the" + 36 | " other side of the planet.[203] Thor has the ability to travel through time.", 37 | "http://www.tuexpertojuegos.com/wp-content/uploads//2010/07/thor-marvel.jpg" 38 | )); 39 | 40 | marvelCharacters.add(createMarvelCharacter("Captain America", 41 | "Captain America was the first Marvel Comics to have appeared in media outside " + 42 | "comics with the release of the 1944 movie serial Captain America. Since " + 43 | "then, the character has been featured in such other films and television" + 44 | " series, more recently in the Marvel Cinematic Universe (MCU) portrayed " + 45 | "by Chris Evans in Captain America: The First Avenger, The Avengers, " + 46 | "Captain America: The Winter Soldier and the upcoming Avengers: " + 47 | "Age of Ultron.", 48 | "http://www.marvelnoise.com/wp-content/uploads/2009/06/CaptainAmerica_Reborn_01_QuesadaVariant.jpg" 49 | )); 50 | 51 | marvelCharacters.add(createMarvelCharacter("Hulk", 52 | "Lee said that the Hulk's creation was inspired by a combination of " + 53 | "Frankenstein and Dr. Jekyll and Mr. Hyde. Although the Hulk's" + 54 | " coloration has varied throughout the character's publication history," + 55 | " the most usual color is green. As a child, Banner's father Brian Banner" + 56 | " often got mad and physically abused his mother Rebecca, creating the " + 57 | "psychological complex of fear, anger, and the fear of anger and the " + 58 | "destruction it can cause that underlies the character.", 59 | "http://cdn.screenrant.com/wp-content/uploads/Incredible-Hulk-Marvel-Now-Comics.jpg" 60 | )); 61 | marvelCharacters.add(createMarvelCharacter("Iron Man", 62 | "Iron Man (Tony Stark) is a fictional character, a superhero that appears in " + 63 | "books published by Marvel Comics. The character was created by " + 64 | "writer-editor Stan Lee, developed by scripter Larry Lieber, and designed" + 65 | " by artists Don Heck and Jack Kirby. He made his first appearance in " + 66 | "Tales of Suspense #39 (March 1963).", 67 | "http://bigfanboy.com/wp/wp-content/uploads/2010/04/ironman-royalp-front.jpg" 68 | )); 69 | marvelCharacters.add(createMarvelCharacter("Black Widow", 70 | "Natalia \"Natasha\" Alianovna Romanova is the first character to take on the " + 71 | "Black Widow codename in the modern mainstream Marvel Comics. She was " + 72 | "created by editor and plotter Stan Lee, scripter Don Rico and artist" + 73 | " Don Heck, and first appeared in Tales of Suspense #52 (April 1964).", 74 | "http://static.comicvine.com/uploads/original/7/75210/1960320-black_widow_marvel_superheroines_8418333_796_545.jpg" 75 | )); 76 | marvelCharacters.add(createMarvelCharacter("Vision", 77 | "The first Vision was created by the writer-artist team of Joe Simon and Jack Kirby " + 78 | "in Marvel Mystery Comics #13 (Nov. 1940), published by Marvel predecessor" + 79 | " Timely Comics during the 1930s-1940s period which fans and historians " + 80 | "call the Golden Age of Comic Books.", 81 | "http://www.beyondhollywood.com/uploads/2012/05/Vision-Comic-Book-e1336494560758.jpg" 82 | )); 83 | 84 | return marvelCharacters; 85 | } 86 | 87 | private MarvelCharacter createMarvelCharacter(String name, String description, String imageUrl) { 88 | 89 | MarvelCharacter marvelCharacter = new MarvelCharacter(); 90 | marvelCharacter.setId(getRandomNumber()); 91 | marvelCharacter.setName(name); 92 | marvelCharacter.setComics(getRandomNumber()); 93 | marvelCharacter.setDescription(description); 94 | marvelCharacter.setImageURL(imageUrl); 95 | marvelCharacter.setStories(getRandomNumber()); 96 | marvelCharacter.setSeries(getRandomNumber()); 97 | 98 | return marvelCharacter; 99 | } 100 | 101 | private int getRandomNumber() { 102 | return 1 + (int) (Math.random() * 999); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/executor/Interactor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.executor; 2 | 3 | /** 4 | * Each interactor is a Use Case implementation see Clean Architecture for more info 5 | * 6 | * @author glomadrian 7 | */ 8 | public interface Interactor extends Runnable { 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/executor/InteractorExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.executor; 2 | 3 | /** 4 | * Interface for create executors, the executors will only execute Interactors 5 | * 6 | * @author glomadrian 7 | */ 8 | public interface InteractorExecutor { 9 | 10 | void run(Interactor interactor); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/executor/MainThreadExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.executor; 2 | 3 | /** 4 | * The interactors callbacks are executed in a different thread, in Android the ui thread to perform 5 | * view changes 6 | * 7 | * @author glomadrian 8 | */ 9 | public interface MainThreadExecutor { 10 | 11 | void execute(Runnable runnable); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/executor/MainThreadExecutorImp.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.executor; 2 | 3 | 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | 7 | /** 8 | * Main Thread executor using classic Looper.getMainLooper() from Android 9 | * 10 | * @author glomadrian 11 | */ 12 | public class MainThreadExecutorImp implements MainThreadExecutor { 13 | 14 | private Handler handler; 15 | 16 | public MainThreadExecutorImp() { 17 | this.handler = new Handler(Looper.getMainLooper()); 18 | } 19 | 20 | @Override 21 | public void execute(Runnable runnable) { 22 | handler.post(runnable); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/executor/ThreadExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.executor; 2 | 3 | import java.util.concurrent.BlockingQueue; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | import java.util.concurrent.ThreadPoolExecutor; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * @author glomadrian 10 | */ 11 | public class ThreadExecutor implements InteractorExecutor { 12 | 13 | private static final int CORE_POOL_SIZE = 3; 14 | private static final int MAX_POOL_SIZE = 5; 15 | private static final int KEEP_ALIVE_TIME = 120; 16 | private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; 17 | private static final BlockingQueue WORK_QUEUE = new LinkedBlockingQueue(); 18 | 19 | private ThreadPoolExecutor threadPoolExecutor; 20 | 21 | public ThreadExecutor() { 22 | threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT, WORK_QUEUE); 23 | } 24 | 25 | @Override 26 | public void run(Interactor interactor) { 27 | if (interactor != null) { 28 | threadPoolExecutor.submit(interactor); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Parcelable; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | 9 | import com.github.glomadrian.mvpcleanarchitecture.R; 10 | import com.github.glomadrian.mvpcleanarchitecture.app.BaseActivity; 11 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 12 | import com.github.glomadrian.mvpcleanarchitecture.ui.fragment.CharacterInfoFragment; 13 | import com.github.glomadrian.mvpcleanarchitecture.ui.reactive.CharacterSelectedObservable; 14 | import com.github.glomadrian.mvpcleanarchitecture.ui.reactive.CharacterSelectedObserver; 15 | 16 | import org.parceler.Parcels; 17 | 18 | import javax.inject.Inject; 19 | 20 | 21 | /** 22 | * Main activity of the Application, it decide if launch a new activity or update the fragment 23 | * depending of the view inflated 24 | *

25 | * This activity is the container of one fragment if is in portrait and 2 if is in landscape 26 | * The type of the layout inflated will be checked and use for navigation 27 | * 28 | * @author glomadrian 29 | */ 30 | public class MainActivity extends BaseActivity implements CharacterSelectedObserver { 31 | 32 | 33 | public static final String TAG_PORTRAIT = "V11-portrait"; 34 | public static final String TAG_LANDSCAPE = "V11-landscape"; 35 | 36 | @Inject 37 | CharacterSelectedObservable characterSelectedObservable; 38 | 39 | 40 | //The viewTag is the key for the navigation with different sizes 41 | private String viewTag; 42 | 43 | @Override 44 | public void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | View view = LayoutInflater.from(this).inflate(R.layout.main, null); 47 | viewTag = (String) view.getTag(); 48 | setContentView(view); 49 | } 50 | 51 | @Override 52 | protected void onResume() { 53 | super.onResume(); 54 | characterSelectedObservable.register(this); 55 | } 56 | 57 | @Override 58 | protected void onPause() { 59 | super.onPause(); 60 | characterSelectedObservable.unregister(this); 61 | } 62 | 63 | /** 64 | * This method is called when a new marvel character is selected 65 | * 66 | * @param marvelCharacter 67 | */ 68 | @Override 69 | public void characterSelected(MarvelCharacter marvelCharacter) { 70 | if (viewTag.equals(TAG_PORTRAIT)) { 71 | launchCharacterInfoActivity(marvelCharacter); 72 | } 73 | 74 | if (viewTag.equals(TAG_LANDSCAPE)) { 75 | replaceCharacterInfoFragment(marvelCharacter); 76 | } 77 | } 78 | 79 | //TODO Change to navigator class with activity context 80 | private void launchCharacterInfoActivity(MarvelCharacter marvelCharacter) { 81 | Intent intent = new Intent(this, ModelInfoActivity.class); 82 | Parcelable parcelable = Parcels.wrap(marvelCharacter); 83 | intent.putExtra(ModelInfoActivity.KEY_CHARACTER, parcelable); 84 | startActivity(intent); 85 | } 86 | 87 | private void replaceCharacterInfoFragment(MarvelCharacter marvelCharacter) { 88 | CharacterInfoFragment characterInfoFragment = CharacterInfoFragment.newInstance(marvelCharacter); 89 | 90 | getFragmentManager() 91 | .beginTransaction() 92 | .replace(R.id.character_info_fragment, characterInfoFragment) 93 | .disallowAddToBackStack() 94 | .commit(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/activity/ModelInfoActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | 6 | import com.github.glomadrian.mvpcleanarchitecture.R; 7 | import com.github.glomadrian.mvpcleanarchitecture.app.BaseActivity; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 9 | import com.github.glomadrian.mvpcleanarchitecture.ui.fragment.CharacterInfoFragment; 10 | 11 | import org.parceler.Parcels; 12 | 13 | /** 14 | * @author glomadrian 15 | */ 16 | public class ModelInfoActivity extends BaseActivity { 17 | 18 | public static final String KEY_CHARACTER = "character"; 19 | 20 | @Override 21 | public void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.model_info_activity); 24 | 25 | addInfoFragment(); 26 | } 27 | 28 | public void addInfoFragment() { 29 | 30 | Parcelable marvelCharacterParcelable = getIntent().getExtras().getParcelable(KEY_CHARACTER); 31 | MarvelCharacter marvelCharacter = Parcels.unwrap(marvelCharacterParcelable); 32 | 33 | CharacterInfoFragment characterInfoFragment = CharacterInfoFragment.newInstance(marvelCharacter); 34 | 35 | getFragmentManager() 36 | .beginTransaction() 37 | .add(R.id.info_frame, characterInfoFragment) 38 | .commit(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/adapter/ModelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.github.glomadrian.mvpcleanarchitecture.R; 11 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewholder.AbstractRecyclerViewHolder; 12 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.Model; 13 | import com.squareup.picasso.Picasso; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import butterknife.InjectView; 19 | 20 | /** 21 | * @author glomadrian 22 | */ 23 | public class ModelAdapter extends RecyclerView.Adapter { 24 | 25 | private List models; 26 | 27 | public ModelAdapter() { 28 | models = new ArrayList(); 29 | } 30 | 31 | public ModelAdapter(List models) { 32 | this.models = models; 33 | } 34 | 35 | @Override 36 | 37 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { 38 | View modelView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.model, viewGroup, false); 39 | return new ViewHolder(modelView); 40 | } 41 | 42 | @Override 43 | public void onBindViewHolder(ViewHolder viewHolder, int position) { 44 | Model model = models.get(position); 45 | 46 | viewHolder.modelTitle.setText(model.getTitle()); 47 | viewHolder.modelSubtitle.setText(model.getSubtitle()); 48 | 49 | Picasso.with(viewHolder.view.getContext()) 50 | .load(model.getImageUrl()) 51 | .into(viewHolder.imageView); 52 | } 53 | 54 | @Override 55 | public int getItemCount() { 56 | return models.size(); 57 | } 58 | 59 | public void add(Model model) { 60 | models.add(model); 61 | notifyDataSetChanged(); 62 | } 63 | 64 | public void add(List models) { 65 | this.models.addAll(models); 66 | notifyDataSetChanged(); 67 | } 68 | 69 | public class ViewHolder extends AbstractRecyclerViewHolder { 70 | 71 | View view; 72 | 73 | @InjectView(R.id.model_title) 74 | TextView modelTitle; 75 | @InjectView(R.id.model_image) 76 | ImageView imageView; 77 | @InjectView(R.id.model_subtitle) 78 | TextView modelSubtitle; 79 | 80 | public ViewHolder(View itemView) { 81 | super(itemView); 82 | this.view = itemView; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/custom/recycler/ClickItemTouchListener.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.custom.recycler; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.support.v4.view.GestureDetectorCompat; 6 | import android.support.v4.view.MotionEventCompat; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.GestureDetector; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | 12 | /** 13 | * @author lucasr 14 | * from: https://github.com/lucasr/twoway-view 15 | */ 16 | abstract class ClickItemTouchListener implements RecyclerView.OnItemTouchListener { 17 | 18 | private static final int ANDROID_SDK_19 = 19; 19 | private final GestureDetectorCompat mGestureDetector; 20 | 21 | ClickItemTouchListener(RecyclerView hostView) { 22 | mGestureDetector = new ItemClickGestureDetector(hostView.getContext(), 23 | new ItemClickGestureListener(hostView)); 24 | } 25 | 26 | private boolean isAttachedToWindow(RecyclerView hostView) { 27 | if (Build.VERSION.SDK_INT >= ANDROID_SDK_19) { 28 | return hostView.isAttachedToWindow(); 29 | } else { 30 | return hostView.getHandler() != null; 31 | } 32 | } 33 | 34 | private boolean hasAdapter(RecyclerView hostView) { 35 | return hostView.getAdapter() != null; 36 | } 37 | 38 | @Override 39 | public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { 40 | if (!isAttachedToWindow(recyclerView) || !hasAdapter(recyclerView)) { 41 | return false; 42 | } 43 | 44 | mGestureDetector.onTouchEvent(event); 45 | return false; 46 | } 47 | 48 | @Override 49 | public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { 50 | // We can silently track tap and and long presses by silently 51 | // intercepting touch events in the host RecyclerView. 52 | } 53 | 54 | abstract boolean performItemClick(RecyclerView parent, View view, int position, long id); 55 | 56 | abstract boolean performItemLongClick(RecyclerView parent, View view, int position, long id); 57 | 58 | private class ItemClickGestureDetector extends GestureDetectorCompat { 59 | private final ItemClickGestureListener mGestureListener; 60 | 61 | public ItemClickGestureDetector(Context context, ItemClickGestureListener listener) { 62 | super(context, listener); 63 | mGestureListener = listener; 64 | } 65 | 66 | @Override 67 | public boolean onTouchEvent(MotionEvent event) { 68 | final boolean handled = super.onTouchEvent(event); 69 | 70 | final int action = event.getAction() & MotionEventCompat.ACTION_MASK; 71 | if (action == MotionEvent.ACTION_UP) { 72 | mGestureListener.dispatchSingleTapUpIfNeeded(event); 73 | } 74 | 75 | return handled; 76 | } 77 | } 78 | 79 | private class ItemClickGestureListener extends GestureDetector.SimpleOnGestureListener { 80 | private final RecyclerView mHostView; 81 | private View mTargetChild; 82 | 83 | public ItemClickGestureListener(RecyclerView hostView) { 84 | mHostView = hostView; 85 | } 86 | 87 | public void dispatchSingleTapUpIfNeeded(MotionEvent event) { 88 | // When the long press hook is called but the long press listener 89 | // returns false, the target child will be left around to be 90 | // handled later. In this case, we should still treat the gesture 91 | // as potential item click. 92 | if (mTargetChild != null) { 93 | onSingleTapUp(event); 94 | } 95 | } 96 | 97 | @Override 98 | public boolean onDown(MotionEvent event) { 99 | final int x = (int) event.getX(); 100 | final int y = (int) event.getY(); 101 | 102 | mTargetChild = mHostView.findChildViewUnder(x, y); 103 | 104 | return mTargetChild != null; 105 | } 106 | 107 | 108 | @Override 109 | public void onShowPress(MotionEvent event) { 110 | if (mTargetChild != null) { 111 | mTargetChild.setPressed(true); 112 | } 113 | } 114 | 115 | @Override 116 | public boolean onSingleTapUp(MotionEvent event) { 117 | boolean handled = false; 118 | 119 | if (mTargetChild != null) { 120 | mTargetChild.setPressed(false); 121 | 122 | final int position = mHostView.getChildPosition(mTargetChild); 123 | final long id = mHostView.getAdapter().getItemId(position); 124 | handled = performItemClick(mHostView, mTargetChild, position, id); 125 | 126 | mTargetChild = null; 127 | } 128 | 129 | return handled; 130 | } 131 | 132 | @Override 133 | public boolean onScroll(MotionEvent event, MotionEvent event2, float v, float v2) { 134 | if (mTargetChild != null) { 135 | mTargetChild.setPressed(false); 136 | mTargetChild = null; 137 | 138 | return true; 139 | } 140 | 141 | return false; 142 | } 143 | 144 | @Override 145 | public void onLongPress(MotionEvent event) { 146 | if (mTargetChild == null) { 147 | return; 148 | } 149 | 150 | final int position = mHostView.getChildPosition(mTargetChild); 151 | final long id = mHostView.getAdapter().getItemId(position); 152 | final boolean handled = performItemLongClick(mHostView, mTargetChild, position, id); 153 | 154 | if (handled) { 155 | mTargetChild.setPressed(false); 156 | mTargetChild = null; 157 | } 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/custom/recycler/ClickRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.custom.recycler; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.AttributeSet; 6 | import android.view.HapticFeedbackConstants; 7 | import android.view.SoundEffectConstants; 8 | import android.view.View; 9 | 10 | /** 11 | * From: From: https://github.com/lucasr/twoway-view/ 12 | * Custom recycler view adding itemClickListener support 13 | * 14 | * @author glomadrian 15 | */ 16 | public class ClickRecyclerView extends RecyclerView { 17 | 18 | 19 | private TouchListener mTouchListener; 20 | 21 | private OnItemClickListener mItemClickListener; 22 | private OnItemLongClickListener mItemLongClickListener; 23 | 24 | 25 | public ClickRecyclerView(Context context) { 26 | super(context); 27 | init(); 28 | } 29 | 30 | public ClickRecyclerView(Context context, AttributeSet attrs) { 31 | super(context, attrs); 32 | init(); 33 | } 34 | 35 | public ClickRecyclerView(Context context, AttributeSet attrs, int defStyle) { 36 | super(context, attrs, defStyle); 37 | init(); 38 | } 39 | 40 | private void init() { 41 | mTouchListener = new TouchListener(this); 42 | addOnItemTouchListener(mTouchListener); 43 | } 44 | 45 | /** 46 | * Register a callback to be invoked when an item in the 47 | * RecyclerView has been clicked. 48 | * 49 | * @param listener The callback that will be invoked. 50 | */ 51 | public void setOnItemClickListener(OnItemClickListener listener) { 52 | mItemClickListener = listener; 53 | } 54 | 55 | /** 56 | * Register a callback to be invoked when an item in the 57 | * RecyclerView has been clicked and held. 58 | * 59 | * @param listener The callback that will be invoked. 60 | */ 61 | public void setOnItemLongClickListener(OnItemLongClickListener listener) { 62 | if (!isLongClickable()) { 63 | setLongClickable(true); 64 | } 65 | 66 | mItemLongClickListener = listener; 67 | } 68 | 69 | /** 70 | * Interface definition for a callback to be invoked when an item in the 71 | * RecyclerView has been clicked. 72 | */ 73 | public interface OnItemClickListener { 74 | /** 75 | * Callback method to be invoked when an item in the RecyclerView 76 | * has been clicked. 77 | * 78 | * @param parent The RecyclerView where the click happened. 79 | * @param view The view within the RecyclerView that was clicked 80 | * @param position The position of the view in the adapter. 81 | * @param id The row id of the item that was clicked. 82 | */ 83 | void onItemClick(RecyclerView parent, View view, int position, long id); 84 | } 85 | 86 | /** 87 | * Interface definition for a callback to be invoked when an item in the 88 | * RecyclerView has been clicked and held. 89 | */ 90 | public interface OnItemLongClickListener { 91 | /** 92 | * Callback method to be invoked when an item in the RecyclerView 93 | * has been clicked and held. 94 | * 95 | * @param parent The RecyclerView where the click happened 96 | * @param view The view within the RecyclerView that was clicked 97 | * @param position The position of the view in the list 98 | * @param id The row id of the item that was clicked 99 | * @return true if the callback consumed the long click, false otherwise 100 | */ 101 | boolean onItemLongClick(RecyclerView parent, View view, int position, long id); 102 | } 103 | 104 | private class TouchListener extends ClickItemTouchListener { 105 | TouchListener(RecyclerView recyclerView) { 106 | super(recyclerView); 107 | } 108 | 109 | @Override 110 | boolean performItemClick(RecyclerView parent, View view, int position, long id) { 111 | if (mItemClickListener != null) { 112 | view.playSoundEffect(SoundEffectConstants.CLICK); 113 | mItemClickListener.onItemClick(parent, view, position, id); 114 | return true; 115 | } 116 | 117 | return false; 118 | } 119 | 120 | @Override 121 | boolean performItemLongClick(RecyclerView parent, View view, int position, long id) { 122 | if (mItemLongClickListener != null) { 123 | view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 124 | return mItemLongClickListener.onItemLongClick(parent, view, position, id); 125 | } 126 | 127 | return false; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/fragment/CharacterInfoFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | import android.support.annotation.Nullable; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.github.glomadrian.mvpcleanarchitecture.R; 13 | import com.github.glomadrian.mvpcleanarchitecture.app.BaseFragment; 14 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 15 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterInfoPresenter; 16 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.ModelInfoView; 17 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.CharacterInfoViewModel; 18 | import com.squareup.picasso.Picasso; 19 | 20 | import org.parceler.Parcels; 21 | 22 | import javax.inject.Inject; 23 | 24 | import butterknife.InjectView; 25 | import butterknife.Optional; 26 | 27 | /** 28 | * @author glomadrian 29 | */ 30 | public class CharacterInfoFragment extends BaseFragment implements ModelInfoView { 31 | 32 | public static final String KEY_CHARACTER = "character"; 33 | 34 | @Inject 35 | CharacterInfoPresenter characterInfoPresenter; 36 | @Optional 37 | @InjectView(R.id.info_image) 38 | ImageView infoImage; 39 | @InjectView(R.id.info_description) 40 | TextView infoDescription; 41 | @InjectView(R.id.info_title) 42 | TextView infoTitle; 43 | @InjectView(R.id.info_number_1) 44 | TextView infoNumber1; 45 | @InjectView(R.id.info_number_2) 46 | TextView infoNumber2; 47 | @InjectView(R.id.info_number_3) 48 | TextView infoNumber3; 49 | 50 | private String tag; 51 | 52 | public static CharacterInfoFragment newInstance(MarvelCharacter marvelCharacter) { 53 | 54 | Bundle args = new Bundle(); 55 | Parcelable marvelCharacterParcel = Parcels.wrap(marvelCharacter); 56 | args.putParcelable(KEY_CHARACTER, marvelCharacterParcel); 57 | 58 | CharacterInfoFragment characterInfoFragment = new CharacterInfoFragment(); 59 | characterInfoFragment.setArguments(args); 60 | return characterInfoFragment; 61 | } 62 | 63 | @Override 64 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 65 | View view = inflater.inflate(R.layout.model_info, container, false); 66 | this.tag = String.valueOf(view.getTag()); 67 | return view; 68 | } 69 | 70 | @Override 71 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 72 | super.onViewCreated(view, savedInstanceState); 73 | characterInfoPresenter.onViewCreate(); 74 | characterInfoPresenter.setView(this); 75 | 76 | //Get character info 77 | if (getArguments() != null) { 78 | MarvelCharacter marvelCharacter = getMarvelCharacterFromArgs(); 79 | characterInfoPresenter.onCharacter(marvelCharacter); 80 | } 81 | } 82 | 83 | @Override 84 | public void showCharacterInfo(CharacterInfoViewModel characterInfoViewModel) { 85 | 86 | if (tag.equals("character_info_portrait")) { 87 | Picasso.with(getActivity()).load(characterInfoViewModel.getInfoImageUrl()).into(infoImage); 88 | } 89 | 90 | infoNumber1.setText(characterInfoViewModel.getInfoNumber1()); 91 | infoNumber2.setText(characterInfoViewModel.getInfoNumber2()); 92 | infoNumber3.setText(characterInfoViewModel.getInfoNumber3()); 93 | infoDescription.setText(characterInfoViewModel.getInfoDescription()); 94 | infoTitle.setText(characterInfoViewModel.getInfoTitle()); 95 | } 96 | 97 | private MarvelCharacter getMarvelCharacterFromArgs() { 98 | Parcelable marvelCharacterParcelable = getArguments().getParcelable(KEY_CHARACTER); 99 | return Parcels.unwrap(marvelCharacterParcelable); 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/fragment/CharacterListFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.DefaultItemAnimator; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.ProgressBar; 14 | import android.widget.Toast; 15 | 16 | import com.github.glomadrian.mvpcleanarchitecture.R; 17 | import com.github.glomadrian.mvpcleanarchitecture.app.BaseFragment; 18 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 19 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacterList; 20 | import com.github.glomadrian.mvpcleanarchitecture.ui.adapter.ModelAdapter; 21 | import com.github.glomadrian.mvpcleanarchitecture.ui.custom.recycler.ClickRecyclerView; 22 | import com.github.glomadrian.mvpcleanarchitecture.ui.presenter.CharacterListPresenter; 23 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.CharacterListView; 24 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.Model; 25 | 26 | import org.parceler.Parcels; 27 | 28 | import java.util.List; 29 | 30 | import javax.inject.Inject; 31 | 32 | import butterknife.InjectView; 33 | 34 | /** 35 | * @author glomadrian 36 | */ 37 | public class CharacterListFragment extends BaseFragment implements CharacterListView { 38 | 39 | private static final String EXTRA_CHARACTER_COLLECTION = "extraCharacterCollection"; 40 | 41 | @Inject 42 | CharacterListPresenter characterCollectionPresenter; 43 | 44 | @InjectView(R.id.collection_view) 45 | ClickRecyclerView collectionView; 46 | @InjectView(R.id.loading) 47 | ProgressBar loading; 48 | 49 | private ModelAdapter modelAdapter; 50 | private LinearLayoutManager mLayoutManager; 51 | 52 | @Override 53 | public void onCreate(Bundle savedInstanceState) { 54 | Log.i(LogUtils.generateTag(this), "on crate()"); 55 | super.onCreate(savedInstanceState); 56 | modelAdapter = new ModelAdapter(); 57 | mLayoutManager = new LinearLayoutManager(getActivity()); 58 | } 59 | 60 | @Override 61 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 62 | return inflater.inflate(R.layout.character_list, container, false); 63 | } 64 | 65 | @Override 66 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 67 | super.onViewCreated(view, savedInstanceState); 68 | initializeCollectionView(); 69 | characterCollectionPresenter.setView(this); 70 | characterCollectionPresenter.onViewCreate(); 71 | 72 | if (savedInstanceState == null) { 73 | Log.i(LogUtils.generateTag(this), "First time running"); 74 | characterCollectionPresenter.initialize(); 75 | } 76 | 77 | addClickListenerToCharacterList(); 78 | 79 | } 80 | 81 | 82 | /** 83 | * In android the view is not a simple view, there is some cases when the functionality of the 84 | * view is more than the excepted, in this case for example the view save the state 85 | * 86 | * @param outState 87 | */ 88 | @Override 89 | public void onSaveInstanceState(Bundle outState) { 90 | super.onSaveInstanceState(outState); 91 | 92 | //Get the actual state of the characters 93 | MarvelCharacterList marvelCharacters = characterCollectionPresenter.getParcelableCollection(); 94 | 95 | //Parcel the object to be saved in the bundle 96 | Parcelable marvelCharactersWrapped = Parcels.wrap(marvelCharacters); 97 | 98 | //Save the parcelable 99 | outState.putParcelable(EXTRA_CHARACTER_COLLECTION, marvelCharactersWrapped); 100 | } 101 | 102 | @Override 103 | public void onViewStateRestored(Bundle savedInstanceState) { 104 | super.onViewStateRestored(savedInstanceState); 105 | 106 | if (savedInstanceState != null) { 107 | Log.i(LogUtils.generateTag(this), "onViewStateRestored"); 108 | //Get parcelable from bundle 109 | Parcelable marvelCharactersWrapped = savedInstanceState.getParcelable(EXTRA_CHARACTER_COLLECTION); 110 | MarvelCharacterList marvelCharacters = Parcels.unwrap(marvelCharactersWrapped); 111 | characterCollectionPresenter.restoreParcelableCollection(marvelCharacters); 112 | } 113 | } 114 | 115 | private void initializeCollectionView() { 116 | collectionView.setAdapter(modelAdapter); 117 | collectionView.setLayoutManager(mLayoutManager); 118 | collectionView.setItemAnimator(new DefaultItemAnimator()); 119 | } 120 | 121 | @Override 122 | public void add(Model model) { 123 | modelAdapter.add(model); 124 | } 125 | 126 | @Override 127 | public void add(List models) { 128 | modelAdapter.add(models); 129 | } 130 | 131 | @Override 132 | public void remove(Model model) { 133 | //TODO implement 134 | } 135 | 136 | @Override 137 | public int getModelsRenderer() { 138 | return modelAdapter.getItemCount(); 139 | } 140 | 141 | @Override 142 | public void showLoading() { 143 | loading.setVisibility(View.VISIBLE); 144 | } 145 | 146 | @Override 147 | public void hideLoading() { 148 | loading.setVisibility(View.GONE); 149 | } 150 | 151 | 152 | @Override 153 | public void activateLastCharacterViewListener() { 154 | enableSearchOnFinish(); 155 | } 156 | 157 | @Override 158 | public void disableLastCharacterViewListener() { 159 | disableSearchOnFinish(); 160 | } 161 | 162 | @Override 163 | public void onError() { 164 | Toast.makeText(this.getActivity(), getString(R.string.genericError), Toast.LENGTH_LONG).show(); 165 | } 166 | 167 | private void addClickListenerToCharacterList() { 168 | collectionView.setOnItemClickListener(new CharacterClickListener()); 169 | } 170 | 171 | private void enableSearchOnFinish() { 172 | collectionView.setOnScrollListener(new FinishScrollListener()); 173 | } 174 | 175 | private void disableSearchOnFinish() { 176 | collectionView.setOnScrollListener(null); 177 | } 178 | 179 | private class FinishScrollListener extends RecyclerView.OnScrollListener { 180 | @Override 181 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 182 | int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition() + 1; 183 | int modelsCount = modelAdapter.getItemCount(); 184 | 185 | if (lastVisibleItemPosition == modelsCount) { 186 | Log.i(LogUtils.generateTag(this), "finish scroll!"); 187 | characterCollectionPresenter.onLastCharacterViewed(); 188 | } 189 | } 190 | } 191 | 192 | private class CharacterClickListener implements ClickRecyclerView.OnItemClickListener { 193 | 194 | @Override 195 | public void onItemClick(RecyclerView parent, View view, int position, long id) { 196 | characterCollectionPresenter.onCharacterSelected(position); 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/presenter/CharacterInfoPresenter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.presenter; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.ModelInfoView; 5 | 6 | /** 7 | * @author glomadrian 8 | */ 9 | public interface CharacterInfoPresenter extends Presenter { 10 | 11 | /** 12 | * When the view is created it will recibe a marvel character into the bundle, then 13 | * call the presenter 14 | * 15 | * @param marvelCharacter 16 | */ 17 | void onCharacter(MarvelCharacter marvelCharacter); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/presenter/CharacterInfoPresenterImp.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.presenter; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.ModelInfoView; 5 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.CharacterInfoViewModel; 6 | 7 | /** 8 | * @author glomadrian 9 | */ 10 | public class CharacterInfoPresenterImp implements CharacterInfoPresenter { 11 | 12 | ModelInfoView modelInfoView; 13 | 14 | 15 | @Override 16 | public void initialize() { 17 | //Do nothing 18 | } 19 | 20 | @Override 21 | public void onViewCreate() { 22 | //Do nothing 23 | } 24 | 25 | @Override 26 | public void onViewResume() { 27 | //Do nothing 28 | } 29 | 30 | @Override 31 | public void onViewDestroy() { 32 | //Do nothing 33 | } 34 | 35 | @Override 36 | public void setView(ModelInfoView view) { 37 | this.modelInfoView = view; 38 | } 39 | 40 | @Override 41 | public void onCharacter(MarvelCharacter marvelCharacter) { 42 | //Parse the character to characterInfoModelView and call the view to show 43 | CharacterInfoViewModel characterInfoViewModel = new CharacterInfoViewModel(marvelCharacter); 44 | modelInfoView.showCharacterInfo(characterInfoViewModel); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/presenter/CharacterListPresenter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.presenter; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacterList; 4 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.CharacterListView; 5 | 6 | 7 | /** 8 | * @author glomadrian 9 | */ 10 | public interface CharacterListPresenter extends Presenter { 11 | 12 | 13 | void onLastCharacterViewed(); 14 | 15 | MarvelCharacterList getParcelableCollection(); 16 | 17 | void restoreParcelableCollection(MarvelCharacterList marvelCharacters); 18 | 19 | void onCharacterSelected(int position); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/presenter/CharacterListPresenterImp.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.presenter; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.github.glomadrian.mvpcleanarchitecture.app.BasePresenter; 7 | import com.github.glomadrian.mvpcleanarchitecture.domain.LogUtils; 8 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersLimit; 9 | import com.github.glomadrian.mvpcleanarchitecture.domain.interactor.GetMarvelCharactersPaginated; 10 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 11 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacterList; 12 | import com.github.glomadrian.mvpcleanarchitecture.ui.reactive.CharacterSelectedObservable; 13 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.CharacterListView; 14 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.CharacterViewModel; 15 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.Model; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.List; 20 | 21 | /** 22 | * @author glomadrian 23 | */ 24 | public class CharacterListPresenterImp extends BasePresenter implements CharacterListPresenter { 25 | 26 | private static final int LIMIT_GET_CHARACTERS = 10; 27 | private static final int LIMIT_MORE_CHARACTERS = 5; 28 | 29 | private CharacterListView modelCollectionView; 30 | private GetMarvelCharactersLimit getMarvelCharactersLimit; 31 | private GetMarvelCharactersPaginated getMarvelCharactersPaginated; 32 | private MarvelCharacterList marvelCharacterCollection; 33 | private CharacterSelectedObservable characterSelectedObservable; 34 | private Context context; 35 | 36 | public CharacterListPresenterImp(Context context, GetMarvelCharactersLimit getMarvelCharactersLimit, GetMarvelCharactersPaginated getMarvelCharactersPaginated, CharacterSelectedObservable characterSelectedObservable) { 37 | super(context); 38 | this.context = context; 39 | this.getMarvelCharactersLimit = getMarvelCharactersLimit; 40 | this.getMarvelCharactersPaginated = getMarvelCharactersPaginated; 41 | this.characterSelectedObservable = characterSelectedObservable; 42 | } 43 | 44 | @Override 45 | public void initialize() { 46 | marvelCharacterCollection = new MarvelCharacterList(); 47 | searchForCharacters(); 48 | } 49 | 50 | @Override 51 | public void onViewCreate() { 52 | modelCollectionView.activateLastCharacterViewListener(); 53 | } 54 | 55 | @Override 56 | public void onViewResume() { 57 | //Do nothing 58 | } 59 | 60 | @Override 61 | public void onViewDestroy() { 62 | //Do nothing 63 | } 64 | 65 | @Override 66 | public void setView(CharacterListView view) { 67 | this.modelCollectionView = view; 68 | } 69 | 70 | @Override 71 | public void onLastCharacterViewed() { 72 | searchForMoreCharacters(); 73 | } 74 | 75 | @Override 76 | public MarvelCharacterList getParcelableCollection() { 77 | return marvelCharacterCollection; 78 | } 79 | 80 | @Override 81 | public void restoreParcelableCollection(MarvelCharacterList marvelCharacters) { 82 | this.marvelCharacterCollection = marvelCharacters; 83 | modelCollectionView.add(convertToModelViewList(marvelCharacters.getMarvelCharacters())); 84 | } 85 | 86 | @Override 87 | public void onCharacterSelected(int position) { 88 | Collection marvelCharacters = marvelCharacterCollection.getMarvelCharacters(); 89 | MarvelCharacter marvelCharacter = (MarvelCharacter) marvelCharacters.toArray()[position]; 90 | characterSelectedObservable.notifyObservers(marvelCharacter); 91 | } 92 | 93 | private void searchForCharacters() { 94 | 95 | getMarvelCharactersLimit.execute(LIMIT_GET_CHARACTERS, new GetMarvelCharactersLimit.Callback() { 96 | @Override 97 | public void onMarvelCharacterList(List marvelCharacters) { 98 | marvelCharacterCollection.addAll(marvelCharacters); 99 | modelCollectionView.add(convertToModelViewList(marvelCharacters)); 100 | } 101 | 102 | @Override 103 | public void onError() { 104 | Log.e(LogUtils.generateTag(this), "Error on interactor getMarvelCharactersLimit"); 105 | modelCollectionView.onError(); 106 | } 107 | }); 108 | } 109 | 110 | private void searchForMoreCharacters() { 111 | 112 | modelCollectionView.disableLastCharacterViewListener(); 113 | modelCollectionView.showLoading(); 114 | 115 | getMarvelCharactersPaginated.execute(LIMIT_MORE_CHARACTERS, modelCollectionView.getModelsRenderer(), new GetMarvelCharactersPaginated.Callback() { 116 | @Override 117 | public void onMarvelCharacterList(List marvelCharacters) { 118 | marvelCharacterCollection.addAll(marvelCharacters); 119 | modelCollectionView.add(convertToModelViewList(marvelCharacters)); 120 | modelCollectionView.hideLoading(); 121 | modelCollectionView.activateLastCharacterViewListener(); 122 | } 123 | 124 | @Override 125 | public void onError() { 126 | Log.e(LogUtils.generateTag(this), "Error on interactor getMarvelCharactersPaginated"); 127 | modelCollectionView.hideLoading(); 128 | modelCollectionView.activateLastCharacterViewListener(); 129 | modelCollectionView.onError(); 130 | } 131 | }); 132 | } 133 | 134 | private List convertToModelViewList(List marvelCharacters) { 135 | 136 | 137 | List modelList = new ArrayList(); 138 | 139 | for (MarvelCharacter marvelCharacter : marvelCharacters) { 140 | modelList.add(new CharacterViewModel(marvelCharacter)); 141 | } 142 | 143 | return modelList; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/presenter/Presenter.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.presenter; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.ui.view.View; 4 | 5 | /** 6 | * Presenter with lifecycle 7 | * 8 | * @author glomadrian 9 | */ 10 | public interface Presenter { 11 | 12 | void initialize(); 13 | 14 | void onViewCreate(); 15 | 16 | void onViewResume(); 17 | 18 | void onViewDestroy(); 19 | 20 | void setView(T view); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/reactive/CharacterSelectedObservable.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.reactive; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Observable to character selected, this observable call all observer subscribed when a 10 | * character is selected 11 | * 12 | * @author glomadrian 13 | */ 14 | public class CharacterSelectedObservable implements Observable { 15 | 16 | List characterSelectedObservers; 17 | 18 | public CharacterSelectedObservable() { 19 | characterSelectedObservers = new ArrayList(); 20 | } 21 | 22 | 23 | @Override 24 | public void register(CharacterSelectedObserver observer) { 25 | //To avoid duplicated register 26 | if (!characterSelectedObservers.contains(observer)) { 27 | characterSelectedObservers.add(observer); 28 | } 29 | } 30 | 31 | @Override 32 | public void unregister(CharacterSelectedObserver observer) { 33 | characterSelectedObservers.remove(observer); 34 | } 35 | 36 | 37 | public void notifyObservers(MarvelCharacter marvelCharacter) { 38 | for (CharacterSelectedObserver characterSelectedObserver : characterSelectedObservers) { 39 | characterSelectedObserver.characterSelected(marvelCharacter); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/reactive/CharacterSelectedObserver.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.reactive; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | /** 6 | * @author glomadrian 7 | */ 8 | public interface CharacterSelectedObserver { 9 | 10 | void characterSelected(MarvelCharacter marvelCharacter); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/reactive/Observable.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.reactive; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public interface Observable { 7 | 8 | void register(T observer); 9 | 10 | void unregister(T observer); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/view/CharacterListView.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.view; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public interface CharacterListView extends ModelListView { 7 | 8 | int getModelsRenderer(); 9 | 10 | void showLoading(); 11 | 12 | void hideLoading(); 13 | 14 | void activateLastCharacterViewListener(); 15 | 16 | void disableLastCharacterViewListener(); 17 | 18 | void onError(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/view/ModelInfoView.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.view; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.CharacterInfoViewModel; 4 | 5 | /** 6 | * @author glomadrian 7 | */ 8 | public interface ModelInfoView extends View { 9 | 10 | void showCharacterInfo(CharacterInfoViewModel characterInfoViewModel); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/view/ModelListView.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.view; 2 | 3 | 4 | import com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel.Model; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Model collection view is designed to be used in different scenarios, in this concrete case 10 | * the collection is of characters but can be of Users or Animals, but the view can be the same 11 | * 12 | * @author glomadrian 13 | */ 14 | public interface ModelListView extends View { 15 | 16 | 17 | void add(Model model); 18 | 19 | void add(List models); 20 | 21 | void remove(Model model); 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/view/View.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.view; 2 | 3 | /** 4 | * @author glomadrian 5 | */ 6 | public interface View { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/viewholder/AbstractRecyclerViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.viewholder; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | 6 | import butterknife.ButterKnife; 7 | 8 | /** 9 | * ViewHolder for inject views using ButterKnife 10 | * 11 | * @author glomadrian 12 | */ 13 | public abstract class AbstractRecyclerViewHolder extends RecyclerView.ViewHolder { 14 | 15 | 16 | public AbstractRecyclerViewHolder(View itemView) { 17 | super(itemView); 18 | ButterKnife.inject(this, itemView); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/viewmodel/CharacterInfoViewModel.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | /** 6 | * Custom implementation for use with the view ModelInfoView, in this case the model is a Character 7 | * 8 | * @author glomadrian 9 | */ 10 | public class CharacterInfoViewModel extends ModelInfo { 11 | 12 | private MarvelCharacter marvelCharacter; 13 | 14 | public CharacterInfoViewModel(MarvelCharacter model) { 15 | marvelCharacter = model; 16 | } 17 | 18 | @Override 19 | public String getInfoImageUrl() { 20 | return marvelCharacter.getImageURL(); 21 | } 22 | 23 | @Override 24 | public String getInfoNumber1() { 25 | return String.valueOf(marvelCharacter.getComics()); 26 | } 27 | 28 | @Override 29 | public String getInfoNumber2() { 30 | return String.valueOf(marvelCharacter.getComics()); 31 | } 32 | 33 | @Override 34 | public String getInfoNumber3() { 35 | return String.valueOf(marvelCharacter.getStories()); 36 | } 37 | 38 | @Override 39 | public String getInfoDescription() { 40 | return String.valueOf(marvelCharacter.getDescription()); 41 | } 42 | 43 | @Override 44 | public String getInfoTitle() { 45 | return String.valueOf(marvelCharacter.getName()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/viewmodel/CharacterViewModel.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel; 2 | 3 | import com.github.glomadrian.mvpcleanarchitecture.domain.model.MarvelCharacter; 4 | 5 | /** 6 | * Custom implementation of a model to use with ModelCollectionView 7 | * 8 | * @author glomadrian 9 | */ 10 | public class CharacterViewModel extends Model { 11 | 12 | MarvelCharacter model; 13 | 14 | public CharacterViewModel(MarvelCharacter model) { 15 | this.model = model; 16 | } 17 | 18 | @Override 19 | public String getImageUrl() { 20 | return model.getImageURL(); 21 | } 22 | 23 | @Override 24 | public String getTitle() { 25 | return model.getName(); 26 | } 27 | 28 | @Override 29 | public String getSubtitle() { 30 | return String.valueOf(model.getId()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/viewmodel/Model.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel; 2 | 3 | /** 4 | * This view model is a adapter from other model, usually a domain model, if in any moment the 5 | * application need to show other model we only need to create a new adapter for a viewmodel and the 6 | * view continue working 7 | *

8 | * This abstract model is for use with model.xml in layouts (or other with the same information) 9 | * 10 | * @author glomadrian 11 | */ 12 | public abstract class Model { 13 | 14 | public abstract String getImageUrl(); 15 | 16 | public abstract String getTitle(); 17 | 18 | public abstract String getSubtitle(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/glomadrian/mvpcleanarchitecture/ui/viewmodel/ModelInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.glomadrian.mvpcleanarchitecture.ui.viewmodel; 2 | 3 | /** 4 | * Custom model for the view CharacterInfoView 5 | * 6 | * @author glomadrian 7 | */ 8 | public abstract class ModelInfo { 9 | 10 | public abstract String getInfoImageUrl(); 11 | 12 | public abstract String getInfoNumber1(); 13 | 14 | public abstract String getInfoNumber2(); 15 | 16 | public abstract String getInfoNumber3(); 17 | 18 | public abstract String getInfoDescription(); 19 | 20 | public abstract String getInfoTitle(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/spider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-hdpi/spider.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/green_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/yellow_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/marvel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-xhdpi/marvel.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/app/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/blue_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/line.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | 19 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/model_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 17 | 23 | 24 | 31 | 32 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 65 | 66 | 72 | 73 | 74 | 75 | 82 | 83 | 90 | 91 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 112 | 113 | 127 | 128 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app/src/main/res/layout/character_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/model.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 20 | 21 | 26 | 27 | 32 | 33 | 42 | 43 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/model_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 31 | 32 | 38 | 39 | 44 | 45 | 52 | 53 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 78 | 79 | 86 | 87 | 93 | 94 | 95 | 96 | 103 | 104 | 111 | 112 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 128 | 129 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /app/src/main/res/layout/model_info_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #2196F3 6 | #1565C0 7 | #F50057 8 | #FFFF00 9 | 10 | 11 | #4CAF50 12 | #F4FF81 13 | #FFEB3B 14 | 15 | 16 | 17 | @color/material_blue_500 18 | @color/material_blue_800 19 | 20 | #58000000 21 | #FFF 22 | 23 | @color/material_blue_500 24 | @color/material_green_500 25 | @color/material_yellow_500 26 | @color/material_blue_800 27 | #FFF 28 | @color/material_blue_500 29 | #fff 30 | #333333 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MVP Clean Architecture 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sagittis faucibus rhoncus. Donec dignissim finibus diam nec sollicitudin. Nullam sed leo nec felis imperdiet maximus in mattis lacus. Phasellus eget quam ut metus commodo fermentum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec condimentum ultricies risus, et ultricies leo sollicitudin sed. In hac habitasse platea dictumst. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse in lacinia risus, sed mattis justo. Sed a quam laoreet nisl efficitur eleifend nec quis nisi. 4 | Comics 5 | Series 6 | Histories 7 | No description avaiable 8 | Error getting api information 9 | Info image 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.0.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Dec 13 16:53:24 CET 2014 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.2.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /img/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/img/screenshot_1.png -------------------------------------------------------------------------------- /img/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/img/screenshot_2.png -------------------------------------------------------------------------------- /img/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glomadrian/MvpCleanArchitecture/24009eaf8310e907cf5f61b169dabe1c28f362a3/img/screenshot_3.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | equired metadata 2 | sonar.projectKey=MvpCleanArchitecture 3 | sonar.projectName=MvpCleanArchitecture 4 | sonar.projectVersion=1.0 5 | 6 | # Path to the parent source code directory. 7 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 8 | # Since SonarQube 4.2, this property is optional if sonar.modules is set. 9 | # If not set, SonarQube starts looking for source code from the directory containing 10 | # the sonar-project.properties file. 11 | # Language 12 | sonar.language=java 13 | sonar.sources=app/src 14 | --------------------------------------------------------------------------------