├── .gitignore
├── .idea
├── .name
├── codeStyleSettings.xml
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── hugo
│ │ └── mvvmsampleapplication
│ │ ├── OrientationChangeAction.java
│ │ └── features
│ │ ├── searchuser
│ │ └── SearchUserActivityTest.java
│ │ └── userdetails
│ │ └── UserDetailsActivityTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── hugo
│ │ │ └── mvvmsampleapplication
│ │ │ ├── app
│ │ │ └── MVVMApplication.java
│ │ │ ├── features
│ │ │ ├── BaseActivity.java
│ │ │ ├── DefaultSubscriber.java
│ │ │ ├── UseCase.java
│ │ │ ├── searchuser
│ │ │ │ ├── SearchUserActivity.java
│ │ │ │ ├── SearchUserFragment.java
│ │ │ │ ├── SearchUserUseCase.java
│ │ │ │ ├── SearchUserViewModel.java
│ │ │ │ ├── UserListAdapter.java
│ │ │ │ └── UserViewModel.java
│ │ │ └── userdetails
│ │ │ │ ├── LoadUserDetailsUseCase.java
│ │ │ │ ├── RepoViewModel.java
│ │ │ │ ├── RepositoriesAdapter.java
│ │ │ │ ├── UserDetailsActivity.java
│ │ │ │ ├── UserDetailsFragment.java
│ │ │ │ └── UserDetailsViewModel.java
│ │ │ ├── model
│ │ │ ├── entities
│ │ │ │ ├── Repository.java
│ │ │ │ └── User.java
│ │ │ └── network
│ │ │ │ ├── GitHubService.java
│ │ │ │ └── SearchResponse.java
│ │ │ └── util
│ │ │ ├── JobExecutor.java
│ │ │ ├── PostExecutionThread.java
│ │ │ ├── ThreadExecutor.java
│ │ │ ├── UiThread.java
│ │ │ └── dependencyinjection
│ │ │ ├── PerActivity.java
│ │ │ ├── components
│ │ │ ├── ApplicationComponent.java
│ │ │ └── UserComponent.java
│ │ │ └── modules
│ │ │ ├── ApplicationModule.java
│ │ │ └── UserModule.java
│ └── res
│ │ ├── drawable-hdpi
│ │ └── ic_search_white_36dp.png
│ │ ├── drawable-mdpi
│ │ ├── ic_search_white_36dp.png
│ │ ├── octocat.png
│ │ ├── placeholder.png
│ │ └── profile.jpg
│ │ ├── drawable-xhdpi
│ │ └── ic_search_white_36dp.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_search_white_36dp.png
│ │ ├── octocat.png
│ │ └── placeholder.png
│ │ ├── drawable-xxxhdpi
│ │ └── ic_search_white_36dp.png
│ │ ├── layout-large
│ │ └── activity_search_user.xml
│ │ ├── layout
│ │ ├── activity_search_user.xml
│ │ ├── activity_user_details.xml
│ │ ├── item_repo.xml
│ │ ├── item_user.xml
│ │ ├── search_user.xml
│ │ ├── user_details.xml
│ │ └── view_progressbar.xml
│ │ ├── menu
│ │ └── menu_main.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-v21
│ │ └── styles.xml
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── sharedTest
│ └── java
│ │ └── MockFactory.java
│ └── test
│ └── java
│ └── com
│ └── hugo
│ └── mvvmsampleapplication
│ ├── app
│ └── MVVMApplicationTest.java
│ ├── features
│ ├── UseCaseTest.java
│ ├── searchuser
│ │ ├── SearchUserUseCaseTest.java
│ │ ├── SearchUserViewModelTest.java
│ │ ├── UserListAdapterTest.java
│ │ └── UserViewModelTest.java
│ └── userdetails
│ │ ├── LoadUserDetailsUseCaseTest.java
│ │ ├── RepoViewModelTest.java
│ │ └── UserDetailsViewModelTest.java
│ └── util
│ ├── JobExecutorTest.java
│ └── UiThreadTest.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/.name:
--------------------------------------------------------------------------------
1 | MVVMSampleApplication
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
227 |
228 |
229 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.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 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'com.neenbedankt.android-apt'
3 |
4 | android {
5 | compileSdkVersion 23
6 | buildToolsVersion "23.0.2"
7 |
8 | dataBinding {
9 | enabled = true
10 | }
11 |
12 | defaultConfig {
13 | applicationId "com.hugo.mvvmsampleapplication"
14 | minSdkVersion 15
15 | targetSdkVersion 23
16 | versionCode 1
17 | versionName "1.0"
18 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 |
28 | sourceSets {
29 | String sharedTestDir = 'src/sharedTest/java'
30 | test {
31 | java.srcDir sharedTestDir
32 | }
33 | androidTest {
34 | java.srcDir sharedTestDir
35 | }
36 | }
37 |
38 | testOptions {
39 | unitTests.returnDefaultValues = true
40 | }
41 | }
42 |
43 | dependencies {
44 | compile fileTree(dir: 'libs', include: ['*.jar'])
45 |
46 | compile 'com.android.support:appcompat-v7:23.1.1'
47 | compile 'com.android.support:design:23.1.1'
48 | compile 'com.android.support:cardview-v7:23.1.1'
49 | compile 'com.android.support:recyclerview-v7:23.1.1'
50 | compile 'com.android.support:support-v4:23.1.1'
51 |
52 | compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
53 | compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
54 | compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'
55 |
56 | compile 'io.reactivex:rxandroid:1.0.1'
57 | compile 'com.jakewharton:butterknife:7.0.1'
58 | compile 'de.hdodenhof:circleimageview:2.0.0'
59 | compile 'com.squareup.picasso:picasso:2.5.2'
60 |
61 | //dagger
62 | compile 'com.google.dagger:dagger:2.0.2'
63 | apt "com.google.dagger:dagger-compiler:2.0.1"
64 | compile 'javax.annotation:jsr250-api:1.0'
65 |
66 | //dagger test
67 | androidTestCompile 'com.google.dagger:dagger:2.0.2'
68 | testApt "com.google.dagger:dagger-compiler:2.0.1"
69 | androidTestCompile 'javax.annotation:jsr250-api:1.0'
70 |
71 | testCompile 'org.mockito:mockito-core:1.10.19'
72 | testCompile 'junit:junit:4.12'
73 | testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
74 |
75 | androidTestCompile 'com.github.fabioCollini:DaggerMock:0.5'
76 | androidTestCompile 'junit:junit:4.12'
77 | androidTestCompile 'org.mockito:mockito-core:1.10.19'
78 | androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
79 | androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
80 | androidTestCompile 'com.android.support:support-annotations:23.1.1'
81 | androidTestCompile 'com.android.support.test:runner:0.4.1'
82 | androidTestCompile 'com.android.support.test:rules:0.4.1'
83 | androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
84 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
85 | androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') {
86 | exclude group: 'com.android.support', module: 'appcompat'
87 | exclude group: 'com.android.support', module: 'support-v4'
88 | exclude module: 'recyclerview-v7'
89 | }
90 | compile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
91 | testCompile 'org.robolectric:robolectric:3.0'
92 | }
93 |
--------------------------------------------------------------------------------
/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 /home/hugo/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/androidTest/java/com/hugo/mvvmsampleapplication/OrientationChangeAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2015 - Nathan Barraille
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 | package com.hugo.mvvmsampleapplication;
26 |
27 | import android.app.Activity;
28 | import android.content.pm.ActivityInfo;
29 | import android.support.test.espresso.UiController;
30 | import android.support.test.espresso.ViewAction;
31 | import android.view.View;
32 |
33 | import org.hamcrest.Matcher;
34 |
35 | import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
36 |
37 | /**
38 | * An Espresso ViewAction that changes the orientation of the screen
39 | */
40 | public class OrientationChangeAction implements ViewAction {
41 | private final int orientation;
42 |
43 | private OrientationChangeAction(int orientation) {
44 | this.orientation = orientation;
45 | }
46 |
47 | @Override
48 | public Matcher getConstraints() {
49 | return isRoot();
50 | }
51 |
52 | @Override
53 | public String getDescription() {
54 | return "change orientation to " + orientation;
55 | }
56 |
57 | @Override
58 | public void perform(UiController uiController, View view) {
59 | uiController.loopMainThreadUntilIdle();
60 | final Activity activity = (Activity) view.getContext();
61 | activity.setRequestedOrientation(orientation);
62 | }
63 |
64 | public static ViewAction orientationLandscape() {
65 | return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
66 | }
67 |
68 | public static ViewAction orientationPortrait() {
69 | return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
70 | }
71 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.support.test.InstrumentationRegistry;
4 | import android.support.test.espresso.contrib.RecyclerViewActions;
5 | import android.support.test.rule.ActivityTestRule;
6 | import android.support.test.runner.AndroidJUnit4;
7 | import android.test.suitebuilder.annotation.LargeTest;
8 | import com.hugo.mvvmsampleapplication.MockFactory;
9 | import com.hugo.mvvmsampleapplication.R;
10 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
11 | import com.hugo.mvvmsampleapplication.model.entities.User;
12 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
13 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
14 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.ApplicationComponent;
15 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.ApplicationModule;
16 | import it.cosenonjaviste.daggermock.DaggerMockRule;
17 | import java.util.List;
18 | import org.junit.Before;
19 | import org.junit.Rule;
20 | import org.junit.Test;
21 | import org.junit.runner.RunWith;
22 | import org.mockito.Mock;
23 | import retrofit.HttpException;
24 | import retrofit.Response;
25 | import rx.Observable;
26 |
27 | import static android.support.test.espresso.Espresso.onView;
28 | import static android.support.test.espresso.action.ViewActions.click;
29 | import static android.support.test.espresso.action.ViewActions.typeText;
30 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
31 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
32 | import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
33 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
34 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
35 | import static com.hugo.mvvmsampleapplication.OrientationChangeAction.orientationLandscape;
36 | import static org.hamcrest.Matchers.not;
37 | import static org.hamcrest.core.AllOf.allOf;
38 | import static org.mockito.Mockito.when;
39 |
40 | @RunWith(AndroidJUnit4.class)
41 | @LargeTest
42 | public class SearchUserActivityTest {
43 |
44 | @Rule public DaggerMockRule daggerMockRule =
45 | new DaggerMockRule<>(ApplicationComponent.class, new ApplicationModule()).set(
46 | new DaggerMockRule.ComponentSetter() {
47 | @Override
48 | public void setComponent(ApplicationComponent applicationComponent) {
49 | getApp().setApplicationComponent(applicationComponent);
50 | }
51 | });
52 |
53 | @Rule public final ActivityTestRule activityTestRule =
54 | new ActivityTestRule<>(SearchUserActivity.class, false, false);
55 |
56 | @Mock GitHubService mockGitHubService;
57 |
58 | private MVVMApplication getApp() {
59 | return (MVVMApplication) InstrumentationRegistry.getInstrumentation()
60 | .getTargetContext()
61 | .getApplicationContext();
62 | }
63 |
64 | @Before
65 | public void setUp() {
66 | setUpMockGitHubService();
67 | activityTestRule.launchActivity(null);
68 | }
69 |
70 | @SuppressWarnings("unchecked")
71 | private void setUpMockGitHubService() {
72 | when(mockGitHubService.searchUser(MockFactory.TEST_USERNAME)).thenReturn(
73 | Observable.just(MockFactory.buildMockSearchResponse()));
74 | when(mockGitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME)).thenReturn(
75 | Observable.just(MockFactory.buildMockUserDetailsResponse()));
76 | when(mockGitHubService.searchUser(MockFactory.TEST_USERNAME_NO_RESULTS)).thenReturn(
77 | Observable.just(MockFactory.buildEmptyMockSearchResponse()));
78 |
79 | HttpException mockHttpException = new HttpException(Response.error(404, null));
80 | when(mockGitHubService.searchUser(MockFactory.TEST_USERNAME_ERROR)).thenReturn(Observable.error(mockHttpException));
81 | }
82 |
83 | @Test
84 | public void clickSearchWithNoQueryInputShouldReturnErrorMessage() throws Exception {
85 | onView(withId(R.id.button_search)).perform(click());
86 | onView(allOf(withId(android.support.design.R.id.snackbar_text),
87 | withText("Enter a username"))).check(matches(isDisplayed()));
88 | }
89 |
90 | @Test
91 | public void shouldDisplayNetworkErrorInSnackBar() throws Exception {
92 | performSearch(MockFactory.TEST_USERNAME_ERROR);
93 | onView(withId(android.support.design.R.id.snackbar_text)).check(
94 | matches(withText("Error loading users")));
95 | }
96 |
97 | @Test
98 | public void shouldDisplayNoUserErrorInSnackBar() throws Exception {
99 | performSearch(MockFactory.TEST_USERNAME_NO_RESULTS);
100 | onView(withId(android.support.design.R.id.snackbar_text)).check(
101 | matches(withText("No users found")));
102 | }
103 |
104 | @Test
105 | public void searchResultsShouldBeHiddenIfNoSearch() throws Exception {
106 | onView(withId(R.id.search_user_list)).check(matches(not(isDisplayed())));
107 | }
108 |
109 | @Test
110 | public void clickSearchShouldDisplayResults() throws Exception {
111 | performSearch(MockFactory.TEST_USERNAME);
112 | onView(withId(R.id.search_user_list)).check(matches(isDisplayed()));
113 | }
114 |
115 | @Test
116 | public void clickOnSearchResultItemShouldDisplayUserDetails() throws Exception {
117 | performSearch(MockFactory.TEST_USERNAME);
118 | onView(withId(R.id.search_user_list)).perform(
119 | RecyclerViewActions.actionOnItemAtPosition(0, click()));
120 | onView(withText(MockFactory.TEST_REPOSITORY)).check(matches(isDisplayed()));
121 | }
122 |
123 | @Test
124 | public void searchQueryTextShouldPersistAfterOrientationChange() throws Exception {
125 | onView(withId(R.id.edit_text_username)).perform(typeText("test"));
126 | onView(isRoot()).perform(orientationLandscape());
127 | onView(withId(R.id.edit_text_username)).check(matches(withText("test")));
128 | }
129 |
130 | @Test
131 | public void searchResultsShouldPersistAfterOrientationChange() throws Exception {
132 | performSearch(MockFactory.TEST_USERNAME);
133 | onView(isRoot()).perform(orientationLandscape());
134 | onView(withId(R.id.search_user_list)).check(matches(isDisplayed()));
135 | }
136 |
137 | private void performSearch(String query) {
138 | onView(withId(R.id.edit_text_username)).perform(typeText(query));
139 | onView(withId(R.id.button_search)).perform(click());
140 | }
141 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/hugo/mvvmsampleapplication/features/userdetails/UserDetailsActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.content.Intent;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.rule.ActivityTestRule;
6 | import android.support.test.runner.AndroidJUnit4;
7 | import android.test.suitebuilder.annotation.LargeTest;
8 | import com.hugo.mvvmsampleapplication.MockFactory;
9 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
10 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
11 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.ApplicationComponent;
12 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.ApplicationModule;
13 | import it.cosenonjaviste.daggermock.DaggerMockRule;
14 | import org.junit.Before;
15 | import org.junit.Rule;
16 | import org.junit.Test;
17 | import org.junit.runner.RunWith;
18 | import org.mockito.Mock;
19 | import rx.Observable;
20 |
21 | import static android.support.test.espresso.Espresso.onView;
22 | import static android.support.test.espresso.assertion.ViewAssertions.matches;
23 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
24 | import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
25 | import static android.support.test.espresso.matcher.ViewMatchers.withId;
26 | import static android.support.test.espresso.matcher.ViewMatchers.withText;
27 | import static com.hugo.mvvmsampleapplication.OrientationChangeAction.orientationLandscape;
28 | import static org.mockito.Mockito.when;
29 |
30 | @RunWith(AndroidJUnit4.class)
31 | @LargeTest
32 | public class UserDetailsActivityTest {
33 |
34 | @Rule public DaggerMockRule daggerMockRule =
35 | new DaggerMockRule<>(ApplicationComponent.class, new ApplicationModule()).set(
36 | new DaggerMockRule.ComponentSetter() {
37 | @Override
38 | public void setComponent(ApplicationComponent applicationComponent) {
39 | getApp().setApplicationComponent(applicationComponent);
40 | }
41 | });
42 |
43 | @Rule public final ActivityTestRule activityTestRule =
44 | new ActivityTestRule<>(UserDetailsActivity.class, false, false);
45 |
46 | @Mock GitHubService mockGitHubService;
47 |
48 | private MVVMApplication getApp() {
49 | return (MVVMApplication) InstrumentationRegistry.getInstrumentation()
50 | .getTargetContext()
51 | .getApplicationContext();
52 | }
53 |
54 | @Before
55 | public void setUp() {
56 | setUpMockGitHubService();
57 | }
58 |
59 | @SuppressWarnings("unchecked")
60 | private void setUpMockGitHubService() {
61 | when(mockGitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME)).thenReturn(
62 | Observable.just(MockFactory.buildMockUserDetailsResponse()));
63 | Observable error = Observable.error(new Throwable("Error"));
64 | when(mockGitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME_ERROR)).thenReturn(
65 | error);
66 | when(
67 | mockGitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME_NO_RESULTS)).thenReturn(
68 | Observable.just(MockFactory.buildEmptyRepositoryList()));
69 | }
70 |
71 | @Test
72 | public void shouldDisplayRepositories() throws Exception {
73 | activityTestRule.launchActivity(buildIntent(MockFactory.TEST_USERNAME));
74 | onView(withText(MockFactory.TEST_REPOSITORY)).check(matches(isDisplayed()));
75 | }
76 |
77 | @Test
78 | public void shouldDisplayRepositoriesAfterConfigurationChange() throws Exception {
79 | activityTestRule.launchActivity(buildIntent(MockFactory.TEST_USERNAME));
80 | onView(isRoot()).perform(orientationLandscape());
81 | onView(withText(MockFactory.TEST_REPOSITORY)).check(matches(isDisplayed()));
82 | }
83 |
84 | @Test
85 | public void shouldDisplayNetworkErrorInSnackBar() throws Exception {
86 | activityTestRule.launchActivity(buildIntent(MockFactory.TEST_USERNAME_ERROR));
87 | onView(withId(android.support.design.R.id.snackbar_text)).check(matches(withText("Error loading repositories")));
88 | }
89 |
90 | @Test
91 | public void shouldDisplayEmptyRepositoriesErrorInSnackBar() throws Exception {
92 | activityTestRule.launchActivity(buildIntent(MockFactory.TEST_USERNAME_NO_RESULTS));
93 | onView(withId(android.support.design.R.id.snackbar_text)).check(matches(withText("No public repositories")));
94 | }
95 |
96 | private Intent buildIntent(String username) {
97 | Intent intent = new Intent();
98 | intent.putExtra("USERNAME", username);
99 | return intent;
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/app/MVVMApplication.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.app;
2 |
3 | import android.app.Application;
4 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.ApplicationComponent;
5 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.DaggerApplicationComponent;
6 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.ApplicationModule;
7 |
8 | /**
9 | * Handles singleton instances which the rest of the system uses.
10 | */
11 | public class MVVMApplication extends Application {
12 |
13 | private ApplicationComponent applicationComponent;
14 |
15 | @Override
16 | public void onCreate() {
17 | super.onCreate();
18 | if(applicationComponent == null) {
19 | applicationComponent =
20 | DaggerApplicationComponent.builder().applicationModule(new ApplicationModule()).build();
21 | }
22 | }
23 |
24 | public void setApplicationComponent(ApplicationComponent applicationComponent) {
25 | this.applicationComponent = applicationComponent;
26 | }
27 |
28 | public ApplicationComponent getApplicationComponent() {
29 | return applicationComponent;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.support.v4.app.FragmentTransaction;
6 | import android.support.v7.app.AppCompatActivity;
7 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
8 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.ApplicationComponent;
9 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.DaggerUserComponent;
10 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.UserComponent;
11 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.UserModule;
12 |
13 | /**
14 | * Base class used by other activities.
15 | */
16 | public abstract class BaseActivity extends AppCompatActivity {
17 |
18 | private UserComponent userComponent;
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | getApplicationComponent().inject(this);
24 | initializeInject();
25 | }
26 |
27 | protected void addFragment(int containerViewId, Fragment fragment) {
28 | FragmentTransaction fragmentTransaction = this.getSupportFragmentManager().beginTransaction();
29 | fragmentTransaction.replace(containerViewId, fragment);
30 | fragmentTransaction.commit();
31 | }
32 |
33 | public ApplicationComponent getApplicationComponent() {
34 | return ((MVVMApplication) getApplication()).getApplicationComponent();
35 | }
36 |
37 | private void initializeInject() {
38 | userComponent = DaggerUserComponent.builder()
39 | .applicationComponent(getApplicationComponent())
40 | .userModule(new UserModule()).build();
41 | }
42 |
43 | public UserComponent getUserComponent() {
44 | return userComponent;
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/DefaultSubscriber.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features;
2 |
3 | /**
4 | * Default subscriber class implemented in ViewModels.
5 | */
6 | public class DefaultSubscriber extends rx.Subscriber {
7 | @Override public void onCompleted() {
8 | // no-op by default.
9 | }
10 |
11 | @Override public void onError(Throwable e) {
12 | // no-op by default.
13 | }
14 |
15 | @Override public void onNext(T t) {
16 | // no-op by default.
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/UseCase.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features;
2 |
3 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
4 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
5 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
6 | import java.util.concurrent.TimeUnit;
7 | import rx.Observable;
8 | import rx.Subscriber;
9 | import rx.Subscription;
10 | import rx.subscriptions.Subscriptions;
11 |
12 | /**
13 | * Abstract class handling execution of use cases.
14 | */
15 | public abstract class UseCase {
16 |
17 | private final GitHubService gitHubService;
18 | private final ThreadExecutor threadExecutor;
19 | private final PostExecutionThread postExecutionThread;
20 | private Subscription subscription = Subscriptions.empty();
21 |
22 | public UseCase(GitHubService gitHubService, ThreadExecutor threadExecutor,
23 | PostExecutionThread postExecutionThread) {
24 | this.gitHubService = gitHubService;
25 | this.threadExecutor = threadExecutor;
26 | this.postExecutionThread = postExecutionThread;
27 | }
28 |
29 | public GitHubService getGitHubService() {
30 | return gitHubService;
31 | }
32 |
33 | public Subscription getSubscription() {
34 | return subscription;
35 | }
36 |
37 | public abstract Observable buildUseCase(String query);
38 |
39 | @SuppressWarnings("unchecked")
40 | public void execute(Subscriber useCaseSubscriber, String query) {
41 | subscription = buildUseCase(query)
42 | .observeOn(postExecutionThread.getScheduler())
43 | .subscribeOn(threadExecutor.getScheduler())
44 | .subscribe(useCaseSubscriber);
45 | }
46 |
47 | public void unsubscribe() {
48 | if (subscription != null && !subscription.isUnsubscribed()) {
49 | subscription.unsubscribe();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserActivity.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.os.Bundle;
4 |
5 | import android.util.Log;
6 | import com.hugo.mvvmsampleapplication.R;
7 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
8 | import com.hugo.mvvmsampleapplication.features.BaseActivity;
9 | import com.hugo.mvvmsampleapplication.features.userdetails.UserDetailsActivity;
10 | import com.hugo.mvvmsampleapplication.features.userdetails.UserDetailsFragment;
11 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.DaggerApplicationComponent;
12 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.DaggerUserComponent;
13 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.UserComponent;
14 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.UserModule;
15 | import com.squareup.leakcanary.RefWatcher;
16 |
17 | /**
18 | * Sets the content view to activity_search_user which host SearchUserFragment and/or
19 | * UserDetailsFragment. In order to communicate between the two fragments this class
20 | * implements SearchUserFragment.ActivityListener and implements startUserDetails. If
21 | * UserDetailsFragment is currently in the view, loading repositories directly is possible,
22 | * otherwise a new UserDetailsActivity is started.
23 | */
24 | public class SearchUserActivity extends BaseActivity
25 | implements SearchUserFragment.ActivityListener {
26 |
27 | private static final String TAG = "SearchUserActivity";
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.activity_search_user);
33 | if (savedInstanceState == null) {
34 | addFragment(R.id.content_activity_search_user, SearchUserFragment.newInstance());
35 | }
36 | }
37 |
38 | @Override
39 | public void startUserDetails(String username) {
40 | UserDetailsFragment userDetailsFragment =
41 | (UserDetailsFragment) getSupportFragmentManager().findFragmentById(
42 | R.id.userDetailsFragment);
43 | if (userDetailsFragment == null) {
44 | Log.d(TAG, "starting activity, no fragment in view");
45 | startActivity(UserDetailsActivity.newIntent(this, username));
46 | } else {
47 | Log.d(TAG, "fragment in view");
48 | userDetailsFragment.loadRepositories(username);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserFragment.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import android.support.design.widget.Snackbar;
6 | import android.support.v4.app.Fragment;
7 | import android.support.v7.widget.LinearLayoutManager;
8 | import android.support.v7.widget.RecyclerView;
9 | import android.util.Log;
10 | import android.view.LayoutInflater;
11 | import android.view.View;
12 | import android.view.ViewGroup;
13 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
14 | import com.hugo.mvvmsampleapplication.databinding.SearchUserBinding;
15 | import com.hugo.mvvmsampleapplication.features.BaseActivity;
16 | import com.hugo.mvvmsampleapplication.model.entities.User;
17 | import com.squareup.leakcanary.RefWatcher;
18 | import java.util.List;
19 | import javax.inject.Inject;
20 |
21 | /**
22 | * A passive view with the purpose of setting up UI and communicating with the
23 | * ActivityListener. Implements SearchUserViewModel.FragmentListener in order to receive calls
24 | * from {@link SearchUserViewModel}.
25 | */
26 | public class SearchUserFragment extends Fragment implements SearchUserViewModel.FragmentListener {
27 |
28 | private static final String TAG = "SearchUserFragment";
29 |
30 | @Inject SearchUserViewModel searchUserViewModel;
31 | private SearchUserBinding binding;
32 | private ActivityListener activityListener;
33 | private UserListAdapter userListAdapter;
34 |
35 | public interface ActivityListener {
36 | void startUserDetails(String username);
37 | }
38 |
39 | public SearchUserFragment() {
40 | super();
41 | }
42 |
43 | public static SearchUserFragment newInstance() {
44 | return new SearchUserFragment();
45 | }
46 |
47 | @Override
48 | public void onAttach(Context context) {
49 | super.onAttach(context);
50 | if (context instanceof ActivityListener) {
51 | this.activityListener = (ActivityListener) context;
52 | }
53 | }
54 |
55 | @Override
56 | public void onCreate(Bundle savedInstanceState) {
57 | super.onCreate(savedInstanceState);
58 | setRetainInstance(true);
59 | ((BaseActivity)getActivity()).getUserComponent().inject(this);
60 | }
61 |
62 | @Override
63 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
64 | Bundle savedInstanceState) {
65 | searchUserViewModel.setFragmentListener(this);
66 | binding = SearchUserBinding.inflate(inflater, container, false);
67 | binding.setViewModel(searchUserViewModel);
68 | setupUserList(binding.searchUserList);
69 | return binding.getRoot();
70 | }
71 |
72 | private void setupUserList(RecyclerView searchUserList) {
73 | if (userListAdapter == null) {
74 | userListAdapter = new UserListAdapter();
75 | userListAdapter.setOnItemClickListener(onItemClickListener);
76 | }
77 | searchUserList.setAdapter(userListAdapter);
78 | searchUserList.setLayoutManager(new LinearLayoutManager(getActivity()));
79 | }
80 |
81 | private UserListAdapter.OnItemClickListener onItemClickListener =
82 | new UserListAdapter.OnItemClickListener() {
83 | @Override
84 | public void onItemClick(String username) {
85 | if (activityListener != null) {
86 | activityListener.startUserDetails(username);
87 | }
88 | }
89 | };
90 |
91 | @Override
92 | public void addUsers(List users) {
93 | userListAdapter = (UserListAdapter) binding.searchUserList.getAdapter();
94 | userListAdapter.setUsers(users);
95 | userListAdapter.notifyDataSetChanged();
96 | }
97 |
98 | @Override
99 | public void showMessage(String message) {
100 | View rootView = getActivity().getWindow().getDecorView().findViewById(android.R.id.content);
101 | Snackbar snackbar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT);
102 | snackbar.show();
103 | }
104 |
105 | @Override
106 | public void onDetach() {
107 | super.onDetach();
108 | activityListener = null;
109 | }
110 |
111 | @Override
112 | public void onDestroyView() {
113 | super.onDestroyView();
114 | searchUserViewModel.destroy(false);
115 | binding.unbind();
116 | // binding.executePendingBindings();
117 | // binding.invalidateAll();
118 | }
119 |
120 | @Override
121 | public void onDestroy() {
122 | super.onDestroy();
123 | searchUserViewModel.destroy(true);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserUseCase.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import com.hugo.mvvmsampleapplication.features.UseCase;
4 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
5 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
6 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
7 | import javax.inject.Inject;
8 | import rx.Observable;
9 |
10 | /**
11 | * Use case for searching for users.
12 | */
13 | public class SearchUserUseCase extends UseCase {
14 |
15 | @Inject
16 | public SearchUserUseCase(GitHubService gitHubService, ThreadExecutor threadExecutor,
17 | PostExecutionThread postExecutionThread) {
18 | super(gitHubService, threadExecutor, postExecutionThread);
19 | }
20 |
21 | @Override
22 | public Observable buildUseCase(String username) throws NullPointerException {
23 | if (username == null) {
24 | throw new NullPointerException("Query must not be null");
25 | }
26 | return getGitHubService().searchUser(username);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserViewModel.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.databinding.BaseObservable;
4 | import android.databinding.BindingAdapter;
5 | import android.databinding.ObservableInt;
6 | import android.text.Editable;
7 | import android.text.TextWatcher;
8 | import android.util.Log;
9 | import android.view.KeyEvent;
10 | import android.view.View;
11 | import android.view.inputmethod.EditorInfo;
12 | import android.widget.TextView;
13 |
14 | import com.hugo.mvvmsampleapplication.R;
15 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
16 | import com.hugo.mvvmsampleapplication.features.UseCase;
17 | import com.hugo.mvvmsampleapplication.model.entities.User;
18 | import com.hugo.mvvmsampleapplication.features.DefaultSubscriber;
19 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
20 |
21 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.PerActivity;
22 | import com.squareup.leakcanary.RefWatcher;
23 | import java.util.List;
24 | import javax.inject.Inject;
25 | import javax.inject.Named;
26 | import rx.Subscriber;
27 |
28 | /**
29 | * Bound to the view search_user.xml. All user interactions are handled by this class and updates
30 | * UI elements depending on the action. SearchUserViewModel fetches data from the model via
31 | * {@link SearchUserUseCase}, and when data is received in {@link SearchUserSubscriber} the view is
32 | * updated
33 | * accordingly. Since the list displaying results has its own ViewModel, this class has to
34 | * send the result back to the FragmentListener which can update the adapter in the fragment.
35 | */
36 | @PerActivity
37 | public class SearchUserViewModel extends BaseObservable {
38 |
39 | public final ObservableInt progressVisibility;
40 | public final ObservableInt userListVisibility;
41 |
42 | private FragmentListener fragmentListener;
43 | private String username;
44 | private UseCase searchUserUseCase;
45 |
46 | @Inject
47 | public SearchUserViewModel(@Named("searchUser") UseCase searchUserUseCase) {
48 | this.searchUserUseCase = searchUserUseCase;
49 | progressVisibility = new ObservableInt(View.INVISIBLE);
50 | userListVisibility = new ObservableInt(View.INVISIBLE);
51 | }
52 |
53 | public void setFragmentListener(FragmentListener fragmentListener) {
54 | this.fragmentListener = fragmentListener;
55 | }
56 |
57 | public TextWatcher getUsernameTextWatcher() {
58 | return new TextWatcher() {
59 | @Override
60 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
61 |
62 | }
63 |
64 | @Override
65 | public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
66 | username = charSequence.toString();
67 | }
68 |
69 | @Override
70 | public void afterTextChanged(Editable s) {
71 |
72 | }
73 | };
74 | }
75 |
76 | public boolean onSearchAction(TextView view, int actionId, KeyEvent event) {
77 | if (actionId == EditorInfo.IME_ACTION_SEARCH) {
78 | loadUsers();
79 | return true;
80 | }
81 | return false;
82 | }
83 |
84 | public void onClickSearch(View view) {
85 | loadUsers();
86 | }
87 |
88 | private void loadUsers() {
89 | if (username == null) {
90 | fragmentListener.showMessage("Enter a username");
91 | } else {
92 | showProgressIndicator(true);
93 | searchUserUseCase.execute(new SearchUserSubscriber(), username);
94 | }
95 | }
96 |
97 | private final class SearchUserSubscriber extends DefaultSubscriber {
98 |
99 | @Override
100 | public void onCompleted() {
101 | showProgressIndicator(false);
102 | }
103 |
104 | @Override
105 | public void onError(Throwable e) {
106 | showProgressIndicator(false);
107 | fragmentListener.showMessage("Error loading users");
108 | }
109 |
110 | @Override
111 | public void onNext(SearchResponse searchResponse) {
112 | List users = searchResponse.getUsers();
113 | if (users.isEmpty()) {
114 | fragmentListener.showMessage("No users found");
115 | } else {
116 | fragmentListener.addUsers(users);
117 | }
118 | }
119 | }
120 |
121 | private void showProgressIndicator(boolean showProgress) {
122 | if(showProgress) {
123 | progressVisibility.set(View.VISIBLE);
124 | userListVisibility.set(View.INVISIBLE);
125 | } else {
126 | userListVisibility.set(View.VISIBLE);
127 | progressVisibility.set(View.INVISIBLE);
128 | }
129 | }
130 |
131 | public void destroy(Boolean unsubsricbe) {
132 | fragmentListener = null;
133 | if(unsubsricbe) {
134 | searchUserUseCase.unsubscribe();
135 | }
136 | }
137 |
138 | public void setUsername(String username) {
139 | this.username = username;
140 | }
141 |
142 | public ObservableInt getProgressVisibility() {
143 | return progressVisibility;
144 | }
145 |
146 | public ObservableInt getUserListVisibility() {
147 | return userListVisibility;
148 | }
149 |
150 | public interface FragmentListener {
151 |
152 | void addUsers(List users);
153 |
154 | void showMessage(String message);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/UserListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import com.hugo.mvvmsampleapplication.R;
9 | import com.hugo.mvvmsampleapplication.databinding.ItemUserBinding;
10 | import com.hugo.mvvmsampleapplication.model.entities.User;
11 | import java.util.Collections;
12 | import java.util.List;
13 |
14 | /**
15 | * Adapter for the list displayed in search_user.xml. This adapter uses the {@link UserViewModel}
16 | * to
17 | * bind
18 | * data to the view. The onClickListener needs to be handled by this adapter (not the
19 | * UserViewModel) in order to communicate with the {@link SearchUserFragment} which in turn calls
20 | * its ActivityListener.
21 | */
22 | public class UserListAdapter extends RecyclerView.Adapter {
23 |
24 | private List users;
25 | private OnItemClickListener onItemClickListener;
26 |
27 | public interface OnItemClickListener {
28 | void onItemClick(String username);
29 | }
30 |
31 | public UserListAdapter() {
32 | this.users = Collections.emptyList();
33 | }
34 |
35 | public void setUsers(List users) {
36 | this.users = users;
37 | }
38 |
39 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
40 | this.onItemClickListener = onItemClickListener;
41 | }
42 |
43 | @Override
44 | public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
45 | final ItemUserBinding binding =
46 | ItemUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
47 | return new UserViewHolder(binding);
48 | }
49 |
50 | @Override
51 | public void onBindViewHolder(UserViewHolder userViewHolder, int position) {
52 | final User user = users.get(position);
53 | userViewHolder.bindUser(user);
54 | userViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
55 | @Override
56 | public void onClick(View view) {
57 | if (onItemClickListener != null) {
58 | onItemClickListener.onItemClick(user.getLogin());
59 | }
60 | }
61 | });
62 | }
63 |
64 | @Override
65 | public int getItemCount() {
66 | return users.size();
67 | }
68 |
69 | public class UserViewHolder extends RecyclerView.ViewHolder {
70 | final ItemUserBinding binding;
71 |
72 | public UserViewHolder(ItemUserBinding binding) {
73 | super(binding.getRoot());
74 | this.binding = binding;
75 | }
76 |
77 | void bindUser(User user) {
78 | if (binding.getViewModel() == null) {
79 | binding.setViewModel(new UserViewModel(user));
80 | binding.executePendingBindings();
81 | } else {
82 | binding.getViewModel().setUser(user);
83 | }
84 | }
85 |
86 | public String getUsername() {
87 | return binding.getViewModel().getUsername();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/searchuser/UserViewModel.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.databinding.BaseObservable;
4 | import android.databinding.BindingAdapter;
5 | import android.widget.ImageView;
6 | import com.hugo.mvvmsampleapplication.R;
7 | import com.hugo.mvvmsampleapplication.model.entities.User;
8 | import com.squareup.picasso.Picasso;
9 |
10 | /**
11 | * Bound to the view item_user.xml.
12 | */
13 | public class UserViewModel extends BaseObservable {
14 |
15 | private User user;
16 |
17 | public UserViewModel(User user) {
18 | this.user = user;
19 | }
20 |
21 | public String getUsername() {
22 | return user.getLogin();
23 | }
24 |
25 | public String getImageUrl() {
26 | return user.getAvatarUrl();
27 | }
28 |
29 | @BindingAdapter({ "bind:imageUrl" })
30 | public static void loadImage(ImageView view, String imageUrl) {
31 | Picasso.with(view.getContext()).load(imageUrl).placeholder(R.drawable.placeholder).fit().into(view);
32 | }
33 |
34 | public void setUser(User user) {
35 | this.user = user;
36 | notifyChange();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/LoadUserDetailsUseCase.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import com.hugo.mvvmsampleapplication.features.UseCase;
4 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
5 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
6 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
7 | import javax.inject.Inject;
8 | import rx.Observable;
9 |
10 | /**
11 | * Use case for loading a users repository.
12 | */
13 | public class LoadUserDetailsUseCase extends UseCase {
14 |
15 | @Inject
16 | public LoadUserDetailsUseCase(GitHubService gitHubService, ThreadExecutor threadExecutor,
17 | PostExecutionThread postExecutionThread) {
18 | super(gitHubService, threadExecutor, postExecutionThread);
19 | }
20 |
21 | @Override public Observable buildUseCase(String username) throws NullPointerException {
22 | if (username == null) {
23 | throw new NullPointerException("Username must not be null");
24 | }
25 | return getGitHubService().getRepositoriesFromUser(username);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/RepoViewModel.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.databinding.BaseObservable;
4 |
5 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
6 |
7 | /**
8 | * Bound to the view item_repo.xml.
9 | */
10 | public class RepoViewModel extends BaseObservable {
11 |
12 | private Repository repository;
13 |
14 | public RepoViewModel(Repository repository) {
15 | this.repository = repository;
16 | }
17 |
18 | public void setRepository(Repository repository) {
19 | this.repository = repository;
20 | notifyChange();
21 | }
22 |
23 | public String getRepoTitle() {
24 | return repository.getName();
25 | }
26 |
27 | public String getRepoDescription() {
28 | return repository.getDescription();
29 | }
30 |
31 | public String getWatchers() {
32 | String watchersString = Integer.toString(repository.getWatchers()) + "\n Watchers";
33 | return watchersString;
34 | }
35 |
36 | public String getStars() {
37 | String starsString = Integer.toString(repository.getStars()) + "\n Stars";
38 | return starsString;
39 | }
40 |
41 | public String getForks() {
42 | String forksString = Integer.toString(repository.getForks()) + "\n Forks";
43 | return forksString;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/RepositoriesAdapter.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.ViewGroup;
7 |
8 | import com.hugo.mvvmsampleapplication.R;
9 | import com.hugo.mvvmsampleapplication.databinding.ItemRepoBinding;
10 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
11 |
12 | import java.util.Collections;
13 | import java.util.List;
14 |
15 | /**
16 | * Adapter for the list displayed in user_details.xml. This adapter uses the {@link RepoViewModel}
17 | * to bind
18 | * data to the view. The onClickListener needs to be handled by this adapter (not the
19 | * UserViewModel) in order to communicate with the {@link UserDetailsFragment} which in turn calls
20 | * its ActivityListener.
21 | */
22 | public class RepositoriesAdapter extends RecyclerView.Adapter {
23 |
24 | private List repositories;
25 |
26 | public RepositoriesAdapter() {
27 | this.repositories = Collections.emptyList();
28 | }
29 |
30 | public void setRepositories(List repositories) {
31 | this.repositories = repositories;
32 | }
33 |
34 | @Override public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
35 | ItemRepoBinding binding =
36 | DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_repo,
37 | parent, false);
38 | return new RepoViewHolder(binding);
39 | }
40 |
41 | @Override public void onBindViewHolder(RepoViewHolder holder, int position) {
42 | Repository repository = repositories.get(position);
43 | holder.bindRepo(repository);
44 | }
45 |
46 | @Override public int getItemCount() {
47 | return repositories.size();
48 | }
49 |
50 | public class RepoViewHolder extends RecyclerView.ViewHolder {
51 |
52 | ItemRepoBinding binding;
53 |
54 | public RepoViewHolder(ItemRepoBinding binding) {
55 | super(binding.cardView);
56 | this.binding = binding;
57 | }
58 |
59 | void bindRepo(Repository repository) {
60 | if (binding.getViewModel() == null) {
61 | binding.setViewModel(new RepoViewModel(repository));
62 | } else {
63 | binding.getViewModel().setRepository(repository);
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/UserDetailsActivity.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import com.hugo.mvvmsampleapplication.R;
8 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
9 | import com.hugo.mvvmsampleapplication.features.BaseActivity;
10 | import com.squareup.leakcanary.RefWatcher;
11 |
12 | public class UserDetailsActivity extends BaseActivity {
13 |
14 | private static final String EXTRA_USERNAME = "USERNAME";
15 |
16 | public static Intent newIntent(Context context, String username) {
17 | Intent intent = new Intent(context, UserDetailsActivity.class);
18 | intent.putExtra(EXTRA_USERNAME, username);
19 | return intent;
20 | }
21 |
22 | @Override
23 | protected void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | setContentView(R.layout.activity_user_details);
26 | if (savedInstanceState == null) {
27 | String username = getIntent().getStringExtra(EXTRA_USERNAME);
28 | addFragment(R.id.content_activity_user_details, UserDetailsFragment.newInstance(username));
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/UserDetailsFragment.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.os.Bundle;
5 | import android.support.design.widget.Snackbar;
6 | import android.support.v4.app.Fragment;
7 | import android.support.v7.widget.LinearLayoutManager;
8 | import android.support.v7.widget.RecyclerView;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 |
13 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
14 | import com.hugo.mvvmsampleapplication.R;
15 | import com.hugo.mvvmsampleapplication.databinding.UserDetailsBinding;
16 | import com.hugo.mvvmsampleapplication.features.BaseActivity;
17 | import com.hugo.mvvmsampleapplication.features.searchuser.SearchUserViewModel;
18 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
19 |
20 | import com.squareup.leakcanary.RefWatcher;
21 | import java.util.List;
22 | import javax.inject.Inject;
23 |
24 | /**
25 | * A passive view with the purpose of setting up UI and communicating with the
26 | * ActivityListener. Implements UserDetailsUserViewModel.FragmentListener in order to receive calls
27 | * from {@link UserDetailsViewModel}.
28 | */
29 | public class UserDetailsFragment extends Fragment implements UserDetailsViewModel.FragmentListener {
30 |
31 | private static final String EXTRA_USERNAME = "USERNAME";
32 |
33 | @Inject UserDetailsViewModel userDetailsViewModel;
34 | private UserDetailsBinding binding;
35 | private RepositoriesAdapter userDetailsAdapter;
36 |
37 | public UserDetailsFragment() {
38 |
39 | }
40 |
41 | public static UserDetailsFragment newInstance(String username) {
42 | UserDetailsFragment userDetailsFragment = new UserDetailsFragment();
43 | Bundle bundle = new Bundle();
44 | bundle.putString(EXTRA_USERNAME, username);
45 | userDetailsFragment.setArguments(bundle);
46 | return userDetailsFragment;
47 | }
48 |
49 | @Override
50 | public void onCreate(Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | setRetainInstance(true);
53 | ((BaseActivity)getActivity()).getUserComponent().inject(this);
54 | if (getArguments() != null) {
55 | String username = getArguments().getString(EXTRA_USERNAME);
56 | userDetailsViewModel.loadRepositories(username);
57 | }
58 | }
59 |
60 | @Override
61 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
62 | Bundle savedInstanceState) {
63 | userDetailsViewModel.setFragmentListener(this);
64 | binding = UserDetailsBinding.inflate(inflater, container, false);
65 | binding.setViewModel(userDetailsViewModel);
66 | setupRepositoriesList(binding.repositoriesList);
67 | return binding.getRoot();
68 | }
69 |
70 | public void setupRepositoriesList(RecyclerView repositoriesList) {
71 | if (userDetailsAdapter == null) {
72 | userDetailsAdapter = new RepositoriesAdapter();
73 | }
74 | repositoriesList.setAdapter(userDetailsAdapter);
75 | repositoriesList.setLayoutManager(new LinearLayoutManager(getActivity()));
76 | }
77 |
78 | @Override
79 | public void addRepositories(List repositories) {
80 | RepositoriesAdapter adapter = (RepositoriesAdapter) binding.repositoriesList.getAdapter();
81 | adapter.setRepositories(repositories);
82 | adapter.notifyDataSetChanged();
83 | }
84 |
85 | public void loadRepositories(String username) {
86 | userDetailsViewModel.loadRepositories(username);
87 | }
88 |
89 | @Override
90 | public void showMessage(String message) {
91 | View rootView = getActivity().getWindow().getDecorView().findViewById(android.R.id.content);
92 | Snackbar snackbar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT);
93 | snackbar.show();
94 | }
95 |
96 | @Override
97 | public void onDestroyView() {
98 | super.onDestroyView();
99 | userDetailsViewModel.destroy(false);
100 | }
101 |
102 | @Override
103 | public void onDestroy() {
104 | super.onDestroy();
105 | userDetailsViewModel.destroy(true);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/features/userdetails/UserDetailsViewModel.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.databinding.ObservableInt;
4 | import android.view.View;
5 |
6 | import com.hugo.mvvmsampleapplication.features.UseCase;
7 | import com.hugo.mvvmsampleapplication.features.searchuser.SearchUserUseCase;
8 | import com.hugo.mvvmsampleapplication.features.searchuser.SearchUserViewModel;
9 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
10 | import com.hugo.mvvmsampleapplication.features.DefaultSubscriber;
11 |
12 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.PerActivity;
13 | import java.util.List;
14 | import javax.inject.Inject;
15 | import javax.inject.Named;
16 |
17 | /**
18 | * Bound to the view search_user.xml. All user interactions are handled by this class and updates
19 | * UI elements depending on the action. SearchUserViewModel fetches data from the model via
20 | * {@link LoadUserDetailsUseCase}, and when data is received in {@link LoadRepositoriesSubscriber}
21 | * the view is updated
22 | * accordingly. Since the list displaying results has its own ViewModel, this class has to
23 | * send the result back to the FragmentListener which can update the adapter in the fragment.
24 | */
25 | @PerActivity
26 | public class UserDetailsViewModel {
27 |
28 | public final ObservableInt progressVisibility;
29 | public final ObservableInt repoListVisibility;
30 |
31 | private FragmentListener fragmentListener;
32 | private UseCase loadUserDetailsUseCase;
33 |
34 | @Inject
35 | public UserDetailsViewModel(@Named("userDetails") UseCase loadUserDetailsUseCase) {
36 | this.loadUserDetailsUseCase = loadUserDetailsUseCase;
37 | progressVisibility = new ObservableInt(View.INVISIBLE);
38 | repoListVisibility = new ObservableInt(View.INVISIBLE);
39 | }
40 |
41 | public void setFragmentListener(FragmentListener fragmentListener) {
42 | this.fragmentListener = fragmentListener;
43 | }
44 |
45 | public void loadRepositories(String username) {
46 | showProgressIndicator(true);
47 | loadUserDetailsUseCase.execute(new LoadRepositoriesSubscriber(), username);
48 | }
49 |
50 | private final class LoadRepositoriesSubscriber extends DefaultSubscriber> {
51 |
52 | @Override
53 | public void onCompleted() {
54 | showProgressIndicator(false);
55 | }
56 |
57 | @Override
58 | public void onError(Throwable e) {
59 | showProgressIndicator(false);
60 | fragmentListener.showMessage("Error loading repositories");
61 | }
62 |
63 | @Override
64 | public void onNext(List repositories) {
65 | if (repositories.isEmpty()) {
66 | fragmentListener.showMessage("No public repositories");
67 | } else {
68 | fragmentListener.addRepositories(repositories);
69 | }
70 | }
71 | }
72 |
73 | private void showProgressIndicator(boolean showProgress) {
74 | if (showProgress) {
75 | progressVisibility.set(View.VISIBLE);
76 | repoListVisibility.set(View.INVISIBLE);
77 | } else {
78 | repoListVisibility.set(View.VISIBLE);
79 | progressVisibility.set(View.INVISIBLE);
80 | }
81 | }
82 |
83 | public void destroy(Boolean unsubscribe) {
84 | fragmentListener = null;
85 | if (unsubscribe) {
86 | loadUserDetailsUseCase.unsubscribe();
87 | }
88 | }
89 |
90 | public ObservableInt getProgressVisibility() {
91 | return progressVisibility;
92 | }
93 |
94 | public ObservableInt getRepoListVisibility() {
95 | return repoListVisibility;
96 | }
97 |
98 | public interface FragmentListener {
99 | void addRepositories(List repositories);
100 |
101 | void showMessage(String message);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/model/entities/Repository.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.model.entities;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
5 |
6 | /**
7 | * A POJO representing the JSON data of a repository witch is fetched from {@link GitHubService}.
8 | */
9 | public class Repository {
10 |
11 | private int id;
12 | private String name;
13 | private String description;
14 | private String language;
15 | private int forks;
16 | private int watchers;
17 | @SerializedName("stargazers_count")
18 | private int stars;
19 |
20 | public Repository() {}
21 |
22 | public int getId() {
23 | return id;
24 | }
25 |
26 | public void setId(int id) {
27 | this.id = id;
28 | }
29 |
30 | public String getName() {
31 | return name;
32 | }
33 |
34 | public void setName(String name) {
35 | this.name = name;
36 | }
37 |
38 | public String getDescription() {
39 | return description;
40 | }
41 |
42 | public void setDescription(String description) {
43 | this.description = description;
44 | }
45 |
46 | public String getLanguage() {
47 | return language;
48 | }
49 |
50 | public void setLanguage(String language) {
51 | this.language = language;
52 | }
53 |
54 | public int getForks() {
55 | return forks;
56 | }
57 |
58 | public void setForks(int forks) {
59 | this.forks = forks;
60 | }
61 |
62 | public int getWatchers() {
63 | return watchers;
64 | }
65 |
66 | public void setWatchers(int watchers) {
67 | this.watchers = watchers;
68 | }
69 |
70 | public int getStars() {
71 | return stars;
72 | }
73 |
74 | public void setStars(int stars) {
75 | this.stars = stars;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/model/entities/User.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.model.entities;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
5 |
6 | /**
7 | * A POJO representing the JSON data of a user witch is fetched from {@link GitHubService}.
8 | */
9 | public class User {
10 |
11 | private long id;
12 | private String login;
13 | @SerializedName("avatar_url")
14 | private String avatarUrl;
15 | @SerializedName("repos_url")
16 | private String reposUrl;
17 |
18 | public User() {}
19 |
20 | public long getId() {
21 | return id;
22 | }
23 |
24 | public void setId(int id) {
25 | this.id = id;
26 | }
27 |
28 | public String getLogin() {
29 | return login;
30 | }
31 |
32 | public void setLogin(String login) {
33 | this.login = login;
34 | }
35 |
36 | public String getAvatarUrl() {
37 | return avatarUrl;
38 | }
39 |
40 | public void setAvatarUrl(String avatarUrl) {
41 | this.avatarUrl = avatarUrl;
42 | }
43 |
44 | public String getReposUrl() {
45 | return reposUrl;
46 | }
47 |
48 | public void setReposUrl(String reposUrl) {
49 | this.reposUrl = reposUrl;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/model/network/GitHubService.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.model.network;
2 |
3 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
4 | import java.util.List;
5 | import retrofit.GsonConverterFactory;
6 | import retrofit.Retrofit;
7 | import retrofit.RxJavaCallAdapterFactory;
8 | import retrofit.http.GET;
9 | import retrofit.http.Path;
10 | import retrofit.http.Query;
11 | import rx.Observable;
12 |
13 | /**
14 | * Interface for connecting to GitHub API.
15 | */
16 | public interface GitHubService {
17 |
18 | @GET("users/{username}/repos")
19 | Observable> getRepositoriesFromUser(@Path("username") String username);
20 |
21 | @GET("search/users")
22 | Observable searchUser(@Query("q") String username);
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/model/network/SearchResponse.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.model.network;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import com.hugo.mvvmsampleapplication.model.entities.User;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * A POJO representing the JSON data of a search response witch is fetched from {@link GitHubService}.
11 | */
12 | public class SearchResponse {
13 | @SerializedName("total_count")
14 | private int totalCount;
15 | @SerializedName("incomplete_results")
16 | private boolean incompleteResults;
17 | @SerializedName("items")
18 | private List users = new ArrayList();
19 |
20 | public SearchResponse() {}
21 |
22 | public List getUsers() {
23 | return users;
24 | }
25 |
26 | public void setUsers(List users) {
27 | this.users = users;
28 | }
29 |
30 | public int getTotalCount() {
31 | return totalCount;
32 | }
33 |
34 | public void setTotalCount(int totalCount) {
35 | this.totalCount = totalCount;
36 | }
37 |
38 | public boolean isIncompleteResults() {
39 | return incompleteResults;
40 | }
41 |
42 | public void setIncompleteResults(boolean incompleteResults) {
43 | this.incompleteResults = incompleteResults;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/JobExecutor.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Fernando Cejas Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.hugo.mvvmsampleapplication.util;
17 |
18 | import javax.inject.Inject;
19 | import rx.Scheduler;
20 | import rx.schedulers.Schedulers;
21 |
22 | /**
23 | * Implementation of ThreadExecutor which returns Schedulers.io().
24 | */
25 | public class JobExecutor implements ThreadExecutor {
26 |
27 | @Inject
28 | public JobExecutor() {
29 |
30 | }
31 |
32 | @Override
33 | public Scheduler getScheduler() {
34 | return Schedulers.io();
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/PostExecutionThread.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util;
2 |
3 | import rx.Scheduler;
4 |
5 | /**
6 | * Interface for retrieving a Scheduler.
7 | */
8 | public interface PostExecutionThread {
9 | Scheduler getScheduler();
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/ThreadExecutor.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util;
2 |
3 | import rx.Scheduler;
4 |
5 | /**
6 | * Interface for retrieving a Scheduler.
7 | */
8 | public interface ThreadExecutor {
9 | Scheduler getScheduler();
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/UiThread.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util;
2 |
3 | import javax.inject.Inject;
4 | import rx.Scheduler;
5 | import rx.android.schedulers.AndroidSchedulers;
6 |
7 | /**
8 | * Implementation of PostExecutionThread which returns AndroidSchedulers.mainThread().
9 | */
10 | public class UiThread implements PostExecutionThread {
11 |
12 | @Inject
13 | public UiThread() {}
14 |
15 | @Override
16 | public Scheduler getScheduler() {
17 | return AndroidSchedulers.mainThread();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/dependencyinjection/PerActivity.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util.dependencyinjection;
2 |
3 | import java.lang.annotation.Retention;
4 | import javax.inject.Scope;
5 |
6 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
7 |
8 | @Scope
9 | @Retention(RUNTIME)
10 | public @interface PerActivity {}
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/dependencyinjection/components/ApplicationComponent.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util.dependencyinjection.components;
2 |
3 | import android.content.Context;
4 | import com.hugo.mvvmsampleapplication.features.BaseActivity;
5 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
6 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
7 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
8 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.ApplicationModule;
9 | import dagger.Component;
10 | import javax.inject.Singleton;
11 |
12 | @Singleton
13 | @Component(modules = ApplicationModule.class)
14 | public interface ApplicationComponent {
15 | void inject(BaseActivity baseActivity);
16 |
17 | GitHubService gitHubService();
18 | ThreadExecutor threadExecutor();
19 | PostExecutionThread postExecutionThread();
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/dependencyinjection/components/UserComponent.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util.dependencyinjection.components;
2 |
3 | import com.hugo.mvvmsampleapplication.features.searchuser.SearchUserFragment;
4 | import com.hugo.mvvmsampleapplication.features.userdetails.UserDetailsFragment;
5 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.PerActivity;
6 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.ApplicationModule;
7 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.modules.UserModule;
8 | import dagger.Component;
9 |
10 | @PerActivity
11 | @Component(dependencies = ApplicationComponent.class, modules = UserModule.class)
12 | public interface UserComponent {
13 | void inject(SearchUserFragment searchUserFragment);
14 | void inject(UserDetailsFragment userDetailsFragment);
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/dependencyinjection/modules/ApplicationModule.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util.dependencyinjection.modules;
2 |
3 | import android.content.Context;
4 | import com.hugo.mvvmsampleapplication.app.MVVMApplication;
5 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
6 | import com.hugo.mvvmsampleapplication.util.JobExecutor;
7 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
8 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
9 | import com.hugo.mvvmsampleapplication.util.UiThread;
10 | import dagger.Module;
11 | import dagger.Provides;
12 | import javax.inject.Singleton;
13 | import retrofit.GsonConverterFactory;
14 | import retrofit.Retrofit;
15 | import retrofit.RxJavaCallAdapterFactory;
16 |
17 | @Module
18 | public class ApplicationModule {
19 |
20 | public ApplicationModule() {
21 |
22 | }
23 |
24 | @Provides
25 | @Singleton
26 | public GitHubService provideGitHubService() {
27 | Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/")
28 | .addConverterFactory(GsonConverterFactory.create())
29 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
30 | .build();
31 | return retrofit.create(GitHubService.class);
32 | }
33 |
34 | @Provides
35 | @Singleton
36 | public ThreadExecutor provideThreadExecutor(JobExecutor jobExecutor) {
37 | return jobExecutor;
38 | }
39 |
40 | @Provides
41 | @Singleton
42 | public PostExecutionThread providePostExecutionThread(UiThread uiThread) {
43 | return uiThread;
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hugo/mvvmsampleapplication/util/dependencyinjection/modules/UserModule.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util.dependencyinjection.modules;
2 |
3 | import com.hugo.mvvmsampleapplication.features.UseCase;
4 | import com.hugo.mvvmsampleapplication.features.searchuser.SearchUserUseCase;
5 | import com.hugo.mvvmsampleapplication.features.userdetails.LoadUserDetailsUseCase;
6 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
7 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
8 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
9 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.PerActivity;
10 | import dagger.Module;
11 | import dagger.Provides;
12 | import javax.inject.Named;
13 |
14 | @Module
15 | public class UserModule {
16 |
17 | public UserModule() {
18 | }
19 |
20 | @Provides
21 | @PerActivity
22 | @Named("searchUser")
23 | public UseCase provideSearchUserUseCase(GitHubService gitHubService, ThreadExecutor threadExecutor,
24 | PostExecutionThread postExecutionThread) {
25 | return new SearchUserUseCase(gitHubService, threadExecutor, postExecutionThread);
26 | }
27 |
28 | @Provides
29 | @PerActivity
30 | @Named("userDetails")
31 | public UseCase provideUserDetailsUseCase(GitHubService gitHubService, ThreadExecutor threadExecutor,
32 | PostExecutionThread postExecutionThread) {
33 | return new LoadUserDetailsUseCase(gitHubService, threadExecutor, postExecutionThread);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_search_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-hdpi/ic_search_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_search_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-mdpi/ic_search_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/octocat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-mdpi/octocat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-mdpi/placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-mdpi/profile.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_search_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-xhdpi/ic_search_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-xxhdpi/ic_search_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/octocat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-xxhdpi/octocat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-xxhdpi/placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/drawable-xxxhdpi/ic_search_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/layout-large/activity_search_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_search_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_user_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_repo.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
12 |
21 |
22 |
28 |
29 |
40 |
41 |
52 |
53 |
57 |
58 |
62 |
63 |
71 |
72 |
80 |
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
23 |
24 |
30 |
31 |
41 |
42 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/search_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
9 |
10 |
11 |
14 |
15 |
23 |
24 |
34 |
35 |
45 |
46 |
59 |
60 |
61 |
62 |
71 |
72 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/user_details.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
10 |
11 |
14 |
15 |
23 |
24 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_progressbar.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 | >
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/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 | #000000
7 | #ffffff
8 | #75ffffff
9 | #e1e1e1
10 | #3F51B5
11 | #303F9F
12 | #C5CAE9
13 | #03A9F4
14 | #212121
15 | #727272
16 | #FFFFFF
17 | #cbcbcb
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 12dp
5 | 12dp
6 | 6dp
7 | 6dp
8 |
9 | 16dp
10 | 16dp
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MVVMSampleApplication
3 | Settings
4 | User Details
5 | Github Username
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
23 |
24 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/sharedTest/java/MockFactory.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication;
2 |
3 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
4 | import com.hugo.mvvmsampleapplication.model.entities.User;
5 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
6 | import java.util.ArrayList;
7 | import java.util.Collections;
8 | import java.util.List;
9 | import rx.Observable;
10 |
11 | /**
12 | * Created by hugo on 2/15/16.
13 | */
14 | public class MockFactory {
15 |
16 | public static final String TEST_USERNAME = "Tester";
17 | public static final String TEST_REPOSITORY = "TestRepo";
18 | public static final String TEST_USERNAME_NO_RESULTS = "TesterNoResults";
19 | public static final String TEST_USERNAME_ERROR = "TesterError";
20 | private static final String TEST_REPO_URL = "http://repo.url";
21 | private static final String TEST_AVATAR_URL = "http://avatar.url";
22 | private static final String TEST_LANGUAGE = "Java";
23 | private static final String TEST_DESCRIPTION = "Test Description";
24 |
25 | public static SearchResponse buildMockSearchResponse() {
26 | User testUser = new User();
27 | testUser.setLogin(TEST_USERNAME);
28 | ArrayList users = new ArrayList<>();
29 | users.add(testUser);
30 | SearchResponse searchResponse = new SearchResponse();
31 | searchResponse.setUsers(users);
32 | return searchResponse;
33 | }
34 |
35 | public static List buildMockUserDetailsResponse() {
36 | Repository repository = new Repository();
37 | repository.setId(1);
38 | repository.setStars(2);
39 | repository.setForks(3);
40 | repository.setWatchers(4);
41 | repository.setDescription("Test Repository");
42 | repository.setName(TEST_REPOSITORY);
43 | repository.setLanguage("Java");
44 | ArrayList repositories = new ArrayList<>();
45 | repositories.add(repository);
46 | return repositories;
47 | }
48 |
49 | public static User buildMockUser() {
50 | User user = new User();
51 | user.setId(1);
52 | user.setLogin(TEST_USERNAME);
53 | user.setReposUrl(TEST_REPO_URL);
54 | user.setAvatarUrl(TEST_AVATAR_URL);
55 | return user;
56 | }
57 |
58 | public static Repository buildMockRepository() {
59 | Repository repository = new Repository();
60 | repository.setId(1);
61 | repository.setLanguage(TEST_LANGUAGE);
62 | repository.setName(TEST_USERNAME);
63 | repository.setDescription(TEST_DESCRIPTION);
64 | repository.setForks(1);
65 | repository.setStars(2);
66 | repository.setWatchers(3);
67 | return repository;
68 | }
69 |
70 | public static SearchResponse buildEmptyMockSearchResponse() {
71 | List users = Collections.emptyList();
72 | SearchResponse searchResponse = new SearchResponse();
73 | searchResponse.setUsers(users);
74 | return searchResponse;
75 | }
76 |
77 | public static List buildEmptyRepositoryList() {
78 | ArrayList repositories = new ArrayList<>();
79 | return repositories;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/app/MVVMApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.app;
2 |
3 | import com.hugo.mvvmsampleapplication.util.dependencyinjection.components.ApplicationComponent;
4 | import junit.framework.Assert;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.mockito.Mock;
8 | import org.mockito.Mockito;
9 | import org.mockito.MockitoAnnotations;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Created by hugo on 2/24/16.
15 | */
16 | public class MVVMApplicationTest {
17 |
18 | @Mock
19 | private ApplicationComponent applicationComponent;
20 | private MVVMApplication mvvmApplication;
21 |
22 | @Before
23 | public void setUp() {
24 | MockitoAnnotations.initMocks(this);
25 | mvvmApplication = new MVVMApplication();
26 | }
27 |
28 | @Test
29 | public void testSetAndGetApplicationComponent() throws Exception {
30 | ApplicationComponent nullApplicationComponent = mvvmApplication.getApplicationComponent();
31 | Assert.assertNull(nullApplicationComponent);
32 |
33 | mvvmApplication.setApplicationComponent(applicationComponent);
34 | applicationComponent = mvvmApplication.getApplicationComponent();
35 | Assert.assertNotNull(applicationComponent);
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/UseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features;
2 |
3 | import com.hugo.mvvmsampleapplication.MockFactory;
4 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
5 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
6 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
7 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
8 | import junit.framework.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.junit.runner.RunWith;
12 | import org.mockito.Mock;
13 | import org.mockito.MockitoAnnotations;
14 | import org.mockito.runners.MockitoJUnitRunner;
15 | import rx.Observable;
16 | import rx.Subscription;
17 | import rx.observers.TestSubscriber;
18 | import rx.schedulers.Schedulers;
19 |
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.assertNotNull;
22 | import static org.mockito.Matchers.any;
23 | import static org.mockito.Mockito.when;
24 |
25 | @RunWith(MockitoJUnitRunner.class)
26 | public class UseCaseTest {
27 |
28 | @Mock
29 | private GitHubService gitHubService;
30 | @Mock
31 | private PostExecutionThread mockPostExecutionThread;
32 | @Mock
33 | private ThreadExecutor mockThreadExecutor;
34 |
35 | private UseCase useCase;
36 |
37 | @Before
38 | public void setUp() {
39 | MockitoAnnotations.initMocks(this);
40 | when(mockPostExecutionThread.getScheduler()).thenReturn(Schedulers.immediate());
41 | when(mockThreadExecutor.getScheduler()).thenReturn(Schedulers.immediate());
42 | useCase = new UseCase(gitHubService, mockThreadExecutor, mockPostExecutionThread) {
43 | @Override public Observable buildUseCase(String query) {
44 | return Observable.just(MockFactory.buildMockSearchResponse());
45 | }
46 | };
47 | }
48 |
49 | @Test
50 | public void buildUseCaseShouldReturnObservable() throws Exception {
51 | Observable observable = useCase.buildUseCase(MockFactory.TEST_USERNAME);
52 | assertNotNull(observable);
53 | }
54 | @Test
55 | public void getGitHubServiceShouldNotReturnNull() throws Exception {
56 | GitHubService gitHubService = useCase.getGitHubService();
57 | assertNotNull(gitHubService);
58 | }
59 |
60 | @Test
61 | public void unsubcribeShouldUnsubscribeSubscription() throws Exception {
62 | TestSubscriber testSubscriber = new TestSubscriber<>();
63 | useCase.execute(testSubscriber, MockFactory.TEST_USERNAME);
64 | useCase.unsubscribe();
65 | Subscription subscription = useCase.getSubscription();
66 | assertEquals(true, subscription.isUnsubscribed());
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserUseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import com.hugo.mvvmsampleapplication.MockFactory;
4 | import com.hugo.mvvmsampleapplication.model.entities.User;
5 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
6 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
7 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
8 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
9 | import java.util.List;
10 | import junit.framework.Assert;
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 | import org.mockito.Mock;
15 | import org.mockito.MockitoAnnotations;
16 | import org.mockito.runners.MockitoJUnitRunner;
17 | import rx.Observable;
18 | import rx.observers.TestSubscriber;
19 | import rx.schedulers.Schedulers;
20 |
21 | import static org.mockito.Matchers.any;
22 | import static org.mockito.Mockito.verify;
23 | import static org.mockito.Mockito.when;
24 |
25 | @RunWith(MockitoJUnitRunner.class)
26 | public class SearchUserUseCaseTest {
27 |
28 | @Mock
29 | private GitHubService gitHubService;
30 | @Mock
31 | private PostExecutionThread mockPostExecutionThread;
32 | @Mock
33 | private ThreadExecutor mockThreadExecutor;
34 |
35 | private SearchUserUseCase searchUserUseCase;
36 |
37 | @Before
38 | public void setUp() {
39 | MockitoAnnotations.initMocks(this);
40 | when(mockPostExecutionThread.getScheduler()).thenReturn(Schedulers.immediate());
41 | when(mockThreadExecutor.getScheduler()).thenReturn(Schedulers.immediate());
42 | SearchResponse searchResponse = MockFactory.buildMockSearchResponse();
43 | when(gitHubService.searchUser(any(String.class))).thenReturn(Observable.just(searchResponse));
44 | searchUserUseCase = new SearchUserUseCase(gitHubService, mockThreadExecutor, mockPostExecutionThread);
45 | }
46 |
47 | @Test
48 | public void buildUseCaseShouldCallSearchUser() throws Exception {
49 | searchUserUseCase.buildUseCase(MockFactory.TEST_USERNAME);
50 | verify(gitHubService).searchUser(MockFactory.TEST_USERNAME);
51 | }
52 |
53 | @Test(expected = NullPointerException.class)
54 | public void buildUseCaseShouldThrowNullPointerExceptionIfQueryNotSet() throws Exception {
55 | searchUserUseCase.buildUseCase(null);
56 | }
57 |
58 | @Test
59 | public void executeShouldReturnOneSearchResponse() {
60 | TestSubscriber testSubscriber = new TestSubscriber<>();
61 | searchUserUseCase.execute(testSubscriber, MockFactory.TEST_USERNAME);
62 | testSubscriber.assertCompleted();
63 | testSubscriber.assertNoErrors();
64 | List searchResponseList = testSubscriber.getOnNextEvents();
65 | Assert.assertEquals(1, searchResponseList.size());
66 | }
67 |
68 | @Test
69 | public void executeShouldReturnSearchResponseWithUser() {
70 | TestSubscriber testSubscriber = new TestSubscriber<>();
71 | searchUserUseCase.execute(testSubscriber, MockFactory.TEST_USERNAME);
72 | testSubscriber.assertCompleted();
73 | testSubscriber.assertNoErrors();
74 | List searchResponseList = testSubscriber.getOnNextEvents();
75 | List users = searchResponseList.get(0).getUsers();
76 | Assert.assertEquals(1, users.size());
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/searchuser/SearchUserViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.text.TextWatcher;
4 | import android.view.View;
5 | import android.view.inputmethod.EditorInfo;
6 | import android.widget.TextView;
7 | import com.hugo.mvvmsampleapplication.BuildConfig;
8 | import com.hugo.mvvmsampleapplication.MockFactory;
9 | import com.hugo.mvvmsampleapplication.features.DefaultSubscriber;
10 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
11 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
12 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
13 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
14 | import org.junit.Before;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 | import org.mockito.ArgumentCaptor;
18 | import org.mockito.Captor;
19 | import org.mockito.Mock;
20 | import org.mockito.MockitoAnnotations;
21 | import org.mockito.runners.MockitoJUnitRunner;
22 | import org.robolectric.RobolectricGradleTestRunner;
23 | import org.robolectric.annotation.Config;
24 | import retrofit.HttpException;
25 | import retrofit.Response;
26 | import rx.Observable;
27 | import rx.android.schedulers.AndroidSchedulers;
28 | import rx.schedulers.Schedulers;
29 |
30 | import static org.junit.Assert.assertEquals;
31 | import static org.junit.Assert.assertNotNull;
32 | import static org.mockito.Matchers.any;
33 | import static org.mockito.Matchers.anyList;
34 | import static org.mockito.Matchers.anyString;
35 | import static org.mockito.Matchers.eq;
36 | import static org.mockito.Mockito.verify;
37 | import static org.mockito.Mockito.verifyZeroInteractions;
38 | import static org.mockito.Mockito.when;
39 |
40 | @RunWith(MockitoJUnitRunner.class)
41 | public class SearchUserViewModelTest {
42 |
43 | @Mock private SearchUserUseCase mockSearchUserUseCase;
44 | @Mock private GitHubService gitHubService;
45 | @Mock private ThreadExecutor threadExecutor;
46 | @Mock private PostExecutionThread postExecutionThread;
47 | @Mock private TextView textView;
48 | @Mock private View view;
49 | @Mock private SearchUserViewModel.FragmentListener fragmentListener;
50 | @Captor private ArgumentCaptor subscriberCaptor;
51 |
52 | private SearchUserViewModel searchUserViewModel;
53 | private SearchUserUseCase searchUserUseCase;
54 | private HttpException mockHttpException;
55 |
56 | @Before
57 | public void setUp() {
58 | MockitoAnnotations.initMocks(this);
59 | setUpMocks();
60 | searchUserUseCase = new SearchUserUseCase(gitHubService, threadExecutor, postExecutionThread);
61 | searchUserViewModel = new SearchUserViewModel(mockSearchUserUseCase);
62 | searchUserViewModel.setFragmentListener(fragmentListener);
63 | }
64 |
65 | private void setUpMocks() {
66 | when(gitHubService.searchUser(MockFactory.TEST_USERNAME_NO_RESULTS)).thenReturn(
67 | Observable.just(MockFactory.buildEmptyMockSearchResponse()));
68 | when(gitHubService.searchUser(MockFactory.TEST_USERNAME)).thenReturn(
69 | Observable.just(MockFactory.buildMockSearchResponse()));
70 | mockHttpException = new HttpException(Response.error(404, null));
71 | when(gitHubService.searchUser(MockFactory.TEST_USERNAME_ERROR)).thenReturn(
72 | Observable.error(mockHttpException));
73 | when(postExecutionThread.getScheduler()).thenReturn(Schedulers.immediate());
74 | when(threadExecutor.getScheduler()).thenReturn(Schedulers.immediate());
75 | }
76 |
77 | @Test
78 | public void getUsernameTextWatcherShouldNotReturnNull() {
79 | TextWatcher usernameTextWatcher = searchUserViewModel.getUsernameTextWatcher();
80 | assertNotNull(usernameTextWatcher);
81 | }
82 |
83 | @Test
84 | public void onSearchActionShouldDisplayMessageWhenNoUsernameInput() {
85 | searchUserViewModel.onSearchAction(textView, EditorInfo.IME_ACTION_SEARCH, null);
86 | verify(fragmentListener).showMessage("Enter a username");
87 | }
88 |
89 | @Test
90 | public void onClickSearchShouldDisplayMessageWhenNoUsernameInput() {
91 | searchUserViewModel.onClickSearch(view);
92 | verify(fragmentListener).showMessage("Enter a username");
93 | }
94 |
95 | @Test
96 | public void onClickSearchShouldLoadUsersFromUseCase() {
97 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
98 | searchUserViewModel.onClickSearch(view);
99 | verify(mockSearchUserUseCase).execute(any(DefaultSubscriber.class),
100 | eq(MockFactory.TEST_USERNAME));
101 | }
102 |
103 | @Test
104 | public void onSearchActionShouldLoadUsersFromUseCase() {
105 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
106 | searchUserViewModel.onSearchAction(textView, EditorInfo.IME_ACTION_SEARCH, null);
107 | verify(mockSearchUserUseCase).execute(any(DefaultSubscriber.class),
108 | eq(MockFactory.TEST_USERNAME));
109 | }
110 |
111 | @Test
112 | public void onClickSearchShouldShowProgressIndicatorWhenLoadingUsers() {
113 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
114 | searchUserViewModel.onClickSearch(view);
115 | assertEquals(View.VISIBLE, searchUserViewModel.getProgressVisibility().get());
116 | }
117 |
118 | @Test
119 | public void onClickSearchShouldHideUserListWhileLoadingUsers() {
120 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
121 | searchUserViewModel.onClickSearch(view);
122 | assertEquals(View.INVISIBLE, searchUserViewModel.getUserListVisibility().get());
123 | }
124 |
125 | @Test
126 | public void onClickSearchShouldAddUsersToViewAfterReceivedUsers() {
127 | searchUserViewModel = new SearchUserViewModel(searchUserUseCase);
128 | searchUserViewModel.setFragmentListener(fragmentListener);
129 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
130 | searchUserViewModel.onClickSearch(view);
131 | verify(fragmentListener).addUsers(anyList());
132 | }
133 |
134 | @Test
135 | public void onClickSearchShouldDisplayErrorMessageIfErrorWhenLoadingUsers() {
136 | searchUserViewModel = new SearchUserViewModel(searchUserUseCase);
137 | searchUserViewModel.setFragmentListener(fragmentListener);
138 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME_ERROR);
139 | searchUserViewModel.onClickSearch(view);
140 | verify(fragmentListener).showMessage("Error loading users");
141 | }
142 |
143 | @Test
144 | public void onClickSearchShouldDisplayErrorMessageIfNoUsersReceived() {
145 | searchUserViewModel = new SearchUserViewModel(searchUserUseCase);
146 | searchUserViewModel.setFragmentListener(fragmentListener);
147 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME_NO_RESULTS);
148 | searchUserViewModel.onClickSearch(view);
149 | verify(fragmentListener).showMessage("No users found");
150 | }
151 |
152 | @Test
153 | public void onClickSearchShouldHideProgressIndicatorAfterCompleted() {
154 | searchUserViewModel = new SearchUserViewModel(searchUserUseCase);
155 | searchUserViewModel.setFragmentListener(fragmentListener);
156 | searchUserViewModel.setUsername(MockFactory.TEST_USERNAME);
157 | searchUserViewModel.onClickSearch(view);
158 | assertEquals(View.VISIBLE, searchUserViewModel.getUserListVisibility().get());
159 | assertEquals(View.INVISIBLE, searchUserViewModel.getProgressVisibility().get());
160 | }
161 |
162 | @Test
163 | public void onDestroyTrueShouldUnsubscribeFromUseCase() {
164 | searchUserViewModel.destroy(true);
165 | verify(mockSearchUserUseCase).unsubscribe();
166 | }
167 |
168 | @Test
169 | public void onDestroyFalseShouldNotUnsubscribeFromUseCase() {
170 | searchUserViewModel.destroy(false);
171 | verifyZeroInteractions(mockSearchUserUseCase);
172 | }
173 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/searchuser/UserListAdapterTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import com.hugo.mvvmsampleapplication.MockFactory;
4 | import com.hugo.mvvmsampleapplication.model.entities.User;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.mockito.runners.MockitoJUnitRunner;
11 |
12 | import static org.junit.Assert.assertEquals;
13 |
14 | @RunWith(MockitoJUnitRunner.class)
15 | public class UserListAdapterTest {
16 |
17 | private List users = new ArrayList<>();
18 | private UserListAdapter userListAdapter;
19 |
20 | @Before
21 | public void setUp() {
22 | User user = MockFactory.buildMockUser();
23 | users.add(user);
24 | userListAdapter = new UserListAdapter();
25 | }
26 |
27 | @Test
28 | public void getItemCountShouldReturnNumberOfUsersInList() {
29 | userListAdapter.setUsers(users);
30 | int itemCount = userListAdapter.getItemCount();
31 | assertEquals(users.size(), itemCount);
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/searchuser/UserViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.searchuser;
2 |
3 | import android.widget.ImageView;
4 | import com.hugo.mvvmsampleapplication.MockFactory;
5 | import com.hugo.mvvmsampleapplication.model.entities.User;
6 | import org.junit.Assert;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.mockito.Mock;
11 | import org.mockito.MockitoAnnotations;
12 | import org.mockito.runners.MockitoJUnitRunner;
13 |
14 | @RunWith(MockitoJUnitRunner.class)
15 | public class UserViewModelTest {
16 |
17 | @Mock private ImageView imageView;
18 | private UserViewModel userViewModel;
19 | private User mockUser;
20 |
21 | @Before
22 | public void setUp() {
23 | MockitoAnnotations.initMocks(this);
24 | mockUser = MockFactory.buildMockUser();
25 | userViewModel = new UserViewModel(mockUser);
26 | }
27 |
28 | @Test
29 | public void testGetUsername() throws Exception {
30 | Assert.assertEquals(mockUser.getLogin(), userViewModel.getUsername());
31 | }
32 |
33 | @Test
34 | public void testGetImageUrl() throws Exception {
35 | Assert.assertEquals(mockUser.getAvatarUrl(), userViewModel.getImageUrl());
36 | }
37 |
38 | @Test
39 | public void testSetUser() throws Exception {
40 | mockUser.setLogin("NewName");
41 | userViewModel.setUser(mockUser);
42 | Assert.assertEquals("NewName", userViewModel.getUsername());
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/userdetails/LoadUserDetailsUseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import com.hugo.mvvmsampleapplication.MockFactory;
4 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
5 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
6 | import com.hugo.mvvmsampleapplication.util.PostExecutionThread;
7 | import com.hugo.mvvmsampleapplication.util.ThreadExecutor;
8 | import java.util.List;
9 | import junit.framework.Assert;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.mockito.Mock;
14 | import org.mockito.MockitoAnnotations;
15 | import org.mockito.runners.MockitoJUnitRunner;
16 | import rx.Observable;
17 | import rx.observers.TestSubscriber;
18 | import rx.schedulers.Schedulers;
19 |
20 | import static org.junit.Assert.assertEquals;
21 | import static org.mockito.Matchers.any;
22 | import static org.mockito.Mockito.verify;
23 | import static org.mockito.Mockito.when;
24 |
25 | /**
26 | * Created by hugo on 2/15/16.
27 | */
28 | @RunWith(MockitoJUnitRunner.class)
29 | public class LoadUserDetailsUseCaseTest {
30 |
31 | @Mock private GitHubService gitHubService;
32 | @Mock private PostExecutionThread mockPostExecutionThread;
33 | @Mock private ThreadExecutor mockThreadExecutor;
34 |
35 | private LoadUserDetailsUseCase loadUserDetailsUseCase;
36 |
37 | @Before
38 | public void setUp() {
39 | MockitoAnnotations.initMocks(this);
40 | when(mockPostExecutionThread.getScheduler()).thenReturn(Schedulers.immediate());
41 | when(mockThreadExecutor.getScheduler()).thenReturn(Schedulers.immediate());
42 | List mockRepositories = MockFactory.buildMockUserDetailsResponse();
43 | when(gitHubService.getRepositoriesFromUser(any(String.class))).thenReturn(
44 | Observable.just(mockRepositories));
45 | loadUserDetailsUseCase =
46 | new LoadUserDetailsUseCase(gitHubService, mockThreadExecutor, mockPostExecutionThread);
47 | }
48 |
49 | @Test
50 | public void buildUseCaseShouldGetRepositoriesFromUser() throws Exception {
51 | loadUserDetailsUseCase.buildUseCase(MockFactory.TEST_USERNAME);
52 | verify(gitHubService).getRepositoriesFromUser(MockFactory.TEST_USERNAME);
53 | }
54 |
55 | @Test(expected = NullPointerException.class)
56 | public void buildUseCaseShouldThrowNullPointerExceptionIfUsernameNotSet() throws Exception {
57 | loadUserDetailsUseCase.buildUseCase(null);
58 | }
59 |
60 | @Test
61 | public void executeShouldReturnRepositoryList() {
62 | TestSubscriber testSubscriber = new TestSubscriber<>();
63 | loadUserDetailsUseCase.execute(testSubscriber, MockFactory.TEST_USERNAME);
64 | testSubscriber.assertCompleted();
65 | testSubscriber.assertNoErrors();
66 | List searchResponseList = testSubscriber.getOnNextEvents();
67 | assertEquals(1, searchResponseList.size());
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/userdetails/RepoViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import com.hugo.mvvmsampleapplication.MockFactory;
4 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
5 | import org.junit.Assert;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 | import org.mockito.Mock;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | public class RepoViewModelTest {
13 |
14 | private RepoViewModel repoViewModel;
15 | private Repository mockRepository;
16 |
17 | @Before
18 | public void setUp() {
19 | mockRepository = MockFactory.buildMockRepository();
20 | repoViewModel = new RepoViewModel(mockRepository);
21 | }
22 |
23 | @Test public void testSetRepository() throws Exception {
24 | mockRepository.setName("NewName");
25 | repoViewModel.setRepository(mockRepository);
26 | Assert.assertEquals("NewName", repoViewModel.getRepoTitle());
27 | }
28 |
29 | @Test public void testGetRepoTitle() throws Exception {
30 | Assert.assertEquals(mockRepository.getName(), repoViewModel.getRepoTitle());
31 | }
32 |
33 | @Test public void testGetRepoDescription() throws Exception {
34 | Assert.assertEquals(mockRepository.getDescription(), repoViewModel.getRepoDescription());
35 | }
36 |
37 | @Test public void testGetWatchers() throws Exception {
38 | Assert.assertEquals(mockRepository.getWatchers() + "\n Watchers", repoViewModel.getWatchers());
39 | }
40 |
41 | @Test public void testGetStars() throws Exception {
42 | Assert.assertEquals(mockRepository.getStars() + "\n Stars", repoViewModel.getStars());
43 | }
44 |
45 | @Test public void testGetForks() throws Exception {
46 | Assert.assertEquals(mockRepository.getForks() + "\n Forks", repoViewModel.getForks());
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/features/userdetails/UserDetailsViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.features.userdetails;
2 |
3 | import android.view.View;
4 | import android.widget.TextView;
5 | import com.hugo.mvvmsampleapplication.MockFactory;
6 | import com.hugo.mvvmsampleapplication.features.DefaultSubscriber;
7 | import com.hugo.mvvmsampleapplication.model.entities.Repository;
8 | import com.hugo.mvvmsampleapplication.model.network.GitHubService;
9 | import com.hugo.mvvmsampleapplication.model.network.SearchResponse;
10 | import com.hugo.mvvmsampleapplication.util.JobExecutor;
11 | import com.hugo.mvvmsampleapplication.util.UiThread;
12 | import java.util.List;
13 | import org.junit.Before;
14 | import org.junit.Test;
15 | import org.junit.runner.RunWith;
16 | import org.mockito.Mock;
17 | import org.mockito.MockitoAnnotations;
18 | import org.mockito.runners.MockitoJUnitRunner;
19 | import retrofit.HttpException;
20 | import retrofit.Response;
21 | import rx.Observable;
22 | import rx.schedulers.Schedulers;
23 |
24 | import static org.junit.Assert.assertEquals;
25 | import static org.mockito.Matchers.any;
26 | import static org.mockito.Matchers.anyList;
27 | import static org.mockito.Matchers.eq;
28 | import static org.mockito.Mockito.verify;
29 | import static org.mockito.Mockito.verifyZeroInteractions;
30 | import static org.mockito.Mockito.when;
31 |
32 | @RunWith(MockitoJUnitRunner.class)
33 | public class UserDetailsViewModelTest {
34 |
35 | @Mock private LoadUserDetailsUseCase mockLoadUserDetailsUseCase;
36 | @Mock private TextView textView;
37 | @Mock private View view;
38 | @Mock private UserDetailsViewModel.FragmentListener fragmentListener;
39 | @Mock private GitHubService gitHubService;
40 | @Mock private JobExecutor threadExecutor;
41 | @Mock private UiThread postExecutionThread;
42 |
43 | private HttpException mockHttpException;
44 | private UserDetailsViewModel userDetailsViewModel;
45 | private LoadUserDetailsUseCase loadUserDetailsUseCase;
46 |
47 | @Before
48 | public void setUp() {
49 | MockitoAnnotations.initMocks(this);
50 | setUpMocks();
51 | loadUserDetailsUseCase =
52 | new LoadUserDetailsUseCase(gitHubService, threadExecutor, postExecutionThread);
53 | userDetailsViewModel = new UserDetailsViewModel(mockLoadUserDetailsUseCase);
54 | userDetailsViewModel.setFragmentListener(fragmentListener);
55 | }
56 |
57 | private void setUpMocks() {
58 | when(gitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME_NO_RESULTS)).thenReturn(
59 | Observable.just(MockFactory.buildEmptyRepositoryList()));
60 | when(gitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME)).thenReturn(
61 | Observable.just(MockFactory.buildMockUserDetailsResponse()));
62 | mockHttpException = new HttpException(Response.error(404, null));
63 | when(gitHubService.getRepositoriesFromUser(MockFactory.TEST_USERNAME_ERROR)).thenReturn(
64 | Observable.>error(mockHttpException));
65 | when(threadExecutor.getScheduler()).thenReturn(Schedulers.immediate());
66 | when(postExecutionThread.getScheduler()).thenReturn(Schedulers.immediate());
67 | }
68 |
69 | @Test
70 | public void loadRepositoriesShouldLoadUserRepositoriesFromUseCase() {
71 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME);
72 | verify(mockLoadUserDetailsUseCase).execute(any(DefaultSubscriber.class),
73 | eq(MockFactory.TEST_USERNAME));
74 | }
75 |
76 | @Test
77 | public void loadRepositoriesShouldShowProgress() {
78 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME);
79 | assertEquals(userDetailsViewModel.getProgressVisibility().get(), View.VISIBLE);
80 | }
81 |
82 | @Test
83 | public void loadRepositoriesShouldHideRepoList() {
84 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME);
85 | assertEquals(userDetailsViewModel.getRepoListVisibility().get(), View.INVISIBLE);
86 | }
87 |
88 | @Test
89 | public void loadRepositoriesShouldShowMessageIfEmptyResponse() {
90 | userDetailsViewModel = new UserDetailsViewModel(loadUserDetailsUseCase);
91 | userDetailsViewModel.setFragmentListener(fragmentListener);
92 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME_NO_RESULTS);
93 | verify(fragmentListener).showMessage("No public repositories");
94 | }
95 |
96 | @Test
97 | public void loadRepositoriesShouldCallAddRepositories() {
98 | userDetailsViewModel = new UserDetailsViewModel(loadUserDetailsUseCase);
99 | userDetailsViewModel.setFragmentListener(fragmentListener);
100 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME);
101 | verify(fragmentListener).addRepositories(anyList());
102 | }
103 |
104 | @Test
105 | public void loadRepositoriesShouldShowMessageIfError() {
106 | userDetailsViewModel = new UserDetailsViewModel(loadUserDetailsUseCase);
107 | userDetailsViewModel.setFragmentListener(fragmentListener);
108 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME_ERROR);
109 | verify(fragmentListener).showMessage("Error loading repositories");
110 | }
111 |
112 | @Test
113 | public void loadRepositoriesShouldHideProgressIndicatorWhenComplete() {
114 | userDetailsViewModel = new UserDetailsViewModel(loadUserDetailsUseCase);
115 | userDetailsViewModel.setFragmentListener(fragmentListener);
116 | userDetailsViewModel.loadRepositories(MockFactory.TEST_USERNAME);
117 | assertEquals(View.VISIBLE, userDetailsViewModel.getRepoListVisibility().get());
118 | assertEquals(View.INVISIBLE, userDetailsViewModel.getProgressVisibility().get());
119 | }
120 |
121 | @Test
122 | public void onDestroyTrueShouldUnsubscribeFromUseCase() {
123 | userDetailsViewModel.destroy(true);
124 | verify(mockLoadUserDetailsUseCase).unsubscribe();
125 | }
126 |
127 | @Test
128 | public void onDestroyFalseShouldNotUnsubscribeFromUseCase() {
129 | userDetailsViewModel.destroy(false);
130 | verifyZeroInteractions(mockLoadUserDetailsUseCase);
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/util/JobExecutorTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util;
2 |
3 | import org.junit.Test;
4 | import rx.schedulers.Schedulers;
5 |
6 | import static org.junit.Assert.*;
7 |
8 | /**
9 | * Created by hugo on 2/24/16.
10 | */
11 | public class JobExecutorTest {
12 |
13 | @Test
14 | public void getSchedulerShouldReturnIoScheduler() {
15 | JobExecutor jobExecutor = new JobExecutor();
16 | assertEquals(Schedulers.io(), jobExecutor.getScheduler());
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hugo/mvvmsampleapplication/util/UiThreadTest.java:
--------------------------------------------------------------------------------
1 | package com.hugo.mvvmsampleapplication.util;
2 |
3 | import org.junit.Test;
4 | import rx.android.schedulers.AndroidSchedulers;
5 |
6 | import static org.junit.Assert.*;
7 |
8 | /**
9 | * Created by hugo on 2/24/16.
10 | */
11 | public class UiThreadTest {
12 |
13 | @Test
14 | public void getSchedulerShouldReturnAndroidSchedulersMainThread() {
15 | UiThread uiThread = new UiThread();
16 | assertEquals(AndroidSchedulers.mainThread(), uiThread.getScheduler());
17 | }
18 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:1.5.0'
9 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | maven { url "https://jitpack.io" }
19 | }
20 | }
21 |
22 | task clean(type: Delete) {
23 | delete rootProject.buildDir
24 | }
25 |
--------------------------------------------------------------------------------
/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/hugokallstrom/MVVMSampleApplication/488932ad39fc22dc28e5065d5b5d228686673975/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 21 11:34:03 PDT 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-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 |
--------------------------------------------------------------------------------