Repository: patrykpoborca/CleanArchitecture Branch: master Commit: d8437d80fe2e Files: 102 Total size: 135.7 KB Directory structure: gitextract_n6u9vjvq/ ├── .gitignore ├── .idea/ │ ├── .name │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── encodings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── io/ │ │ └── patrykpoborca/ │ │ └── cleanarchitecture/ │ │ ├── TestHelper.java │ │ ├── dagger/ │ │ │ ├── TestClassInjector.java │ │ │ └── mockmodules/ │ │ │ ├── MockLocalModule.java │ │ │ ├── MockNetworkModule.java │ │ │ └── MockTestModule.java │ │ ├── mockimpl/ │ │ │ ├── MockLocalDataCache.java │ │ │ ├── MockMVPCIPview.java │ │ │ ├── MockOkHTTP.java │ │ │ ├── MockRetrofit.java │ │ │ ├── MockTweeterActivityPview.java │ │ │ └── MockTweeterApi.java │ │ └── tests/ │ │ ├── MVPCITest.java │ │ ├── MVPTest.java │ │ ├── MVVMTest.java │ │ └── PlainTweeterTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── io/ │ │ │ └── patrykpoborca/ │ │ │ └── cleanarchitecture/ │ │ │ ├── CleanArchitectureApplication.java │ │ │ ├── dagger/ │ │ │ │ ├── components/ │ │ │ │ │ ├── ActivityInjectorComponent.java │ │ │ │ │ ├── ApplicationComponent.java │ │ │ │ │ └── BaseComponent.java │ │ │ │ ├── interactors/ │ │ │ │ │ ├── NetworkInteractor.java │ │ │ │ │ └── base/ │ │ │ │ │ └── BaseInteractor.java │ │ │ │ ├── modules/ │ │ │ │ │ ├── ApplicationModule.java │ │ │ │ │ ├── InteractorModule.java │ │ │ │ │ ├── LocalModule.java │ │ │ │ │ ├── NetworkModule.java │ │ │ │ │ ├── PresenterModule.java │ │ │ │ │ └── ThreadingModule.java │ │ │ │ └── scopes/ │ │ │ │ ├── ActivityScope.java │ │ │ │ ├── ApplicationScope.java │ │ │ │ └── ExposedAPIScope.java │ │ │ ├── localdata/ │ │ │ │ └── LocalDataCache.java │ │ │ ├── network/ │ │ │ │ ├── TweeterApi.java │ │ │ │ └── base/ │ │ │ │ ├── OKHttp.java │ │ │ │ └── Retrofit.java │ │ │ ├── ui/ │ │ │ │ ├── BaseCleanArchitectureActivity.java │ │ │ │ ├── MVP/ │ │ │ │ │ ├── TweeterActivityMVP.java │ │ │ │ │ ├── TweeterMVPPresenterImpl.java │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── BasePresenterActivity.java │ │ │ │ │ │ └── Interfaces/ │ │ │ │ │ │ ├── PView.java │ │ │ │ │ │ └── Presenter.java │ │ │ │ │ └── interfaces/ │ │ │ │ │ ├── TweeterMVPPView.java │ │ │ │ │ └── TweeterMVPPresenter.java │ │ │ │ ├── MVPCI/ │ │ │ │ │ ├── TweeterActivityMVPCI.java │ │ │ │ │ ├── TweeterMVPCIPresenter.java │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── BasePresenterActivityMVPCI.java │ │ │ │ │ │ └── BasePresenterMVPCI.java │ │ │ │ │ ├── interfaces/ │ │ │ │ │ │ └── TweeterActivityMVPCIPview.java │ │ │ │ │ └── models/ │ │ │ │ │ └── UserProfile.java │ │ │ │ ├── MVVM/ │ │ │ │ │ ├── MainViewmodel.java │ │ │ │ │ ├── TweeterActivityMVVM.java │ │ │ │ │ └── base/ │ │ │ │ │ ├── BaseViewModel.java │ │ │ │ │ └── BaseViewModelActivity.java │ │ │ │ ├── PlainTweeterActivity.java │ │ │ │ └── RouterActivity.java │ │ │ └── util/ │ │ │ ├── Constants.java │ │ │ ├── LoadingFragment.java │ │ │ └── utility.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_router.xml │ │ │ └── fragment_progress.xml │ │ ├── menu/ │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ └── test/ │ └── java/ │ ├── dagger/ │ │ ├── TestClassInjector.java │ │ └── mockmodules/ │ │ ├── MockLocalModule.java │ │ ├── MockNetworkModule.java │ │ ├── MockTestModule.java │ │ └── MockThreadingModule.java │ ├── helper/ │ │ └── TestHelper.java │ ├── mockimpl/ │ │ ├── MockLocalDataCache.java │ │ ├── MockMVPCIPview.java │ │ ├── MockOkHTTP.java │ │ ├── MockRetrofit.java │ │ ├── MockTweeterActivityPview.java │ │ └── MockTweeterApi.java │ └── tests/ │ ├── MVPCITest.java │ ├── MVPTest.java │ └── MVVMTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle *.iml /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: .idea/.name ================================================ CleanArchitecture ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ Android Lint C/C++ Class structureJava Code maturity issuesJava Code style issuesJava Compiler issuesJava Declaration orderC/C++ Finalization issuesJava Groovy Inheritance issuesJava J2ME issuesJava Java Java language level migration aidsJava JavaBeans issuesJava Javadoc issuesJava Numeric issuesJava OtherGroovy Performance issuesJava Probable bugsJava Security issuesJava Serialization issuesJava TestNGJava Threading issuesJava Type checksC/C++ Verbose or redundant code constructsJava Android 1.7 ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: README.md ================================================ # CleanArchitecture Thanks for checking out my github repo. There's not much to say in this readme since I wrote two articles about this repo here: [Article 1 - Dagger 2 introduction](https://medium.com/@patrykpoborca/making-a-best-practice-app-4-dagger-2-267ec5f6c89a) [Article 2 - Clean Architecture and Testing in JUnit and AndroidJunit with Espresso](https://medium.com/@patrykpoborca/making-a-best-practice-app-5-clean-architecture-testing-84a1672dd000) To sum, the articles and this repo go into how to utilize Dagger 2 in order to setup and start your project. How to properly implement some variant of an Architecture so that your app is scalable and testable. Finally we go over how to actually implement those tests. I have similar tests in both POJO (Plain old java object) Junit and Android Junit which utilizes Some mockito. I also show how annoying/and or obnoxious it is to test imporoperly architected applications by actually testing my "PlainTweeterActivity" I hope you learn something from this, if you have any questions or issues feel free to start an issue here or [tweet me here](https://twitter.com/patrykpoborca) Also if you want to see the talk about Dagger 2 I did here [it is](https://www.youtube.com/watch?v=JNbz_rgdQ10) Thanks! ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' //apply plugin: "net.ltgt.apt" android { compileSdkVersion 22 buildToolsVersion '23.0.2' defaultConfig { applicationId "io.patrykpoborca.cleanarchitecture" minSdkVersion 22 targetSdkVersion 22 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { encoding "UTF-8" sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { dependencies { apt 'com.google.dagger:dagger-compiler:2.0' testProvided 'com.google.dagger:dagger-compiler:2.0' //needed to resolve compilation errors, thanks to tutplus.org for finding the dependency // http://stackoverflow.com/questions/27036933/how-to-set-up-dagger-dependency-injection-from-scratch-in-android-project //test compiles androidTestApt 'com.google.dagger:dagger-compiler:2.0' compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.google.dagger:dagger:2.0' provided 'org.glassfish:javax.annotation:10.0-b28' compile 'io.reactivex:rxandroid:0.25.0' compile 'com.jakewharton:butterknife:7.0.1' androidTestCompile 'com.google.dagger:dagger:2.0' testCompile 'com.google.dagger:dagger:2.0' testCompile 'junit:junit:4.12' androidTestCompile 'com.android.support.test:runner:0.3' // Set this dependency to use JUnit 4 rules androidTestCompile 'com.android.support.test:rules:0.3' androidTestCompile 'org.mockito:mockito-core:1.+' androidTestCompile('com.android.support.test:runner:0.3') { exclude group: 'com.android.support', module: 'support-annotations' } androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'com.android.support.test:runner:0.3' androidTestCompile 'com.android.support.test:rules:0.3' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' } } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Users\Patryk\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/TestHelper.java ================================================ package io.patrykpoborca.cleanarchitecture; import android.app.Application; import io.patrykpoborca.cleanarchitecture.dagger.DaggerTestClassInjector; import io.patrykpoborca.cleanarchitecture.dagger.TestClassInjector; import io.patrykpoborca.cleanarchitecture.dagger.components.ApplicationComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.BaseComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerApplicationComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerBaseComponent; import io.patrykpoborca.cleanarchitecture.dagger.mockmodules.MockLocalModule; import io.patrykpoborca.cleanarchitecture.dagger.mockmodules.MockNetworkModule; import io.patrykpoborca.cleanarchitecture.dagger.modules.ApplicationModule; public class TestHelper { private static ApplicationComponent sApplicationComponent; private static BaseComponent sBaseComponent; private static TestClassInjector sTestClassInjector; public static ApplicationComponent getApplicationComponent(){ if(sApplicationComponent == null) { sApplicationComponent = DaggerApplicationComponent.builder() .applicationModule(new ApplicationModule(new Application())) .build(); } return sApplicationComponent; } public static BaseComponent getBaseComponent(){ if(sBaseComponent == null){ sBaseComponent = DaggerBaseComponent.builder() .applicationComponent(getApplicationComponent()) .localModule(new MockLocalModule()) .networkModule(new MockNetworkModule()) .build(); } return sBaseComponent; } public static TestClassInjector getTestClassInjector(){ if(sTestClassInjector == null){ sTestClassInjector = DaggerTestClassInjector.builder() .baseComponent(getBaseComponent()) .build(); } return sTestClassInjector; } public static void waitFor(IWaitingCallback callback){ waitFor(10, callback); } public static void waitFor(int maxCycles, IWaitingCallback callback){ while(true){ maxCycles --; try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } if(callback.checkCondition()){ break; } if(maxCycles <= 0){ break; } } } public static interface IWaitingCallback{ /** * * @return true if condition is met, false if we should keep waiting */ public boolean checkCondition(); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/dagger/TestClassInjector.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger; import dagger.Component; import io.patrykpoborca.cleanarchitecture.dagger.mockmodules.MockTestModule; import io.patrykpoborca.cleanarchitecture.tests.MVPCITest; import io.patrykpoborca.cleanarchitecture.tests.MVPTest; import io.patrykpoborca.cleanarchitecture.tests.MVVMTest; import io.patrykpoborca.cleanarchitecture.dagger.components.BaseComponent; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ActivityScope; @ActivityScope @Component(dependencies = BaseComponent.class, modules = MockTestModule.class) public interface TestClassInjector { void inject(MVVMTest test); void inject(MVPTest MVPTest); void inject(MVPCITest mvpciTest); } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/dagger/mockmodules/MockLocalModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.mockmodules; import android.app.Application; import io.patrykpoborca.cleanarchitecture.dagger.modules.LocalModule; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockLocalDataCache; public class MockLocalModule extends LocalModule{ @Override protected LocalDataCache providesDataCache(Application application) { return new MockLocalDataCache(application); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/dagger/mockmodules/MockNetworkModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.mockmodules; import javax.inject.Named; import io.patrykpoborca.cleanarchitecture.dagger.modules.NetworkModule; import io.patrykpoborca.cleanarchitecture.mockimpl.MockOkHTTP; import io.patrykpoborca.cleanarchitecture.mockimpl.MockRetrofit; import io.patrykpoborca.cleanarchitecture.network.base.OKHttp; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Scheduler; public class MockNetworkModule extends NetworkModule { @Override protected OKHttp providesOkHTTP() { return new MockOkHTTP(); } @Override protected Retrofit providesRetrofit(OKHttp okHttp, @Named(Constants.MAIN_THREAD)Scheduler mainThread) { return new MockRetrofit(okHttp, mainThread); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/dagger/mockmodules/MockTestModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.mockmodules; import javax.inject.Named; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ActivityScope; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockMVPCIPview; import io.patrykpoborca.cleanarchitecture.mockimpl.MockTweeterActivityPview; import io.patrykpoborca.cleanarchitecture.mockimpl.MockTweeterApi; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVP.TweeterMVPPresenterImpl; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPresenter; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Scheduler; @Module public class MockTestModule { @Provides @ActivityScope TweeterMVPPresenter providesMainPresenter(TweeterApi api, Retrofit retrofit) { return new TweeterMVPPresenterImpl(api, retrofit); } @ActivityScope @Provides public MockTweeterActivityPview providesMockMainPview(TweeterMVPPresenter presenter) { MockTweeterActivityPview mockTweeterActivityPview = new MockTweeterActivityPview(); presenter.registerView(mockTweeterActivityPview); return mockTweeterActivityPview; } @ActivityScope @Provides public TweeterApi providesMockTweeter(Retrofit retrofit, LocalDataCache dataCache, @Named(Constants.MAIN_THREAD) Scheduler mainThread) { return new MockTweeterApi(retrofit, dataCache, mainThread); } @ActivityScope @Provides public MockMVPCIPview providesMockMVPCCIView(){ return new MockMVPCIPview(); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockLocalDataCache.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import android.content.Context; import java.util.List; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import rx.Observable; public class MockLocalDataCache extends LocalDataCache { public MockLocalDataCache(Context context) { super(context); } @Override public void saveTweet(String tweet) { sPastTweets.add(tweet); } @Override public Observable> fetchRecentTweets() { return Observable.just(sPastTweets) .map(arrayList -> { List list = arrayList; return list; }); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockMVPCIPview.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.interfaces.TweeterActivityMVPCIPview; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; public class MockMVPCIPview implements TweeterActivityMVPCIPview { public UserProfile loggedInProfile; public boolean loggedOutCalled = false; public boolean toggleProgresbarCalled = false; @Override public void loggedIn(UserProfile profile) { this.loggedInProfile = profile; } @Override public void loggedOut() { loggedOutCalled = true; } @Override public void toggleProgressBar(boolean loading) { toggleProgresbarCalled = true; } @Override public void displayToast(String toast) { } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockOkHTTP.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import io.patrykpoborca.cleanarchitecture.network.base.OKHttp; public class MockOkHTTP extends OKHttp { @Override public String rawResponse() { return "Mocked raw response: "; } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockRetrofit.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import io.patrykpoborca.cleanarchitecture.network.base.OKHttp; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import rx.Observable; import rx.Scheduler; public class MockRetrofit extends Retrofit { private static final String MOCK_PARSE = "SomeMockResponse"; public static final String MOCKED_STRING = "MOCKED PAGE:"; public MockRetrofit(OKHttp okHttp, Scheduler mainScheduler) { super(okHttp, mainScheduler); } @Override public Observable completeRequest() { return Observable.just(okHttp.rawResponse() + MOCK_PARSE) .observeOn(mainScheduler); } @Override public Observable fetchSomePage(String url) { return Observable.just("

