├── .gitignore
├── .idea
├── caches
│ └── build_file_checksums.ser
├── codeStyles
│ └── Project.xml
├── gradle.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTestMock
│ └── java
│ │ └── com
│ │ └── codemate
│ │ └── blogreader
│ │ ├── RecyclerViewMatcher.java
│ │ ├── data
│ │ └── cache
│ │ │ ├── SharedPrefCommentCacheTest.java
│ │ │ └── SharedPrefsPostCacheTest.java
│ │ └── presentation
│ │ ├── postdetails
│ │ └── PostDetailsScreenTest.java
│ │ └── posts
│ │ └── PostsScreenTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── codemate
│ │ │ └── blogreader
│ │ │ ├── MVPApplication.java
│ │ │ ├── data
│ │ │ ├── cache
│ │ │ │ ├── SharedPrefCache.java
│ │ │ │ ├── SharedPrefCommentCache.java
│ │ │ │ ├── SharedPrefPostCache.java
│ │ │ │ └── converter
│ │ │ │ │ ├── CommentJsonConverter.java
│ │ │ │ │ └── PostJsonConverter.java
│ │ │ └── network
│ │ │ │ └── BlogService.java
│ │ │ ├── domain
│ │ │ ├── cache
│ │ │ │ ├── CommentCache.java
│ │ │ │ └── PostCache.java
│ │ │ ├── interactors
│ │ │ │ ├── BaseInteractor.java
│ │ │ │ ├── GetPostCommentsInteractor.java
│ │ │ │ └── GetPostsInteractor.java
│ │ │ └── model
│ │ │ │ ├── BlogPost.java
│ │ │ │ └── Comment.java
│ │ │ ├── injection
│ │ │ └── AppComponent.java
│ │ │ ├── presentation
│ │ │ ├── anim
│ │ │ │ └── ListItemAnimator.java
│ │ │ ├── postdetails
│ │ │ │ ├── CommentsAdapter.java
│ │ │ │ ├── PostDetailActivity.java
│ │ │ │ ├── PostDetailPresenter.java
│ │ │ │ └── PostDetailView.java
│ │ │ ├── posts
│ │ │ │ ├── PostsActivity.java
│ │ │ │ ├── PostsAdapter.java
│ │ │ │ ├── PostsPresenter.java
│ │ │ │ └── PostsView.java
│ │ │ └── view
│ │ │ │ └── ErrorViewLayout.java
│ │ │ └── util
│ │ │ ├── IntentFactory.java
│ │ │ └── ScreenUtils.java
│ └── res
│ │ ├── drawable-hdpi
│ │ └── ic_alert_error.png
│ │ ├── drawable-mdpi
│ │ └── ic_alert_error.png
│ │ ├── drawable-xhdpi
│ │ └── ic_alert_error.png
│ │ ├── drawable-xxhdpi
│ │ └── ic_alert_error.png
│ │ ├── drawable-xxxhdpi
│ │ └── ic_alert_error.png
│ │ ├── layout
│ │ ├── activity_post_details.xml
│ │ ├── activity_posts.xml
│ │ ├── item_comment.xml
│ │ ├── item_post.xml
│ │ └── view_error.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── mock
│ ├── java
│ │ └── com
│ │ │ └── codemate
│ │ │ └── blogreader
│ │ │ ├── FakeBlogApi.java
│ │ │ └── injection
│ │ │ └── AppModule.java
│ └── res
│ │ └── values
│ │ └── strings.xml
│ ├── prod
│ └── java
│ │ └── com
│ │ └── codemate
│ │ └── blogreader
│ │ └── injection
│ │ └── AppModule.java
│ └── testMock
│ └── java
│ └── com
│ └── codemate
│ └── blogreader
│ ├── data
│ └── cache
│ │ └── converter
│ │ ├── CommentJsonConverterTest.java
│ │ └── PostJsonConverterTest.java
│ ├── presentation
│ ├── postdetails
│ │ ├── PostDetailActivityTest.java
│ │ └── PostDetailPresenterTest.java
│ └── posts
│ │ └── PostsPresenterTest.java
│ └── util
│ └── MockIntentFactory.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android MVP & Clean Architecture sample with Unit and Instrumentation tests
2 |
3 | ## BlogReader App
4 |
5 | This sample app loads and displays blog posts in a list. By clicking a blog post, you are taken to a detail UI, which shows the entire blog post body and comments.
6 |
7 | The app uses a [fake blogpost API](https://jsonplaceholder.typicode.com) to load the posts and comments. All data is cached locally, so no unneeded network requests are being made until the cache becomes invalid.
8 |
9 | ### Maintainability
10 |
11 | The application has **almost 90% test coverage**, so adding / changing features is not scary at all. You can always run the tests after making changes, and if they pass, you can be pretty sure you didn't break anything vital.
12 |
13 | 
14 |
15 | Actually, there's **only about 10 test functions**, and the longest one only has **8 lines of code**. That's not so much, is it?
16 |
17 | ## MVP? Clean? SOLID?
18 |
19 | By following [Model-View-Presenter](https://en.wikipedia.org/wiki/Model–view–presenter), [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) and [SOLID Principles](https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design), this app becomes:
20 | * Easy to test. Business rules can be easily tested without Android emulators / devices in milliseconds. And the UI tests are't that horrible to write either.
21 | * Easy to maintain. Adding / modifying functionality is easy, and doesn't cause a regression bug hell as everything is separated and tested.
22 | * Independent of external third-party libraries. *Cache* can easily be replaced with Realm, without modifying the main application logic. **The application simply doesn't care about what frameworks we're using.**
23 | * Independent of UI. The UI can easily be changed without changing the existing business logic. This could be easily converted to a desktop or even a command line app.
24 |
25 | Everything is nicely decoupled. The application isn't dependent on concrete implementation details. They are more like plugins, *and they actually depend on the application; not the other way around*.
26 |
27 | ## How to run
28 |
29 | The project has two build variants:
30 |
31 |
32 | mock
33 | Instead of loading real data, populates the blog list and comments from a static FakeBlogApi to make testing easy. Also contains all the tests.
34 |
35 | prod
36 | The "real" app that actually loads real blog and comment data from a real API. Does not contain tests, they are in the mock variant.
37 |
38 |
39 | ## Package structure
40 |
41 | ### com.codemate.blogreader.data
42 |
43 | Blog API and Cache implementations. The Blog API is defined in a simple interface that has ```getPosts()``` and ```getComments(int postId)``` methods. The implementation is then automatically generated by the [Retrofit library](http://square.github.io/retrofit/).
44 |
45 | ### com.codemate.blogreader.domain
46 |
47 | Contains *the business rules* (usecases) that are our applications main functionality. No Android or external dependencies here, just plain Java.
48 |
49 | Also contains the *BlogPost* and *Comment* models and the *PostCache* and *CommentCache* interfaces that the **data layer implements** for *SharedPrefCache*.
50 |
51 | ### com.codemate.blogreader.presentation
52 |
53 | All the Android-related UI stuff, such as `Activities` and `RecyclerAdapters`. Also just plain Java `Presenter` objects and `View` interfaces implemented by the `Activities`.
54 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'jacoco'
3 |
4 | android {
5 | compileSdkVersion rootProject.ext.compileSdkVersion
6 | buildToolsVersion '28.0.3'
7 |
8 | defaultConfig {
9 | applicationId "com.codemate.blogreader"
10 | minSdkVersion rootProject.ext.minSdkVersion
11 | targetSdkVersion rootProject.ext.targetSdkVersion
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | debug {
19 | testCoverageEnabled = true
20 | }
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | flavorDimensions "version"
28 |
29 | productFlavors{
30 | mock {
31 | applicationIdSuffix = ".mock"
32 | }
33 | prod {
34 |
35 | }
36 | }
37 |
38 | android.variantFilter { variant ->
39 | if(variant.buildType.name.equals('release')
40 | && variant.getFlavors().get(0).name.equals('mock')) {
41 | variant.setIgnore(true);
42 | }
43 | }
44 |
45 | testOptions {
46 | unitTests.returnDefaultValues = true
47 | }
48 | testOptions.unitTests.all {
49 | testLogging {
50 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
51 | }
52 | }
53 | }
54 |
55 | configurations.all {
56 | resolutionStrategy {
57 | force 'com.android.support:support-annotations:23.1.1'
58 | }
59 | }
60 |
61 | dependencies {
62 | implementation fileTree(dir: 'libs', include: ['*.jar'])
63 |
64 | // test dependencies
65 | testImplementation jUnit
66 | testImplementation mockito
67 | testImplementation hamcrest
68 |
69 | // Android Testing Support Library's runner and rules
70 | androidTestImplementation supportTest.runner
71 | androidTestImplementation supportTest.rules
72 |
73 | // Espresso UI Testing dependencies.
74 | androidTestImplementation (supportTest.espressoCore) {
75 | espressoExcludes.each { exclude module : "$it" }
76 | }
77 |
78 | androidTestImplementation (supportTest.espressoContrib) {
79 | espressoExcludes.each { exclude module : "$it" }
80 | }
81 |
82 | androidTestImplementation (supportTest.espressoIntents) {
83 | espressoExcludes.each { exclude module : "$it" }
84 | }
85 |
86 | // app dependencies
87 | implementation support.appcompat
88 | implementation support.design
89 | implementation support.recyclerview
90 | implementation support.cardview
91 |
92 | annotationProcessor daggerCompiler
93 | implementation dagger
94 | compileOnly jsr250
95 |
96 | implementation retrofit
97 | implementation rxJava
98 | implementation rxAndroid
99 | }
--------------------------------------------------------------------------------
/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 /Users/ironman/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/androidTestMock/java/com/codemate/blogreader/RecyclerViewMatcher.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader;
2 |
3 | import android.content.res.Resources;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 |
7 | import org.hamcrest.Description;
8 | import org.hamcrest.Matcher;
9 | import org.hamcrest.TypeSafeMatcher;
10 |
11 | /**
12 | * Created by ironman on 03/08/16.
13 | */
14 | public class RecyclerViewMatcher {
15 | private final int recyclerViewId;
16 |
17 | public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
18 | return new RecyclerViewMatcher(recyclerViewId);
19 | }
20 |
21 | public RecyclerViewMatcher(int recyclerViewId) {
22 | this.recyclerViewId = recyclerViewId;
23 | }
24 |
25 | public Matcher atPosition(final int position) {
26 | return atPositionOnView(position, -1);
27 | }
28 |
29 | public Matcher atPositionOnView(final int position, final int targetViewId) {
30 |
31 | return new TypeSafeMatcher() {
32 | Resources resources = null;
33 | View childView;
34 |
35 | public void describeTo(Description description) {
36 | String idDescription = Integer.toString(recyclerViewId);
37 | if (this.resources != null) {
38 | try {
39 | idDescription = this.resources.getResourceName(recyclerViewId);
40 | } catch (Resources.NotFoundException var4) {
41 | idDescription = String.format("%s (resource name not found)",
42 | recyclerViewId);
43 | }
44 | }
45 |
46 | description.appendText("with id: " + idDescription);
47 | }
48 |
49 | public boolean matchesSafely(View view) {
50 |
51 | this.resources = view.getResources();
52 |
53 | if (childView == null) {
54 | RecyclerView recyclerView =
55 | (RecyclerView) view.getRootView().findViewById(recyclerViewId);
56 | if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
57 | childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
58 | }
59 | else {
60 | return false;
61 | }
62 | }
63 |
64 | if (targetViewId == -1) {
65 | return view == childView;
66 | } else {
67 | View targetView = childView.findViewById(targetViewId);
68 | return view == targetView;
69 | }
70 |
71 | }
72 | };
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/androidTestMock/java/com/codemate/blogreader/data/cache/SharedPrefCommentCacheTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache;
2 |
3 | import android.support.test.InstrumentationRegistry;
4 | import android.support.test.runner.AndroidJUnit4;
5 | import android.test.suitebuilder.annotation.LargeTest;
6 |
7 | import com.codemate.blogreader.FakeBlogApi;
8 | import com.codemate.blogreader.domain.model.Comment;
9 | import com.codemate.blogreader.domain.cache.CommentCache;
10 |
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 |
15 | import java.util.List;
16 |
17 | import static org.junit.Assert.*;
18 |
19 | /**
20 | * Created by ironman on 04/08/16.
21 | */
22 | @RunWith(AndroidJUnit4.class)
23 | @LargeTest
24 | public class SharedPrefCommentCacheTest {
25 | private CommentCache commentCache;
26 |
27 | @Before
28 | public void setUp() {
29 | commentCache = new SharedPrefCommentCache(InstrumentationRegistry.getContext());
30 | }
31 |
32 | @Test
33 | public void cacheWorks() {
34 | int postId = FakeBlogApi.COMMENTS.get(0).postId;
35 |
36 | commentCache.persistAll(postId, FakeBlogApi.COMMENTS);
37 | List comments = commentCache.getAll(postId);
38 |
39 | assertNotNull(comments);
40 | assertSame(3, comments.size());
41 |
42 | ensureCommentSame(FakeBlogApi.COMMENTS.get(0), comments.get(0));
43 | ensureCommentSame(FakeBlogApi.COMMENTS.get(1), comments.get(1));
44 | ensureCommentSame(FakeBlogApi.COMMENTS.get(2), comments.get(2));
45 | }
46 |
47 | private void ensureCommentSame(Comment actual, Comment cached) {
48 | assertEquals(actual.postId, cached.postId);
49 | assertEquals(actual.id, cached.id);
50 | assertEquals(actual.name, cached.name);
51 | assertEquals(actual.email, cached.email);
52 | assertEquals(actual.body, cached.body);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/androidTestMock/java/com/codemate/blogreader/data/cache/SharedPrefsPostCacheTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache;
2 |
3 | import android.support.test.InstrumentationRegistry;
4 | import android.support.test.runner.AndroidJUnit4;
5 | import android.test.suitebuilder.annotation.LargeTest;
6 |
7 | import com.codemate.blogreader.FakeBlogApi;
8 | import com.codemate.blogreader.domain.model.BlogPost;
9 | import com.codemate.blogreader.domain.cache.PostCache;
10 |
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 |
15 | import java.util.List;
16 |
17 | import static junit.framework.Assert.assertEquals;
18 | import static junit.framework.Assert.assertNotNull;
19 | import static junit.framework.Assert.assertSame;
20 |
21 | /**
22 | * Created by ironman on 04/08/16.
23 | */
24 | @RunWith(AndroidJUnit4.class)
25 | @LargeTest
26 | public class SharedPrefsPostCacheTest {
27 | private PostCache postsCache;
28 |
29 | @Before
30 | public void setUp() {
31 | postsCache = new SharedPrefPostCache(InstrumentationRegistry.getContext());
32 | }
33 |
34 | @Test
35 | public void cacheWorks() {
36 | postsCache.persistAll(FakeBlogApi.POSTS);
37 |
38 | List posts = postsCache.getAll();
39 |
40 | assertNotNull(posts);
41 | assertSame(3, posts.size());
42 |
43 | ensurePostSame(FakeBlogApi.POSTS.get(0), posts.get(0));
44 | ensurePostSame(FakeBlogApi.POSTS.get(1), posts.get(1));
45 | ensurePostSame(FakeBlogApi.POSTS.get(2), posts.get(2));
46 | }
47 |
48 | private void ensurePostSame(BlogPost actual, BlogPost cached) {
49 | assertEquals(actual.userId, cached.userId);
50 | assertEquals(actual.id, cached.id);
51 | assertEquals(actual.title, cached.title);
52 | assertEquals(actual.body, cached.body);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/androidTestMock/java/com/codemate/blogreader/presentation/postdetails/PostDetailsScreenTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import android.content.Intent;
4 | import android.support.test.rule.ActivityTestRule;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import com.codemate.blogreader.FakeBlogApi;
8 | import com.codemate.blogreader.domain.model.BlogPost;
9 | import com.codemate.blogreader.domain.model.Comment;
10 |
11 | import org.junit.Before;
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 |
16 | import static android.support.test.espresso.Espresso.onView;
17 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
18 | import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
19 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
20 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
21 | import static com.codemate.blogreader.RecyclerViewMatcher.withRecyclerView;
22 |
23 | /**
24 | * Created by ironman on 03/08/16.
25 | */
26 | @RunWith(AndroidJUnit4.class)
27 | public class PostDetailsScreenTest {
28 | private static final BlogPost POST =
29 | new BlogPost(1, 69, "Blog post title.", "This is a blog post content.");
30 |
31 | @Rule
32 | public ActivityTestRule postDetailTestRule =
33 | new ActivityTestRule<>(PostDetailActivity.class, true, false);
34 |
35 | @Before
36 | public void launchDetailActivityBeforeEachTest() {
37 | Intent intent = new Intent();
38 | intent.putExtra(PostDetailActivity.EXTRA_POST_ID, POST.id);
39 | intent.putExtra(PostDetailActivity.EXTRA_POST_TITLE, POST.getFormattedTitle());
40 | intent.putExtra(PostDetailActivity.EXTRA_POST_BODY, POST.body);
41 |
42 | postDetailTestRule.launchActivity(intent);
43 | }
44 |
45 | @Test
46 | public void postDetails_DisplayedInUi() {
47 | onView(withId(R.id.postTitle)).check(matches(withText(POST.title)));
48 | onView(withId(R.id.postBody)).check(matches(withText(POST.body)));
49 | }
50 |
51 | @Test
52 | public void comment_DisplayedInUi() {
53 | Comment fakeCommentOne = FakeBlogApi.COMMENTS.get(0);
54 |
55 | onView(withRecyclerView(R.id.commentList).atPosition(0))
56 | .check(matches(hasDescendant(withText(fakeCommentOne.name))))
57 | .check(matches(hasDescendant(withText(fakeCommentOne.email))))
58 | .check(matches(hasDescendant(withText(fakeCommentOne.body))));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/androidTestMock/java/com/codemate/blogreader/presentation/posts/PostsScreenTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import android.support.test.espresso.intent.Intents;
4 | import android.support.test.rule.ActivityTestRule;
5 | import android.support.test.runner.AndroidJUnit4;
6 | import android.test.suitebuilder.annotation.LargeTest;
7 |
8 | import com.codemate.blogreader.R;
9 | import com.codemate.blogreader.RecyclerViewMatcher;
10 | import com.codemate.blogreader.domain.model.BlogPost;
11 | import com.codemate.blogreader.FakeBlogApi;
12 | import com.codemate.blogreader.presentation.postdetails.PostDetailActivity;
13 |
14 | import org.junit.Rule;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 |
18 | import static android.support.test.espresso.Espresso.onView;
19 | import static android.support.test.espresso.action.ViewActions.click;
20 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
21 | import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
22 | import static android.support.test.espresso.intent.Intents.intended;
23 | import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey;
24 | import static android.support.test.espresso.intent.matcher.IntentMatchers.toPackage;
25 | import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
26 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
27 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
28 |
29 | /**
30 | * Created by ironman on 26/07/16.
31 | */
32 | @RunWith(AndroidJUnit4.class)
33 | public class PostsScreenTest {
34 | @Rule
35 | public ActivityTestRule postsActivityActivityTestRule =
36 | new ActivityTestRule<>(PostsActivity.class);
37 |
38 | @Test
39 | public void testPosts_DisplayedInUi() {
40 | BlogPost fakePostOne = FakeBlogApi.POSTS.get(0);
41 |
42 | onView(RecyclerViewMatcher.withRecyclerView(R.id.postList).atPosition(0))
43 | .check(matches(hasDescendant(withText(fakePostOne.getFormattedTitle()))))
44 | .check(matches(hasDescendant(withText(fakePostOne.getConcatenatedBody()))));
45 | }
46 |
47 | @Test
48 | public void testClickingPost_OpensDetails() {
49 | Intents.init();
50 |
51 | onView(withId(R.id.postList))
52 | .perform(actionOnItemAtPosition(0, click()));
53 |
54 | intended(toPackage("com.codemate.blogreader.mock"));
55 | intended(hasExtraWithKey(PostDetailActivity.EXTRA_POST_ID));
56 | intended(hasExtraWithKey(PostDetailActivity.EXTRA_POST_TITLE));
57 | intended(hasExtraWithKey(PostDetailActivity.EXTRA_POST_BODY));
58 |
59 | Intents.release();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/MVPApplication.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader;
2 |
3 | import android.app.Application;
4 |
5 | import com.codemate.blogreader.injection.AppComponent;
6 |
7 | /**
8 | * Created by ironman on 04/08/16.
9 | */
10 | public class MVPApplication extends Application {
11 | private static AppComponent appComponent;
12 |
13 | @Override
14 | public void onCreate() {
15 | super.onCreate();
16 |
17 | appComponent = AppComponent.Initializer.init(this);
18 | }
19 |
20 | public static AppComponent component() {
21 | return appComponent;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/cache/SharedPrefCache.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 |
6 | /**
7 | * Created by ironman on 04/08/16.
8 | */
9 | public class SharedPrefCache {
10 | private static final String KEY_PERSIST_TIME = "persist_time";
11 | private static final String KEY_DATA = "data";
12 |
13 | private SharedPreferences prefs;
14 | private String cacheKeySuffix;
15 |
16 | public SharedPrefCache(Context context, String cacheName) {
17 | if (context == null) {
18 | return;
19 | }
20 |
21 | this.prefs = context.getSharedPreferences(cacheName, Context.MODE_PRIVATE);
22 | }
23 |
24 | protected void setCacheKeySuffix(String cacheKeySuffix) {
25 | this.cacheKeySuffix = cacheKeySuffix;
26 | }
27 |
28 | protected boolean isDataStillValid(long validityPeriod) {
29 | if (prefs == null) {
30 | return false;
31 | }
32 |
33 | long currentTime = System.currentTimeMillis();
34 | long persistTime = prefs.getLong(getProperKey(KEY_PERSIST_TIME), 0);
35 |
36 | return currentTime - persistTime <= validityPeriod;
37 | }
38 |
39 | protected void persistData(String data) {
40 | if (prefs == null) {
41 | return;
42 | }
43 |
44 | prefs.edit()
45 | .putLong(getProperKey(KEY_PERSIST_TIME), System.currentTimeMillis())
46 | .putString(getProperKey(KEY_DATA), data)
47 | .apply();
48 | }
49 |
50 | protected String getData() {
51 | if (prefs == null) {
52 | return null;
53 | }
54 |
55 | return prefs.getString(getProperKey(KEY_DATA), null);
56 | }
57 |
58 | protected void clearCache() {
59 | if (prefs == null) {
60 | return;
61 | }
62 |
63 | prefs.edit().clear().apply();
64 | }
65 |
66 | private String getProperKey(String input) {
67 | if (cacheKeySuffix != null) {
68 | return input + cacheKeySuffix;
69 | }
70 |
71 | return input;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/cache/SharedPrefCommentCache.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache;
2 |
3 | import android.content.Context;
4 |
5 | import com.codemate.blogreader.data.cache.converter.CommentJsonConverter;
6 | import com.codemate.blogreader.domain.cache.CommentCache;
7 | import com.codemate.blogreader.domain.model.Comment;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | /**
14 | * Created by ironman on 04/08/16.
15 | */
16 | public class SharedPrefCommentCache extends SharedPrefCache implements CommentCache {
17 | private static final long VALIDITY_PERIOD = TimeUnit.MINUTES.toMillis(15);
18 | private final CommentJsonConverter converter;
19 |
20 | public SharedPrefCommentCache(Context context) {
21 | super(context, "comment_cache");
22 |
23 | converter = new CommentJsonConverter();
24 | }
25 |
26 | @Override
27 | public List getAll(int postId) {
28 | setCacheKeySuffix("_post_id_" + postId);
29 |
30 | if (isDataStillValid(VALIDITY_PERIOD)) {
31 | String data = getData();
32 |
33 | if (data != null) {
34 | return converter.convertJsonToComments(getData());
35 | }
36 | }
37 |
38 | return new ArrayList<>(0);
39 | }
40 |
41 | @Override
42 | public void persistAll(int postId, List comments) {
43 | String json = converter.convertCommentsToJson(comments);
44 | setCacheKeySuffix("_post_id_" + postId);
45 | persistData(json);
46 | }
47 |
48 | @Override
49 | public void clearAll() {
50 | clearCache();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/cache/SharedPrefPostCache.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache;
2 |
3 | import android.content.Context;
4 |
5 | import com.codemate.blogreader.data.cache.converter.PostJsonConverter;
6 | import com.codemate.blogreader.domain.cache.PostCache;
7 | import com.codemate.blogreader.domain.model.BlogPost;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | /**
14 | * Created by ironman on 04/08/16.
15 | */
16 | public class SharedPrefPostCache extends SharedPrefCache implements PostCache {
17 | private static final long VALIDITY_PERIOD = TimeUnit.HOURS.toMillis(1);
18 | private final PostJsonConverter converter;
19 |
20 | public SharedPrefPostCache(Context context) {
21 | super(context, "post_cache");
22 |
23 | converter = new PostJsonConverter();
24 | }
25 |
26 | @Override
27 | public List getAll() {
28 | if (isDataStillValid(VALIDITY_PERIOD)) {
29 | String data = getData();
30 |
31 | if (data != null) {
32 | return converter.convertJsonToPosts(data);
33 | }
34 | }
35 |
36 | return new ArrayList<>(0);
37 | }
38 |
39 | @Override
40 | public void persistAll(List items) {
41 | persistData(converter.convertPostsToJson(items));
42 | }
43 |
44 | @Override
45 | public void clearAll() {
46 | clearCache();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/cache/converter/CommentJsonConverter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache.converter;
2 |
3 | import com.codemate.blogreader.domain.model.Comment;
4 | import com.google.gson.Gson;
5 | import com.google.gson.reflect.TypeToken;
6 |
7 | import java.lang.reflect.Type;
8 | import java.util.List;
9 |
10 | public class CommentJsonConverter {
11 | private final Gson gson;
12 |
13 | public CommentJsonConverter() {
14 | gson = new Gson();
15 | }
16 |
17 | public List convertJsonToComments(String data) {
18 | Type listType = new TypeToken>() {}.getType();
19 |
20 | return gson.fromJson(data, listType);
21 | }
22 |
23 | public String convertCommentsToJson(List items) {
24 | return gson.toJson(items);
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/cache/converter/PostJsonConverter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache.converter;
2 |
3 | import com.codemate.blogreader.domain.model.BlogPost;
4 | import com.google.gson.Gson;
5 | import com.google.gson.reflect.TypeToken;
6 |
7 | import java.lang.reflect.Type;
8 | import java.util.List;
9 |
10 | public class PostJsonConverter {
11 | private final Gson gson;
12 |
13 | public PostJsonConverter() {
14 | gson = new Gson();
15 | }
16 |
17 | public List convertJsonToPosts(String data) {
18 | Type listType = new TypeToken>() {}.getType();
19 |
20 | return gson.fromJson(data, listType);
21 | }
22 |
23 | public String convertPostsToJson(List items) {
24 | return gson.toJson(items);
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/data/network/BlogService.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.network;
2 |
3 | import com.codemate.blogreader.domain.model.BlogPost;
4 | import com.codemate.blogreader.domain.model.Comment;
5 |
6 | import java.util.List;
7 |
8 | import retrofit.RequestInterceptor;
9 | import retrofit.RestAdapter;
10 | import retrofit.http.GET;
11 | import retrofit.http.Query;
12 | import rx.Observable;
13 |
14 | /**
15 | * Created by ironman on 26/07/16.
16 | */
17 | public class BlogService {
18 | private static final String FORUM_SERVER_URL = "http://jsonplaceholder.typicode.com";
19 | private BlogApi blogApi;
20 |
21 | public BlogService() {
22 | RequestInterceptor requestInterceptor = new RequestInterceptor() {
23 | @Override
24 | public void intercept(RequestFacade request) {
25 | request.addHeader("Accept", "application/json");
26 | }
27 | };
28 |
29 | RestAdapter restAdapter = new RestAdapter.Builder()
30 | .setEndpoint(FORUM_SERVER_URL)
31 | .setRequestInterceptor(requestInterceptor)
32 | .build();
33 |
34 | blogApi = restAdapter.create(BlogApi.class);
35 | }
36 |
37 | public BlogApi getApi() {
38 | return blogApi;
39 | }
40 |
41 | public interface BlogApi {
42 | @GET("/posts")
43 | Observable> getPosts();
44 |
45 | @GET("/comments")
46 | Observable> getComments(@Query("postId") int postId);
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/cache/CommentCache.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.cache;
2 |
3 | import com.codemate.blogreader.domain.model.Comment;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by ironman on 04/08/16.
9 | */
10 | public interface CommentCache {
11 | List getAll(int postId);
12 | void persistAll(int postId, List comments);
13 | void clearAll();
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/cache/PostCache.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.cache;
2 |
3 | import com.codemate.blogreader.domain.model.BlogPost;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by ironman on 04/08/16.
9 | */
10 | public interface PostCache {
11 | List getAll();
12 | void persistAll(List items);
13 | void clearAll();
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/interactors/BaseInteractor.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.interactors;
2 |
3 | import rx.Observable;
4 | import rx.Scheduler;
5 | import rx.Subscriber;
6 | import rx.Subscription;
7 | import rx.subscriptions.Subscriptions;
8 |
9 | /**
10 | * Created by ironman on 26/07/16.
11 | */
12 | public abstract class BaseInteractor {
13 | private final Scheduler worker;
14 | private final Scheduler observer;
15 | private Subscription subscription = Subscriptions.empty();
16 |
17 | protected BaseInteractor(Scheduler worker, Scheduler observer) {
18 | this.worker = worker;
19 | this.observer = observer;
20 | }
21 |
22 | protected abstract Observable buildObservable();
23 |
24 | public void execute(Subscriber subscriber) {
25 | this.subscription = buildObservable()
26 | .subscribeOn(worker)
27 | .observeOn(observer)
28 | .subscribe(subscriber);
29 | }
30 |
31 | public void unsubscribe() {
32 | if (!subscription.isUnsubscribed()) {
33 | subscription.unsubscribe();
34 | }
35 | }
36 |
37 | public abstract void setForceRefresh(boolean forceRefresh);
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/interactors/GetPostCommentsInteractor.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.interactors;
2 |
3 | import android.util.Log;
4 |
5 | import com.codemate.blogreader.data.network.BlogService;
6 | import com.codemate.blogreader.domain.cache.CommentCache;
7 | import com.codemate.blogreader.domain.model.Comment;
8 |
9 | import java.util.List;
10 |
11 | import rx.Observable;
12 | import rx.Scheduler;
13 | import rx.functions.Func1;
14 |
15 | /**
16 | * Created by ironman on 28/07/16.
17 | */
18 | public class GetPostCommentsInteractor extends BaseInteractor> {
19 | private final BlogService.BlogApi blogApi;
20 | private final CommentCache commentCache;
21 |
22 | private int postId;
23 | private boolean forceRefresh;
24 |
25 | public GetPostCommentsInteractor(BlogService.BlogApi blogApi, CommentCache commentCache,
26 | Scheduler worker, Scheduler observer) {
27 | super(worker, observer);
28 | this.blogApi = blogApi;
29 | this.commentCache = commentCache;
30 | }
31 |
32 | public void setPostId(int postId) {
33 | this.postId = postId;
34 | }
35 |
36 | @Override
37 | protected Observable> buildObservable() {
38 | if (forceRefresh) {
39 | commentCache.clearAll();
40 | }
41 |
42 | List cachedComments = commentCache.getAll(postId);
43 |
44 | if (cachedComments.size() > 0) {
45 | Log.d("GetComments", "using cached comments");
46 | return Observable.just(cachedComments);
47 | }
48 |
49 | return blogApi.getComments(postId)
50 | .filter(new Func1, Boolean>() {
51 | @Override
52 | public Boolean call(List comments) {
53 | commentCache.persistAll(postId, comments);
54 | return true;
55 | }
56 | });
57 | }
58 |
59 | @Override
60 | public void setForceRefresh(boolean forceRefresh) {
61 | this.forceRefresh = forceRefresh;
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/interactors/GetPostsInteractor.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.interactors;
2 |
3 | import android.util.Log;
4 |
5 | import com.codemate.blogreader.data.network.BlogService;
6 | import com.codemate.blogreader.domain.cache.PostCache;
7 | import com.codemate.blogreader.domain.model.BlogPost;
8 |
9 | import java.util.List;
10 |
11 | import javax.inject.Inject;
12 |
13 | import rx.Observable;
14 | import rx.Scheduler;
15 | import rx.functions.Func1;
16 |
17 | /**
18 | * Created by ironman on 26/07/16.
19 | */
20 | public class GetPostsInteractor extends BaseInteractor> {
21 | private final BlogService.BlogApi blogApi;
22 | private final PostCache blogPostCache;
23 | private boolean forceRefresh;
24 |
25 | @Inject
26 | public GetPostsInteractor(BlogService.BlogApi blogApi, PostCache blogPostCache,
27 | Scheduler worker, Scheduler observer) {
28 | super(worker, observer);
29 | this.blogApi = blogApi;
30 | this.blogPostCache = blogPostCache;
31 | }
32 |
33 | @Override
34 | protected Observable> buildObservable() {
35 | if (forceRefresh) {
36 | blogPostCache.clearAll();
37 | }
38 |
39 | final List cachedPosts = blogPostCache.getAll();
40 |
41 | if (cachedPosts.size() > 0) {
42 | Log.d("GetPostsInteractor", "using cached posts");
43 | return Observable.just(cachedPosts);
44 | }
45 |
46 | return blogApi.getPosts()
47 | .filter(new Func1, Boolean>() {
48 | @Override
49 | public Boolean call(List blogPosts) {
50 | blogPostCache.persistAll(blogPosts);
51 | return true;
52 | }
53 | });
54 | }
55 |
56 | @Override
57 | public void setForceRefresh(boolean forceRefresh) {
58 | this.forceRefresh = forceRefresh;
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/model/BlogPost.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.model;
2 |
3 | /**
4 | * Created by ironman on 26/07/16.
5 | */
6 | public class BlogPost {
7 | private static final int MAX_BODY_LENGTH = 100;
8 |
9 | public int userId;
10 | public int id;
11 | public String title;
12 | public String body;
13 |
14 | public BlogPost(int userId, int id, String title, String body) {
15 | this.userId = userId;
16 | this.id = id;
17 | this.title = title;
18 | this.body = body;
19 | }
20 |
21 | public String getFormattedTitle() {
22 | return title.substring(0, 1).toUpperCase() + title.substring(1, title.length());
23 | }
24 |
25 | public String getConcatenatedBody() {
26 | boolean tooLong = body != null && body.length() > MAX_BODY_LENGTH;
27 |
28 | if (!tooLong) {
29 | return body;
30 | }
31 |
32 | return body.substring(0, MAX_BODY_LENGTH) + "...";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/domain/model/Comment.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.domain.model;
2 |
3 | /**
4 | * Created by ironman on 26/07/16.
5 | */
6 | public class Comment {
7 | public int postId;
8 | public int id;
9 | public String name;
10 | public String email;
11 | public String body;
12 |
13 | public Comment(int postId, int id, String name, String email, String body) {
14 | this.postId = postId;
15 | this.id = id;
16 | this.name = name;
17 | this.email = email;
18 | this.body = body;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/injection/AppComponent.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.injection;
2 |
3 | import com.codemate.blogreader.MVPApplication;
4 | import com.codemate.blogreader.presentation.postdetails.PostDetailActivity;
5 | import com.codemate.blogreader.presentation.posts.PostsActivity;
6 |
7 | import javax.inject.Singleton;
8 |
9 | import dagger.Component;
10 |
11 | /**
12 | * Created by ironman on 05/08/16.
13 | */
14 | @Singleton
15 | @Component(modules = {AppModule.class})
16 | public interface AppComponent {
17 | void inject(PostsActivity postsActivity);
18 | void inject(PostDetailActivity postDetailActivity);
19 |
20 | final class Initializer {
21 | private Initializer(){
22 | }
23 |
24 | public static AppComponent init(MVPApplication application) {
25 | return DaggerAppComponent.builder()
26 | .appModule(new AppModule(application))
27 | .build();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/anim/ListItemAnimator.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.anim;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.support.v7.widget.DefaultItemAnimator;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.view.animation.DecelerateInterpolator;
8 |
9 | import com.codemate.blogreader.util.ScreenUtils;
10 |
11 | /**
12 | * Created by ironman on 09/08/16.
13 | */
14 | public class ListItemAnimator extends DefaultItemAnimator {
15 | private final int itemTypeToAnimate;
16 |
17 | public ListItemAnimator(int itemTypeToAnimate) {
18 | this.itemTypeToAnimate = itemTypeToAnimate;
19 | }
20 |
21 | @Override
22 | public boolean animateAdd(RecyclerView.ViewHolder viewHolder) {
23 | if (viewHolder.getItemViewType() == itemTypeToAnimate) {
24 | runEnterAnimation(viewHolder);
25 | return false;
26 | }
27 |
28 | super.dispatchAddFinished(viewHolder);
29 | return false;
30 | }
31 |
32 | private void runEnterAnimation(final RecyclerView.ViewHolder viewHolder) {
33 | int screenHeight = ScreenUtils.getScreenHeight(viewHolder.itemView.getContext());
34 |
35 | viewHolder.itemView.setTranslationY(screenHeight);
36 | viewHolder.itemView.setScaleX(0.9f);
37 | viewHolder.itemView.setAlpha(0.5f);
38 | viewHolder.itemView.animate()
39 | .setInterpolator(new DecelerateInterpolator(3f))
40 | .setDuration(1000)
41 | .setStartDelay(250 + (viewHolder.getLayoutPosition() * 75))
42 | .setListener(new AnimatorListenerAdapter() {
43 | @Override
44 | public void onAnimationEnd(Animator animation) {
45 | ListItemAnimator.super.dispatchAddFinished(viewHolder);
46 | }
47 | })
48 | .translationY(0)
49 | .scaleX(1)
50 | .alpha(1)
51 | .start();
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/postdetails/CommentsAdapter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.ImageView;
9 | import android.widget.TextView;
10 |
11 | import com.codemate.blogreader.R;
12 | import com.codemate.blogreader.domain.model.BlogPost;
13 | import com.codemate.blogreader.domain.model.Comment;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 |
18 | /**
19 | * Created by ironman on 03/08/16.
20 | */
21 | public class CommentsAdapter extends RecyclerView.Adapter {
22 | public static final int ITEM_TYPE_COMMENT = 2;
23 |
24 | private List comments;
25 |
26 | public CommentsAdapter(List comments) {
27 | this.comments = comments;
28 | }
29 |
30 | @Override
31 | public CommentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
32 | Context context = parent.getContext();
33 | LayoutInflater inflater = LayoutInflater.from(context);
34 | View commentView = inflater.inflate(R.layout.item_comment, parent, false);
35 |
36 | return new CommentViewHolder(commentView);
37 | }
38 |
39 | @Override
40 | public void onBindViewHolder(CommentViewHolder holder, int position) {
41 | Comment comment = comments.get(position);
42 |
43 | holder.name.setText(comment.name);
44 | holder.email.setText(comment.email);
45 | holder.body.setText(comment.body);
46 | }
47 |
48 | @Override
49 | public int getItemCount() {
50 | return comments.size();
51 | }
52 |
53 | @Override
54 | public int getItemViewType(int position) {
55 | return ITEM_TYPE_COMMENT;
56 | }
57 |
58 | public void setComments(boolean animated, List comments) {
59 | this.comments = new ArrayList<>(comments);
60 |
61 | if (animated) {
62 | notifyItemRangeInserted(0, comments.size());
63 | } else {
64 | notifyDataSetChanged();
65 | }
66 | }
67 |
68 | public boolean isEmpty() {
69 | return getItemCount() == 0;
70 | }
71 |
72 | public void clear() {
73 | comments.clear();
74 | notifyDataSetChanged();
75 | }
76 |
77 | public class CommentViewHolder extends RecyclerView.ViewHolder {
78 | public ImageView profilePic;
79 | public TextView name;
80 | public TextView email;
81 | public TextView body;
82 |
83 | public CommentViewHolder(View itemView) {
84 | super(itemView);
85 |
86 | profilePic = (ImageView) itemView.findViewById(R.id.commentProfilePic);
87 | name = (TextView) itemView.findViewById(R.id.commentName);
88 | email = (TextView) itemView.findViewById(R.id.commentEmail);
89 | body = (TextView) itemView.findViewById(R.id.commentBody);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/postdetails/PostDetailActivity.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.support.annotation.Nullable;
7 | import android.support.v4.content.ContextCompat;
8 | import android.support.v4.widget.SwipeRefreshLayout;
9 | import android.support.v7.app.ActionBar;
10 | import android.support.v7.app.AppCompatActivity;
11 | import android.support.v7.widget.LinearLayoutManager;
12 | import android.support.v7.widget.RecyclerView;
13 | import android.support.v7.widget.Toolbar;
14 | import android.view.MenuItem;
15 | import android.widget.TextView;
16 |
17 | import com.codemate.blogreader.MVPApplication;
18 | import com.codemate.blogreader.R;
19 | import com.codemate.blogreader.domain.interactors.GetPostCommentsInteractor;
20 | import com.codemate.blogreader.domain.model.BlogPost;
21 | import com.codemate.blogreader.domain.model.Comment;
22 | import com.codemate.blogreader.presentation.anim.ListItemAnimator;
23 | import com.codemate.blogreader.presentation.view.ErrorViewLayout;
24 | import com.codemate.blogreader.util.IntentFactory;
25 |
26 | import java.util.ArrayList;
27 | import java.util.List;
28 |
29 | import javax.inject.Inject;
30 |
31 | /**
32 | * Created by ironman on 26/07/16.
33 | */
34 | public class PostDetailActivity extends AppCompatActivity implements PostDetailView {
35 | public static final String EXTRA_POST_USER_ID = "USER_ID";
36 | public static final String EXTRA_POST_ID = "POST_ID";
37 | public static final String EXTRA_POST_TITLE = "POST_TITLE";
38 | public static final String EXTRA_POST_BODY = "POST_BODY";
39 |
40 | @Inject
41 | GetPostCommentsInteractor getPostCommentsInteractor;
42 |
43 | private boolean shouldAnimate = true;
44 | private PostDetailPresenter presenter;
45 | private CommentsAdapter commentsAdapter;
46 | private int postId;
47 |
48 | private SwipeRefreshLayout refreshLayout;
49 | private ErrorViewLayout errorLayout;
50 | private RecyclerView commentList;
51 |
52 | public static Intent create(Context context, BlogPost blogPost) {
53 | Intent intent = IntentFactory.createIntent(context, PostDetailActivity.class);
54 | intent.putExtra(PostDetailActivity.EXTRA_POST_USER_ID, blogPost.userId);
55 | intent.putExtra(PostDetailActivity.EXTRA_POST_ID, blogPost.id);
56 | intent.putExtra(PostDetailActivity.EXTRA_POST_TITLE, blogPost.getFormattedTitle());
57 | intent.putExtra(PostDetailActivity.EXTRA_POST_BODY, blogPost.body);
58 |
59 | return intent;
60 | }
61 |
62 | private BlogPost getPostFromIntent() {
63 | Intent intent = getIntent();
64 |
65 | return new BlogPost(
66 | intent.getIntExtra(EXTRA_POST_USER_ID, 0),
67 | intent.getIntExtra(EXTRA_POST_ID, 0),
68 | intent.getStringExtra(EXTRA_POST_TITLE),
69 | intent.getStringExtra(EXTRA_POST_BODY)
70 | );
71 | }
72 |
73 | @Override
74 | protected void onCreate(@Nullable Bundle savedInstanceState) {
75 | super.onCreate(savedInstanceState);
76 | setContentView(R.layout.activity_post_details);
77 |
78 | if (savedInstanceState != null) {
79 | shouldAnimate = false;
80 | }
81 |
82 | MVPApplication.component().inject(this);
83 | setUpActionBar();
84 |
85 | postId = getIntent().getIntExtra(EXTRA_POST_ID, 0);
86 | presenter = new PostDetailPresenter(this, getPostCommentsInteractor);
87 | commentsAdapter = new CommentsAdapter(new ArrayList(0));
88 |
89 | setUpViews();
90 | }
91 |
92 | private void setUpActionBar() {
93 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
94 | ActionBar actionBar = getSupportActionBar();
95 | assert actionBar != null;
96 |
97 | actionBar.setDisplayHomeAsUpEnabled(true);
98 | actionBar.setDisplayShowHomeEnabled(true);
99 | }
100 |
101 | private void setUpViews() {
102 | BlogPost post = getPostFromIntent();
103 | ((TextView) findViewById(R.id.postTitle)).setText(post.getFormattedTitle());
104 | ((TextView) findViewById(R.id.postBody)).setText(post.body);
105 |
106 | refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
107 | errorLayout = (ErrorViewLayout) findViewById(R.id.errorLayout);
108 | commentList = (RecyclerView) findViewById(R.id.commentList);
109 |
110 | refreshLayout.setColorSchemeColors(
111 | ContextCompat.getColor(this, R.color.colorPrimary),
112 | ContextCompat.getColor(this, R.color.colorAccent),
113 | ContextCompat.getColor(this, R.color.colorPrimaryDark)
114 | );
115 |
116 | refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
117 | @Override
118 | public void onRefresh() {
119 | shouldAnimate = true;
120 |
121 | commentsAdapter.clear();
122 | presenter.loadComments(postId, true);
123 | }
124 | });
125 |
126 | commentList.setAdapter(commentsAdapter);
127 | commentList.setLayoutManager(new LinearLayoutManager(this));
128 | commentList.setItemAnimator(new ListItemAnimator(CommentsAdapter.ITEM_TYPE_COMMENT));
129 |
130 | errorLayout.setOnActionPressedListener(new ErrorViewLayout.OnActionPressedListener() {
131 | @Override
132 | public void onActionPressed() {
133 | presenter.loadComments(postId, true);
134 | }
135 | });
136 | }
137 |
138 | @Override
139 | public void onResume() {
140 | super.onResume();
141 |
142 | if (commentsAdapter.isEmpty()) {
143 | presenter.loadComments(postId, false);
144 | }
145 | }
146 |
147 | @Override
148 | public void onDestroy() {
149 | super.onDestroy();
150 | presenter.destroy();
151 | }
152 |
153 | @Override
154 | public boolean onOptionsItemSelected(MenuItem item) {
155 | if (item.getItemId() == android.R.id.home) {
156 | onBackPressed();
157 | return true;
158 | }
159 |
160 | return super.onOptionsItemSelected(item);
161 | }
162 |
163 | @Override
164 | public void setProgressIndicator(final boolean active) {
165 | if (refreshLayout == null) {
166 | return;
167 | }
168 |
169 | refreshLayout.post(new Runnable() {
170 | @Override
171 | public void run() {
172 | refreshLayout.setRefreshing(active);
173 | }
174 | });
175 | }
176 |
177 | @Override
178 | public void showComments(List comments) {
179 | commentsAdapter.setComments(shouldAnimate, comments);
180 | }
181 |
182 | @Override
183 | public void onError(Throwable e) {
184 | refreshLayout.post(new Runnable() {
185 | @Override
186 | public void run() {
187 | refreshLayout.setRefreshing(false);
188 | }
189 | });
190 |
191 | errorLayout.showError();
192 | }
193 |
194 | @Override
195 | public void hideError() {
196 | errorLayout.hideError();
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/postdetails/PostDetailPresenter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import com.codemate.blogreader.domain.interactors.GetPostCommentsInteractor;
4 | import com.codemate.blogreader.domain.model.Comment;
5 |
6 | import java.util.List;
7 |
8 | import rx.Subscriber;
9 |
10 | /**
11 | * Created by ironman on 28/07/16.
12 | */
13 | public class PostDetailPresenter {
14 | private final PostDetailView postDetailView;
15 | private final GetPostCommentsInteractor getPostCommentsInteractor;
16 |
17 | public PostDetailPresenter(PostDetailView postDetailView, GetPostCommentsInteractor getPostCommentsInteractor) {
18 | this.postDetailView = postDetailView;
19 | this.getPostCommentsInteractor = getPostCommentsInteractor;
20 | }
21 |
22 | public void loadComments(int postId, boolean forceRefresh) {
23 | postDetailView.hideError();
24 | postDetailView.setProgressIndicator(true);
25 |
26 | getPostCommentsInteractor.setPostId(postId);
27 | getPostCommentsInteractor.setForceRefresh(forceRefresh);
28 | getPostCommentsInteractor.execute(new Subscriber>() {
29 | @Override
30 | public void onCompleted() {
31 | postDetailView.setProgressIndicator(false);
32 | }
33 |
34 | @Override
35 | public void onError(Throwable e) {
36 | postDetailView.onError(e);
37 | }
38 |
39 | @Override
40 | public void onNext(List comments) {
41 | postDetailView.showComments(comments);
42 | }
43 | });
44 | }
45 |
46 | public void destroy() {
47 | getPostCommentsInteractor.unsubscribe();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/postdetails/PostDetailView.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import com.codemate.blogreader.domain.model.Comment;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by ironman on 05/08/16.
9 | */
10 |
11 | public interface PostDetailView {
12 | void setProgressIndicator(boolean active);
13 |
14 | void showComments(List comments);
15 |
16 | void onError(Throwable e);
17 |
18 | void hideError();
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/posts/PostsActivity.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.content.ContextCompat;
5 | import android.support.v4.widget.SwipeRefreshLayout;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.support.v7.widget.LinearLayoutManager;
8 | import android.support.v7.widget.RecyclerView;
9 | import android.support.v7.widget.Toolbar;
10 |
11 | import com.codemate.blogreader.MVPApplication;
12 | import com.codemate.blogreader.R;
13 | import com.codemate.blogreader.domain.interactors.GetPostsInteractor;
14 | import com.codemate.blogreader.domain.model.BlogPost;
15 | import com.codemate.blogreader.presentation.anim.ListItemAnimator;
16 | import com.codemate.blogreader.presentation.postdetails.PostDetailActivity;
17 | import com.codemate.blogreader.presentation.view.ErrorViewLayout;
18 |
19 | import java.util.ArrayList;
20 | import java.util.List;
21 |
22 | import javax.inject.Inject;
23 |
24 | public class PostsActivity extends AppCompatActivity implements PostsView {
25 | @Inject
26 | GetPostsInteractor getPostsInteractor;
27 |
28 | private boolean shouldAnimate = true;
29 | private PostsPresenter presenter;
30 | private PostsAdapter postAdapter;
31 |
32 | private RecyclerView postList;
33 | private ErrorViewLayout errorLayout;
34 | private SwipeRefreshLayout refreshLayout;
35 |
36 | PostItemListener postItemListener = new PostItemListener() {
37 | @Override
38 | public void onPostClicked(BlogPost clickedBlogPost) {
39 | presenter.openPostDetails(clickedBlogPost);
40 | }
41 | };
42 |
43 | @Override
44 | protected void onCreate(Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 | setContentView(R.layout.activity_posts);
47 |
48 | if (savedInstanceState != null) {
49 | shouldAnimate = false;
50 | }
51 |
52 | MVPApplication.component().inject(this);
53 | setUpActionBar();
54 |
55 | presenter = new PostsPresenter(this, getPostsInteractor);
56 | postAdapter = new PostsAdapter(new ArrayList(0), postItemListener);
57 |
58 | setUpViews();
59 | }
60 |
61 | private void setUpActionBar() {
62 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
63 | setSupportActionBar(toolbar);
64 | }
65 |
66 | private void setUpViews() {
67 | refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
68 | errorLayout = (ErrorViewLayout) findViewById(R.id.errorLayout);
69 | postList = (RecyclerView) findViewById(R.id.postList);
70 |
71 | refreshLayout.setColorSchemeColors(
72 | ContextCompat.getColor(this, R.color.colorPrimary),
73 | ContextCompat.getColor(this, R.color.colorAccent),
74 | ContextCompat.getColor(this, R.color.colorPrimaryDark)
75 | );
76 |
77 | refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
78 | @Override
79 | public void onRefresh() {
80 | shouldAnimate = true;
81 |
82 | postAdapter.clear();
83 | presenter.loadPosts(true);
84 | }
85 | });
86 |
87 | postList.setAdapter(postAdapter);
88 | postList.setLayoutManager(new LinearLayoutManager(this));
89 | postList.setItemAnimator(new ListItemAnimator(PostsAdapter.ITEM_TYPE_BLOG_POST));
90 |
91 | errorLayout.setOnActionPressedListener(new ErrorViewLayout.OnActionPressedListener() {
92 | @Override
93 | public void onActionPressed() {
94 | presenter.loadPosts(true);
95 | }
96 | });
97 | }
98 |
99 | @Override
100 | public void onResume() {
101 | super.onResume();
102 |
103 | if (postAdapter.isEmpty()) {
104 | presenter.loadPosts(false);
105 | }
106 | }
107 |
108 | @Override
109 | public void onDestroy() {
110 | super.onDestroy();
111 | presenter.destroy();
112 | }
113 |
114 | @Override
115 | public void setProgressIndicator(final boolean active) {
116 | if (refreshLayout == null) {
117 | return;
118 | }
119 |
120 | refreshLayout.post(new Runnable() {
121 | @Override
122 | public void run() {
123 | refreshLayout.setRefreshing(active);
124 | }
125 | });
126 | }
127 |
128 | @Override
129 | public void showPosts(List blogPosts) {
130 | postAdapter.setBlogPosts(shouldAnimate, blogPosts);
131 | }
132 |
133 | @Override
134 | public void showPostDetails(BlogPost post) {
135 | startActivity(PostDetailActivity.create(this, post));
136 | }
137 |
138 | @Override
139 | public void showError(Throwable e) {
140 | refreshLayout.post(new Runnable() {
141 | @Override
142 | public void run() {
143 | refreshLayout.setRefreshing(false);
144 | }
145 | });
146 |
147 | errorLayout.showError();
148 | }
149 |
150 | @Override
151 | public void hideError() {
152 | errorLayout.hideError();
153 | }
154 |
155 | public interface PostItemListener {
156 | void onPostClicked(BlogPost clickedBlogPost);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/posts/PostsAdapter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.TextView;
9 |
10 | import com.codemate.blogreader.R;
11 | import com.codemate.blogreader.domain.model.BlogPost;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | /**
17 | * Created by ironman on 26/07/16.
18 | */
19 | public class PostsAdapter extends RecyclerView.Adapter {
20 | public static final int ITEM_TYPE_BLOG_POST = 1;
21 |
22 | private final PostsActivity.PostItemListener postItemListener;
23 | private List blogPosts;
24 |
25 | public PostsAdapter(List blogPosts, PostsActivity.PostItemListener postItemListener) {
26 | this.postItemListener = postItemListener;
27 | this.blogPosts = blogPosts;
28 | }
29 |
30 | @Override
31 | public PostViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
32 | Context context = parent.getContext();
33 | LayoutInflater inflater = LayoutInflater.from(context);
34 | View postView = inflater.inflate(R.layout.item_post, parent, false);
35 |
36 | return new PostViewHolder(postView, postItemListener);
37 | }
38 |
39 | @Override
40 | public void onBindViewHolder(PostViewHolder holder, int position) {
41 | BlogPost blogPost = blogPosts.get(position);
42 |
43 | holder.title.setText(blogPost.getFormattedTitle());
44 | holder.body.setText(blogPost.getConcatenatedBody());
45 | }
46 |
47 | @Override
48 | public int getItemCount() {
49 | return blogPosts.size();
50 | }
51 |
52 | @Override
53 | public int getItemViewType(int position) {
54 | return ITEM_TYPE_BLOG_POST;
55 | }
56 |
57 | public void setBlogPosts(boolean animated, List blogPosts) {
58 | this.blogPosts = new ArrayList<>(blogPosts);
59 |
60 | if (animated) {
61 | notifyItemRangeInserted(0, blogPosts.size());
62 | } else {
63 | notifyDataSetChanged();
64 | }
65 | }
66 |
67 | public boolean isEmpty() {
68 | return getItemCount() == 0;
69 | }
70 |
71 | public void clear() {
72 | blogPosts.clear();
73 | notifyDataSetChanged();
74 | }
75 |
76 | public class PostViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
77 | public TextView title;
78 | public TextView body;
79 |
80 | private PostsActivity.PostItemListener postItemListener;
81 |
82 | public PostViewHolder(View itemView, PostsActivity.PostItemListener postItemListener) {
83 | super(itemView);
84 |
85 | title = (TextView) itemView.findViewById(R.id.postTitle);
86 | body = (TextView) itemView.findViewById(R.id.postBody);
87 | this.postItemListener = postItemListener;
88 |
89 | itemView.setOnClickListener(this);
90 | }
91 |
92 | @Override
93 | public void onClick(View v) {
94 | int position = getAdapterPosition();
95 | BlogPost blogPost = blogPosts.get(position);
96 |
97 | postItemListener.onPostClicked(blogPost);
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/posts/PostsPresenter.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import com.codemate.blogreader.domain.interactors.GetPostsInteractor;
4 | import com.codemate.blogreader.domain.model.BlogPost;
5 |
6 | import java.util.List;
7 |
8 | import rx.Subscriber;
9 |
10 | /**
11 | * Created by ironman on 26/07/16.
12 | */
13 | public class PostsPresenter {
14 | private final PostsView postsView;
15 | private final GetPostsInteractor getPostsInteractor;
16 |
17 | public PostsPresenter(PostsView postsView, GetPostsInteractor getPostsInteractor) {
18 | this.postsView = postsView;
19 | this.getPostsInteractor = getPostsInteractor;
20 | }
21 |
22 | public void loadPosts(boolean forceRefresh) {
23 | postsView.hideError();
24 | postsView.setProgressIndicator(true);
25 |
26 | getPostsInteractor.setForceRefresh(forceRefresh);
27 | getPostsInteractor.execute(new Subscriber>() {
28 | @Override
29 | public void onCompleted() {
30 | postsView.setProgressIndicator(false);
31 | }
32 |
33 | @Override
34 | public void onError(Throwable e) {
35 | postsView.showError(e);
36 | }
37 |
38 | @Override
39 | public void onNext(List blogPosts) {
40 | postsView.showPosts(blogPosts);
41 | }
42 | });
43 | }
44 |
45 | public void openPostDetails(BlogPost blogPost) {
46 | postsView.showPostDetails(blogPost);
47 | }
48 |
49 | public void destroy() {
50 | getPostsInteractor.unsubscribe();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/posts/PostsView.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import com.codemate.blogreader.domain.model.BlogPost;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by ironman on 26/07/16.
9 | */
10 | public interface PostsView {
11 | void setProgressIndicator(boolean active);
12 |
13 | void showPosts(List blogPosts);
14 |
15 | void showPostDetails(BlogPost post);
16 |
17 | void showError(Throwable throwable);
18 |
19 | void hideError();
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/presentation/view/ErrorViewLayout.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.view;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 | import android.view.animation.BounceInterpolator;
7 | import android.widget.FrameLayout;
8 |
9 | import com.codemate.blogreader.R;
10 |
11 | /**
12 | * Created by ironman on 09/08/16.
13 | */
14 | public class ErrorViewLayout extends FrameLayout implements View.OnClickListener {
15 | private View errorView;
16 | private View actualView;
17 |
18 | private OnActionPressedListener onActionPressedListener;
19 | private boolean errorVisible;
20 |
21 | public interface OnActionPressedListener {
22 | void onActionPressed();
23 | }
24 |
25 | public ErrorViewLayout(Context context) {
26 | super(context);
27 | init(context);
28 | }
29 |
30 | public ErrorViewLayout(Context context, AttributeSet attrs) {
31 | super(context, attrs);
32 | init(context);
33 | }
34 |
35 | private void init(Context context) {
36 | errorView = View.inflate(context, R.layout.view_error, null);
37 | addView(errorView, 0);
38 |
39 | errorView.findViewById(R.id.errorActionButton).setOnClickListener(this);
40 | }
41 |
42 | @Override
43 | protected void onFinishInflate() {
44 | super.onFinishInflate();
45 |
46 | actualView = getChildAt(1);
47 | }
48 |
49 | @Override
50 | public void onClick(View v) {
51 | if (onActionPressedListener != null) {
52 | onActionPressedListener.onActionPressed();
53 | }
54 | }
55 |
56 | public void setOnActionPressedListener(OnActionPressedListener listener) {
57 | this.onActionPressedListener = listener;
58 | }
59 |
60 | public void showError() {
61 | errorView.setVisibility(VISIBLE);
62 | errorView.animate()
63 | .setInterpolator(new BounceInterpolator())
64 | .setStartDelay(500)
65 | .setDuration(500)
66 | .scaleX(1)
67 | .scaleY(1)
68 | .alpha(1)
69 | .start();
70 |
71 | actualView.setVisibility(GONE);
72 | }
73 |
74 | public void hideError() {
75 | errorView.setAlpha(0);
76 | errorView.setScaleX(0.5f);
77 | errorView.setScaleY(0.5f);
78 | errorView.setVisibility(GONE);
79 | actualView.setVisibility(VISIBLE);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/util/IntentFactory.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.util;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 |
6 | /**
7 | * A class for creating Intents, that can be intercepted during unit testing.
8 | * See the testMock/util/MockIntentFactory.java and testMock/PostDetailActivityTest.java under the mock Build Variant.
9 | */
10 | public class IntentFactory {
11 | static IntentFactory instance = new IntentFactory();
12 |
13 | IntentFactory() {}
14 |
15 | public static Intent createIntent(Context context, Class extends Context> clazz) {
16 | return IntentFactory.instance.makeIntent(context, clazz);
17 | }
18 |
19 | Intent makeIntent(Context context, Class extends Context> clazz) {
20 | return new Intent(context, clazz);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/blogreader/util/ScreenUtils.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.util;
2 |
3 | import android.content.Context;
4 | import android.util.DisplayMetrics;
5 | import android.view.WindowManager;
6 |
7 | /**
8 | * Created by ironman on 09/08/16.
9 | */
10 | public class ScreenUtils {
11 | public static int getScreenHeight(Context context) {
12 | DisplayMetrics displayMetrics = new DisplayMetrics();
13 | WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
14 | windowManager.getDefaultDisplay().getMetrics(displayMetrics);
15 |
16 | return displayMetrics.heightPixels;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_alert_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/drawable-hdpi/ic_alert_error.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_alert_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/drawable-mdpi/ic_alert_error.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_alert_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/drawable-xhdpi/ic_alert_error.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_alert_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/drawable-xxhdpi/ic_alert_error.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_alert_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/drawable-xxxhdpi/ic_alert_error.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_post_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
22 |
23 |
31 |
32 |
38 |
39 |
45 |
46 |
47 |
48 |
49 |
50 |
55 |
56 |
60 |
61 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_posts.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
24 |
25 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_comment.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
22 |
23 |
31 |
32 |
41 |
42 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_post.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
21 |
22 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
23 |
24 |
31 |
32 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #ECECEC
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BlogReader
3 | Oopsy daisy!
4 | For some reason, we couldn\'t load data for you. :(
5 | Try again!
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/mock/java/com/codemate/blogreader/FakeBlogApi.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader;
2 |
3 | import com.codemate.blogreader.data.network.BlogService;
4 | import com.codemate.blogreader.domain.model.BlogPost;
5 | import com.codemate.blogreader.domain.model.Comment;
6 |
7 | import java.util.Arrays;
8 | import java.util.List;
9 |
10 | import retrofit.http.Path;
11 | import retrofit.http.Query;
12 | import rx.Observable;
13 |
14 | /**
15 | * Created by ironman on 03/08/16.
16 | */
17 | public class FakeBlogApi implements BlogService.BlogApi {
18 | public static final List POSTS = Arrays.asList(
19 | new BlogPost(1, 69, "Example BlogPost One", "Example One BlogPost Body"),
20 | new BlogPost(2, 96, "Example BlogPost Two", "Example Two BlogPost Body"),
21 | new BlogPost(3, 96, "Example BlogPost Three", "Example Three BlogPost Body")
22 | );
23 |
24 | public static final List COMMENTS = Arrays.asList(
25 | new Comment(1, 1, "Jorma", "jor@ma", "This is a nice little comment."),
26 | new Comment(1, 2, "Jarmo", "jar@mo", "I like turtles."),
27 | new Comment(1, 3, "Tarmo", "tar@mo", "Ken Lee, tulibudibudouchoo.")
28 | );
29 |
30 | @Override
31 | public Observable> getPosts() {
32 | return Observable.just(POSTS);
33 | }
34 |
35 | @Override
36 | public Observable> getComments(@Query("postId") int postId) {
37 | return Observable.just(COMMENTS);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/mock/java/com/codemate/blogreader/injection/AppModule.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.injection;
2 |
3 | import android.app.Application;
4 |
5 | import com.codemate.blogreader.FakeBlogApi;
6 | import com.codemate.blogreader.MVPApplication;
7 | import com.codemate.blogreader.data.cache.SharedPrefCommentCache;
8 | import com.codemate.blogreader.data.cache.SharedPrefPostCache;
9 | import com.codemate.blogreader.domain.interactors.GetPostCommentsInteractor;
10 | import com.codemate.blogreader.domain.interactors.GetPostsInteractor;
11 |
12 | import javax.inject.Singleton;
13 |
14 | import dagger.Module;
15 | import dagger.Provides;
16 | import rx.schedulers.Schedulers;
17 |
18 | /**
19 | * Created by ironman on 05/08/16.
20 | */
21 | @Module
22 | public class AppModule {
23 | MVPApplication application;
24 |
25 | public AppModule(MVPApplication application) {
26 | this.application = application;
27 | }
28 |
29 | @Provides
30 | @Singleton
31 | protected Application provideApplication() {
32 | return application;
33 | }
34 |
35 | @Provides
36 | @Singleton
37 | GetPostsInteractor provideGetPosts() {
38 | return new GetPostsInteractor(
39 | new FakeBlogApi(),
40 | new SharedPrefPostCache(application),
41 | Schedulers.immediate(),
42 | Schedulers.immediate()
43 | );
44 | }
45 |
46 | @Provides
47 | @Singleton
48 | GetPostCommentsInteractor provideGetPostComments() {
49 | return new GetPostCommentsInteractor(
50 | new FakeBlogApi(),
51 | new SharedPrefCommentCache(application),
52 | Schedulers.immediate(),
53 | Schedulers.immediate()
54 | );
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/mock/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MOCK-BlogReader
3 |
4 |
--------------------------------------------------------------------------------
/app/src/prod/java/com/codemate/blogreader/injection/AppModule.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.injection;
2 |
3 | import android.app.Application;
4 |
5 | import com.codemate.blogreader.MVPApplication;
6 | import com.codemate.blogreader.data.cache.SharedPrefCommentCache;
7 | import com.codemate.blogreader.data.cache.SharedPrefPostCache;
8 | import com.codemate.blogreader.data.network.BlogService;
9 | import com.codemate.blogreader.domain.interactors.GetPostCommentsInteractor;
10 | import com.codemate.blogreader.domain.interactors.GetPostsInteractor;
11 |
12 | import javax.inject.Singleton;
13 |
14 | import dagger.Module;
15 | import dagger.Provides;
16 | import rx.android.schedulers.AndroidSchedulers;
17 | import rx.schedulers.Schedulers;
18 |
19 | /**
20 | * Created by ironman on 05/08/16.
21 | */
22 | @Module
23 | public class AppModule {
24 | MVPApplication application;
25 |
26 | public AppModule(MVPApplication application) {
27 | this.application = application;
28 | }
29 |
30 | @Provides
31 | @Singleton
32 | protected Application provideApplication() {
33 | return application;
34 | }
35 |
36 | @Provides
37 | @Singleton
38 | GetPostsInteractor provideGetPosts() {
39 | return new GetPostsInteractor(
40 | new BlogService().getApi(),
41 | new SharedPrefPostCache(application),
42 | Schedulers.newThread(),
43 | AndroidSchedulers.mainThread()
44 | );
45 | }
46 |
47 | @Provides
48 | @Singleton
49 | GetPostCommentsInteractor provideGetPostComments() {
50 | return new GetPostCommentsInteractor(
51 | new BlogService().getApi(),
52 | new SharedPrefCommentCache(application),
53 | Schedulers.newThread(),
54 | AndroidSchedulers.mainThread()
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/data/cache/converter/CommentJsonConverterTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache.converter;
2 |
3 | import com.codemate.blogreader.FakeBlogApi;
4 | import com.codemate.blogreader.domain.model.Comment;
5 |
6 | import org.junit.Test;
7 |
8 | import java.util.List;
9 |
10 | import static org.junit.Assert.assertEquals;
11 | import static org.junit.Assert.assertNotNull;
12 |
13 | /**
14 | * Created by ironman on 09/08/16.
15 | */
16 | public class CommentJsonConverterTest {
17 | @Test
18 | public void testConverterWorks() {
19 | CommentJsonConverter converter = new CommentJsonConverter();
20 |
21 | List original = FakeBlogApi.COMMENTS;
22 | String json = converter.convertCommentsToJson(original);
23 |
24 | assertNotNull(json);
25 |
26 | List converted = converter.convertJsonToComments(json);
27 |
28 | assertNotNull(converted);
29 | assertEquals(original.size(), converted.size());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/data/cache/converter/PostJsonConverterTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.data.cache.converter;
2 |
3 | import com.codemate.blogreader.FakeBlogApi;
4 | import com.codemate.blogreader.domain.model.BlogPost;
5 |
6 | import org.junit.Test;
7 |
8 | import java.util.List;
9 |
10 | import static org.junit.Assert.assertEquals;
11 | import static org.junit.Assert.assertNotNull;
12 |
13 | /**
14 | * Created by ironman on 09/08/16.
15 | */
16 | public class PostJsonConverterTest {
17 | @Test
18 | public void testConverterWorks() {
19 | PostJsonConverter converter = new PostJsonConverter();
20 |
21 | List original = FakeBlogApi.POSTS;
22 | String json = converter.convertPostsToJson(original);
23 |
24 | assertNotNull(json);
25 |
26 | List converted = converter.convertJsonToPosts(json);
27 |
28 | assertNotNull(converted);
29 | assertEquals(original.size(), converted.size());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/presentation/postdetails/PostDetailActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 |
6 | import com.codemate.blogreader.util.MockIntentFactory;
7 | import com.codemate.blogreader.domain.model.BlogPost;
8 |
9 | import org.junit.After;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.Mock;
13 | import org.mockito.MockitoAnnotations;
14 |
15 | import static org.mockito.Mockito.mock;
16 | import static org.mockito.Mockito.verify;
17 |
18 | /**
19 | * Created by ironman on 05/08/16.
20 | */
21 | public class PostDetailActivityTest {
22 | @Mock
23 | private Intent intent;
24 |
25 | @Mock
26 | private Context context;
27 |
28 | @Before
29 | public void setUp() {
30 | MockitoAnnotations.initMocks(this);
31 | MockIntentFactory.startSpyingIntents(intent);
32 | }
33 |
34 | @Test
35 | public void intentCreatedProperly() {
36 | BlogPost post = new BlogPost(69, 1, "Test Post", "Test post body.");
37 | Intent intent = PostDetailActivity.create(context, post);
38 |
39 | verify(intent).putExtra(PostDetailActivity.EXTRA_POST_USER_ID, post.userId);
40 | verify(intent).putExtra(PostDetailActivity.EXTRA_POST_ID, post.id);
41 | verify(intent).putExtra(PostDetailActivity.EXTRA_POST_TITLE, post.title);
42 | verify(intent).putExtra(PostDetailActivity.EXTRA_POST_BODY, post.body);
43 | }
44 |
45 | @After
46 | public void tearDown() {
47 | MockIntentFactory.stopSpyingIntents();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/presentation/postdetails/PostDetailPresenterTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.postdetails;
2 |
3 | import com.codemate.blogreader.FakeBlogApi;
4 | import com.codemate.blogreader.data.network.BlogService;
5 | import com.codemate.blogreader.domain.cache.CommentCache;
6 | import com.codemate.blogreader.domain.interactors.GetPostCommentsInteractor;
7 | import com.codemate.blogreader.domain.model.BlogPost;
8 | import com.codemate.blogreader.domain.model.Comment;
9 |
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.InOrder;
13 | import org.mockito.Mock;
14 | import org.mockito.MockitoAnnotations;
15 |
16 | import java.util.List;
17 |
18 | import rx.Observable;
19 | import rx.Subscriber;
20 | import rx.schedulers.Schedulers;
21 |
22 | import static org.mockito.Mockito.inOrder;
23 | import static org.mockito.Mockito.times;
24 | import static org.mockito.Mockito.verify;
25 | import static org.mockito.Mockito.when;
26 |
27 | /**
28 | * Created by ironman on 03/08/16.
29 | */
30 | public class PostDetailPresenterTest {
31 | @Mock
32 | private BlogService.BlogApi blogApi;
33 |
34 | @Mock
35 | private PostDetailView postDetailView;
36 |
37 | @Mock
38 | private CommentCache commentCache;
39 |
40 | private PostDetailPresenter postDetailPresenter;
41 |
42 | @Before
43 | public void setUp() {
44 | MockitoAnnotations.initMocks(this);
45 |
46 | GetPostCommentsInteractor getPostComments = new GetPostCommentsInteractor(
47 | blogApi,
48 | commentCache,
49 | Schedulers.immediate(),
50 | Schedulers.immediate()
51 | );
52 |
53 | postDetailPresenter = new PostDetailPresenter(postDetailView, getPostComments);
54 | }
55 |
56 | @Test
57 | public void loadComments_andDisplayInView() {
58 | when(blogApi.getComments(1))
59 | .thenReturn(Observable.just(FakeBlogApi.COMMENTS));
60 | postDetailPresenter.loadComments(1, false);
61 |
62 | // Verify that each of these methods get called at once, in the correct order.
63 | InOrder inOrder = inOrder(postDetailView, commentCache);
64 | inOrder.verify(postDetailView, times(1)).setProgressIndicator(true);
65 | inOrder.verify(commentCache, times(1)).getAll(1);
66 | inOrder.verify(postDetailView, times(1)).showComments(FakeBlogApi.COMMENTS);
67 | inOrder.verify(postDetailView, times(1)).setProgressIndicator(false);
68 | }
69 |
70 | @Test
71 | public void onError_ErrorDisplayedInUI() {
72 | final Throwable exception = new Exception();
73 | when(blogApi.getComments(1))
74 | .thenReturn(Observable.create(new Observable.OnSubscribe>() {
75 | @Override
76 | public void call(Subscriber super List> subscriber) {
77 | subscriber.onError(exception);
78 | }
79 | }));
80 |
81 | postDetailPresenter.loadComments(1, false);
82 | verify(postDetailView).onError(exception);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/presentation/posts/PostsPresenterTest.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.presentation.posts;
2 |
3 | import com.codemate.blogreader.FakeBlogApi;
4 | import com.codemate.blogreader.data.network.BlogService;
5 | import com.codemate.blogreader.domain.cache.PostCache;
6 | import com.codemate.blogreader.domain.interactors.GetPostsInteractor;
7 | import com.codemate.blogreader.domain.model.BlogPost;
8 |
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.mockito.InOrder;
12 | import org.mockito.Mock;
13 | import org.mockito.Mockito;
14 | import org.mockito.MockitoAnnotations;
15 |
16 | import java.util.List;
17 |
18 | import rx.Observable;
19 | import rx.Subscriber;
20 | import rx.schedulers.Schedulers;
21 |
22 | import static org.mockito.Mockito.inOrder;
23 | import static org.mockito.Mockito.mock;
24 | import static org.mockito.Mockito.times;
25 | import static org.mockito.Mockito.verify;
26 | import static org.mockito.Mockito.when;
27 |
28 | /**
29 | * Created by ironman on 26/07/16.
30 | */
31 | public class PostsPresenterTest {
32 | @Mock
33 | private BlogService.BlogApi blogApi;
34 |
35 | @Mock
36 | private PostsView postsView;
37 |
38 | @Mock
39 | private PostCache postCache;
40 |
41 | private PostsPresenter postsPresenter;
42 |
43 | @Before
44 | public void setUp() {
45 | MockitoAnnotations.initMocks(this);
46 |
47 | GetPostsInteractor getPosts = new GetPostsInteractor(
48 | blogApi,
49 | postCache,
50 | Schedulers.immediate(),
51 | Schedulers.immediate()
52 | );
53 |
54 | postsPresenter = new PostsPresenter(postsView, getPosts);
55 | }
56 |
57 | @Test
58 | public void loadPosts_AndDisplayInView() {
59 | when(blogApi.getPosts())
60 | .thenReturn(Observable.just(FakeBlogApi.POSTS));
61 | postsPresenter.loadPosts(false);
62 |
63 | // Verify that each of these methods get called once, in the correct order.
64 | InOrder inOrder = inOrder(postsView, postCache);
65 | inOrder.verify(postsView, times(1)).setProgressIndicator(true);
66 | inOrder.verify(postCache, times(1)).getAll();
67 | inOrder.verify(postsView, times(1)).showPosts(FakeBlogApi.POSTS);
68 | inOrder.verify(postsView, times(1)).setProgressIndicator(false);
69 | }
70 |
71 | @Test
72 | public void onError_ErrorDisplayedInUI() {
73 | final Throwable exception = new Exception();
74 | when(blogApi.getPosts())
75 | .thenReturn(Observable.create(new Observable.OnSubscribe>() {
76 | @Override
77 | public void call(Subscriber super List> subscriber) {
78 | subscriber.onError(exception);
79 | }
80 | }));
81 |
82 | postsPresenter.loadPosts(false);
83 | verify(postsView).showError(exception);
84 | }
85 |
86 | @Test
87 | public void clickOnPost_ShowsPostDetails() {
88 | BlogPost post = new BlogPost(69, 3, "Test post", "Test post Body.");
89 |
90 | postsPresenter.openPostDetails(post);
91 | verify(postsView).showPostDetails(post);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/testMock/java/com/codemate/blogreader/util/MockIntentFactory.java:
--------------------------------------------------------------------------------
1 | package com.codemate.blogreader.util;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 |
6 | import static org.mockito.Mockito.any;
7 | import static org.mockito.Mockito.mock;
8 | import static org.mockito.Mockito.when;
9 |
10 | /**
11 | * Created by ironman on 05/08/16.
12 | */
13 | public class MockIntentFactory {
14 | public static void startSpyingIntents(Intent intent) {
15 | IntentFactory.instance = mockIntentFactory(intent);
16 | }
17 |
18 | public static void stopSpyingIntents() {
19 | IntentFactory.instance = new IntentFactory();
20 | }
21 |
22 | private static IntentFactory mockIntentFactory(Intent intent) {
23 | IntentFactory mockIntentFactory = mock(IntentFactory.class);
24 |
25 | //noinspection unchecked
26 | when(
27 | mockIntentFactory.makeIntent(
28 | any(Context.class), (Class extends Context>) any()
29 | )
30 | ).thenReturn(intent);
31 |
32 | return mockIntentFactory;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/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 | google()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.2.1'
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | google()
19 | jcenter()
20 | }
21 | }
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
26 |
27 | ext {
28 | minSdkVersion = 16
29 | targetSdkVersion = 24
30 | compileSdkVersion = 24
31 | buildToolsVersion = '24.0.3'
32 |
33 | // test dependencies
34 | jUnit = 'junit:junit:4.12'
35 | mockito = 'org.mockito:mockito-all:1.10.19'
36 | hamcrest = 'org.hamcrest:hamcrest-all:1.3'
37 |
38 | supportVersion = '25.2.0'
39 | support = [
40 | appcompat: "com.android.support:appcompat-v7:$supportVersion",
41 | design: "com.android.support:design:$supportVersion",
42 | recyclerview: "com.android.support:recyclerview-v7:$supportVersion",
43 | cardview: "com.android.support:cardview-v7:$supportVersion"
44 | ]
45 |
46 | // android test dependencies
47 | testSupportVersion = '0.4.1'
48 | espressoVersion = '2.2.1'
49 | espressoExcludes = ['support-annotations', 'support-v4', 'support-v13', 'recyclerview-v7']
50 | supportTest = [
51 | runner: "com.android.support.test:runner:$testSupportVersion",
52 | rules: "com.android.support.test:rules:$testSupportVersion",
53 | espressoCore: "com.android.support.test.espresso:espresso-core:$espressoVersion",
54 | espressoContrib: "com.android.support.test.espresso:espresso-contrib:$espressoVersion",
55 | espressoIntents: "com.android.support.test.espresso:espresso-intents:$espressoVersion"
56 | ]
57 |
58 | // app dependencies
59 | daggerCompiler = 'com.google.dagger:dagger-compiler:2.5'
60 | dagger = 'com.google.dagger:dagger:2.5'
61 | jsr250 = 'javax.annotation:jsr250-api:1.0'
62 |
63 | retrofit = 'com.squareup.retrofit:retrofit:1.9.0'
64 | rxJava = 'io.reactivex:rxjava:1.0.4'
65 | rxAndroid = 'io.reactivex:rxandroid:0.24.0'
66 | }
67 |
--------------------------------------------------------------------------------
/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/CodemateLtd/android-architecture-and-testing-sample/a796120e38aa8a09f931165c37c0465a75cf1cdc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 20 15:18:10 EET 2018
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-4.6-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 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------