" + MOCKED_STRING + " " + url + "

") .observeOn(mainScheduler); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockTweeterActivityPview.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import java.util.List; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPView; public class MockTweeterActivityPview implements TweeterMVPPView { public String fetchedTweet = null; public List previousTweets = null; public boolean setUserButtonTextCalled = false; public boolean toggleProgressBarCalled = false; public boolean toggleLoginContainerCalled = false; public String displayWebpage = null; public boolean displayToastCalled = false; @Override public void displayFetchedTweet(String tweet) { fetchedTweet = tweet; } @Override public void displayPreviousTweets(List list) { previousTweets = list; } @Override public void setUserButtonText(String text) { setUserButtonTextCalled = true; } @Override public void toggleLoginContainer(boolean b) { toggleLoginContainerCalled = true; } @Override public void displayWebpage(String html) { displayWebpage = html; } @Override public void toggleProgressBar(boolean loading) { toggleProgressBarCalled = true; } @Override public void displayToast(String toast) { displayToastCalled = true; } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/mockimpl/MockTweeterApi.java ================================================ package io.patrykpoborca.cleanarchitecture.mockimpl; import android.util.Log; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import rx.Observable; import rx.Scheduler; public class MockTweeterApi extends TweeterApi{ public MockTweeterApi(Retrofit retrofit, LocalDataCache dataCache, Scheduler mainScheduler) { super(retrofit, dataCache, mainScheduler); } @Override public Observable login(String username, String password) { return Observable.just(new UserProfile(username, password)); } @Override public Observable logout() { return Observable.just(null); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/tests/MVPCITest.java ================================================ package io.patrykpoborca.cleanarchitecture.tests; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import javax.inject.Inject; import io.patrykpoborca.cleanarchitecture.TestHelper; import io.patrykpoborca.cleanarchitecture.mockimpl.MockLocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockMVPCIPview; import io.patrykpoborca.cleanarchitecture.mockimpl.MockOkHTTP; import io.patrykpoborca.cleanarchitecture.mockimpl.MockRetrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.TweeterMVPCIPresenter; @RunWith(JUnit4.class) public class MVPCITest { private static final String SOME_URL = "SOME_URL"; private static final String USER_PASSWORD = "USER_PASSWORD"; private static final String USER_NAME = "USER_NAME"; private String tweetedTweet; @Inject TweeterMVPCIPresenter presenter; @Inject MockMVPCIPview pView; @Before public void setUp(){ TestHelper.getTestClassInjector() .inject(this); presenter.registerPresenter(pView); junit.framework.Assert.assertTrue(TestHelper.getBaseComponent().getLocalDataCache() instanceof MockLocalDataCache); junit.framework.Assert.assertTrue(TestHelper.getBaseComponent().getRetrofit() instanceof MockRetrofit); junit.framework.Assert.assertTrue(TestHelper.getBaseComponent().getOkHTTP() instanceof MockOkHTTP); } @Test public void testWebPage(){ presenter.loadWebPage(SOME_URL) .toBlocking() .forEach(s -> { junit.framework.Assert.assertTrue(s.contains(MockRetrofit.MOCKED_STRING) && s.contains(SOME_URL)); }); } @Test public void testLogin(){ Assert.assertTrue(pView.loggedInProfile == null); presenter.toggleLogin(USER_NAME, USER_PASSWORD); TestHelper.waitFor(() -> pView.loggedInProfile != null); Assert.assertTrue(pView.loggedInProfile.getUserName().equals(USER_NAME)); presenter.toggleLogin(USER_NAME, USER_PASSWORD); TestHelper.waitFor(() -> pView.loggedOutCalled); Assert.assertTrue(pView.loggedOutCalled); } @Test public void testTweets() { //Sanity test first to see if underlying logic of tweeter api has changed presenter.fetchPreviousTweets() .toBlocking() .forEach(list -> { Assert.assertNotNull(list); }); } @Test public void testTweetList(){ presenter.fetchCurrentTweet() .toBlocking() .forEach(tweet -> { tweetedTweet = tweet; }); presenter.fetchPreviousTweets() .toBlocking() .forEach(list -> { boolean exists = false; for (int i = 0; i < list.size(); i++) { if (tweetedTweet.contains(list.get(i))) { exists = true; break; } } Assert.assertTrue(exists); }); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/tests/MVPTest.java ================================================ package io.patrykpoborca.cleanarchitecture.tests; import android.support.test.runner.AndroidJUnit4; import junit.framework.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import javax.inject.Inject; import io.patrykpoborca.cleanarchitecture.TestHelper; import io.patrykpoborca.cleanarchitecture.mockimpl.MockLocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockTweeterActivityPview; import io.patrykpoborca.cleanarchitecture.mockimpl.MockOkHTTP; import io.patrykpoborca.cleanarchitecture.mockimpl.MockRetrofit; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPresenter; @RunWith(AndroidJUnit4.class) public class MVPTest { private static final String SOME_URL = "SOME_URL"; private static final String USER_PASSWORD = "USER_PASSWORD"; private static final String USER_NAME = "USER_NAME"; @Inject MockTweeterActivityPview pView; @Inject TweeterMVPPresenter presenter; @Before public void setUp(){ TestHelper.getTestClassInjector() .inject(this); Assert.assertTrue(TestHelper.getBaseComponent().getLocalDataCache() instanceof MockLocalDataCache); Assert.assertTrue(TestHelper.getBaseComponent().getRetrofit() instanceof MockRetrofit); Assert.assertTrue(TestHelper.getBaseComponent().getOkHTTP() instanceof MockOkHTTP); } @Test public void testLogin(){ presenter.toggleLogin(USER_NAME, USER_PASSWORD); TestHelper.waitFor(() -> pView.toggleLoginContainerCalled); Assert.assertTrue(pView.toggleLoginContainerCalled); pView.toggleLoginContainerCalled = false; presenter.toggleLogin(USER_NAME, USER_PASSWORD); TestHelper.waitFor(() -> pView.toggleLoginContainerCalled); Assert.assertTrue(pView.toggleLoginContainerCalled); } @Test public void testFetchTweets(){ presenter.fetchCurrentTweet(); TestHelper.waitFor(() -> pView.fetchedTweet != null); String tweet = pView.fetchedTweet; presenter.fetchPreviousTweets(); TestHelper.waitFor(() -> pView.previousTweets != null); boolean found = false; for(int i=0; i < pView.previousTweets.size(); i++){ found = tweet.contains(pView.previousTweets.get(i)); if(found){ break; } } Assert.assertTrue(found); } @Test public void testWebPage() { presenter.loadWebPage(SOME_URL); TestHelper.waitFor(() -> pView.displayWebpage != null); String webPage = pView.displayWebpage; Assert.assertTrue(webPage.contains(SOME_URL)); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/tests/MVVMTest.java ================================================ package io.patrykpoborca.cleanarchitecture.tests; import android.support.test.runner.AndroidJUnit4; import junit.framework.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import javax.inject.Inject; import io.patrykpoborca.cleanarchitecture.TestHelper; import io.patrykpoborca.cleanarchitecture.mockimpl.MockLocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockOkHTTP; import io.patrykpoborca.cleanarchitecture.mockimpl.MockRetrofit; import io.patrykpoborca.cleanarchitecture.ui.MVVM.MainViewModel; @RunWith(AndroidJUnit4.class) public class MVVMTest { private static final String SOME_URL = "SOME_URL"; private static final String USER_PASSWORD = "USER_PASSWORD"; private static final String USER_NAME = "USER_NAME"; private String tweetedTweet; @Inject MainViewModel viewModel; @Before public void setUp() { TestHelper.getTestClassInjector() .inject(this); Assert.assertTrue(TestHelper.getBaseComponent().getLocalDataCache() instanceof MockLocalDataCache); Assert.assertTrue(TestHelper.getBaseComponent().getRetrofit() instanceof MockRetrofit); Assert.assertTrue(TestHelper.getBaseComponent().getOkHTTP() instanceof MockOkHTTP); } @Test public void testWebPage() { viewModel.loadWebPage(SOME_URL) .toBlocking() .forEach(s -> { Assert.assertTrue(s.contains(MockRetrofit.MOCKED_STRING) && s.contains(SOME_URL)); }); } @Test public void testLogin() { Assert.assertFalse(viewModel.isLoggedIn()); viewModel.toggleLogin(USER_NAME, USER_PASSWORD) .toBlocking() .forEach( user -> { Assert.assertNotNull(user); Assert.assertEquals(user.getUserName(), USER_NAME); } ); Assert.assertTrue(viewModel.isLoggedIn()); } @Test public void testTweets() { //Sanity test first to see if underlying logic of tweeter api has changed viewModel.fetchPreviousTweets() .toBlocking() .forEach(list -> { Assert.assertNotNull(list); }); } @Test public void testTweetList() { viewModel.fetchCurrentTweet() .toBlocking() .forEach(tweet -> { tweetedTweet = tweet; }); viewModel.fetchPreviousTweets() .toBlocking() .forEach(list -> { boolean exists = false; for (int i = 0; i < list.size(); i++) { if (tweetedTweet.contains(list.get(i))) { exists = true; break; } } Assert.assertTrue(exists); }); } } ================================================ FILE: app/src/androidTest/java/io/patrykpoborca/cleanarchitecture/tests/PlainTweeterTest.java ================================================ package io.patrykpoborca.cleanarchitecture.tests; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import junit.framework.Assert; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.TestHelper; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerActivityInjectorComponent; import io.patrykpoborca.cleanarchitecture.mockimpl.MockLocalDataCache; import io.patrykpoborca.cleanarchitecture.mockimpl.MockOkHTTP; import io.patrykpoborca.cleanarchitecture.mockimpl.MockRetrofit; import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.action.ViewActions.typeText; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.matcher.ViewMatchers.withText; @RunWith(AndroidJUnit4.class) public class PlainTweeterTest { @Rule public ActivityTestRule plainTweeterActivity = new ActivityTestRule<>(io.patrykpoborca.cleanarchitecture.ui.PlainTweeterActivity.class, false, true); private static final String SOME_URL = "SOME_URL"; private static final String USER_PASSWORD = "USER_PASSWORD"; private static final String USER_NAME = "USER_NAME"; private String fetchedTweet; @Before public void setUp() { DaggerActivityInjectorComponent .builder() .baseComponent(TestHelper.getBaseComponent()) .build() .inject(plainTweeterActivity.getActivity()); Assert.assertTrue(TestHelper.getBaseComponent().getLocalDataCache() instanceof MockLocalDataCache); Assert.assertTrue(TestHelper.getBaseComponent().getRetrofit() instanceof MockRetrofit); Assert.assertTrue(TestHelper.getBaseComponent().getOkHTTP() instanceof MockOkHTTP); } @Test public void testWebpage(){ onView(withId(R.id.some_url)) .perform(typeText(SOME_URL)); onView(withId(R.id.some_url)) .check(matches(new TypeSafeMatcher() { @Override protected boolean matchesSafely(View item) { TextView text = ((TextView) item); return text.getText().toString().contains(SOME_URL); } @Override public void describeTo(Description description) { } })); onView(withId(R.id.request_website_button)) .perform(click()); onView(withId(R.id.webpage_text)) .check(matches(new TypeSafeMatcher(){ @Override protected boolean matchesSafely(View item) { TextView text = ((TextView)item); return text.getText().toString().contains(SOME_URL); } @Override public void describeTo(Description description) { } })); } @Test public void testLogin(){ onView(withId(R.id.user_name)) .perform(typeText(USER_NAME)); onView(withId(R.id.user_password)) .perform(scrollTo()) .perform(typeText(USER_PASSWORD)); //login onView(withId(R.id.user_login_button)) .perform(scrollTo()) .perform(click()); onView(withId(R.id.container)) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); //logout onView(withId(R.id.user_login_button)) .perform(click()); onView(withId(R.id.container)) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); } @Test public void testTweets(){ onView(withId(R.id.current_tweet)).check(matches(withText(R.string.hello_world))); onView(withId(R.id.fetch_tweet_button)) .perform(scrollTo()) .perform(click()); onView(withId(R.id.current_tweet)).check(matches(new TypeSafeMatcher() { @Override protected boolean matchesSafely(View item) { boolean result = item instanceof TextView && !((TextView) item).getText().toString().contains(plainTweeterActivity.getActivity().getResources().getString(R.string.hello_world)); fetchedTweet = ((TextView) item).getText().toString(); return result; } @Override public void describeTo(Description description) { } })); onView(withId(R.id.fetch_last_two_tweets)).perform(click()); onView(withId(R.id.past_tweets_container)).check(matches(new TypeSafeMatcher() { @Override protected boolean matchesSafely(View item) { ViewGroup parent = ((ViewGroup) item); for (int i = 0; i < parent.getChildCount(); i++) { TextView textView = (TextView) parent.getChildAt(i); if (fetchedTweet.contains(textView.getText().toString())) { return true; } } return false; } @Override public void describeTo(Description description) { } })); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/CleanArchitectureApplication.java ================================================ package io.patrykpoborca.cleanarchitecture; import android.app.Application; import io.patrykpoborca.cleanarchitecture.dagger.components.ApplicationComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.BaseComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerApplicationComponent; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerBaseComponent; import io.patrykpoborca.cleanarchitecture.dagger.modules.ApplicationModule; public class CleanArchitectureApplication extends Application{ private static BaseComponent sBaseComponent; @Override public void onCreate() { super.onCreate(); ApplicationComponent applicationComponent = DaggerApplicationComponent .builder() .applicationModule(new ApplicationModule(this)) .build(); sBaseComponent = DaggerBaseComponent.builder() .applicationComponent(applicationComponent) .build(); } public static BaseComponent getBaseComponent(){ return sBaseComponent; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/components/ActivityInjectorComponent.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.components; import dagger.Component; import io.patrykpoborca.cleanarchitecture.dagger.modules.PresenterModule; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ActivityScope; import io.patrykpoborca.cleanarchitecture.ui.MVP.TweeterActivityMVP; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.TweeterActivityMVPCI; import io.patrykpoborca.cleanarchitecture.ui.MVVM.TweeterActivityMVVM; import io.patrykpoborca.cleanarchitecture.ui.PlainTweeterActivity; /** * Created by Patryk on 7/28/2015. */ @ActivityScope @Component(dependencies = {BaseComponent.class}, modules = PresenterModule.class) public interface ActivityInjectorComponent { void inject(PlainTweeterActivity activity); void inject(TweeterActivityMVP activityMVP); void inject(TweeterActivityMVVM activityMVVM); void inject(TweeterActivityMVPCI activityMVPCI); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/components/ApplicationComponent.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.components; import android.app.Application; import javax.inject.Singleton; import dagger.Component; import io.patrykpoborca.cleanarchitecture.dagger.modules.ApplicationModule; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ApplicationScope; @Singleton @Component(modules = ApplicationModule.class) public interface ApplicationComponent { Application getApplication(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/components/BaseComponent.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.components; import javax.inject.Named; import javax.inject.Singleton; import dagger.Component; import io.patrykpoborca.cleanarchitecture.dagger.modules.LocalModule; import io.patrykpoborca.cleanarchitecture.dagger.modules.NetworkModule; import io.patrykpoborca.cleanarchitecture.dagger.modules.ThreadingModule; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ApplicationScope; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import io.patrykpoborca.cleanarchitecture.network.base.OKHttp; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Scheduler; // AppModule [Application] |AppComponent| <- BaseComponent needs that stuff.. @ApplicationScope @Component(modules = {LocalModule.class, NetworkModule.class, ThreadingModule.class}, dependencies = ApplicationComponent.class) public interface BaseComponent { OKHttp getOkHTTP(); Retrofit getRetrofit(); LocalDataCache getLocalDataCache(); @Named(Constants.MAIN_THREAD) Scheduler getMainScheduler(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/interactors/NetworkInteractor.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.interactors; import java.util.List; import javax.inject.Inject; import io.patrykpoborca.cleanarchitecture.dagger.interactors.base.BaseInteractor; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import rx.Observable; /** * Created by Patryk on 7/29/2015. */ public class NetworkInteractor extends BaseInteractor { private final Retrofit retrofit; private final TweeterApi tweeterAPI; @Inject public NetworkInteractor(Retrofit retrofit, TweeterApi api){ this.retrofit = retrofit; this.tweeterAPI = api; } public Observable attemptLogin(String username, String password) { return tweeterAPI.login(username, password); } public Observable logout(){ return tweeterAPI.logout(); } public Observable fetchTweet() { return tweeterAPI.getTweet(); } public Observable> fetchTweets(int count) { return tweeterAPI.fetchXrecents(count); } public Observable loadWebpage(String url){ return retrofit.fetchSomePage(url); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/interactors/base/BaseInteractor.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.interactors.base; public class BaseInteractor { } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/ApplicationModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import android.app.Application; import android.content.Context; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ApplicationScope; @Module public class ApplicationModule { private static Application sApplication; public ApplicationModule(Application application) { sApplication = application; } @Singleton @Provides Application providesApplication(){ return sApplication; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/InteractorModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.interactors.NetworkInteractor; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; /** * Created by Patryk on 7/31/2015. */ @Module public class InteractorModule { @Provides NetworkInteractor providesNetworkInteractor(Retrofit retrofit, TweeterApi api){ return new NetworkInteractor(retrofit, api); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/LocalModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import android.app.Application; import android.content.Context; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ApplicationScope; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; @Module public class LocalModule { @ApplicationScope @Provides protected LocalDataCache providesDataCache(Application application){ return new LocalDataCache(application); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/NetworkModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import javax.inject.Named; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ApplicationScope; import io.patrykpoborca.cleanarchitecture.network.base.OKHttp; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Scheduler; @Module public class NetworkModule { @ApplicationScope @Provides protected OKHttp providesOkHTTP(){ return new OKHttp(); } @ApplicationScope @Provides protected Retrofit providesRetrofit(OKHttp okHttp, @Named(Constants.MAIN_THREAD) Scheduler mainThread) { return new Retrofit(okHttp, mainThread); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/PresenterModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.dagger.scopes.ActivityScope; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVP.TweeterMVPPresenterImpl; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPresenter; @Module public class PresenterModule { @Provides @ActivityScope TweeterMVPPresenter providesMainPresenter(TweeterApi api, Retrofit retrofit){ return new TweeterMVPPresenterImpl(api, retrofit); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/modules/ThreadingModule.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.modules; import javax.inject.Named; import dagger.Module; import dagger.Provides; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Scheduler; import rx.android.schedulers.AndroidSchedulers; @Module public class ThreadingModule { @Named(Constants.MAIN_THREAD) @Provides public Scheduler providesMainThread(){ return AndroidSchedulers.mainThread(); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/scopes/ActivityScope.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.scopes; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; @Retention(RetentionPolicy.RUNTIME) @Scope public @interface ActivityScope { } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/scopes/ApplicationScope.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.scopes; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; @Retention(RetentionPolicy.RUNTIME) @Scope public @interface ApplicationScope { } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/dagger/scopes/ExposedAPIScope.java ================================================ package io.patrykpoborca.cleanarchitecture.dagger.scopes; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ExposedAPIScope { } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/localdata/LocalDataCache.java ================================================ package io.patrykpoborca.cleanarchitecture.localdata; import android.content.Context; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import rx.Observable; public class LocalDataCache { //pretend this is some read/write to disk :) protected static ArrayList sPastTweets; private Context context; public LocalDataCache(Context context) { this.context = context; sPastTweets = new ArrayList<>(); } public void saveTweet(String tweet) { sPastTweets.add(tweet); } public Observable> fetchRecentTweets() { return Observable.just(sPastTweets) .map(arrayList -> { List list = arrayList; return list; }) .delay(2, TimeUnit.SECONDS); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/network/TweeterApi.java ================================================ package io.patrykpoborca.cleanarchitecture.network; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; import io.patrykpoborca.cleanarchitecture.localdata.LocalDataCache; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Observable; import rx.Scheduler; public class TweeterApi { protected final Scheduler mainScheduler; Retrofit retrofit; LocalDataCache localDataCache; private UserProfile userName; @Inject public TweeterApi(Retrofit retro, LocalDataCache cache, @Named(Constants.MAIN_THREAD) Scheduler mainScheduler) { this.localDataCache = cache; this.retrofit = retro; this.mainScheduler = mainScheduler; } public Observable getTweet(){ return retrofit.completeRequest() .map(tweet -> { localDataCache.saveTweet(tweet); if (isLoggedIn()) { tweet = userName.getUserName() + " -> " + tweet; } else { tweet = "Some user -> " + tweet; } return tweet; }) .observeOn(mainScheduler); } public Observable> fetchXrecents(int count){ return localDataCache.fetchRecentTweets() .map(list ->{ List tweets = new ArrayList<>(count); int size = list.size() <= count ? list.size() : count; for(int i=list.size() -1; i >= 0 && size > tweets.size(); i--){ tweets.add(list.get(i)); } return tweets; }) .observeOn(mainScheduler); } public Observable login(String username, String password) { Observable observable = Observable.just(new UserProfile(username, password)) .delay(2, TimeUnit.SECONDS) .observeOn(mainScheduler); observable.subscribe(user -> this.userName = user); return observable; } public Observable logout(){ userName = null; return Observable.just(null) .delay(2, TimeUnit.SECONDS) .observeOn(mainScheduler); } public boolean isLoggedIn(){ return this.userName != null; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/network/base/OKHttp.java ================================================ package io.patrykpoborca.cleanarchitecture.network.base; import java.util.UUID; public class OKHttp { public String rawResponse(){ return UUID.randomUUID().toString(); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/network/base/Retrofit.java ================================================ package io.patrykpoborca.cleanarchitecture.network.base; import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.android.schedulers.AndroidSchedulers; public class Retrofit { protected OKHttp okHttp; protected final Scheduler mainScheduler; public Retrofit(OKHttp okHttp, Scheduler mainScheduler) { this.okHttp = okHttp; this.mainScheduler = mainScheduler; } public Observable completeRequest(){ return Observable.just(okHttp.rawResponse() + " Some Parsing Done") .delay(2, TimeUnit.SECONDS); } public Observable fetchSomePage(String url){ return Observable.just("

" + "Fake response from fake retrofit: " + url + "

") .delay(2, TimeUnit.SECONDS) .observeOn(mainScheduler); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/BaseCleanArchitectureActivity.java ================================================ package io.patrykpoborca.cleanarchitecture.ui; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import java.util.ArrayList; import java.util.List; import rx.Subscription; public class BaseCleanArchitectureActivity extends AppCompatActivity{ private List subscriptions; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(subscriptions == null){ subscriptions = new ArrayList<>(); } } protected void registerSubscription(Subscription subscription){ subscriptions.add(subscription); } @Override protected void onStop() { super.onStop(); unsubscribeSubscriptions(); } protected void unsubscribeSubscriptions(){ for(int i= 0; i < subscriptions.size(); i++){ subscriptions.get(i).unsubscribe(); } } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/TweeterActivityMVP.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.text.Html; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import java.util.List; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import io.patrykpoborca.cleanarchitecture.CleanArchitectureApplication; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerActivityInjectorComponent; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.BasePresenterActivity; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPView; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPresenter; import io.patrykpoborca.cleanarchitecture.util.Utility; public class TweeterActivityMVP extends BasePresenterActivity implements TweeterMVPPView { @Inject TweeterMVPPresenter presenter; @Bind(R.id.fetch_tweet_button) Button fetchTweetButton; @Bind(R.id.fetch_last_two_tweets) Button fetchLastTwoButton; @Bind(R.id.current_tweet) TextView currentTweetTextView; @Bind(R.id.past_tweets_container) LinearLayout pastTweetContainer; @Bind(R.id.user_login_button) Button loginButton; @Bind(R.id.user_name) TextView userNameTextView; @Bind(R.id.user_password) TextView userPasswordTextView; @Bind(R.id.container) ViewGroup container; @Bind(R.id.some_url) EditText urlText; @Bind(R.id.webpage_text) TextView websiteText; @Bind(R.id.request_website_button) Button websiteFetchbutton; @Bind(R.id.help_history) View helpHistory; @Bind(R.id.help_login) View helpLogin; @Bind(R.id.help_url) View helpUrl; private View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if(view == fetchLastTwoButton){ getPresenter().fetchPreviousTweets(); } else if(view == fetchTweetButton){ getPresenter().fetchCurrentTweet(); } else if(view == loginButton){ getPresenter().toggleLogin(userNameTextView.getText().toString(), userPasswordTextView.getText().toString()); } else if(view == websiteFetchbutton){ getPresenter().loadWebPage(urlText.getText().toString()); } } }; private final View.OnClickListener dialogClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if(view == helpHistory){ new AlertDialog.Builder(TweeterActivityMVP.this) .setMessage(R.string.history_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpUrl){ new AlertDialog.Builder(TweeterActivityMVP.this) .setMessage(R.string.url_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpLogin){ new AlertDialog.Builder(TweeterActivityMVP.this) .setMessage(R.string.login_text) .setPositiveButton("Ok", null) .create() .show(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); this.fetchLastTwoButton.setOnClickListener(onClickListener); this.fetchTweetButton.setOnClickListener(onClickListener); this.loginButton.setOnClickListener(onClickListener); this.websiteFetchbutton.setOnClickListener(onClickListener); this.helpHistory.setOnClickListener(dialogClickListener); this.helpLogin.setOnClickListener(dialogClickListener); this.helpUrl.setOnClickListener(dialogClickListener); setTitle("MVP Activity IMPL"); } @Override protected TweeterMVPPresenter getPresenter() { if(presenter == null){ DaggerActivityInjectorComponent.builder() .baseComponent(CleanArchitectureApplication.getBaseComponent()) .build() .inject(this); } return presenter; } @Override public void displayFetchedTweet(String tweet) { currentTweetTextView.setText(tweet); } @Override public void displayPreviousTweets(List tweets) { pastTweetContainer.removeAllViews(); //clear container... for(int i= 0; i < tweets.size(); i++){ TextView text = new TextView(this); text.setText(tweets.get(i)); pastTweetContainer.addView(text); } } @Override protected void registerViewToPresenter() { getPresenter().registerView(this); } @Override public void displayToast(String toast) { Toast.makeText(this, toast, Toast.LENGTH_LONG).show(); } @Override public void toggleProgressBar(boolean loading) { Utility.toggleProgressbar(this, loading); } @Override public void setUserButtonText(String text) { this.loginButton.setText(text); } @Override public void toggleLoginContainer(boolean b) { container.setVisibility(b ? View.VISIBLE : View.GONE); } @Override public void displayWebpage(String html) { websiteText.setText(Html.fromHtml(html)); } public static Intent newInstance(Context context) { return new Intent(context, TweeterActivityMVP.class); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/TweeterMVPPresenterImpl.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPView; import io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces.TweeterMVPPresenter; /** * Created by Patryk on 7/28/2015. */ public class TweeterMVPPresenterImpl implements TweeterMVPPresenter { private static final int TWEET_COUNT = 2; private final TweeterApi tweeterApi; private final Retrofit retrofit; private TweeterMVPPView mainMVPView; private int tweetsAdded = 0; public TweeterMVPPresenterImpl(TweeterApi tweeterApi, Retrofit retrofit) { this.tweeterApi = tweeterApi; this.retrofit = retrofit; } @Override public void registerView(TweeterMVPPView activity) { this.mainMVPView = activity; } @Override public void onAttach() { } @Override public void onDetach() { } @Override public void fetchCurrentTweet() { mainMVPView.toggleProgressBar(true); tweeterApi.getTweet().subscribe(s -> { mainMVPView.toggleProgressBar(false); tweetsAdded ++; if(tweetsAdded > TWEET_COUNT){ mainMVPView.displayToast("Tweet size exceeded " + TWEET_COUNT); } this.mainMVPView.displayFetchedTweet(s); }); } @Override public void fetchPreviousTweets() { mainMVPView.toggleProgressBar(true); tweeterApi.fetchXrecents(TWEET_COUNT) .subscribe(l -> { mainMVPView.displayPreviousTweets(l); mainMVPView.toggleProgressBar(false); }); } @Override public void toggleLogin(String userName, String userPassword) { mainMVPView.toggleProgressBar(true); if(tweeterApi.isLoggedIn()){ tweeterApi.logout() .subscribe(s -> { mainMVPView.toggleProgressBar(false); mainMVPView.setUserButtonText("Login"); mainMVPView.displayToast("User logged out"); mainMVPView.toggleLoginContainer(true); //could implement more literal less reusable methods, such as loggedIn and loggedOut such as in the MVPCI example. //However I wanted to be extremely verbose in the MVP example. }); } else { this.tweeterApi.login(userName, userPassword) .subscribe(userProfile -> { mainMVPView.displayToast(userProfile.getFormattedCredentials() + " Logged in"); mainMVPView.setUserButtonText("Log " + userProfile.getUserName() + " out"); mainMVPView.toggleProgressBar(false); mainMVPView.toggleLoginContainer(false); }); } } @Override public void loadWebPage(String url) { mainMVPView.toggleProgressBar(true); retrofit.fetchSomePage(url) .subscribe(s -> { mainMVPView.displayWebpage(s); mainMVPView.toggleProgressBar(false); }); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/base/BasePresenterActivity.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP.base; import android.os.Bundle; import io.patrykpoborca.cleanarchitecture.ui.BaseCleanArchitectureActivity; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.PView; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.Presenter; /** * Created by Patryk on 7/28/2015. */ public abstract class BasePresenterActivity extends BaseCleanArchitectureActivity implements PView { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPresenter().registerView(this); } @Override protected void onResume() { super.onResume(); getPresenter().onAttach(); } @Override protected void onPause() { super.onPause(); getPresenter().onDetach(); } protected abstract void registerViewToPresenter(); protected abstract T getPresenter(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/base/Interfaces/PView.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces; /** * Created by Patryk on 7/28/2015. */ public interface PView { public void toggleProgressBar(boolean loading); public void displayToast(String toast); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/base/Interfaces/Presenter.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces; /** * Created by Patryk on 7/28/2015. */ public interface Presenter { public void registerView(T activity); public void onAttach(); public void onDetach(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/interfaces/TweeterMVPPView.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces; import java.util.List; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.PView; /** * Created by Patryk on 7/28/2015. */ public interface TweeterMVPPView extends PView { public void displayFetchedTweet(String tweet); public void displayPreviousTweets(List list); public void setUserButtonText(String text); void toggleLoginContainer(boolean b); void displayWebpage(String html); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVP/interfaces/TweeterMVPPresenter.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVP.interfaces; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.Presenter; /** * Created by Patryk on 7/28/2015. */ public interface TweeterMVPPresenter extends Presenter { public void fetchCurrentTweet(); public void fetchPreviousTweets(); public void toggleLogin(String userName, String userPassword); void loadWebPage(String url); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/TweeterActivityMVPCI.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.text.Html; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import java.util.List; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import io.patrykpoborca.cleanarchitecture.CleanArchitectureApplication; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerActivityInjectorComponent; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.base.BasePresenterActivityMVPCI; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.interfaces.TweeterActivityMVPCIPview; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.util.Utility; /** * Presenter as a Supervising controller is actually a bridge between observable models and the view, any complex operations are performed within the controller, basic databinding * circumvents a lot of unecessary boilerplate. * Model <- Presenter -> View */ public class TweeterActivityMVPCI extends BasePresenterActivityMVPCI implements TweeterActivityMVPCIPview { @Bind(R.id.fetch_tweet_button) Button fetchTweetButton; @Bind(R.id.fetch_last_two_tweets) Button fetchLastTwoButton; @Bind(R.id.current_tweet) TextView currentTweetTextView; @Bind(R.id.past_tweets_container) LinearLayout pastTweetContainer; @Bind(R.id.user_login_button) Button loginButton; @Bind(R.id.user_name) TextView userNameTextView; @Bind(R.id.user_password) TextView userPasswordTextView; @Bind(R.id.container) ViewGroup container; @Bind(R.id.some_url) EditText urlText; @Bind(R.id.webpage_text) TextView websiteText; @Bind(R.id.request_website_button) Button websiteFetchbutton; @Bind(R.id.help_history) View helpHistory; @Bind(R.id.help_login) View helpLogin; @Bind(R.id.help_url) View helpUrl; @Inject TweeterMVPCIPresenter presenter; private final View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if(view == fetchTweetButton){ registerSubscription( getPresenter().fetchCurrentTweet().subscribe(s -> currentTweetTextView.setText(s))); } else if(view == fetchLastTwoButton){ pastTweetContainer.removeAllViews(); //clear container... registerSubscription( getPresenter().fetchPreviousTweets().subscribe(TweeterActivityMVPCI.this::displayTweets) ); } else if(view == loginButton){ getPresenter().toggleLogin(userNameTextView.getText().toString(), userPasswordTextView.getText().toString()); } else if(view == websiteFetchbutton){ registerSubscription(getPresenter().loadWebPage(urlText.getText().toString()) .subscribe(s -> websiteText.setText(Html.fromHtml(s)))); } } }; private final View.OnClickListener dialogClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if(view == helpHistory){ new AlertDialog.Builder(TweeterActivityMVPCI.this) .setMessage(R.string.history_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpUrl){ new AlertDialog.Builder(TweeterActivityMVPCI.this) .setMessage(R.string.url_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpLogin){ new AlertDialog.Builder(TweeterActivityMVPCI.this) .setMessage(R.string.login_text) .setPositiveButton("Ok", null) .create() .show(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); this.fetchLastTwoButton.setOnClickListener(onClickListener); this.fetchTweetButton.setOnClickListener(onClickListener); this.loginButton.setOnClickListener(onClickListener); this.websiteFetchbutton.setOnClickListener(onClickListener); this.helpHistory.setOnClickListener(dialogClickListener); this.helpLogin.setOnClickListener(dialogClickListener); this.helpUrl.setOnClickListener(dialogClickListener); setTitle("MVPCI activity"); } @Override protected TweeterMVPCIPresenter getPresenter() { if(presenter == null){ DaggerActivityInjectorComponent.builder() .baseComponent(CleanArchitectureApplication.getBaseComponent()) .build() .inject(this); } return presenter; } @Override public void displayToast(String toast) { Toast.makeText(this, toast, Toast.LENGTH_LONG).show(); } private void displayTweets(List list) { pastTweetContainer.removeAllViews(); for(int i= 0; i < list.size(); i++){ TextView textView = new TextView(this); textView.setText(list.get(i)); pastTweetContainer.addView(textView); } } @Override public void toggleProgressBar(boolean show) { Utility.toggleProgressbar(this, show); } @Override public void loggedIn(UserProfile profile) { Toast.makeText(this, (profile.getFormattedCredentials() + " Logged in"), Toast.LENGTH_SHORT).show(); loginButton.setText("Log " + profile.getUserName() + " out"); container.setVisibility(View.GONE); } @Override public void loggedOut() { loginButton.setText(R.string.log_user_in); container.setVisibility(View.VISIBLE); } public static Intent newInstance(Context context) { return new Intent(context, TweeterActivityMVPCI.class); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/TweeterMVPCIPresenter.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import io.patrykpoborca.cleanarchitecture.dagger.interactors.NetworkInteractor; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.base.BasePresenterMVPCI; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.interfaces.TweeterActivityMVPCIPview; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.util.Constants; import rx.Observable; import rx.Scheduler; /** * Created by Patryk on 7/29/2015. */ public class TweeterMVPCIPresenter extends BasePresenterMVPCI { private static final int TWEET_COUNT = 2; private final NetworkInteractor interactor; private final Scheduler mainScheduler; private boolean loggedIn = false; private int tweetsAdded = 0; @Inject public TweeterMVPCIPresenter(NetworkInteractor interactor, @Named(Constants.MAIN_THREAD) Scheduler mainScheduler) { this.interactor = interactor; this.mainScheduler = mainScheduler; } @Override public void onAttach() { super.onAttach(); } @Override public void onDettach() { super.onDettach(); } public Observable fetchCurrentTweet() { getPView().toggleProgressBar(true); Observable observable = interactor.fetchTweet(); observable.subscribe(s -> { getPView().toggleProgressBar(false); tweetsAdded++; if (tweetsAdded > TWEET_COUNT) { getPView().displayToast("Tweet size exceeded " + TWEET_COUNT); } }); return observable; } public Observable> fetchPreviousTweets() { getPView().toggleProgressBar(true); Observable> observable = interactor.fetchTweets(TWEET_COUNT); observable.subscribe(l -> { getPView().toggleProgressBar(false); }); return observable; } public void toggleLogin(String userName, String password){ getPView().toggleProgressBar(true); if(!loggedIn) { loggedIn = true; Observable observable = interactor.attemptLogin(userName, password); observable.subscribe(p-> { getPView().toggleProgressBar(false); getPView().loggedIn(p); }); } else{ UserProfile profile = null; loggedIn = false; //avoid relying on timer task/handler interactor.logout() .observeOn(mainScheduler) .subscribe(s -> { getPView().loggedOut(); getPView().toggleProgressBar(false); }); } } public Observable loadWebPage(String url){ getPView().toggleProgressBar(true); Observable observable = interactor.loadWebpage(url); observable.subscribe(s -> getPView().toggleProgressBar(false)); return observable; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/base/BasePresenterActivityMVPCI.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI.base; import android.os.Bundle; import io.patrykpoborca.cleanarchitecture.ui.BaseCleanArchitectureActivity; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.PView; public abstract class BasePresenterActivityMVPCI extends BaseCleanArchitectureActivity implements PView{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPresenter().registerPresenter(this); } @Override protected void onResume() { super.onResume(); getPresenter().onAttach(); } @Override protected void onPause() { super.onPause(); getPresenter().onDettach(); } protected abstract T getPresenter(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/base/BasePresenterMVPCI.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI.base; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.PView; public class BasePresenterMVPCI { private T pView; public void onAttach(){ } public void onDettach(){ } public void registerPresenter(T view){ this.pView = view; } protected T getPView(){ return this.pView; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/interfaces/TweeterActivityMVPCIPview.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI.interfaces; import io.patrykpoborca.cleanarchitecture.ui.MVP.base.Interfaces.PView; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; /** * Created by Patryk on 7/29/2015. */ public interface TweeterActivityMVPCIPview extends PView{ public void loggedIn(UserProfile profile); public void loggedOut(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVPCI/models/UserProfile.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVPCI.models; /** * Created by Patryk on 7/29/2015. */ public class UserProfile { private final String password; private final String username; public UserProfile(String username, String password) { this.username = username; this.password =password; } public String getFormattedCredentials(){ return "User Profile = " + username + " " + password; } public String getUserName() { return username; } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVVM/MainViewmodel.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVVM; import java.util.List; import javax.inject.Inject; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.ui.MVVM.base.BaseViewModel; import rx.Observable; import rx.subjects.PublishSubject; public class MainViewModel extends BaseViewModel { private static final int TWEET_COUNT = 2; private final TweeterApi tweeterApi; private final Retrofit retroFit; private boolean loggedIn = false; private int tweetsAdded = 0; private PublishSubject messageStream; @Inject public MainViewModel(TweeterApi api, Retrofit retrofit){ this.tweeterApi = api; this.retroFit = retrofit; } public Observable fetchCurrentTweet(){ tweetsAdded ++; Observable observable= tweeterApi.getTweet(); if(tweetsAdded > TWEET_COUNT){ observable.subscribe(s -> messageStream.onNext("Tweet size exceeded " + TWEET_COUNT)); } return observable; } public Observable> fetchPreviousTweets(){ return tweeterApi.fetchXrecents(2); } public boolean isLoggedIn(){ return loggedIn; } public Observable toggleLogin(String userName, String password){ loggedIn = !loggedIn; if(loggedIn) { return this.tweeterApi.login(userName, password); } else{ UserProfile profile = null; return tweeterApi.logout() .map(o -> profile); } } public Observable getMessageStream() { if(messageStream == null){ messageStream = PublishSubject.create(); } return messageStream.asObservable(); } public Observable loadWebPage(String url){ return retroFit.fetchSomePage(url); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVVM/TweeterActivityMVVM.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVVM; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.text.Html; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import java.util.List; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import io.patrykpoborca.cleanarchitecture.CleanArchitectureApplication; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerActivityInjectorComponent; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.ui.MVVM.base.BaseViewModelActivity; import io.patrykpoborca.cleanarchitecture.util.Utility; public class TweeterActivityMVVM extends BaseViewModelActivity { @Bind(R.id.fetch_tweet_button) Button fetchTweetButton; @Bind(R.id.fetch_last_two_tweets) Button fetchLastTwoButton; @Bind(R.id.current_tweet) TextView currentTweetTextView; @Bind(R.id.past_tweets_container) LinearLayout pastTweetContainer; @Bind(R.id.user_login_button) Button loginButton; @Bind(R.id.user_name) TextView userNameTextView; @Bind(R.id.user_password) TextView userPasswordTextView; @Bind(R.id.container) ViewGroup container; @Bind(R.id.some_url) EditText urlText; @Bind(R.id.webpage_text) TextView websiteText; @Bind(R.id.request_website_button) Button websiteFetchbutton; @Bind(R.id.help_history) View helpHistory; @Bind(R.id.help_login) View helpLogin; @Bind(R.id.help_url) View helpUrl; @Inject MainViewModel viewModel; private View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { Utility.toggleProgressbar(TweeterActivityMVVM.this, true); if(view == loginButton){ registerSubscription( getViewModel().toggleLogin(userNameTextView.getText().toString(), userPasswordTextView.getText().toString()) .subscribe(TweeterActivityMVVM.this::toggleLogin) ); } else if(view == fetchLastTwoButton){ registerSubscription( getViewModel().fetchPreviousTweets() .subscribe(TweeterActivityMVVM.this::displayTweets) ); } else if(view == fetchTweetButton){ registerSubscription( getViewModel().fetchCurrentTweet() .subscribe(tweet -> { currentTweetTextView.setText(tweet); Utility.toggleProgressbar(TweeterActivityMVVM.this, false); }) ); } else if(view == websiteFetchbutton){ registerSubscription( getViewModel().loadWebPage(urlText.getText().toString()) .subscribe(s -> { websiteText.setText(Html.fromHtml(s)); Utility.toggleProgressbar(TweeterActivityMVVM.this, false); }) ); } } }; private final View.OnClickListener dialogClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if(view == helpHistory){ new AlertDialog.Builder(TweeterActivityMVVM.this) .setMessage(R.string.history_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpUrl){ new AlertDialog.Builder(TweeterActivityMVVM.this) .setMessage(R.string.url_text) .setPositiveButton("Ok", null) .create() .show(); } else if(view == helpLogin){ new AlertDialog.Builder(TweeterActivityMVVM.this) .setMessage(R.string.login_text) .setPositiveButton("Ok", null) .create() .show(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); this.fetchLastTwoButton.setOnClickListener(onClickListener); this.fetchTweetButton.setOnClickListener(onClickListener); this.loginButton.setOnClickListener(onClickListener); this.websiteFetchbutton.setOnClickListener(onClickListener); this.helpHistory.setOnClickListener(dialogClickListener); this.helpLogin.setOnClickListener(dialogClickListener); this.helpUrl.setOnClickListener(dialogClickListener); setTitle("Tweeteractivity MVVM Impl"); } @Override protected MainViewModel getViewModel() { if(viewModel == null){ DaggerActivityInjectorComponent.builder() .baseComponent(CleanArchitectureApplication.getBaseComponent()) .build() .inject(this); } return viewModel; } @Override protected void onResume() { super.onResume(); registerSubscription(getViewModel().getMessageStream() .subscribe(s -> Toast.makeText(this, s, Toast.LENGTH_LONG).show())); } private void displayTweets(List list) { pastTweetContainer.removeAllViews(); Utility.toggleProgressbar(this, false); for(int i= 0; i < list.size(); i++){ TextView textView = new TextView(this); textView.setText(list.get(i)); pastTweetContainer.addView(textView); } } private void toggleLogin(UserProfile profile){ Utility.toggleProgressbar(this, false); //unlike the variations of MVP, the if(getViewModel().isLoggedIn()) { Toast.makeText(this, (profile.getFormattedCredentials() + " Logged in"), Toast.LENGTH_SHORT).show(); loginButton.setText("Log " + profile.getUserName() + " out"); container.setVisibility(View.GONE); } else{ loginButton.setText(R.string.log_user_in); container.setVisibility(View.VISIBLE); } } public static Intent newInstance(Context context) { return new Intent(context, TweeterActivityMVVM.class); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVVM/base/BaseViewModel.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVVM.base; public class BaseViewModel { public void onAttach(){ } public void onDettach(){ } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/MVVM/base/BaseViewModelActivity.java ================================================ package io.patrykpoborca.cleanarchitecture.ui.MVVM.base; import android.os.Bundle; import io.patrykpoborca.cleanarchitecture.ui.BaseCleanArchitectureActivity; public abstract class BaseViewModelActivity extends BaseCleanArchitectureActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getViewModel(); } @Override protected void onResume() { super.onResume(); getViewModel().onAttach(); } @Override protected void onPause() { super.onPause(); getViewModel().onDettach(); } protected abstract T getViewModel(); } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/PlainTweeterActivity.java ================================================ package io.patrykpoborca.cleanarchitecture.ui; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.text.Html; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import java.util.List; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import io.patrykpoborca.cleanarchitecture.CleanArchitectureApplication; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.dagger.components.DaggerActivityInjectorComponent; import io.patrykpoborca.cleanarchitecture.network.TweeterApi; import io.patrykpoborca.cleanarchitecture.network.base.Retrofit; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.models.UserProfile; import io.patrykpoborca.cleanarchitecture.util.Utility; public class PlainTweeterActivity extends BaseCleanArchitectureActivity { private static final int TWEET_COUNT = 2; @Inject TweeterApi tweeterApi; @Inject Retrofit retrofit; @Bind(R.id.fetch_tweet_button) Button fetchTweetButton; @Bind(R.id.fetch_last_two_tweets) Button fetchLastTwoButton; @Bind(R.id.current_tweet) TextView currentTweetTextView; @Bind(R.id.past_tweets_container) LinearLayout pastTweetContainer; @Bind(R.id.user_login_button) Button loginButton; @Bind(R.id.user_name) TextView userNameTextView; @Bind(R.id.user_password) TextView userPasswordTextView; @Bind(R.id.container) ViewGroup container; @Bind(R.id.some_url) EditText urlText; @Bind(R.id.webpage_text) TextView websiteText; @Bind(R.id.request_website_button) Button websiteFetchbutton; @Bind(R.id.help_history) View helpHistory; @Bind(R.id.help_login) View helpLogin; @Bind(R.id.help_url) View helpUrl; private int tweetsAdded = 0; private final View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { Utility.toggleProgressbar(PlainTweeterActivity.this, true); if (view == fetchLastTwoButton) { registerSubscription( tweeterApi.fetchXrecents(2) .subscribe(PlainTweeterActivity.this::displayPreviousTweets) ); } else if (view == fetchTweetButton) { registerSubscription( tweeterApi.getTweet() .subscribe(s -> { currentTweetTextView.setText(s); tweetsAdded++; if (tweetsAdded > TWEET_COUNT) { Toast.makeText(PlainTweeterActivity.this, "Tweet size exceeded " + TWEET_COUNT, Toast.LENGTH_LONG).show(); } Utility.toggleProgressbar(PlainTweeterActivity.this, false); }) ); } else if (view == loginButton) { if (tweeterApi.isLoggedIn()) { registerSubscription(tweeterApi.logout() .subscribe(s -> { container.setVisibility(View.VISIBLE); loginButton.setText(R.string.log_user_in); Utility.toggleProgressbar(PlainTweeterActivity.this, false); })); } else { registerSubscription( tweeterApi.login( userNameTextView.getText().toString(), userPasswordTextView.getText().toString()) .subscribe(PlainTweeterActivity.this::userLogin) ); } } else if (view == websiteFetchbutton) { retrofit.fetchSomePage(urlText.getText().toString()) .subscribe(s -> { websiteText.setText(Html.fromHtml(s)); Utility.toggleProgressbar(PlainTweeterActivity.this, false); }); } } }; private final View.OnClickListener dialogClickListener = new View.OnClickListener() { @Override public void onClick(View view) { if (view == helpHistory) { new AlertDialog.Builder(PlainTweeterActivity.this) .setMessage(R.string.history_text) .setPositiveButton("Ok", null) .create() .show(); } else if (view == helpUrl) { new AlertDialog.Builder(PlainTweeterActivity.this) .setMessage(R.string.url_text) .setPositiveButton("Ok", null) .create() .show(); } else if (view == helpLogin) { new AlertDialog.Builder(PlainTweeterActivity.this) .setMessage(R.string.login_text) .setPositiveButton("Ok", null) .create() .show(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DaggerActivityInjectorComponent.builder() .baseComponent(CleanArchitectureApplication.getBaseComponent()) .build() .inject(this); ButterKnife.bind(this); setTitle("Plain Activity IMPL"); this.fetchLastTwoButton.setOnClickListener(onClickListener); this.fetchTweetButton.setOnClickListener(onClickListener); this.loginButton.setOnClickListener(onClickListener); this.websiteFetchbutton.setOnClickListener(onClickListener); this.helpHistory.setOnClickListener(dialogClickListener); this.helpLogin.setOnClickListener(dialogClickListener); this.helpUrl.setOnClickListener(dialogClickListener); } public void displayPreviousTweets(List tweets) { pastTweetContainer.removeAllViews(); //clear container... Utility.toggleProgressbar(PlainTweeterActivity.this, false); for (int i = 0; i < tweets.size(); i++) { TextView text = new TextView(this); text.setText(tweets.get(i)); pastTweetContainer.addView(text); } } public void userLogin(UserProfile profile) { Utility.toggleProgressbar(this, false); container.setVisibility(View.GONE); loginButton.setText("Log " + profile.getUserName() + " out"); } public static Intent newInstance(Context context) { return new Intent(context, PlainTweeterActivity.class); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/ui/RouterActivity.java ================================================ package io.patrykpoborca.cleanarchitecture.ui; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Toast; import butterknife.Bind; import butterknife.ButterKnife; import io.patrykpoborca.cleanarchitecture.R; import io.patrykpoborca.cleanarchitecture.ui.MVP.TweeterActivityMVP; import io.patrykpoborca.cleanarchitecture.ui.MVPCI.TweeterActivityMVPCI; import io.patrykpoborca.cleanarchitecture.ui.MVVM.TweeterActivityMVVM; /** * Created by Patryk on 7/28/2015. */ public class RouterActivity extends AppCompatActivity { @Bind(R.id.stupid_activity) View stupidActivity; @Bind(R.id.mvp_activity) View mvpActivity; @Bind(R.id.mvpci_activity) View mvpciActivity; @Bind(R.id.mvvm_activity) View mvvmActivity; private View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = null; if(view == stupidActivity){ intent = PlainTweeterActivity.newInstance(RouterActivity.this); } else if(view == mvpciActivity){ intent = TweeterActivityMVPCI.newInstance(RouterActivity.this); } else if(view == mvvmActivity) { intent = TweeterActivityMVVM.newInstance(RouterActivity.this); } else if(view == mvpActivity){ intent = TweeterActivityMVP.newInstance(RouterActivity.this); } startActivity(intent); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_router); ButterKnife.bind(this); mvvmActivity.setOnClickListener(onClickListener); mvpciActivity.setOnClickListener(onClickListener); mvpActivity.setOnClickListener(onClickListener); stupidActivity.setOnClickListener(onClickListener); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/util/Constants.java ================================================ package io.patrykpoborca.cleanarchitecture.util; public class Constants { public static final String MAIN_THREAD = "MAIN_THREAD"; } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/util/LoadingFragment.java ================================================ package io.patrykpoborca.cleanarchitecture.util; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import io.patrykpoborca.cleanarchitecture.R; public class LoadingFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_progress, container, false); } } ================================================ FILE: app/src/main/java/io/patrykpoborca/cleanarchitecture/util/utility.java ================================================ package io.patrykpoborca.cleanarchitecture.util; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.util.Log; import rx.Observable; public class Utility { private static final String PROGRESS_TAG = "PROGRESS"; public static void toggleProgressbar(AppCompatActivity activity, boolean show) { try { Fragment fragment = activity.getSupportFragmentManager() .findFragmentByTag(PROGRESS_TAG); if (show && fragment == null) { activity.getSupportFragmentManager().beginTransaction() .add(android.R.id.content, new LoadingFragment(), PROGRESS_TAG) .commit(); } else if (!show && fragment != null) { activity.getSupportFragmentManager().beginTransaction() .remove(fragment) .commit(); } } catch (Exception e) { Log.e(Utility.class.getSimpleName(), "", e); } } public static void toggleProgressbar(AppCompatActivity activity, Observable observable) { toggleProgressbar(activity, true); observable.doOnCompleted(() -> toggleProgressbar(activity, false)); } } ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================