├── .buckconfig ├── .gitignore ├── .watchmanconfig ├── LICENSE ├── README.MD ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── vn │ │ └── tale │ │ └── architecture │ │ ├── ExampleInstrumentedTest.java │ │ ├── LoginTest.java │ │ ├── MockApplication.java │ │ ├── MockTestRunner.java │ │ └── TestAppComponent.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── vn │ │ │ └── tale │ │ │ └── architecture │ │ │ ├── ActivityScope.java │ │ │ ├── App.java │ │ │ ├── AppComponent.java │ │ │ ├── AppModule.java │ │ │ ├── AppSingletonModule.java │ │ │ ├── GlideImageLoader.java │ │ │ ├── common │ │ │ ├── AppRouter.java │ │ │ ├── CollectionsX.java │ │ │ ├── EmailValidator.java │ │ │ ├── IntentFactory.java │ │ │ ├── Preconditions.java │ │ │ ├── SchedulerObservableTransformer.java │ │ │ ├── SchedulerSingleTransformer.java │ │ │ ├── base │ │ │ │ ├── BaseActivity.java │ │ │ │ └── RvvmActivity.java │ │ │ ├── dagger │ │ │ │ ├── DaggerComponentFactory.java │ │ │ │ └── DaggerLifecycleDelegate.java │ │ │ ├── redux │ │ │ │ ├── Action.java │ │ │ │ ├── Effect.java │ │ │ │ ├── Function0.java │ │ │ │ ├── LifecycleDelegate.java │ │ │ │ ├── Reducer.java │ │ │ │ ├── Result.java │ │ │ │ └── Store.java │ │ │ └── util │ │ │ │ ├── ImageLoader.java │ │ │ │ └── InfiniteScrollListener.java │ │ │ ├── counter │ │ │ ├── CounterActivity.java │ │ │ ├── CounterComponent.java │ │ │ ├── CounterModule.java │ │ │ ├── CounterReducer.java │ │ │ ├── CounterState.java │ │ │ ├── action │ │ │ │ └── ChangeValueAction.java │ │ │ ├── effect │ │ │ │ └── ChangeValueEffect.java │ │ │ └── result │ │ │ │ └── ChangeValueResult.java │ │ │ ├── home │ │ │ ├── HomeActivity.java │ │ │ ├── HomeComponent.java │ │ │ ├── HomeModule.java │ │ │ ├── HomeReducer.java │ │ │ ├── HomeState.java │ │ │ ├── HomeViewModel.java │ │ │ ├── action │ │ │ │ └── HomeAction.java │ │ │ ├── component │ │ │ │ ├── Demos.java │ │ │ │ ├── HomeListComponentSpec.java │ │ │ │ ├── ProductComponentSpec.java │ │ │ │ ├── ProductSlideComponentSpec.java │ │ │ │ ├── SingleBannerComponentSpec.java │ │ │ │ └── TripleBannersComponentSpec.java │ │ │ ├── epic │ │ │ │ ├── LoadEpic.java │ │ │ │ ├── LoadMoreEpic.java │ │ │ │ └── RefreshEpic.java │ │ │ └── result │ │ │ │ ├── LoadMoreResult.java │ │ │ │ ├── LoadResult.java │ │ │ │ └── RefreshResult.java │ │ │ ├── login │ │ │ ├── LoginActivity.java │ │ │ ├── LoginComponent.java │ │ │ ├── LoginModule.java │ │ │ ├── LoginReducer.java │ │ │ ├── LoginState.java │ │ │ ├── LoginViewModel.java │ │ │ ├── action │ │ │ │ ├── CheckEmailAction.java │ │ │ │ └── SubmitAction.java │ │ │ ├── epic │ │ │ │ ├── CheckEmailEpic.java │ │ │ │ └── SubmitEpic.java │ │ │ └── result │ │ │ │ ├── CheckEmailResult.java │ │ │ │ └── SubmitResult.java │ │ │ ├── model │ │ │ ├── Banner.java │ │ │ ├── Constants.java │ │ │ ├── HomeSection.java │ │ │ ├── Product.java │ │ │ ├── ProductSlideSection.java │ │ │ ├── SingleBannerSection.java │ │ │ ├── TripleBannerSection.java │ │ │ ├── User.java │ │ │ ├── api │ │ │ │ ├── GithubApi.java │ │ │ │ └── reponse │ │ │ │ │ ├── RepoResponse.java │ │ │ │ │ ├── SearchRepoResponse.java │ │ │ │ │ └── UserResponse.java │ │ │ ├── error │ │ │ │ ├── AuthenticateError.java │ │ │ │ ├── InvalidEmailError.java │ │ │ │ └── OnErrorNotImplementedException.java │ │ │ └── manager │ │ │ │ ├── HomeModel.java │ │ │ │ ├── MockManager.java │ │ │ │ └── UserModel.java │ │ │ └── util │ │ │ ├── DisplayUtil.java │ │ │ └── FormatUtil.java │ └── res │ │ ├── drawable │ │ ├── ic_device_hub_24dp.xml │ │ ├── ic_fork_24dp.xml │ │ ├── ic_logout_24dp.xml │ │ ├── ic_person_24dp.xml │ │ ├── ic_placeholder_24dp.xml │ │ ├── ic_public_24dp.xml │ │ ├── ic_star_black_24dp.xml │ │ └── nav_item_color_state.xml │ │ ├── layout │ │ ├── activity_counter.xml │ │ ├── activity_login.xml │ │ ├── activity_repos.xml │ │ ├── activity_top_repo_list.xml │ │ ├── item_repo.xml │ │ └── item_repo_list.xml │ │ ├── menu │ │ ├── repos_bottom_menu.xml │ │ └── repos_menu.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── vn │ └── tale │ └── architecture │ ├── ExampleUnitTest.java │ ├── common │ ├── AppRouterTest.java │ ├── CollectionsXTest.java │ ├── EmailValidatorTest.java │ └── dagger │ │ └── DaggerLifecycleDelegateTest.java │ └── rxjava │ └── ImmediateSchedulersRule.java ├── buckw ├── build.gradle ├── circle.yml ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── runBuck ├── settings.gradle ├── tools ├── environmentSetup.sh ├── rules-findbugs.xml ├── rules-lint.xml ├── rules-pmd.xml ├── rules-proguard-debug.pro ├── rules-proguard.pro ├── script-dependencies.gradle ├── script-findbugs.gradle ├── script-git-version.gradle ├── script-java-code-coverage-library.gradle ├── script-java-code-coverage.gradle ├── script-lint.gradle └── script-pmd.gradle └── wiki ├── Architecture.md └── Usage.md /.buckconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbof10/redux-observable/8ee0a2e3de50d67f55fe812ec6d55d274179f236/.buckconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/buck,android,androidstudio 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | 47 | # Keystore files 48 | *.jks 49 | 50 | # External native build folder generated in Android Studio 2.2 and later 51 | .externalNativeBuild 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | ### Android Patch ### 62 | gen-external-apklibs 63 | 64 | ### AndroidStudio ### 65 | # Covers files to be ignored for android development using Android Studio. 66 | 67 | # Built application files 68 | 69 | # Files for the ART/Dalvik VM 70 | 71 | # Java class files 72 | 73 | # Generated files 74 | 75 | # Gradle files 76 | .gradle 77 | 78 | # Signing files 79 | .signing/ 80 | 81 | # Local configuration file (sdk path, etc) 82 | 83 | # Proguard folder generated by Eclipse 84 | 85 | # Log Files 86 | 87 | # Android Studio 88 | /*/build/ 89 | /*/local.properties 90 | /*/out 91 | /*/*/build 92 | /*/*/production 93 | *.ipr 94 | *~ 95 | *.swp 96 | 97 | # Android Patch 98 | 99 | # External native build folder generated in Android Studio 2.2 and later 100 | 101 | # NDK 102 | obj/ 103 | 104 | # IntelliJ IDEA 105 | *.iws 106 | /out/ 107 | 108 | # User-specific configurations 109 | .idea 110 | .idea/libraries/ 111 | .idea/.name 112 | .idea/compiler.xml 113 | .idea/copyright/profiles_settings.xml 114 | .idea/encodings.xml 115 | .idea/misc.xml 116 | .idea/modules.xml 117 | .idea/scopes/scope_settings.xml 118 | .idea/vcs.xml 119 | .idea/jsLibraryMappings.xml 120 | .idea/datasources.xml 121 | .idea/dataSources.ids 122 | .idea/sqlDataSources.xml 123 | .idea/dynamic.xml 124 | .idea/uiDesigner.xml 125 | 126 | # Keystore files 127 | 128 | # OS-specific files 129 | .DS_Store 130 | .DS_Store? 131 | ._* 132 | .Spotlight-V100 133 | .Trashes 134 | ehthumbs.db 135 | Thumbs.db 136 | 137 | # Legacy Eclipse project files 138 | .classpath 139 | .project 140 | 141 | # Mobile Tools for Java (J2ME) 142 | .mtj.tmp/ 143 | 144 | # Package Files # 145 | *.jar 146 | *.war 147 | *.ear 148 | 149 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 150 | hs_err_pid* 151 | 152 | ## Plugin-specific files: 153 | 154 | # mpeltonen/sbt-idea plugin 155 | .idea_modules/ 156 | 157 | # JIRA plugin 158 | atlassian-ide-plugin.xml 159 | 160 | # Mongo Explorer plugin 161 | .idea/mongoSettings.xml 162 | 163 | # Crashlytics plugin (for Android Studio and IntelliJ) 164 | com_crashlytics_export_strings.xml 165 | crashlytics.properties 166 | crashlytics-build.properties 167 | fabric.properties 168 | 169 | ### Buck ### 170 | buck-out/ 171 | .buckconfig.local 172 | .buckd/ 173 | .buckversion 174 | .fakebuckversion 175 | .okbuck 176 | **/BUCK 177 | # End of https://www.gitignore.io/api/buck,android,androidstudio -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | "buck-out", 4 | ".buckd", 5 | ".idea", 6 | "build", 7 | ".gradle", 8 | ".swp", 9 | ], 10 | "fsevents_latency": 0.05, 11 | "suppress_recrawl_warnings": true 12 | } 13 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Redux + View + ViewModel = RVVM 2 | 3 | 1. [Architecute](./wiki/Architecture.md) 4 | 2. [Usage](./wiki/Usage.md) 5 | 6 | ## Build & Run 7 | 8 | ### Gradle 9 | 10 | 1. Build 11 | 12 | >./gradlew assemble[Dev|Prod][Debug|Release] 13 | 14 | 2. Install 15 | 16 | >./gradlew install[Dev|Prod][Debug|Release] 17 | 18 | ### Buck 19 | 20 | 1. Run 21 | 22 | >./runBuck -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'me.tatarka.retrolambda' 3 | apply plugin: 'com.jakewharton.butterknife' 4 | apply from: "$project.rootDir/tools/script-git-version.gradle" 5 | apply from: "$project.rootDir/tools/script-findbugs.gradle" 6 | apply from: "$project.rootDir/tools/script-lint.gradle" 7 | apply from: "$project.rootDir/tools/script-pmd.gradle" 8 | apply from: "$project.rootDir/tools/script-java-code-coverage.gradle" 9 | 10 | android { 11 | compileOptions { 12 | sourceCompatibility JavaVersion.VERSION_1_8 13 | targetCompatibility JavaVersion.VERSION_1_8 14 | } 15 | compileSdkVersion rootProject.ext.compileSdkVersion 16 | buildToolsVersion rootProject.ext.buildToolsVersion 17 | defaultConfig { 18 | minSdkVersion rootProject.ext.minSdkVersion 19 | targetSdkVersion rootProject.ext.compileSdkVersion 20 | testInstrumentationRunner "vn.tale.architecture.MockTestRunner" 21 | vectorDrawables.useSupportLibrary = true 22 | } 23 | 24 | signingConfigs { 25 | debug { 26 | keyAlias 'tale-debug' 27 | keyPassword 'JwsJoHw9ziFmJn[K' 28 | storePassword 'JwsJoHw9ziFmJn[K' 29 | storeFile file('../keystore/debug.jks') 30 | } 31 | release { 32 | keyAlias 'tale-release' 33 | keyPassword 'JwsJoHw9ziFmJn[K' 34 | storePassword 'JwsJoHw9ziFmJn[K' 35 | storeFile file('../keystore/release.jks') 36 | } 37 | } 38 | 39 | productFlavors { 40 | dev { 41 | signingConfig signingConfigs.debug 42 | versionCode gitVersionCodeTime 43 | versionName gitVersionName 44 | applicationId "vn.tale.architecture.dev" 45 | } 46 | 47 | prod { 48 | signingConfig signingConfigs.release 49 | versionCode gitVersionCode 50 | versionName gitVersionName 51 | applicationId "vn.tale.architecture" 52 | } 53 | } 54 | 55 | buildTypes { 56 | release { 57 | minifyEnabled true 58 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 59 | "$project.rootDir/tools/rules-proguard.pro" 60 | signingConfig signingConfigs.release 61 | } 62 | debug { 63 | minifyEnabled false 64 | testCoverageEnabled true 65 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 66 | "$project.rootDir/tools/rules-proguard-debug.pro" 67 | signingConfig signingConfigs.debug 68 | } 69 | } 70 | 71 | packagingOptions { 72 | exclude 'META-INF/rxjava.properties' 73 | } 74 | } 75 | 76 | dependencies { 77 | compile fileTree(dir: 'libs', include: ['*.jar']) 78 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 79 | exclude group: 'com.android.support', module: 'support-annotations' 80 | }) 81 | 82 | compile 'com.android.support:appcompat-v7:25.3.1' 83 | compile "com.android.support:design:25.3.1" 84 | 85 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 86 | // Because RxAndroid releases are few and far between, it is recommended you also 87 | // explicitly depend on RxJava's latest version for bug fixes and new features. 88 | compile 'io.reactivex.rxjava2:rxjava:2.0.1' 89 | 90 | compile 'vn.tiki.noadapter2:noadapter:2.0.1-SNAPSHOT' 91 | 92 | compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' 93 | 94 | compile 'com.jakewharton:butterknife:8.5.1' 95 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' 96 | compile 'com.google.dagger:dagger:2.9' 97 | annotationProcessor 'com.google.dagger:dagger-compiler:2.9' 98 | 99 | provided 'com.google.auto.value:auto-value:1.3' 100 | annotationProcessor 'com.jakewharton.auto.value:auto-value-annotations:1.3' 101 | 102 | compile 'com.github.bumptech.glide:glide:3.7.0' 103 | 104 | compile "com.squareup.retrofit2:retrofit:2.2.0" 105 | compile "com.squareup.retrofit2:adapter-rxjava2:2.2.0" 106 | compile "com.squareup.okhttp3:okhttp:3.5.0" 107 | compile "com.squareup.okhttp3:logging-interceptor:3.5.0" 108 | compile 'com.squareup.retrofit2:converter-gson:2.1.0' 109 | compile 'com.google.code.gson:gson:2.8.0' 110 | 111 | compile 'com.jakewharton.timber:timber:4.5.0' 112 | 113 | compile 'com.github.akarnokd:ixjava:1.0.0-RC5' 114 | 115 | testCompile 'junit:junit:4.12' 116 | testCompile "org.mockito:mockito-core:2.7.0" 117 | testCompile "com.google.truth:truth:0.31" 118 | 119 | // Litho 120 | compile 'com.facebook.litho:litho-core:0.2.1' 121 | compile 'com.facebook.litho:litho-widget:0.2.1' 122 | provided 'com.facebook.litho:litho-annotations:0.2.1' 123 | annotationProcessor 'com.facebook.litho:litho-processor:0.2.1' 124 | compile 'com.facebook.soloader:soloader:0.2.0' 125 | debugCompile 'com.facebook.litho:litho-stetho:0.2.1' 126 | debugCompile 'com.facebook.litho:litho-fresco:0.2.1' 127 | testCompile 'com.facebook.litho:litho-testing:0.2.1' 128 | 129 | androidTestCompile 'com.android.support:support-annotations:25.3.1' 130 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 131 | androidTestCompile 'com.android.support.test:runner:0.5' 132 | androidTestCompile 'org.mockito:mockito-android:2.7.0' 133 | androidTestCompile 'com.google.dagger:dagger-compiler:2.9' 134 | 135 | configurations.all { 136 | resolutionStrategy.force 'com.google.code.findbugs:jsr305:2.0.1' 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/giang.nguyen/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/vn/tale/architecture/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("vn.tale.architecture", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/androidTest/java/vn/tale/architecture/LoginTest.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import android.app.Instrumentation; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.espresso.action.ViewActions; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | import io.reactivex.Single; 9 | import javax.inject.Inject; 10 | import org.junit.Before; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import vn.tale.architecture.login.LoginActivity; 15 | import vn.tale.architecture.model.User; 16 | import vn.tale.architecture.model.error.AuthenticateError; 17 | import vn.tale.architecture.model.manager.UserModel; 18 | 19 | import static android.support.test.espresso.Espresso.closeSoftKeyboard; 20 | import static android.support.test.espresso.Espresso.onView; 21 | import static android.support.test.espresso.action.ViewActions.click; 22 | import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; 23 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 24 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 25 | import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; 26 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 28 | import static org.hamcrest.Matchers.not; 29 | import static org.mockito.ArgumentMatchers.eq; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | /** 34 | * Created by Giang Nguyen on 2/27/17. 35 | */ 36 | @RunWith(AndroidJUnit4.class) 37 | public class LoginTest { 38 | 39 | private static final String INVALID_EMAIL = "foo@bar"; 40 | private static final String VALID_EMAIL = "foo@tiki.vn"; 41 | private static final String VALID_PASSWORD = "123456"; 42 | private static final String INVALID_PASSWORD = "123"; 43 | 44 | @Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>( 45 | LoginActivity.class, 46 | true, 47 | true); 48 | 49 | @Inject UserModel mockedUserModel; 50 | 51 | @Before 52 | public void setUp() throws Exception { 53 | Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 54 | App app = (App) instrumentation.getTargetContext().getApplicationContext(); 55 | ((TestAppComponent) app.getAppComponent()).inject(this); 56 | 57 | when(mockedUserModel.login(eq(VALID_EMAIL), eq(VALID_PASSWORD))) 58 | .thenReturn(Single.just(mock(User.class))); 59 | when(mockedUserModel.login(eq(VALID_EMAIL), eq(INVALID_PASSWORD))) 60 | .thenReturn(Single.error(new AuthenticateError())); 61 | } 62 | 63 | @Test 64 | public void should_disable_login_button_by_default() throws Exception { 65 | loginScreen() 66 | .assertLoginView() 67 | .loginButtonDisabled(); 68 | } 69 | 70 | @Test 71 | public void should_validate_invalid_email_realtime() throws Exception { 72 | loginScreen() 73 | .inputEmail(INVALID_EMAIL) 74 | .assertLoginView() 75 | .invalidEmailMessageShowed(); 76 | } 77 | 78 | @Test 79 | public void should_validate_valid_email_realtime() throws Exception { 80 | loginScreen() 81 | .inputEmail(VALID_EMAIL) 82 | .assertLoginView() 83 | .invalidEmailMessageNotShowed() 84 | .loginButtonEnabled(); 85 | } 86 | 87 | @Test 88 | public void should_show_error_when_login_fail() throws Exception { 89 | loginScreen() 90 | .inputEmail(VALID_EMAIL) 91 | .inputPassword(INVALID_PASSWORD) 92 | .submit() 93 | .assertLoginView() 94 | .loginFailMessageShowed(); 95 | } 96 | 97 | @Test 98 | public void should_hide_when_login_success() throws Exception { 99 | loginScreen() 100 | .inputEmail(VALID_EMAIL) 101 | .inputPassword(VALID_PASSWORD) 102 | .submit() 103 | .assertLoginView() 104 | .loginSuccessMessageShowed() 105 | .hidden(); 106 | } 107 | 108 | private LoginRobot loginScreen() { 109 | return new LoginRobot(); 110 | } 111 | 112 | private static class LoginRobot { 113 | 114 | LoginRobot inputEmail(String email) { 115 | onView(withId(R2.id.etEmail)) 116 | .perform(ViewActions.typeText(email)); 117 | return this; 118 | } 119 | 120 | LoginRobot inputPassword(String password) { 121 | closeSoftKeyboard(); 122 | onView(withId(R2.id.etPassword)) 123 | .perform(ViewActions.typeText(password)); 124 | return this; 125 | } 126 | 127 | LoginRobot submit() { 128 | closeSoftKeyboard(); 129 | onView(withText(R.string.sign_in)) 130 | .perform(click()); 131 | return this; 132 | } 133 | 134 | LoginAssertion assertLoginView() { 135 | closeSoftKeyboard(); 136 | return new LoginAssertion(); 137 | } 138 | } 139 | 140 | private static class LoginAssertion { 141 | 142 | LoginAssertion invalidEmailMessageShowed() { 143 | onView(withText(R.string.email_is_invalid)) 144 | .check(matches(isDisplayed())); 145 | return this; 146 | } 147 | 148 | LoginAssertion invalidEmailMessageNotShowed() { 149 | onView(withText(R.string.email_is_invalid)) 150 | .check(doesNotExist()); 151 | return this; 152 | } 153 | 154 | LoginAssertion loginFailMessageShowed() { 155 | onView(withText(R.string.email_and_password_are_mismatched)) 156 | .check(matches(isDisplayed())); 157 | return this; 158 | } 159 | 160 | LoginAssertion loginSuccessMessageShowed() { 161 | onView(withText(R.string.successfully)) 162 | .check(matches(isDisplayed())); 163 | return this; 164 | } 165 | 166 | LoginAssertion loginButtonEnabled() { 167 | onView(withText(R.string.sign_in)) 168 | .check(matches(isEnabled())); 169 | return this; 170 | } 171 | 172 | LoginAssertion loginButtonDisabled() { 173 | onView(withText(R.string.sign_in)) 174 | .check(matches(not(isEnabled()))); 175 | return this; 176 | } 177 | 178 | void hidden() { 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/src/androidTest/java/vn/tale/architecture/MockApplication.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import vn.tale.architecture.common.SchedulerObservableTransformer; 4 | import vn.tale.architecture.common.SchedulerSingleTransformer; 5 | import vn.tale.architecture.model.manager.UserModel; 6 | 7 | import static org.mockito.Mockito.mock; 8 | 9 | /** 10 | * Created by Giang Nguyen on 2/27/17. 11 | */ 12 | 13 | public class MockApplication extends App { 14 | 15 | private AppComponent appComponent; 16 | 17 | @Override public void onCreate() { 18 | super.onCreate(); 19 | appComponent = DaggerTestAppComponent.builder() 20 | .appSingletonModule(new AppSingletonModule() { 21 | @Override public UserModel provideUserModel() { 22 | return mock(UserModel.class); 23 | } 24 | }) 25 | .appModule(new AppModule() { 26 | @Override SchedulerObservableTransformer provideSchedulerObservableTransformer() { 27 | return SchedulerObservableTransformer.TEST; 28 | } 29 | 30 | @Override SchedulerSingleTransformer provideSchedulerSingleTransformer() { 31 | return SchedulerSingleTransformer.TEST; 32 | } 33 | }) 34 | .build(); 35 | } 36 | 37 | @Override public AppComponent getAppComponent() { 38 | return appComponent; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/androidTest/java/vn/tale/architecture/MockTestRunner.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.test.runner.AndroidJUnitRunner; 6 | 7 | /** 8 | * Created by Giang Nguyen on 2/27/17. 9 | */ 10 | 11 | public class MockTestRunner extends AndroidJUnitRunner { 12 | 13 | @Override public Application newApplication(ClassLoader cl, String className, Context context) 14 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 15 | return super.newApplication(cl, MockApplication.class.getName(), context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/vn/tale/architecture/TestAppComponent.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import dagger.Component; 4 | import javax.inject.Singleton; 5 | 6 | /** 7 | * Created by Giang Nguyen on 2/27/17. 8 | */ 9 | @Singleton 10 | @Component(modules = { 11 | AppSingletonModule.class, 12 | AppModule.class 13 | }) 14 | public interface TestAppComponent extends AppComponent { 15 | 16 | void inject(LoginTest loginTest); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/ActivityScope.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import javax.inject.Scope; 4 | 5 | @Scope 6 | public @interface ActivityScope { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/App.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | import com.facebook.drawee.backends.pipeline.Fresco; 8 | import com.facebook.litho.LithoWebKitInspector; 9 | import com.facebook.soloader.SoLoader; 10 | import com.facebook.stetho.Stetho; 11 | 12 | import timber.log.Timber; 13 | 14 | /** 15 | Created by Giang Nguyen on 2/21/17. 16 | */ 17 | 18 | public class App extends Application { 19 | 20 | private AppComponent appComponent; 21 | 22 | public static App get(Context context) { 23 | return ((App) context.getApplicationContext()); 24 | } 25 | 26 | @Override 27 | public void onCreate() { 28 | super.onCreate(); 29 | initTimber(); 30 | SoLoader.init(this, false); 31 | Stetho.initialize( 32 | Stetho.newInitializerBuilder(this) 33 | .enableWebKitInspector(new LithoWebKitInspector(this)) 34 | .build()); 35 | Fresco.initialize(this); 36 | 37 | appComponent = makeAppComponent(); 38 | } 39 | 40 | public AppComponent getAppComponent() { 41 | return appComponent; 42 | } 43 | 44 | private AppComponent makeAppComponent() { 45 | return DaggerAppComponent.builder() 46 | .appModule(new AppModule()) 47 | .appSingletonModule(new AppSingletonModule()) 48 | .build(); 49 | } 50 | 51 | private void initTimber() { 52 | if (BuildConfig.DEBUG) { 53 | Timber.plant(new Timber.DebugTree()); 54 | } else { 55 | Timber.plant(new CrashReportingTree()); 56 | } 57 | } 58 | 59 | private static class CrashReportingTree extends Timber.Tree { 60 | 61 | @Override 62 | protected void log(int priority, String tag, String message, Throwable t) { 63 | if (priority == Log.VERBOSE || priority == Log.DEBUG) { 64 | return; 65 | } 66 | 67 | Log.println(priority, tag, message); 68 | 69 | if (t != null) { 70 | if (priority == Log.ERROR) { 71 | Log.e(tag, message, t); 72 | } else if (priority == Log.WARN) { 73 | Log.w(tag, message, t); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/AppComponent.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Component; 6 | import vn.tale.architecture.counter.CounterComponent; 7 | import vn.tale.architecture.counter.CounterModule; 8 | import vn.tale.architecture.home.HomeComponent; 9 | import vn.tale.architecture.home.HomeModule; 10 | import vn.tale.architecture.login.LoginComponent; 11 | import vn.tale.architecture.login.LoginModule; 12 | 13 | @Singleton 14 | @Component(modules = { 15 | AppSingletonModule.class, 16 | AppModule.class 17 | }) 18 | public interface AppComponent { 19 | 20 | LoginComponent plus(LoginModule loginModule); 21 | 22 | HomeComponent plus(HomeModule homeModule); 23 | 24 | CounterComponent plus(CounterModule counterModule); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/AppModule.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import io.reactivex.ObservableTransformer; 6 | import io.reactivex.SingleTransformer; 7 | import io.reactivex.android.schedulers.AndroidSchedulers; 8 | import io.reactivex.schedulers.Schedulers; 9 | import vn.tale.architecture.common.EmailValidator; 10 | import vn.tale.architecture.common.SchedulerObservableTransformer; 11 | import vn.tale.architecture.common.SchedulerSingleTransformer; 12 | import vn.tale.architecture.common.util.ImageLoader; 13 | 14 | /** 15 | * Created by Giang Nguyen on 2/27/17. 16 | */ 17 | @Module class AppModule { 18 | 19 | @Provides EmailValidator emailValidator() { 20 | return new EmailValidator(); 21 | } 22 | 23 | @Provides 24 | SchedulerObservableTransformer provideSchedulerObservableTransformer() { 25 | return new SchedulerObservableTransformer() { 26 | @SuppressWarnings("unchecked") 27 | @Override public ObservableTransformer transformer() { 28 | return upstream -> upstream.subscribeOn(Schedulers.io()) 29 | .observeOn(AndroidSchedulers.mainThread()); 30 | } 31 | }; 32 | } 33 | 34 | @Provides 35 | SchedulerSingleTransformer provideSchedulerSingleTransformer() { 36 | return new SchedulerSingleTransformer() { 37 | @SuppressWarnings("unchecked") 38 | @Override public SingleTransformer transformer() { 39 | return upstream -> upstream.subscribeOn(Schedulers.io()) 40 | .observeOn(AndroidSchedulers.mainThread()); 41 | } 42 | }; 43 | } 44 | 45 | @Provides ImageLoader provideImageLoader() { 46 | return new GlideImageLoader(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/AppSingletonModule.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import okhttp3.OkHttpClient; 11 | import okhttp3.Request; 12 | import okhttp3.logging.HttpLoggingInterceptor; 13 | import retrofit2.Retrofit; 14 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 15 | import retrofit2.converter.gson.GsonConverterFactory; 16 | import timber.log.Timber; 17 | import vn.tale.architecture.common.AppRouter; 18 | import vn.tale.architecture.model.api.GithubApi; 19 | import vn.tale.architecture.model.manager.HomeModel; 20 | import vn.tale.architecture.model.manager.UserModel; 21 | 22 | @Module 23 | class AppSingletonModule { 24 | 25 | @Provides 26 | @Singleton 27 | public UserModel provideUserModel() { 28 | return new UserModel(); 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | HomeModel provideHomeModel() { 34 | return new HomeModel(); 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | AppRouter provideAppRouter() { 40 | return new AppRouter(); 41 | } 42 | 43 | @Singleton 44 | @Provides 45 | OkHttpClient provideOkHttpClient() { 46 | final OkHttpClient.Builder builder = new OkHttpClient.Builder(); 47 | builder.addInterceptor(chain -> { 48 | final Request originalRequest = chain.request(); 49 | final Request request = originalRequest.newBuilder() 50 | .header("Accept", "application/vnd.github.v3.full+json") 51 | .build(); 52 | return chain.proceed(request); 53 | }); 54 | if (BuildConfig.DEBUG) { 55 | final HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(message -> Timber.tag("OkHttp").d(message)); 56 | loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); 57 | builder.addInterceptor(loggingInterceptor); 58 | } 59 | return builder 60 | .followRedirects(false) 61 | .build(); 62 | } 63 | 64 | @Singleton 65 | @Provides 66 | GithubApi provideGithubApi(OkHttpClient okHttpClient) { 67 | return createRestServices(okHttpClient, "https://api.github.com", GithubApi.class); 68 | } 69 | 70 | private T createRestServices(OkHttpClient okHttpClient, String baseUrl, 71 | Class servicesClass) { 72 | final Gson gson = new GsonBuilder().create(); 73 | Retrofit retrofit = new Retrofit.Builder() 74 | .baseUrl(baseUrl) 75 | .client(okHttpClient) 76 | .addConverterFactory(GsonConverterFactory.create(gson)) 77 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 78 | .build(); 79 | return retrofit.create(servicesClass); 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/GlideImageLoader.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.widget.ImageView; 5 | import com.bumptech.glide.Glide; 6 | import vn.tale.architecture.common.util.ImageLoader; 7 | 8 | class GlideImageLoader implements ImageLoader { 9 | 10 | @Override public void cancel(@NonNull ImageView imageView) { 11 | Glide.clear(imageView); 12 | } 13 | 14 | @Override public void downloadInto(@NonNull String url, @NonNull ImageView imageView) { 15 | Glide.with(imageView.getContext()) 16 | .load(url) 17 | .placeholder(R.drawable.ic_placeholder_24dp) 18 | .into(imageView); 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/AppRouter.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.annotation.VisibleForTesting; 6 | import vn.tale.architecture.login.LoginActivity; 7 | 8 | /** 9 | * Created by Giang Nguyen on 2/21/17. 10 | */ 11 | 12 | public class AppRouter { 13 | private final IntentFactory intentFactory; 14 | 15 | public AppRouter() { 16 | intentFactory = new IntentFactory(); 17 | } 18 | 19 | @VisibleForTesting AppRouter(IntentFactory intentFactory) { 20 | this.intentFactory = intentFactory; 21 | } 22 | 23 | public Intent loginIntent(Context context) { 24 | return intentFactory.create(context, LoginActivity.class); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/CollectionsX.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.support.annotation.NonNull; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | /** 8 | * Created by Giang Nguyen on 3/8/17. 9 | */ 10 | 11 | public final class CollectionsX { 12 | 13 | private CollectionsX() { 14 | //no instance 15 | } 16 | 17 | public static List concat(@NonNull List list1, @NonNull List list2) { 18 | final ArrayList list = new ArrayList<>(list1.size() + list2.size()); 19 | if (!list1.isEmpty()) { 20 | list.addAll(list1); 21 | } 22 | if (!list2.isEmpty()) { 23 | list.addAll(list2); 24 | } 25 | return list; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/EmailValidator.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import io.reactivex.Observable; 4 | import java.util.regex.Pattern; 5 | import vn.tale.architecture.model.error.InvalidEmailError; 6 | 7 | /** 8 | * Created by Giang Nguyen on 2/21/17. 9 | */ 10 | 11 | public class EmailValidator { 12 | private static final Pattern EMAIL_ADDRESS 13 | = Pattern.compile( 14 | "[a-zA-Z0-9\\+\\._%\\-\\+]{1,256}" + 15 | "@" + 16 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + 17 | "(" + 18 | "\\." + 19 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + 20 | ")+" 21 | ); 22 | 23 | public boolean isValid(CharSequence email) { 24 | return email != null && EMAIL_ADDRESS.matcher(email).matches(); 25 | } 26 | 27 | public Observable checkEmail(CharSequence email) { 28 | return Observable.fromCallable(() -> { 29 | if (isValid(email)) { 30 | return true; 31 | } 32 | throw new InvalidEmailError(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/IntentFactory.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | /** 8 | * Created by Giang Nguyen on 2/21/17. 9 | */ 10 | 11 | class IntentFactory { 12 | 13 | Intent create(Context context, Class activityClass) { 14 | return new Intent(context, activityClass); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/Preconditions.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.support.annotation.RestrictTo; 4 | 5 | import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 6 | 7 | @RestrictTo(LIBRARY_GROUP) 8 | public final class Preconditions { 9 | private Preconditions() { 10 | throw new AssertionError("No instances."); 11 | } 12 | 13 | public static void checkNotNull(Object value) { 14 | if (value == null) { 15 | throw new NullPointerException(); 16 | } 17 | } 18 | 19 | public static void checkNotNull(Object value, String message) { 20 | if (value == null) { 21 | throw new NullPointerException(message); 22 | } 23 | } 24 | 25 | public static void checkNotEmpty(Object[] array, String message) { 26 | if (array.length == 0) { 27 | throw new IllegalArgumentException(message); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/SchedulerObservableTransformer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.support.annotation.VisibleForTesting; 4 | import io.reactivex.Observable; 5 | import io.reactivex.ObservableSource; 6 | import io.reactivex.ObservableTransformer; 7 | import io.reactivex.schedulers.Schedulers; 8 | 9 | /** 10 | * Created by Giang Nguyen on 2/26/17. 11 | */ 12 | 13 | public interface SchedulerObservableTransformer { 14 | 15 | @VisibleForTesting SchedulerObservableTransformer TEST = new SchedulerObservableTransformer() { 16 | @SuppressWarnings("unchecked") 17 | @Override public ObservableTransformer transformer() { 18 | return (ObservableTransformer) new ObservableTransformer() { 19 | @Override public ObservableSource apply(Observable upstream) { 20 | return upstream.subscribeOn(Schedulers.trampoline()) 21 | .observeOn(Schedulers.trampoline()); 22 | } 23 | }; 24 | } 25 | }; 26 | 27 | ObservableTransformer transformer(); 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/SchedulerSingleTransformer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common; 2 | 3 | import android.support.annotation.VisibleForTesting; 4 | import io.reactivex.Single; 5 | import io.reactivex.SingleSource; 6 | import io.reactivex.SingleTransformer; 7 | import io.reactivex.schedulers.Schedulers; 8 | 9 | /** 10 | * Created by Giang Nguyen on 2/26/17. 11 | */ 12 | 13 | public interface SchedulerSingleTransformer { 14 | 15 | @VisibleForTesting SchedulerSingleTransformer TEST = new SchedulerSingleTransformer() { 16 | @SuppressWarnings("unchecked") 17 | @Override public SingleTransformer transformer() { 18 | return (SingleTransformer) new SingleTransformer() { 19 | @Override public SingleSource apply(Single upstream) { 20 | return upstream.subscribeOn(Schedulers.trampoline()) 21 | .observeOn(Schedulers.trampoline()); 22 | } 23 | }; 24 | } 25 | }; 26 | 27 | SingleTransformer transformer(); 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.base; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.app.AppCompatActivity; 7 | import butterknife.ButterKnife; 8 | import butterknife.Unbinder; 9 | import vn.tale.architecture.common.dagger.DaggerComponentFactory; 10 | import vn.tale.architecture.common.dagger.DaggerLifecycleDelegate; 11 | 12 | /** 13 | * Created by Giang Nguyen on 2/22/17. 14 | */ 15 | 16 | public abstract class BaseActivity extends AppCompatActivity { 17 | 18 | private DaggerLifecycleDelegate daggerLifecycleDelegate; 19 | 20 | private Unbinder unbinder; 21 | 22 | protected abstract DaggerComponentFactory daggerComponentFactory(); 23 | 24 | @Override public final Object onRetainCustomNonConfigurationInstance() { 25 | return daggerLifecycleDelegate.onRetainCustomNonConfigurationInstance(); 26 | } 27 | 28 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | daggerLifecycleDelegate = new DaggerLifecycleDelegate<>(daggerComponentFactory()); 31 | daggerLifecycleDelegate.onCreate(this); 32 | } 33 | 34 | protected final DaggerComponent daggerComponent() { 35 | return daggerLifecycleDelegate.daggerComponent(); 36 | } 37 | 38 | protected void bindViews(Activity activity) { 39 | unbinder = ButterKnife.bind(activity); 40 | } 41 | 42 | @Override protected void onDestroy() { 43 | super.onDestroy(); 44 | if (unbinder != null) { 45 | unbinder.unbind(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/base/RvvmActivity.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import io.reactivex.disposables.CompositeDisposable; 6 | import io.reactivex.disposables.Disposable; 7 | import vn.tale.architecture.common.redux.LifecycleDelegate; 8 | import vn.tale.architecture.common.redux.Store; 9 | 10 | /** 11 | * Created by Giang Nguyen on 3/23/17. 12 | */ 13 | 14 | public abstract class RvvmActivity 15 | extends BaseActivity { 16 | 17 | private LifecycleDelegate lifecycleDelegate; 18 | private CompositeDisposable disposables = new CompositeDisposable(); 19 | 20 | protected abstract void injectDependencies(); 21 | 22 | protected abstract Store store(); 23 | 24 | protected void disposeOnStop(Disposable disposable) { 25 | disposables.add(disposable); 26 | } 27 | 28 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | injectDependencies(); 31 | lifecycleDelegate = new LifecycleDelegate<>(store()); 32 | } 33 | 34 | @Override protected void onStart() { 35 | super.onStart(); 36 | lifecycleDelegate.onStart(); 37 | } 38 | 39 | @Override protected void onStop() { 40 | super.onStop(); 41 | lifecycleDelegate.onStop(isFinishing()); 42 | disposables.clear(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/dagger/DaggerComponentFactory.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.dagger; 2 | 3 | public interface DaggerComponentFactory { 4 | 5 | T makeComponent(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/dagger/DaggerLifecycleDelegate.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.dagger; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.VisibleForTesting; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | /** 8 | * Created by Giang Nguyen on 3/20/17. 9 | */ 10 | 11 | public class DaggerLifecycleDelegate { 12 | 13 | @VisibleForTesting T daggerComponent; 14 | 15 | private DaggerComponentFactory daggerComponentFactory; 16 | 17 | public DaggerLifecycleDelegate(@NonNull DaggerComponentFactory daggerComponentFactory) { 18 | this.daggerComponentFactory = daggerComponentFactory; 19 | } 20 | 21 | @SuppressWarnings("unchecked") public void onCreate(AppCompatActivity activity) { 22 | daggerComponent = (T) activity.getLastCustomNonConfigurationInstance(); 23 | if (daggerComponent == null) { 24 | daggerComponent = daggerComponentFactory.makeComponent(); 25 | } 26 | } 27 | 28 | public Object onRetainCustomNonConfigurationInstance() { 29 | return daggerComponent; 30 | } 31 | 32 | public T daggerComponent() { 33 | return daggerComponent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Action.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/23/17. 5 | */ 6 | 7 | public interface Action { 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Effect.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | import io.reactivex.Observable; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/24/17. 7 | */ 8 | 9 | public interface Effect { 10 | 11 | Observable apply(Observable action$, Function0 getState); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Function0.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | /** 4 | * A functional interface that returns a value, allows throwing a checked exception. 5 | * 6 | * @param the output value type 7 | */ 8 | public interface Function0 { 9 | /** 10 | * Apply some calculation to the input value and return some other value. 11 | * 12 | * @return the output value 13 | * @throws Exception on error 14 | */ 15 | R apply() throws Exception; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/LifecycleDelegate.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/23/17. 5 | */ 6 | 7 | public class LifecycleDelegate { 8 | 9 | private final Store store; 10 | 11 | public LifecycleDelegate(Store store) { 12 | this.store = store; 13 | } 14 | 15 | public void onStart() { 16 | store.startBinding(); 17 | } 18 | 19 | public void onStop(boolean finishing) { 20 | if (finishing) { 21 | store.stopBinding(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Reducer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | import io.reactivex.functions.BiFunction; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/24/17. 7 | */ 8 | 9 | public interface Reducer extends BiFunction { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Result.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/23/17. 5 | */ 6 | 7 | public interface Result { 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/redux/Store.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.redux; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import io.reactivex.Observable; 6 | import io.reactivex.ObservableSource; 7 | import io.reactivex.disposables.Disposable; 8 | import io.reactivex.functions.Function; 9 | import io.reactivex.subjects.BehaviorSubject; 10 | import io.reactivex.subjects.PublishSubject; 11 | import vn.tale.architecture.common.Preconditions; 12 | 13 | public class Store { 14 | 15 | private final Reducer reducer; 16 | private final PublishSubject action$; 17 | private final BehaviorSubject state$; 18 | private final Observable result$; 19 | private Disposable disposable; 20 | 21 | Store(@NonNull State initialState, 22 | @NonNull Reducer reducer, 23 | @NonNull Effect[] effects) { 24 | this.reducer = reducer; 25 | this.action$ = PublishSubject.create(); 26 | this.state$ = BehaviorSubject.createDefault(initialState); 27 | this.result$ = Observable.fromArray(effects) 28 | .flatMap(transformer -> transformer.apply(action$, this::currentState)); 29 | } 30 | 31 | public static Builder builder() { 32 | return new Builder<>(); 33 | } 34 | 35 | private State currentState() { 36 | return state$.getValue(); 37 | } 38 | 39 | public Observable state$() { 40 | return state$; 41 | } 42 | 43 | public void dispatch(Action action) { 44 | action$.onNext(action); 45 | } 46 | 47 | void startBinding() { 48 | if (disposable != null) { 49 | // binding is started already 50 | return; 51 | } 52 | disposable = result$ 53 | .scan(currentState(), reducer) 54 | .subscribe(state$::onNext); 55 | } 56 | 57 | void stopBinding() { 58 | if (disposable != null) { 59 | disposable.dispose(); 60 | disposable = null; 61 | } 62 | } 63 | 64 | public static class Builder { 65 | private State initialState; 66 | private Reducer reducer; 67 | private Effect[] effects; 68 | 69 | Builder() { 70 | // private constructor 71 | } 72 | 73 | public Builder initialState(@NonNull State initialState) { 74 | Preconditions.checkNotNull(initialState, "initialState must not be null"); 75 | this.initialState = initialState; 76 | return this; 77 | } 78 | 79 | public Builder reducer(@NonNull Reducer reducer) { 80 | Preconditions.checkNotNull(reducer, "reducer must not be null"); 81 | this.reducer = reducer; 82 | return this; 83 | } 84 | 85 | @SafeVarargs 86 | public final Builder effects(@NonNull Effect... effects) { 87 | Preconditions.checkNotNull(effects); 88 | Preconditions.checkNotEmpty(effects, "effects must not be empty"); 89 | this.effects = effects; 90 | return this; 91 | } 92 | 93 | public Store make() { 94 | return new Store<>(initialState, reducer, effects); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/util/ImageLoader.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.util; 2 | 3 | import android.widget.ImageView; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/27/17. 7 | */ 8 | 9 | public interface ImageLoader { 10 | void cancel(ImageView imageView); 11 | 12 | void downloadInto(String url, ImageView imageView); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/common/util/InfiniteScrollListener.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.common.util; 2 | 3 | import android.support.v7.widget.LinearLayoutManager; 4 | import android.support.v7.widget.RecyclerView; 5 | 6 | public class InfiniteScrollListener extends RecyclerView.OnScrollListener { 7 | 8 | private final LinearLayoutManager linearLayoutManager; 9 | private final int visibleThreshold; 10 | private final Runnable callbacks; 11 | private int previousTotal = 0; // The total number of items in the dataset after the last load 12 | private boolean loading = true; // True if we are still waiting for the last set of data to load. 13 | 14 | public InfiniteScrollListener(LinearLayoutManager linearLayoutManager, int visibleThreshold, 15 | Runnable callbacks) { 16 | this.linearLayoutManager = linearLayoutManager; 17 | this.visibleThreshold = visibleThreshold; 18 | this.callbacks = callbacks; 19 | } 20 | 21 | @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 22 | super.onScrolled(recyclerView, dx, dy); 23 | final int visibleItemCount = recyclerView.getChildCount(); 24 | final int totalItemCount = linearLayoutManager.getItemCount(); 25 | final int firstVisibleItem = linearLayoutManager.findFirstVisibleItemPosition(); 26 | 27 | if (loading) { 28 | if (totalItemCount > previousTotal || totalItemCount == 0) { 29 | loading = false; 30 | previousTotal = totalItemCount; 31 | } 32 | } 33 | 34 | // End has been reached 35 | if (!loading && totalItemCount - visibleItemCount <= firstVisibleItem + visibleThreshold) { 36 | recyclerView.post(callbacks); 37 | loading = true; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/CounterActivity.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.widget.TextView; 6 | import butterknife.BindView; 7 | import butterknife.OnClick; 8 | import javax.inject.Inject; 9 | import vn.tale.architecture.App; 10 | import vn.tale.architecture.R; 11 | import vn.tale.architecture.R2; 12 | import vn.tale.architecture.common.base.RvvmActivity; 13 | import vn.tale.architecture.common.dagger.DaggerComponentFactory; 14 | import vn.tale.architecture.common.redux.Store; 15 | import vn.tale.architecture.counter.action.ChangeValueAction; 16 | 17 | /** 18 | * Created by Giang Nguyen on 3/24/17. 19 | */ 20 | 21 | public class CounterActivity extends RvvmActivity { 22 | 23 | @Inject Store store; 24 | @BindView(R2.id.tvValue) TextView tvValue; 25 | 26 | @Override protected void injectDependencies() { 27 | daggerComponent().inject(this); 28 | } 29 | 30 | @Override protected Store store() { 31 | return store; 32 | } 33 | 34 | @Override protected DaggerComponentFactory daggerComponentFactory() { 35 | return () -> App.get(this).getAppComponent().plus(new CounterModule()); 36 | } 37 | 38 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_counter); 41 | bindViews(this); 42 | } 43 | 44 | @OnClick(R2.id.increase) public void onIncreaseClick() { 45 | store.dispatch(ChangeValueAction.INCREASE); 46 | } 47 | 48 | @OnClick(R2.id.decrease) public void onDecreaseClick() { 49 | store.dispatch(ChangeValueAction.DECREASE); 50 | } 51 | 52 | @Override protected void onStart() { 53 | super.onStart(); 54 | disposeOnStop(store.state$() 55 | .distinctUntilChanged() 56 | .subscribe((state) -> tvValue.setText(String.valueOf(state.value())))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/CounterComponent.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter; 2 | 3 | import dagger.Subcomponent; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/24/17. 7 | */ 8 | @Subcomponent(modules = CounterModule.class) 9 | public interface CounterComponent { 10 | 11 | void inject(CounterActivity counterActivity); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/CounterModule.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import vn.tale.architecture.common.redux.Store; 6 | import vn.tale.architecture.counter.effect.ChangeValueEffect; 7 | 8 | /** 9 | * Created by Giang Nguyen on 3/24/17. 10 | */ 11 | @Module 12 | public class CounterModule { 13 | 14 | @Provides Store provideCounterUiModelViewModel() { 15 | return Store.builder() 16 | .initialState(CounterState.make(0)) 17 | .effects(new ChangeValueEffect()) 18 | .reducer(new CounterReducer()) 19 | .make(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/CounterReducer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter; 2 | 3 | import vn.tale.architecture.common.redux.Reducer; 4 | import vn.tale.architecture.common.redux.Result; 5 | import vn.tale.architecture.counter.result.ChangeValueResult; 6 | 7 | /** 8 | * Created by Giang Nguyen on 3/24/17. 9 | */ 10 | 11 | public class CounterReducer implements Reducer { 12 | 13 | @Override public CounterState apply(CounterState counterState, Result result) 14 | throws Exception { 15 | if (result instanceof ChangeValueResult) { 16 | return CounterState.make(((ChangeValueResult) result).value()); 17 | } 18 | 19 | throw new IllegalArgumentException("Unknown result " + result); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/CounterState.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/24/17. 7 | */ 8 | @AutoValue 9 | public abstract class CounterState { 10 | 11 | public static CounterState make(int value) { 12 | return new AutoValue_CounterState(value); 13 | } 14 | 15 | public abstract int value(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/action/ChangeValueAction.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter.action; 2 | 3 | import vn.tale.architecture.common.redux.Action; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/24/17. 7 | */ 8 | public final class ChangeValueAction implements Action { 9 | public static final ChangeValueAction INCREASE = new ChangeValueAction(); 10 | public static final ChangeValueAction DECREASE = new ChangeValueAction(); 11 | 12 | private ChangeValueAction() { 13 | //no instance 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/effect/ChangeValueEffect.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter.effect; 2 | 3 | import io.reactivex.Observable; 4 | import vn.tale.architecture.common.redux.Action; 5 | import vn.tale.architecture.common.redux.Function0; 6 | import vn.tale.architecture.common.redux.Result; 7 | import vn.tale.architecture.common.redux.Effect; 8 | import vn.tale.architecture.counter.CounterState; 9 | import vn.tale.architecture.counter.action.ChangeValueAction; 10 | import vn.tale.architecture.counter.result.ChangeValueResult; 11 | 12 | /** 13 | * Created by Giang Nguyen on 3/24/17. 14 | */ 15 | 16 | public class ChangeValueEffect implements Effect { 17 | 18 | @Override public Observable apply(Observable action$, 19 | Function0 getState) { 20 | return action$ 21 | .ofType(ChangeValueAction.class) 22 | .map(action -> { 23 | final CounterState uiModel = getState.apply(); 24 | if (action == ChangeValueAction.INCREASE) { 25 | return ChangeValueResult.make(uiModel.value() + 1); 26 | } else { 27 | return ChangeValueResult.make(uiModel.value() - 1); 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/counter/result/ChangeValueResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.counter.result; 2 | 3 | import com.google.auto.value.AutoValue; 4 | import vn.tale.architecture.common.redux.Result; 5 | 6 | /** 7 | * Created by Giang Nguyen on 3/24/17. 8 | */ 9 | @AutoValue 10 | public abstract class ChangeValueResult implements Result { 11 | 12 | public static ChangeValueResult make(int value) { 13 | return new AutoValue_ChangeValueResult(value); 14 | } 15 | 16 | public abstract int value(); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeActivity.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.design.widget.BaseTransientBottomBar; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v4.widget.SwipeRefreshLayout; 8 | import android.support.v7.widget.OrientationHelper; 9 | import android.widget.Toast; 10 | 11 | import com.facebook.litho.ComponentContext; 12 | import com.facebook.litho.ComponentInfo; 13 | import com.facebook.litho.LithoView; 14 | import com.facebook.litho.widget.LinearLayoutInfo; 15 | import com.facebook.litho.widget.RecyclerBinder; 16 | 17 | import java.util.List; 18 | 19 | import javax.inject.Inject; 20 | 21 | import butterknife.BindView; 22 | import timber.log.Timber; 23 | import vn.tale.architecture.App; 24 | import vn.tale.architecture.R; 25 | import vn.tale.architecture.common.base.RvvmActivity; 26 | import vn.tale.architecture.common.dagger.DaggerComponentFactory; 27 | import vn.tale.architecture.common.redux.Store; 28 | import vn.tale.architecture.home.action.HomeAction; 29 | import vn.tale.architecture.home.component.HomeListComponent; 30 | import vn.tale.architecture.home.component.ProductComponent; 31 | import vn.tale.architecture.home.component.ProductSlideComponent; 32 | import vn.tale.architecture.home.component.SingleBannerComponent; 33 | import vn.tale.architecture.home.component.TripleBannersComponent; 34 | import vn.tale.architecture.model.HomeSection; 35 | import vn.tale.architecture.model.Product; 36 | import vn.tale.architecture.model.ProductSlideSection; 37 | import vn.tale.architecture.model.SingleBannerSection; 38 | import vn.tale.architecture.model.TripleBannerSection; 39 | 40 | public class HomeActivity extends RvvmActivity { 41 | 42 | @Inject 43 | Store store; 44 | 45 | @Inject 46 | HomeViewModel viewModel; 47 | 48 | @BindView(R.id.ltView) 49 | LithoView ltView; 50 | 51 | @BindView(R.id.sRefresh) 52 | SwipeRefreshLayout sRefresh; 53 | 54 | private Snackbar errorSnackbar; 55 | private RecyclerBinder recyclerBinder; 56 | private ComponentContext componentContext; 57 | 58 | @Override 59 | protected DaggerComponentFactory daggerComponentFactory() { 60 | return () -> App.get(this).getAppComponent().plus(new HomeModule()); 61 | } 62 | 63 | @Override 64 | protected void injectDependencies() { 65 | daggerComponent().inject(this); 66 | } 67 | 68 | @Override 69 | protected Store store() { 70 | return store; 71 | } 72 | 73 | @Override 74 | protected void onCreate(@Nullable Bundle savedInstanceState) { 75 | super.onCreate(savedInstanceState); 76 | setContentView(R.layout.activity_repos); 77 | bindViews(this); 78 | componentContext = new ComponentContext(this); 79 | recyclerBinder = new RecyclerBinder( 80 | componentContext, 4.0f, new LinearLayoutInfo(this, OrientationHelper.VERTICAL, false)); 81 | sRefresh.setOnRefreshListener(() -> store.dispatch(HomeAction.REFRESH)); 82 | } 83 | 84 | @Override 85 | protected void onStart() { 86 | super.onStart(); 87 | store.dispatch(HomeAction.LOAD); 88 | 89 | disposeOnStop(viewModel.loading$().subscribe(ignored -> renderLoading())); 90 | disposeOnStop(viewModel.refreshing$().subscribe(ignored -> renderRefreshing())); 91 | disposeOnStop(viewModel.loadingMore$().subscribe(ignored -> renderLoadingMore())); 92 | disposeOnStop(viewModel.loadError$().subscribe(this::renderLoadError)); 93 | disposeOnStop(viewModel.loadMoreError$().subscribe(this::renderLoadMoreError)); 94 | disposeOnStop(viewModel.refreshError$().subscribe(this::renderRefreshError)); 95 | disposeOnStop(viewModel.content$().subscribe(this::renderContent)); 96 | } 97 | 98 | private void renderLoadMoreError(Throwable error) { 99 | sRefresh.setRefreshing(false); 100 | Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show(); 101 | } 102 | 103 | private void renderRefreshError(Throwable error) { 104 | sRefresh.setRefreshing(false); 105 | Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show(); 106 | } 107 | 108 | private void renderRefreshing() { 109 | sRefresh.setRefreshing(true); 110 | if (errorSnackbar != null) { 111 | errorSnackbar.dismiss(); 112 | } 113 | } 114 | 115 | private void renderLoadingMore() { 116 | Timber.d("renderLoadingMore"); 117 | } 118 | 119 | private void renderContent(List sections) { 120 | sRefresh.setRefreshing(false); 121 | ComponentInfo.Builder componentInfoBuilder; 122 | 123 | for (HomeSection section : sections) { 124 | 125 | componentInfoBuilder = ComponentInfo.create(); 126 | 127 | if (section instanceof SingleBannerSection) { 128 | componentInfoBuilder 129 | .component( 130 | SingleBannerComponent 131 | .create(componentContext) 132 | .payload((SingleBannerSection) section) 133 | .key(((SingleBannerSection) section).title()) 134 | .build() 135 | ); 136 | } else if (section instanceof TripleBannerSection) { 137 | 138 | componentInfoBuilder 139 | .component( 140 | TripleBannersComponent.create(componentContext) 141 | .payload((TripleBannerSection) section) 142 | .key(((TripleBannerSection) section).title()) 143 | .build() 144 | ); 145 | } else if (section instanceof ProductSlideSection) { 146 | 147 | final RecyclerBinder productSlideBinder = new RecyclerBinder(componentContext, 4.0f, 148 | new LinearLayoutInfo(this, OrientationHelper.HORIZONTAL, false)); 149 | 150 | for (Product product : ((ProductSlideSection) section).products()) { 151 | componentInfoBuilder = ComponentInfo.create(); 152 | componentInfoBuilder 153 | .component( 154 | ProductComponent.create(componentContext) 155 | .product(product) 156 | .key(product.id()) 157 | .build() 158 | ); 159 | productSlideBinder.insertItemAt(productSlideBinder.getItemCount(), componentInfoBuilder.build()); 160 | } 161 | 162 | componentInfoBuilder = ComponentInfo.create(); 163 | componentInfoBuilder 164 | .component( 165 | ProductSlideComponent.create(componentContext) 166 | .title(((ProductSlideSection) section).title()) 167 | .recyclerBinder(productSlideBinder) 168 | .key(((ProductSlideSection) section).title()) 169 | .build() 170 | ); 171 | } 172 | recyclerBinder.insertItemAt(recyclerBinder.getItemCount(), componentInfoBuilder.build()); 173 | } 174 | 175 | ltView.setComponent( 176 | HomeListComponent 177 | .create(componentContext) 178 | .binder(recyclerBinder) 179 | .build() 180 | ); 181 | } 182 | 183 | private void renderLoadError(Throwable error) { 184 | sRefresh.setRefreshing(false); 185 | errorSnackbar = Snackbar.make(ltView, error.getMessage(), 186 | BaseTransientBottomBar.LENGTH_INDEFINITE); 187 | errorSnackbar.show(); 188 | } 189 | 190 | private void renderLoading() { 191 | sRefresh.setRefreshing(true); 192 | if (errorSnackbar != null) { 193 | errorSnackbar.dismiss(); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeComponent.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import dagger.Subcomponent; 4 | import vn.tale.architecture.ActivityScope; 5 | 6 | /** 7 | * Created by Giang Nguyen on 3/27/17. 8 | */ 9 | @ActivityScope 10 | @Subcomponent(modules = HomeModule.class) 11 | public interface HomeComponent { 12 | void inject(HomeActivity topRepoListActivity); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeModule.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import io.reactivex.Observable; 6 | import io.reactivex.android.schedulers.AndroidSchedulers; 7 | import vn.tale.architecture.ActivityScope; 8 | import vn.tale.architecture.common.redux.Store; 9 | import vn.tale.architecture.home.epic.LoadEpic; 10 | import vn.tale.architecture.home.epic.LoadMoreEpic; 11 | import vn.tale.architecture.home.epic.RefreshEpic; 12 | import vn.tale.architecture.model.manager.HomeModel; 13 | 14 | /** 15 | * Created by Giang Nguyen on 3/27/17. 16 | */ 17 | @Module 18 | public class HomeModule { 19 | 20 | @ActivityScope 21 | @Provides Store provideViewModel(HomeModel homeModel) { 22 | return Store.builder() 23 | .initialState(HomeState.idle()) 24 | .effects( 25 | new LoadEpic(homeModel), 26 | new RefreshEpic(homeModel), 27 | new LoadMoreEpic(homeModel)) 28 | .reducer(new HomeReducer()) 29 | .make(); 30 | } 31 | 32 | @Provides 33 | HomeViewModel provideHomeViewModel(Store store) { 34 | final Observable state$ = store.state$() 35 | .observeOn(AndroidSchedulers.mainThread()); 36 | return new HomeViewModel(state$); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeReducer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import java.util.List; 4 | 5 | import ix.Ix; 6 | import vn.tale.architecture.common.redux.Reducer; 7 | import vn.tale.architecture.common.redux.Result; 8 | import vn.tale.architecture.home.result.LoadMoreResult; 9 | import vn.tale.architecture.home.result.LoadResult; 10 | import vn.tale.architecture.home.result.RefreshResult; 11 | import vn.tale.architecture.model.HomeSection; 12 | 13 | /** 14 | Created by Giang Nguyen on 3/28/17. 15 | */ 16 | 17 | public class HomeReducer implements Reducer { 18 | 19 | @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 20 | @Override 21 | public HomeState apply(HomeState state, Result result) 22 | throws Exception { 23 | if (result instanceof LoadResult) { 24 | final LoadResult topRepoResult = (LoadResult) result; 25 | if (topRepoResult.loading()) { 26 | return HomeState.builder(state) 27 | .loading(true) 28 | .make(); 29 | } else if (topRepoResult.error() != null) { 30 | return HomeState.builder(state) 31 | .loading(false) 32 | .loadError(topRepoResult.error()) 33 | .make(); 34 | } else { 35 | return HomeState.builder(state) 36 | .content(topRepoResult.content()) 37 | .loading(false) 38 | .make(); 39 | } 40 | } else if (result instanceof RefreshResult) { 41 | final RefreshResult refreshResult = (RefreshResult) result; 42 | if (refreshResult.loading()) { 43 | return HomeState.builder(state) 44 | .refreshing(true) 45 | .make(); 46 | } else if (refreshResult.error() != null) { 47 | return HomeState.builder(state) 48 | .refreshing(false) 49 | .refreshError(refreshResult.error()) 50 | .make(); 51 | } else { 52 | return HomeState.builder(state) 53 | .refreshing(false) 54 | .content(refreshResult.content()) 55 | .make(); 56 | } 57 | } else if (result instanceof LoadMoreResult) { 58 | final LoadMoreResult loadMoreResult = (LoadMoreResult) result; 59 | if (loadMoreResult.loading()) { 60 | return HomeState.builder(state) 61 | .loadingMore(true) 62 | .make(); 63 | } else if (loadMoreResult.error() != null) { 64 | return HomeState.builder(state) 65 | .loadingMore(false) 66 | .loadMoreError(loadMoreResult.error()) 67 | .make(); 68 | } else { 69 | final List repoList = Ix.from(state.content()) 70 | .mergeWith(((LoadMoreResult) result).content()) 71 | .toList(); 72 | return HomeState.builder(state) 73 | .loadingMore(false) 74 | .page(((LoadMoreResult) result).page()) 75 | .content(repoList) 76 | .make(); 77 | } 78 | } 79 | throw new IllegalArgumentException("Unknown result"); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeState.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import vn.tale.architecture.model.HomeSection; 9 | 10 | /** 11 | Created by Giang Nguyen on 3/27/17. 12 | */ 13 | @com.google.auto.value.AutoValue 14 | public abstract class HomeState { 15 | 16 | public static Builder builder(HomeState source) { 17 | return new AutoValue_HomeState.Builder(source); 18 | } 19 | 20 | public static Builder builder() { 21 | return new AutoValue_HomeState.Builder() 22 | .loading(false) 23 | .loadingMore(false) 24 | .refreshing(false) 25 | .page(1) 26 | .content(Collections.emptyList()); 27 | } 28 | 29 | public static HomeState idle() { 30 | return builder().make(); 31 | } 32 | 33 | public abstract boolean loading(); 34 | 35 | public abstract boolean loadingMore(); 36 | 37 | public abstract boolean refreshing(); 38 | 39 | @Nullable 40 | public abstract Throwable loadError(); 41 | 42 | @Nullable 43 | public abstract Throwable loadMoreError(); 44 | 45 | @Nullable 46 | public abstract Throwable refreshError(); 47 | 48 | public abstract List content(); 49 | 50 | public abstract int page(); 51 | 52 | @com.google.auto.value.AutoValue.Builder 53 | public static abstract class Builder { 54 | public abstract Builder loading(boolean loading); 55 | 56 | public abstract Builder loadingMore(boolean loadingMore); 57 | 58 | public abstract Builder refreshing(boolean refreshing); 59 | 60 | public abstract Builder loadError(Throwable loadError); 61 | 62 | public abstract Builder loadMoreError(Throwable loadMoreError); 63 | 64 | public abstract Builder refreshError(Throwable refreshError); 65 | 66 | public abstract Builder content(List content); 67 | 68 | public abstract Builder page(int page); 69 | 70 | public abstract HomeState make(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/HomeViewModel.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home; 2 | 3 | import java.util.List; 4 | 5 | import io.reactivex.Observable; 6 | import io.reactivex.functions.Function; 7 | import io.reactivex.functions.Predicate; 8 | import vn.tale.architecture.model.HomeSection; 9 | 10 | public class HomeViewModel { 11 | 12 | private final Observable state$; 13 | 14 | public HomeViewModel(Observable state$) { 15 | this.state$ = state$; 16 | } 17 | 18 | Observable loading$() { 19 | return state$ 20 | .filter(HomeState::loading); 21 | } 22 | 23 | Observable refreshing$() { 24 | return state$ 25 | .filter(HomeState::refreshing); 26 | } 27 | 28 | Observable loadingMore$() { 29 | return state$ 30 | .filter(HomeState::loadingMore); 31 | } 32 | 33 | Observable loadError$() { 34 | return state$ 35 | .filter(state -> state.loadError() != null) 36 | .map(HomeState::loadError); 37 | } 38 | 39 | public Observable refreshError$() { 40 | return state$ 41 | .filter(state -> state.refreshError() != null) 42 | .map(HomeState::refreshError); 43 | } 44 | 45 | public Observable loadMoreError$() { 46 | return state$ 47 | .filter(state -> state.loadMoreError() != null) 48 | .map(HomeState::loadMoreError); 49 | } 50 | 51 | public Observable> content$() { 52 | return state$ 53 | .filter(state -> !state.loading() 54 | && !state.refreshing() 55 | && state.loadError() == null 56 | && state.refreshError() == null) 57 | .map(HomeState::content); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/action/HomeAction.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.action; 2 | 3 | import vn.tale.architecture.common.redux.Action; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/27/17. 7 | */ 8 | 9 | public interface HomeAction { 10 | Action LOAD = new Action() {}; 11 | Action LOAD_MORE = new Action() {}; 12 | Action REFRESH = new Action() {}; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/Demos.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import android.support.v7.widget.OrientationHelper; 4 | 5 | import com.facebook.litho.ComponentContext; 6 | import com.facebook.litho.ComponentInfo; 7 | import com.facebook.litho.widget.LinearLayoutInfo; 8 | import com.facebook.litho.widget.RecyclerBinder; 9 | 10 | import java.util.Arrays; 11 | 12 | import vn.tale.architecture.model.Banner; 13 | import vn.tale.architecture.model.Product; 14 | import vn.tale.architecture.model.ProductSlideSection; 15 | import vn.tale.architecture.model.SingleBannerSection; 16 | import vn.tale.architecture.model.TripleBannerSection; 17 | 18 | class Demos { 19 | 20 | private static SingleBannerSection[] SampleData() { 21 | return new SingleBannerSection[]{ 22 | SingleBannerSection.builder().title("ABC") 23 | .banner(Banner.builder().id("1") 24 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/bd/4e/98/bd4e98d11e2a094d8981191ce594e766.jpg") 25 | .link("/home-app-module/product_group_sale") 26 | .ratio(3.34615385F) 27 | .make()).build() 28 | }; 29 | } 30 | 31 | private static TripleBannerSection[] SampleData1() { 32 | return new TripleBannerSection[]{ 33 | TripleBannerSection.builder().title("Thẻ cào cực hot") 34 | .banners(Arrays.asList( 35 | Banner.builder().id("1") 36 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/87/b5/35/87b5350cb9e1a7c8f1601ee1ea7bc20d.jpg") 37 | .link("/home-app-module/product_group_sale") 38 | .ratio(1.3333333333333F) 39 | .make(), 40 | Banner.builder().id("2") 41 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/02/1a/81/021a8138486635ae4c1bc78192265197.jpg") 42 | .link("https://tiki.vn/dich-vu-tien-ich") 43 | .ratio(1.3333333333333F) 44 | .make(), 45 | Banner.builder().id("3") 46 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/ea/15/c1/ea15c112a4899e2dadbcc54905dd8227.jpg") 47 | .link("https://tiki.vn/lp/samsung-galaxy-s8") 48 | .ratio(0.66666666666667F) 49 | .make() 50 | )).build() 51 | }; 52 | } 53 | 54 | private static ProductSlideSection[] SampleData2() { 55 | return new ProductSlideSection[]{ 56 | ProductSlideSection.builder().title("Siêu phẩm Galaxy S8/S8 Plus") 57 | .products(Arrays.asList( 58 | Product.builder() 59 | .id("1") 60 | .imageUrl( 61 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2769.d20170407.t150619.932685.jpg") 62 | .name("Samsung Galaxy S8") 63 | .price(18490000) 64 | .originalPrice(20490000) 65 | .build() 66 | , 67 | Product.builder() 68 | .id("2") 69 | .imageUrl( 70 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2769.d20170407.t150619.932685.jpg") 71 | .name("Samsung Galaxy S8+") 72 | .price(18490000) 73 | .originalPrice(20490000) 74 | .build(), 75 | Product 76 | .builder() 77 | .id("3") 78 | .imageUrl( 79 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2566.d20170321.t153838.40965.jpg") 80 | .name("Bột Giặt SURF Ngát Hương Chanh 6kg - 32012953") 81 | .price(18490000) 82 | .originalPrice(20490000) 83 | .build(), 84 | Product.builder() 85 | .id("4") 86 | .imageUrl( 87 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/z/c/zc553klgold_1.u504.d20161125.t163659.671549.jpg") 88 | .name("Asus ZenFone 3 Max ZC553KL 32GB RAM 3GB - Vàng") 89 | .price(4150000) 90 | .originalPrice(4990000) 91 | .build())).build() 92 | }; 93 | } 94 | 95 | static void addAllToBinder(RecyclerBinder recyclerBinder, ComponentContext c) { 96 | final SingleBannerSection[] dataModels = SampleData(); 97 | for (SingleBannerSection datum : dataModels) { 98 | ComponentInfo.Builder componentInfoBuilder = ComponentInfo.create(); 99 | componentInfoBuilder 100 | .component( 101 | SingleBannerComponent 102 | .create(c) 103 | .payload(datum) 104 | .key(datum.title()) 105 | .build() 106 | ); 107 | recyclerBinder.insertItemAt(recyclerBinder.getItemCount(), componentInfoBuilder.build()); 108 | } 109 | 110 | final TripleBannerSection[] dataModels1 = SampleData1(); 111 | for (TripleBannerSection datum : dataModels1) { 112 | ComponentInfo.Builder componentInfoBuilder = ComponentInfo.create(); 113 | componentInfoBuilder 114 | .component( 115 | TripleBannersComponent.create(c) 116 | .payload(datum) 117 | .key(datum.title()) 118 | .build() 119 | ); 120 | recyclerBinder.insertItemAt(recyclerBinder.getItemCount(), componentInfoBuilder.build()); 121 | } 122 | 123 | final ProductSlideSection[] dataModels2 = SampleData2(); 124 | for (ProductSlideSection datum : dataModels2) { 125 | 126 | final RecyclerBinder productSlideBinder = new RecyclerBinder(c, 4.0f, 127 | new LinearLayoutInfo(c, OrientationHelper.HORIZONTAL, false)); 128 | 129 | for (Product product : datum.products()) { 130 | ComponentInfo.Builder componentInfoBuilder = ComponentInfo.create(); 131 | componentInfoBuilder 132 | .component( 133 | ProductComponent.create(c) 134 | .product(product) 135 | .key(product.id()) 136 | .build() 137 | ); 138 | productSlideBinder.insertItemAt(productSlideBinder.getItemCount(), componentInfoBuilder.build()); 139 | } 140 | 141 | ComponentInfo.Builder componentInfoBuilder = ComponentInfo.create(); 142 | componentInfoBuilder 143 | .component( 144 | ProductSlideComponent.create(c) 145 | .title(datum.title()) 146 | .recyclerBinder(productSlideBinder) 147 | .key(datum.title()) 148 | .build() 149 | ); 150 | recyclerBinder.insertItemAt(recyclerBinder.getItemCount(), componentInfoBuilder.build()); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/HomeListComponentSpec.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import com.facebook.litho.ComponentContext; 4 | import com.facebook.litho.ComponentLayout; 5 | import com.facebook.litho.annotations.LayoutSpec; 6 | import com.facebook.litho.annotations.OnCreateLayout; 7 | import com.facebook.litho.annotations.Prop; 8 | import com.facebook.litho.widget.Recycler; 9 | import com.facebook.litho.widget.RecyclerBinder; 10 | 11 | @LayoutSpec 12 | public class HomeListComponentSpec { 13 | 14 | private static final String MAIN_SCREEN = "main_screen"; 15 | 16 | @OnCreateLayout 17 | static ComponentLayout onCreateLayout(ComponentContext c, @Prop RecyclerBinder binder) { 18 | return Recycler.create(c) 19 | .binder(binder) 20 | .withLayout().flexShrink(0) 21 | .testKey(MAIN_SCREEN) 22 | .build(); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/ProductComponentSpec.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import android.graphics.Color; 4 | import android.text.Layout; 5 | import android.text.TextUtils; 6 | import android.view.View; 7 | 8 | import com.facebook.drawee.backends.pipeline.Fresco; 9 | import com.facebook.drawee.drawable.ScalingUtils; 10 | import com.facebook.drawee.interfaces.DraweeController; 11 | import com.facebook.litho.ClickEvent; 12 | import com.facebook.litho.Column; 13 | import com.facebook.litho.ComponentContext; 14 | import com.facebook.litho.ComponentLayout; 15 | import com.facebook.litho.annotations.FromEvent; 16 | import com.facebook.litho.annotations.LayoutSpec; 17 | import com.facebook.litho.annotations.OnCreateLayout; 18 | import com.facebook.litho.annotations.OnEvent; 19 | import com.facebook.litho.annotations.Prop; 20 | import com.facebook.litho.fresco.FrescoImage; 21 | import com.facebook.litho.widget.Text; 22 | import com.facebook.yoga.YogaAlign; 23 | 24 | import vn.tale.architecture.model.Product; 25 | import vn.tale.architecture.util.FormatUtil; 26 | 27 | import static com.facebook.yoga.YogaEdge.ALL; 28 | import static com.facebook.yoga.YogaEdge.BOTTOM; 29 | import static com.facebook.yoga.YogaEdge.TOP; 30 | 31 | @LayoutSpec 32 | public class ProductComponentSpec { 33 | 34 | @OnCreateLayout 35 | static ComponentLayout onCreateLayout(ComponentContext c, @Prop Product product) { 36 | final DraweeController controller = Fresco.newDraweeControllerBuilder() 37 | .setUri(product.imageUrl()) 38 | .build(); 39 | return Column.create(c) 40 | .backgroundColor(Color.WHITE) 41 | .child( 42 | FrescoImage.create(c) 43 | .controller(controller) 44 | .actualImageScaleType( 45 | ScalingUtils 46 | .ScaleType 47 | .CENTER_CROP 48 | ) 49 | .withLayout() 50 | .heightDip(96) 51 | .widthDip(96) 52 | .alignSelf(YogaAlign.CENTER) 53 | ).child( 54 | Text.create(c) 55 | .text(product.name()) 56 | .maxLines(2) 57 | .minLines(2) 58 | .ellipsize(TextUtils.TruncateAt.MIDDLE) 59 | .glyphWarming(true) 60 | .textAlignment(Layout.Alignment.ALIGN_CENTER) 61 | .textSizeSp(14) 62 | .withLayout() 63 | .widthDip(128) 64 | .paddingDip(TOP, 8) 65 | ) 66 | .child( 67 | Text.create(c) 68 | .text(FormatUtil.getFormattedCurrency(product.price())) 69 | .glyphWarming(true) 70 | .textSizeSp(14) 71 | .withLayout() 72 | .paddingDip(TOP, 8) 73 | ).child( 74 | Text.create(c) 75 | .text(FormatUtil.getFormattedCurrency(product.originalPrice())) 76 | .glyphWarming(true) 77 | .textSizeSp(12) 78 | .withLayout() 79 | .paddingDip(BOTTOM, 8) 80 | ).clickHandler(ProductComponent.onClick(c)) 81 | .paddingDip(ALL, 8) 82 | .build(); 83 | } 84 | 85 | @OnEvent(ClickEvent.class) 86 | static void onClick( 87 | ComponentContext c, 88 | @FromEvent View view, 89 | @Prop final Product product) { 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/ProductSlideComponentSpec.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import com.facebook.litho.Column; 4 | import com.facebook.litho.ComponentContext; 5 | import com.facebook.litho.ComponentLayout; 6 | import com.facebook.litho.annotations.LayoutSpec; 7 | import com.facebook.litho.annotations.OnCreateLayout; 8 | import com.facebook.litho.annotations.Prop; 9 | import com.facebook.litho.widget.Recycler; 10 | import com.facebook.litho.widget.RecyclerBinder; 11 | import com.facebook.litho.widget.Text; 12 | 13 | import static com.facebook.yoga.YogaEdge.BOTTOM; 14 | import static com.facebook.yoga.YogaEdge.LEFT; 15 | import static com.facebook.yoga.YogaEdge.RIGHT; 16 | import static com.facebook.yoga.YogaEdge.TOP; 17 | 18 | @LayoutSpec 19 | public class ProductSlideComponentSpec { 20 | 21 | @OnCreateLayout 22 | static ComponentLayout onCreateLayout(ComponentContext c, @Prop String title, 23 | @Prop RecyclerBinder recyclerBinder) { 24 | 25 | return Column.create(c) 26 | .child( 27 | Text.create(c) 28 | .text(title) 29 | .glyphWarming(true) 30 | .textSizeSp(16) 31 | .withLayout() 32 | .paddingDip(TOP, 8) 33 | .paddingDip(BOTTOM, 4) 34 | .heightDip(44) 35 | ) 36 | .paddingDip(LEFT, 8) 37 | .paddingDip(RIGHT, 8) 38 | .child( 39 | Recycler.create(c) 40 | .hasFixedSize(true) 41 | .binder(recyclerBinder) 42 | ) 43 | .build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/SingleBannerComponentSpec.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import android.view.View; 4 | 5 | import com.facebook.drawee.backends.pipeline.Fresco; 6 | import com.facebook.drawee.drawable.ScalingUtils; 7 | import com.facebook.drawee.interfaces.DraweeController; 8 | import com.facebook.litho.ClickEvent; 9 | import com.facebook.litho.Column; 10 | import com.facebook.litho.ComponentContext; 11 | import com.facebook.litho.ComponentLayout; 12 | import com.facebook.litho.annotations.FromEvent; 13 | import com.facebook.litho.annotations.LayoutSpec; 14 | import com.facebook.litho.annotations.OnBind; 15 | import com.facebook.litho.annotations.OnCreateLayout; 16 | import com.facebook.litho.annotations.OnEvent; 17 | import com.facebook.litho.annotations.Prop; 18 | import com.facebook.litho.fresco.FrescoImage; 19 | import com.facebook.litho.widget.Text; 20 | 21 | import vn.tale.architecture.model.SingleBannerSection; 22 | import vn.tale.architecture.util.DisplayUtil; 23 | 24 | import static com.facebook.yoga.YogaEdge.BOTTOM; 25 | import static com.facebook.yoga.YogaEdge.LEFT; 26 | import static com.facebook.yoga.YogaEdge.RIGHT; 27 | import static com.facebook.yoga.YogaEdge.TOP; 28 | 29 | @LayoutSpec 30 | public class SingleBannerComponentSpec { 31 | 32 | @OnCreateLayout 33 | static ComponentLayout onCreateLayout(ComponentContext c, @Prop SingleBannerSection payload) { 34 | final DraweeController controller = Fresco.newDraweeControllerBuilder() 35 | .setUri(payload.banner().imageUrl()) 36 | .build(); 37 | return Column.create(c) 38 | .child( 39 | Text.create(c) 40 | .text(payload.title()) 41 | .glyphWarming(true) 42 | .textSizeSp(16) 43 | .withLayout() 44 | .paddingDip(TOP,8) 45 | .paddingDip(BOTTOM,4) 46 | .heightDip(44) 47 | ) 48 | .paddingDip(LEFT, 8) 49 | .paddingDip(RIGHT, 8) 50 | .child( 51 | FrescoImage.create(c) 52 | .controller(controller) 53 | .actualImageScaleType( 54 | ScalingUtils 55 | .ScaleType 56 | .CENTER_CROP 57 | ) 58 | .withLayout() 59 | .heightPx((int) (DisplayUtil.getScreenWidth(c) / payload.banner().ratio())) 60 | 61 | ) 62 | .clickHandler(SingleBannerComponent.onClick(c)) 63 | .build(); 64 | } 65 | 66 | @OnEvent(ClickEvent.class) 67 | static void onClick( 68 | ComponentContext c, 69 | @FromEvent View view, 70 | @Prop final SingleBannerSection payload) { 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/component/TripleBannersComponentSpec.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.component; 2 | 3 | import android.view.View; 4 | 5 | import com.facebook.drawee.backends.pipeline.Fresco; 6 | import com.facebook.drawee.drawable.ScalingUtils; 7 | import com.facebook.drawee.interfaces.DraweeController; 8 | import com.facebook.litho.ClickEvent; 9 | import com.facebook.litho.Column; 10 | import com.facebook.litho.ComponentContext; 11 | import com.facebook.litho.ComponentLayout; 12 | import com.facebook.litho.Row; 13 | import com.facebook.litho.annotations.FromEvent; 14 | import com.facebook.litho.annotations.LayoutSpec; 15 | import com.facebook.litho.annotations.OnCreateLayout; 16 | import com.facebook.litho.annotations.OnEvent; 17 | import com.facebook.litho.annotations.Prop; 18 | import com.facebook.litho.fresco.FrescoImage; 19 | import com.facebook.litho.widget.Text; 20 | 21 | import vn.tale.architecture.model.TripleBannerSection; 22 | import vn.tale.architecture.util.DisplayUtil; 23 | 24 | import static com.facebook.yoga.YogaEdge.BOTTOM; 25 | import static com.facebook.yoga.YogaEdge.LEFT; 26 | import static com.facebook.yoga.YogaEdge.RIGHT; 27 | import static com.facebook.yoga.YogaEdge.TOP; 28 | 29 | @LayoutSpec 30 | public class TripleBannersComponentSpec { 31 | 32 | @OnCreateLayout 33 | static ComponentLayout onCreateLayout(ComponentContext c, @Prop TripleBannerSection payload) { 34 | final DraweeController controllerBanner1 = Fresco.newDraweeControllerBuilder() 35 | .setUri(payload.banners().get(0).imageUrl()) 36 | .build(); 37 | final DraweeController controllerBanner2 = Fresco.newDraweeControllerBuilder() 38 | .setUri(payload.banners().get(1).imageUrl()) 39 | .build(); 40 | 41 | final DraweeController controllerBanner3 = Fresco.newDraweeControllerBuilder() 42 | .setUri(payload.banners().get(2).imageUrl()) 43 | .build(); 44 | return Column.create(c) 45 | .child( 46 | Text.create(c) 47 | .text(payload.title()) 48 | .glyphWarming(true) 49 | .textSizeSp(16) 50 | .withLayout() 51 | .paddingDip(TOP, 8) 52 | .paddingDip(BOTTOM, 4) 53 | .heightDip(44) 54 | ) 55 | .paddingDip(LEFT, 8) 56 | .paddingDip(RIGHT, 8) 57 | .child( 58 | Row.create(c) 59 | .heightPx((int) (DisplayUtil.getScreenWidth(c) / payload.banners().get(0).ratio())) 60 | .child( 61 | FrescoImage.create(c) 62 | .controller(controllerBanner1) 63 | .actualImageScaleType(ScalingUtils 64 | .ScaleType.FIT_XY) 65 | .withLayout() 66 | .flex(1) 67 | .widthPercent(50) 68 | 69 | ) 70 | .clickHandler(TripleBannersComponent.onClickFirstBanner(c)) 71 | .child( 72 | Column.create(c) 73 | .child( 74 | FrescoImage.create(c) 75 | .controller(controllerBanner2) 76 | .actualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) 77 | .withLayout() 78 | .heightPercent(50) 79 | .flex(1) 80 | ) 81 | .clickHandler(TripleBannersComponent.onClickSecondBanner(c)) 82 | .child( 83 | FrescoImage.create(c) 84 | .controller(controllerBanner3) 85 | .actualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) 86 | .withLayout() 87 | .flex(1) 88 | .heightPercent(50) 89 | ) 90 | .widthPercent(100) 91 | .clickHandler(TripleBannersComponent.onClickThirdBanner(c))) 92 | ) 93 | .build(); 94 | } 95 | 96 | @OnEvent(ClickEvent.class) 97 | static void onClickFirstBanner( 98 | ComponentContext c, 99 | @FromEvent View view, 100 | @Prop final TripleBannerSection payload) { 101 | 102 | } 103 | 104 | @OnEvent(ClickEvent.class) 105 | static void onClickSecondBanner( 106 | ComponentContext c, 107 | @FromEvent View view, 108 | @Prop final TripleBannerSection payload) { 109 | 110 | } 111 | 112 | @OnEvent(ClickEvent.class) 113 | static void onClickThirdBanner( 114 | ComponentContext c, 115 | @FromEvent View view, 116 | @Prop final TripleBannerSection payload) { 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/epic/LoadEpic.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.epic; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import vn.tale.architecture.common.redux.Action; 6 | import vn.tale.architecture.common.redux.Effect; 7 | import vn.tale.architecture.common.redux.Function0; 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.home.HomeState; 10 | import vn.tale.architecture.home.action.HomeAction; 11 | import vn.tale.architecture.home.result.LoadResult; 12 | import vn.tale.architecture.model.manager.HomeModel; 13 | 14 | 15 | 16 | public class LoadEpic implements Effect { 17 | 18 | private final HomeModel homeModel; 19 | 20 | public LoadEpic(HomeModel homeModel) { 21 | this.homeModel = homeModel; 22 | } 23 | 24 | @Override 25 | public Observable apply(Observable action$, 26 | Function0 getState) { 27 | return action$ 28 | .filter(action -> action == HomeAction.LOAD) 29 | .filter(ignored -> getState.apply().content().isEmpty()) 30 | .flatMap(ignored -> homeModel.getHome(1) 31 | .map(LoadResult::success) 32 | .onErrorReturn(LoadResult::failure) 33 | .subscribeOn(Schedulers.io()) 34 | .startWith(LoadResult.inProgress())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/epic/LoadMoreEpic.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.epic; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import vn.tale.architecture.common.redux.Action; 6 | import vn.tale.architecture.common.redux.Effect; 7 | import vn.tale.architecture.common.redux.Function0; 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.home.HomeState; 10 | import vn.tale.architecture.home.action.HomeAction; 11 | import vn.tale.architecture.home.result.LoadMoreResult; 12 | import vn.tale.architecture.model.manager.HomeModel; 13 | 14 | public class LoadMoreEpic implements Effect { 15 | 16 | private final HomeModel homeModel; 17 | 18 | public LoadMoreEpic(HomeModel homeModel) { 19 | this.homeModel = homeModel; 20 | } 21 | 22 | @Override public Observable apply(Observable action$, 23 | Function0 getState) { 24 | return action$ 25 | .filter(action -> action == HomeAction.LOAD_MORE) 26 | .filter(ignored -> !getState.apply().loadingMore()) 27 | .flatMap(ignored -> homeModel.getHome(getState.apply().page() + 1) 28 | .map(content -> LoadMoreResult.success(getState.apply().page() + 1, content)) 29 | .onErrorReturn(LoadMoreResult::failure) 30 | .subscribeOn(Schedulers.io()) 31 | .startWith(LoadMoreResult.inProgress())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/epic/RefreshEpic.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.epic; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import vn.tale.architecture.common.redux.Action; 6 | import vn.tale.architecture.common.redux.Effect; 7 | import vn.tale.architecture.common.redux.Function0; 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.home.HomeState; 10 | import vn.tale.architecture.home.action.HomeAction; 11 | import vn.tale.architecture.home.result.RefreshResult; 12 | import vn.tale.architecture.model.manager.HomeModel; 13 | 14 | public class RefreshEpic implements Effect { 15 | 16 | private final HomeModel homeModel; 17 | 18 | public RefreshEpic(HomeModel homeModel) { 19 | this.homeModel = homeModel; 20 | } 21 | 22 | @Override public Observable apply(Observable action$, 23 | Function0 getState) { 24 | return action$ 25 | .filter(action -> action == HomeAction.REFRESH) 26 | .flatMap(ignored -> homeModel.getHome(1) 27 | .map(RefreshResult::success) 28 | .onErrorReturn(RefreshResult::failure) 29 | .subscribeOn(Schedulers.io()) 30 | .startWith(RefreshResult.inProgress())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/result/LoadMoreResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.result; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.model.HomeSection; 10 | 11 | /** 12 | Created by Giang Nguyen on 3/27/17. 13 | */ 14 | @com.google.auto.value.AutoValue 15 | public abstract class LoadMoreResult implements Result { 16 | 17 | public static Builder builder(LoadMoreResult source) { 18 | return new AutoValue_LoadMoreResult.Builder(source); 19 | } 20 | 21 | public static Builder builder() { 22 | return new AutoValue_LoadMoreResult.Builder() 23 | .loading(false) 24 | .content(Collections.emptyList()) 25 | .page(1) 26 | .error(null); 27 | } 28 | 29 | public static LoadMoreResult inProgress() { 30 | return builder() 31 | .loading(true) 32 | .make(); 33 | } 34 | 35 | public static LoadMoreResult success(int page, List content) { 36 | return builder() 37 | .page(page) 38 | .content(content) 39 | .make(); 40 | } 41 | 42 | public static LoadMoreResult failure(Throwable throwable) { 43 | return builder() 44 | .error(throwable) 45 | .make(); 46 | } 47 | 48 | public abstract boolean loading(); 49 | 50 | @Nullable 51 | public abstract Throwable error(); 52 | 53 | public abstract List content(); 54 | 55 | public abstract int page(); 56 | 57 | @com.google.auto.value.AutoValue.Builder 58 | public static abstract class Builder { 59 | public abstract Builder loading(boolean loading); 60 | 61 | public abstract Builder error(Throwable error); 62 | 63 | public abstract Builder content(List content); 64 | 65 | public abstract Builder page(int page); 66 | 67 | public abstract LoadMoreResult make(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/result/LoadResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.result; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.model.HomeSection; 10 | 11 | /** 12 | Created by Giang Nguyen on 3/27/17. 13 | */ 14 | @com.google.auto.value.AutoValue 15 | public abstract class LoadResult implements Result { 16 | 17 | public static Builder builder(LoadResult source) { 18 | return new AutoValue_LoadResult.Builder(source); 19 | } 20 | 21 | public static Builder builder() { 22 | return new AutoValue_LoadResult.Builder() 23 | .loading(false) 24 | .content(Collections.emptyList()) 25 | .error(null); 26 | } 27 | 28 | public static LoadResult inProgress() { 29 | return builder() 30 | .loading(true) 31 | .make(); 32 | } 33 | 34 | public static LoadResult success(List content) { 35 | return builder() 36 | .content(content) 37 | .make(); 38 | } 39 | 40 | public static LoadResult failure(Throwable throwable) { 41 | return builder() 42 | .error(throwable) 43 | .make(); 44 | } 45 | 46 | public abstract boolean loading(); 47 | 48 | @Nullable 49 | public abstract Throwable error(); 50 | 51 | public abstract List content(); 52 | 53 | @com.google.auto.value.AutoValue.Builder 54 | public static abstract class Builder { 55 | public abstract Builder loading(boolean loading); 56 | 57 | public abstract Builder error(Throwable error); 58 | 59 | public abstract Builder content(List content); 60 | 61 | public abstract LoadResult make(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/home/result/RefreshResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.home.result; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import vn.tale.architecture.common.redux.Result; 9 | import vn.tale.architecture.model.HomeSection; 10 | 11 | /** 12 | Created by Giang Nguyen on 3/27/17. 13 | */ 14 | @com.google.auto.value.AutoValue 15 | public abstract class RefreshResult implements Result { 16 | 17 | public static Builder builder(RefreshResult source) { 18 | return new AutoValue_RefreshResult.Builder(source); 19 | } 20 | 21 | public static Builder builder() { 22 | return new AutoValue_RefreshResult.Builder() 23 | .loading(false) 24 | .content(Collections.emptyList()) 25 | .error(null); 26 | } 27 | 28 | public static RefreshResult inProgress() { 29 | return builder() 30 | .loading(true) 31 | .make(); 32 | } 33 | 34 | public static RefreshResult success(List content) { 35 | return builder() 36 | .content(content) 37 | .make(); 38 | } 39 | 40 | public static RefreshResult failure(Throwable throwable) { 41 | return builder() 42 | .error(throwable) 43 | .make(); 44 | } 45 | 46 | public abstract boolean loading(); 47 | 48 | @Nullable 49 | public abstract Throwable error(); 50 | 51 | public abstract List content(); 52 | 53 | @com.google.auto.value.AutoValue.Builder 54 | public static abstract class Builder { 55 | public abstract Builder loading(boolean loading); 56 | 57 | public abstract Builder error(Throwable error); 58 | 59 | public abstract Builder content(List content); 60 | 61 | public abstract RefreshResult make(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.design.widget.TextInputEditText; 6 | import android.support.design.widget.TextInputLayout; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.Toast; 10 | import butterknife.BindString; 11 | import butterknife.BindView; 12 | import com.jakewharton.rxbinding2.view.RxView; 13 | import com.jakewharton.rxbinding2.widget.RxTextView; 14 | import java.util.concurrent.TimeUnit; 15 | import javax.inject.Inject; 16 | import vn.tale.architecture.App; 17 | import vn.tale.architecture.R; 18 | import vn.tale.architecture.R2; 19 | import vn.tale.architecture.common.base.RvvmActivity; 20 | import vn.tale.architecture.common.dagger.DaggerComponentFactory; 21 | import vn.tale.architecture.common.redux.Store; 22 | import vn.tale.architecture.login.action.CheckEmailAction; 23 | import vn.tale.architecture.login.action.SubmitAction; 24 | import vn.tale.architecture.model.error.AuthenticateError; 25 | import vn.tale.architecture.model.error.InvalidEmailError; 26 | import vn.tale.architecture.model.error.OnErrorNotImplementedException; 27 | 28 | /** 29 | * Created by Giang Nguyen on 2/21/17. 30 | */ 31 | 32 | public class LoginActivity extends RvvmActivity { 33 | 34 | @BindView(R2.id.etEmail) TextInputEditText etEmail; 35 | @BindView(R2.id.tilEmailWrapper) TextInputLayout tilEmailWrapper; 36 | @BindView(R2.id.etPassword) TextInputEditText etPassword; 37 | @BindView(R2.id.pbProgress) View pbProgress; 38 | @BindView(R2.id.btSignIn) Button btSignIn; 39 | @BindString(R2.string.successfully) String textSuccessfully; 40 | @BindString(R2.string.email_is_invalid) String textEmailIsInvalid; 41 | @BindString(R2.string.email_and_password_are_mismatched) String textEmailAndPasswordAreMismatch; 42 | 43 | @Inject Store store; 44 | @Inject LoginViewModel viewModel; 45 | 46 | @Override protected DaggerComponentFactory daggerComponentFactory() { 47 | return () -> App.get(this).getAppComponent().plus(new LoginModule()); 48 | } 49 | 50 | @Override protected void injectDependencies() { 51 | daggerComponent().inject(this); 52 | } 53 | 54 | @Override protected Store store() { 55 | return store; 56 | } 57 | 58 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_login); 61 | bindViews(this); 62 | } 63 | 64 | @Override protected void onStart() { 65 | super.onStart(); 66 | 67 | disposeOnStop(RxTextView.textChanges(etEmail) 68 | .debounce(200, TimeUnit.MILLISECONDS) 69 | .map(email -> new CheckEmailAction(email.toString())) 70 | .subscribe(action -> store.dispatch(action))); 71 | 72 | disposeOnStop(RxView.clicks(btSignIn) 73 | .map(ignored -> new SubmitAction( 74 | etEmail.getText().toString(), 75 | etPassword.getText().toString())) 76 | .subscribe(action -> store.dispatch(action))); 77 | 78 | disposeOnStop(viewModel.idle().subscribe(ignored -> renderIdle())); 79 | disposeOnStop(viewModel.loading().subscribe(ignored -> renderLoading())); 80 | disposeOnStop(viewModel.success().subscribe(ignored -> renderSuccess())); 81 | disposeOnStop(viewModel.error().subscribe(this::renderError)); 82 | } 83 | 84 | private void renderLoading() { 85 | btSignIn.setVisibility(View.GONE); 86 | pbProgress.setVisibility(View.VISIBLE); 87 | } 88 | 89 | private void renderSuccess() { 90 | Toast.makeText(this, textSuccessfully, Toast.LENGTH_SHORT).show(); 91 | finish(); 92 | } 93 | 94 | private void renderError(Throwable error) { 95 | btSignIn.setVisibility(View.VISIBLE); 96 | pbProgress.setVisibility(View.GONE); 97 | 98 | if (error instanceof InvalidEmailError) { 99 | tilEmailWrapper.setError(textEmailIsInvalid); 100 | } else if (error instanceof AuthenticateError) { 101 | tilEmailWrapper.setError(textEmailAndPasswordAreMismatch); 102 | } else { 103 | throw new OnErrorNotImplementedException(error); 104 | } 105 | } 106 | 107 | private void renderIdle() { 108 | btSignIn.setVisibility(View.VISIBLE); 109 | pbProgress.setVisibility(View.GONE); 110 | tilEmailWrapper.setError(null); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginComponent.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | import dagger.Subcomponent; 4 | import vn.tale.architecture.ActivityScope; 5 | 6 | /** 7 | * Created by Giang Nguyen on 2/21/17. 8 | */ 9 | @ActivityScope 10 | @Subcomponent(modules = LoginModule.class) 11 | public interface LoginComponent { 12 | 13 | void inject(LoginActivity loginActivity); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginModule.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import io.reactivex.Observable; 6 | import io.reactivex.android.schedulers.AndroidSchedulers; 7 | import vn.tale.architecture.ActivityScope; 8 | import vn.tale.architecture.common.EmailValidator; 9 | import vn.tale.architecture.common.redux.Store; 10 | import vn.tale.architecture.login.epic.CheckEmailEpic; 11 | import vn.tale.architecture.login.epic.SubmitEpic; 12 | import vn.tale.architecture.model.manager.UserModel; 13 | 14 | /** 15 | * Created by Giang Nguyen on 2/27/17. 16 | */ 17 | @Module 18 | public class LoginModule { 19 | 20 | @ActivityScope 21 | @Provides 22 | Store provideLoginStore(UserModel userModel, 23 | EmailValidator emailValidator) { 24 | return Store.builder() 25 | .initialState(LoginState.idle()) 26 | .reducer(new LoginReducer()) 27 | .effects( 28 | new CheckEmailEpic(emailValidator), 29 | new SubmitEpic(userModel) 30 | ) 31 | .make(); 32 | } 33 | 34 | @Provides LoginViewModel provideLoginRenderer(Store store) { 35 | final Observable state$ = store.state$() 36 | .observeOn(AndroidSchedulers.mainThread()); 37 | return new LoginViewModel(state$); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginReducer.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | import vn.tale.architecture.common.redux.Reducer; 4 | import vn.tale.architecture.common.redux.Result; 5 | import vn.tale.architecture.login.result.CheckEmailResult; 6 | import vn.tale.architecture.login.result.SubmitResult; 7 | 8 | /** 9 | * Created by Giang Nguyen on 3/23/17. 10 | */ 11 | 12 | public class LoginReducer implements Reducer { 13 | 14 | @Override public LoginState apply(LoginState loginState, Result result) { 15 | if (result == SubmitResult.IN_FLIGHT) { 16 | return LoginState.inProgress(); 17 | } else if (result == CheckEmailResult.SUCCESS) { 18 | return LoginState.idle(); 19 | } else if (result == SubmitResult.SUCCESS) { 20 | return LoginState.success(); 21 | } 22 | final Throwable error; 23 | if (result instanceof SubmitResult) { 24 | error = ((SubmitResult) result).error(); 25 | } else if (result instanceof CheckEmailResult) { 26 | error = ((CheckEmailResult) result).error(); 27 | } else { 28 | return loginState; 29 | } 30 | return LoginState.error(error); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginState.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/21/17. 5 | */ 6 | public final class LoginState { 7 | 8 | public final boolean inProgress; 9 | 10 | public final boolean success; 11 | 12 | public final Throwable error; 13 | 14 | private LoginState(boolean inProgress, boolean success, Throwable error) { 15 | this.inProgress = inProgress; 16 | this.success = success; 17 | this.error = error; 18 | } 19 | 20 | public static LoginState idle() { 21 | return new LoginState(false, false, null); 22 | } 23 | 24 | public static LoginState inProgress() { 25 | return new LoginState(true, false, null); 26 | } 27 | 28 | public static LoginState success() { 29 | return new LoginState(false, true, null); 30 | } 31 | 32 | public static LoginState error(Throwable throwable) { 33 | return new LoginState(false, false, throwable); 34 | } 35 | 36 | @Override public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | 40 | LoginState that = (LoginState) o; 41 | 42 | if (inProgress != that.inProgress) return false; 43 | if (success != that.success) return false; 44 | return error != null ? error.equals(that.error) : that.error == null; 45 | } 46 | 47 | @Override public int hashCode() { 48 | int result = (inProgress ? 1 : 0); 49 | result = 31 * result + (success ? 1 : 0); 50 | result = 31 * result + (error != null ? error.hashCode() : 0); 51 | return result; 52 | } 53 | 54 | @Override public String toString() { 55 | return "LoginUiModel{" + 56 | "inProgress=" + inProgress + 57 | ", success=" + success + 58 | ", error=" + error + 59 | '}'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/LoginViewModel.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login; 2 | 3 | import io.reactivex.Observable; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/31/17. 7 | */ 8 | public class LoginViewModel { 9 | 10 | private final Observable state$; 11 | 12 | public LoginViewModel(Observable state$) { 13 | this.state$ = state$; 14 | } 15 | 16 | public Observable idle() { 17 | return state$ 18 | .filter(state -> state.equals(LoginState.idle())); 19 | } 20 | 21 | public Observable loading() { 22 | return state$ 23 | .filter(state -> state.inProgress); 24 | } 25 | 26 | public Observable success() { 27 | return state$ 28 | .filter(state -> state.success); 29 | } 30 | 31 | public Observable error() { 32 | return state$ 33 | .filter(state -> state.error != null) 34 | .map(state -> state.error); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/action/CheckEmailAction.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.action; 2 | 3 | import vn.tale.architecture.common.redux.Action; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/23/17. 7 | */ 8 | 9 | public class CheckEmailAction implements Action { 10 | public final String email; 11 | 12 | public CheckEmailAction(String email) { 13 | this.email = email; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/action/SubmitAction.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.action; 2 | 3 | import vn.tale.architecture.common.redux.Action; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/23/17. 7 | */ 8 | public class SubmitAction implements Action { 9 | public final String email; 10 | public final String password; 11 | 12 | public SubmitAction(String email, String password) { 13 | this.email = email; 14 | this.password = password; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/epic/CheckEmailEpic.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.epic; 2 | 3 | import io.reactivex.Observable; 4 | import timber.log.Timber; 5 | import vn.tale.architecture.common.EmailValidator; 6 | import vn.tale.architecture.common.redux.Action; 7 | import vn.tale.architecture.common.redux.Effect; 8 | import vn.tale.architecture.common.redux.Function0; 9 | import vn.tale.architecture.common.redux.Result; 10 | import vn.tale.architecture.login.LoginState; 11 | import vn.tale.architecture.login.action.CheckEmailAction; 12 | import vn.tale.architecture.login.result.CheckEmailResult; 13 | 14 | 15 | public class CheckEmailEpic implements Effect { 16 | 17 | private EmailValidator emailValidator; 18 | 19 | public CheckEmailEpic(EmailValidator emailValidator) { 20 | this.emailValidator = emailValidator; 21 | } 22 | 23 | @Override public Observable apply(Observable action$, 24 | Function0 getState) { 25 | return action$ 26 | .ofType(CheckEmailAction.class) 27 | .skip(1) 28 | .switchMap(action -> emailValidator.checkEmail(action.email) 29 | .map(ignored -> CheckEmailResult.SUCCESS) 30 | .onErrorReturn(CheckEmailResult::error) 31 | .doOnNext(result -> Timber.d("result => %s", result)) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/epic/SubmitEpic.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.epic; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import vn.tale.architecture.common.redux.Action; 6 | import vn.tale.architecture.common.redux.Function0; 7 | import vn.tale.architecture.common.redux.Result; 8 | import vn.tale.architecture.common.redux.Effect; 9 | import vn.tale.architecture.login.LoginState; 10 | import vn.tale.architecture.login.action.SubmitAction; 11 | import vn.tale.architecture.login.result.SubmitResult; 12 | import vn.tale.architecture.model.manager.UserModel; 13 | 14 | 15 | public class SubmitEpic implements Effect { 16 | 17 | private final UserModel userModel; 18 | 19 | public SubmitEpic(UserModel userModel) { 20 | this.userModel = userModel; 21 | } 22 | 23 | @Override public Observable apply(Observable action$, 24 | Function0 getState) { 25 | return action$.ofType(SubmitAction.class) 26 | .flatMap(action -> userModel.login(action.email, action.password) 27 | .toObservable() 28 | .map(user -> SubmitResult.SUCCESS) 29 | .onErrorReturn(SubmitResult::error) 30 | .subscribeOn(Schedulers.io()) 31 | .startWith(SubmitResult.IN_FLIGHT)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/result/CheckEmailResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.result; 2 | 3 | import vn.tale.architecture.common.redux.Result; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/23/17. 7 | */ 8 | 9 | public class CheckEmailResult implements Result { 10 | 11 | public static final CheckEmailResult SUCCESS = new CheckEmailResult(null); 12 | public static final CheckEmailResult IN_FLIGHT = new CheckEmailResult(null); 13 | 14 | public final Throwable error; 15 | 16 | private CheckEmailResult(Throwable throwable) { 17 | error = throwable; 18 | } 19 | 20 | public static CheckEmailResult error(Throwable error) { 21 | return new CheckEmailResult(error); 22 | } 23 | 24 | public Throwable error() { 25 | return error; 26 | } 27 | 28 | @Override public String toString() { 29 | return "CheckEmailResult{" + 30 | "error=" + error + 31 | '}'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/login/result/SubmitResult.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.login.result; 2 | 3 | import vn.tale.architecture.common.redux.Result; 4 | 5 | /** 6 | * Created by Giang Nguyen on 3/23/17. 7 | */ 8 | 9 | public class SubmitResult implements Result { 10 | 11 | public static final SubmitResult SUCCESS = new SubmitResult(null); 12 | public static final SubmitResult IN_FLIGHT = new SubmitResult(null); 13 | 14 | public final Throwable error; 15 | 16 | public SubmitResult(Throwable throwable) { 17 | error = throwable; 18 | } 19 | 20 | public static SubmitResult error(Throwable error) { 21 | return new SubmitResult(error); 22 | } 23 | 24 | public Throwable error() { 25 | return error; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/Banner.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | @AutoValue 6 | public abstract class Banner { 7 | 8 | public abstract String id(); 9 | 10 | public abstract String imageUrl(); 11 | 12 | public abstract String link(); 13 | 14 | public abstract float ratio(); 15 | 16 | public static Builder builder() { 17 | return new AutoValue_Banner.Builder(); 18 | } 19 | 20 | @com.google.auto.value.AutoValue.Builder 21 | public static abstract class Builder { 22 | public abstract Banner.Builder id(String id); 23 | 24 | public abstract Banner.Builder imageUrl(String imageUrl); 25 | 26 | public abstract Banner.Builder link(String link); 27 | 28 | public abstract Banner.Builder ratio(float ratio); 29 | 30 | public abstract Banner make(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/Constants.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/27/17. 5 | */ 6 | 7 | public interface Constants { 8 | interface ListItem { 9 | Object LOADING = new Object(); 10 | Object RETRY = new Object(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/HomeSection.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | public interface HomeSection { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/Product.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | @AutoValue 6 | public abstract class Product { 7 | 8 | public abstract String id(); 9 | 10 | public abstract String name(); 11 | 12 | public abstract int originalPrice(); 13 | 14 | public abstract int price(); 15 | 16 | public abstract String imageUrl(); 17 | 18 | public static Builder builder() {return new AutoValue_Product.Builder();} 19 | 20 | @AutoValue.Builder 21 | public abstract static class Builder { 22 | public abstract Builder id(String id); 23 | 24 | public abstract Builder name(String name); 25 | 26 | public abstract Builder originalPrice(int originalPrice); 27 | 28 | public abstract Builder price(int price); 29 | 30 | public abstract Builder imageUrl(String imageUrl); 31 | 32 | public abstract Product build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/ProductSlideSection.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | import java.util.List; 6 | 7 | @AutoValue 8 | public abstract class ProductSlideSection implements HomeSection{ 9 | 10 | public abstract String title(); 11 | 12 | public abstract List products(); 13 | 14 | public static Builder builder() {return new AutoValue_ProductSlideSection.Builder();} 15 | 16 | @AutoValue.Builder 17 | public abstract static class Builder { 18 | public abstract Builder title(String title); 19 | 20 | public abstract Builder products(List products); 21 | 22 | public abstract ProductSlideSection build(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/SingleBannerSection.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | @AutoValue 6 | public abstract class SingleBannerSection implements HomeSection { 7 | 8 | public abstract String title(); 9 | 10 | public abstract Banner banner(); 11 | 12 | public static Builder builder() { 13 | return new AutoValue_SingleBannerSection.Builder(); 14 | } 15 | 16 | @com.google.auto.value.AutoValue.Builder 17 | public static abstract class Builder { 18 | 19 | public abstract SingleBannerSection.Builder title(String title); 20 | 21 | public abstract SingleBannerSection.Builder banner(Banner banner); 22 | 23 | public abstract SingleBannerSection build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/TripleBannerSection.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | import java.util.List; 6 | 7 | @AutoValue 8 | public abstract class TripleBannerSection implements HomeSection { 9 | 10 | public abstract String title(); 11 | 12 | public abstract List banners(); 13 | 14 | public static TripleBannerSection.Builder builder() { 15 | return new AutoValue_TripleBannerSection.Builder(); 16 | } 17 | 18 | @com.google.auto.value.AutoValue.Builder 19 | public static abstract class Builder { 20 | 21 | public abstract TripleBannerSection.Builder title(String title); 22 | 23 | public abstract TripleBannerSection.Builder banners(List banners); 24 | 25 | public abstract TripleBannerSection build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/User.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | 5 | /** 6 | * Created by Giang Nguyen on 2/21/17. 7 | */ 8 | @AutoValue 9 | public abstract class User { 10 | 11 | public static User user(String email, String name) { 12 | return new AutoValue_User(email, name); 13 | } 14 | 15 | public abstract String email(); 16 | 17 | public abstract String name(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/api/GithubApi.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.api; 2 | 3 | import io.reactivex.Single; 4 | import retrofit2.http.GET; 5 | import retrofit2.http.Query; 6 | import vn.tale.architecture.model.api.reponse.SearchRepoResponse; 7 | 8 | /** 9 | * Created by Giang Nguyen on 3/27/17. 10 | */ 11 | public interface GithubApi { 12 | 13 | @GET("/search/repositories") Single searchRepos( 14 | @Query("q") String query, 15 | @Query("sort") String sort, 16 | @Query("order") String order, 17 | @Query("page") int page, 18 | @Query("per_page") int perPage); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/api/reponse/SearchRepoResponse.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.api.reponse; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | import java.util.List; 6 | 7 | public class SearchRepoResponse { 8 | 9 | @SerializedName("total_count") 10 | @Expose 11 | private int totalCount; 12 | 13 | @SerializedName("incomplete_results") 14 | @Expose 15 | private boolean incompleteResults; 16 | 17 | @SerializedName("items") 18 | @Expose 19 | private List items; 20 | 21 | public int getTotalCount() { 22 | return totalCount; 23 | } 24 | 25 | public boolean isIncompleteResults() { 26 | return incompleteResults; 27 | } 28 | 29 | public List getItems() { 30 | return items; 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/api/reponse/UserResponse.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.api.reponse; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class UserResponse { 7 | 8 | @SerializedName("gists_url") 9 | @Expose 10 | private String gistsUrl; 11 | 12 | @SerializedName("repos_url") 13 | @Expose 14 | private String reposUrl; 15 | 16 | @SerializedName("following_url") 17 | @Expose 18 | private String followingUrl; 19 | 20 | @SerializedName("starred_url") 21 | @Expose 22 | private String starredUrl; 23 | 24 | @SerializedName("login") 25 | @Expose 26 | private String login; 27 | 28 | @SerializedName("followers_url") 29 | @Expose 30 | private String followersUrl; 31 | 32 | @SerializedName("type") 33 | @Expose 34 | private String type; 35 | 36 | @SerializedName("url") 37 | @Expose 38 | private String url; 39 | 40 | @SerializedName("subscriptions_url") 41 | @Expose 42 | private String subscriptionsUrl; 43 | 44 | @SerializedName("received_events_url") 45 | @Expose 46 | private String receivedEventsUrl; 47 | 48 | @SerializedName("avatar_url") 49 | @Expose 50 | private String avatarUrl; 51 | 52 | @SerializedName("events_url") 53 | @Expose 54 | private String eventsUrl; 55 | 56 | @SerializedName("html_url") 57 | @Expose 58 | private String htmlUrl; 59 | 60 | @SerializedName("site_admin") 61 | @Expose 62 | private boolean siteAdmin; 63 | 64 | @SerializedName("id") 65 | @Expose 66 | private int id; 67 | 68 | @SerializedName("gravatar_id") 69 | @Expose 70 | private String gravatarId; 71 | 72 | @SerializedName("organizations_url") 73 | @Expose 74 | private String organizationsUrl; 75 | 76 | public String getGistsUrl() { 77 | return gistsUrl; 78 | } 79 | 80 | public String getReposUrl() { 81 | return reposUrl; 82 | } 83 | 84 | public String getFollowingUrl() { 85 | return followingUrl; 86 | } 87 | 88 | public String getStarredUrl() { 89 | return starredUrl; 90 | } 91 | 92 | public String getLogin() { 93 | return login; 94 | } 95 | 96 | public String getFollowersUrl() { 97 | return followersUrl; 98 | } 99 | 100 | public String getType() { 101 | return type; 102 | } 103 | 104 | public String getUrl() { 105 | return url; 106 | } 107 | 108 | public String getSubscriptionsUrl() { 109 | return subscriptionsUrl; 110 | } 111 | 112 | public String getReceivedEventsUrl() { 113 | return receivedEventsUrl; 114 | } 115 | 116 | public String getAvatarUrl() { 117 | return avatarUrl; 118 | } 119 | 120 | public String getEventsUrl() { 121 | return eventsUrl; 122 | } 123 | 124 | public String getHtmlUrl() { 125 | return htmlUrl; 126 | } 127 | 128 | public boolean isSiteAdmin() { 129 | return siteAdmin; 130 | } 131 | 132 | public int getId() { 133 | return id; 134 | } 135 | 136 | public String getGravatarId() { 137 | return gravatarId; 138 | } 139 | 140 | public String getOrganizationsUrl() { 141 | return organizationsUrl; 142 | } 143 | } -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/error/AuthenticateError.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.error; 2 | 3 | /** 4 | * Created by Giang Nguyen on 2/21/17. 5 | */ 6 | 7 | public class AuthenticateError extends RuntimeException { 8 | 9 | private static final long serialVersionUID = -7666209496472570517L; 10 | private final long id = serialVersionUID; 11 | 12 | @Override public boolean equals(Object o) { 13 | if (this == o) return true; 14 | if (o == null || getClass() != o.getClass()) return false; 15 | 16 | AuthenticateError that = (AuthenticateError) o; 17 | 18 | return id == that.id; 19 | } 20 | 21 | @Override public int hashCode() { 22 | return (int) (id ^ (id >>> 32)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/error/InvalidEmailError.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.error; 2 | 3 | /** 4 | * Created by Giang Nguyen on 3/21/17. 5 | */ 6 | 7 | public class InvalidEmailError extends RuntimeException { 8 | 9 | private static final long serialVersionUID = 7075042945055393445L; 10 | private final long id = serialVersionUID; 11 | 12 | @Override public boolean equals(Object o) { 13 | if (this == o) return true; 14 | if (o == null || getClass() != o.getClass()) return false; 15 | 16 | InvalidEmailError that = (InvalidEmailError) o; 17 | 18 | return id == that.id; 19 | } 20 | 21 | @Override public int hashCode() { 22 | return (int) (id ^ (id >>> 32)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/error/OnErrorNotImplementedException.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.error; 2 | 3 | /** 4 | * Represents an exception used to re-throw {@link Subscriber#onError(Throwable)} when an implementation doesn't 5 | * exist. 6 | *

7 | * Rx Design Guidelines 5.2: 8 | *

9 | * "when calling the Subscribe method that only has an onNext argument, the OnError behavior will be 10 | * to rethrow the exception on the thread that the message comes out from the observable sequence. 11 | * The OnCompleted behavior in this case is to do nothing." 12 | *

13 | * 14 | * @see RxJava issue #198 15 | */ 16 | public class OnErrorNotImplementedException extends RuntimeException { 17 | private static final long serialVersionUID = -6298857009889503852L; 18 | 19 | /** 20 | * Customizes the {@code Throwable} with a custom message and wraps it before it is to be re-thrown as an 21 | * {@code OnErrorNotImplementedException}. 22 | * 23 | * @param message 24 | * the message to assign to the {@code Throwable} to re-throw 25 | * @param e 26 | * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed 27 | */ 28 | public OnErrorNotImplementedException(String message, Throwable e) { 29 | super(message, e != null ? e : new NullPointerException()); 30 | } 31 | 32 | /** 33 | * Wraps the {@code Throwable} before it is to be re-thrown as an {@code OnErrorNotImplementedException}. 34 | * 35 | * @param e 36 | * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed 37 | */ 38 | public OnErrorNotImplementedException(Throwable e) { 39 | super(e != null ? e.getMessage() : null, e != null ? e : new NullPointerException()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/manager/HomeModel.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.manager; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import io.reactivex.Observable; 7 | import vn.tale.architecture.model.Banner; 8 | import vn.tale.architecture.model.HomeSection; 9 | import vn.tale.architecture.model.Product; 10 | import vn.tale.architecture.model.ProductSlideSection; 11 | import vn.tale.architecture.model.SingleBannerSection; 12 | import vn.tale.architecture.model.TripleBannerSection; 13 | 14 | public class HomeModel { 15 | 16 | public Observable> getHome(int page) { 17 | return Observable.just(getStaticData()); 18 | } 19 | 20 | private List getStaticData() { 21 | return Arrays.asList(SingleBannerSection.builder().title("ABC") 22 | .banner(Banner.builder().id("1") 23 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/bd/4e/98/bd4e98d11e2a094d8981191ce594e766.jpg") 24 | .link("/home-app-module/product_group_sale") 25 | .ratio(3.34615385F) 26 | .make()).build(), 27 | TripleBannerSection.builder().title("Thẻ cào cực hot") 28 | .banners(Arrays.asList( 29 | Banner.builder().id("1") 30 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/87/b5/35/87b5350cb9e1a7c8f1601ee1ea7bc20d.jpg") 31 | .link("/home-app-module/product_group_sale") 32 | .ratio(1.3333333333333F) 33 | .make(), 34 | Banner.builder().id("2") 35 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/02/1a/81/021a8138486635ae4c1bc78192265197.jpg") 36 | .link("https://tiki.vn/dich-vu-tien-ich") 37 | .ratio(1.3333333333333F) 38 | .make(), 39 | Banner.builder().id("3") 40 | .imageUrl("https://vcdn.tikicdn.com/ts/banner/ea/15/c1/ea15c112a4899e2dadbcc54905dd8227.jpg") 41 | .link("https://tiki.vn/lp/samsung-galaxy-s8") 42 | .ratio(0.66666666666667F) 43 | .make() 44 | )).build(), 45 | ProductSlideSection.builder().title("Siêu phẩm Galaxy S8/S8 Plus") 46 | .products(Arrays.asList( 47 | Product.builder() 48 | .id("1") 49 | .imageUrl( 50 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2769.d20170407.t150619.932685.jpg") 51 | .name("Samsung Galaxy S8") 52 | .price(18490000) 53 | .originalPrice(20490000) 54 | .build() 55 | , 56 | Product.builder() 57 | .id("2") 58 | .imageUrl( 59 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2769.d20170407.t150619.932685.jpg") 60 | .name("Samsung Galaxy S8+") 61 | .price(18490000) 62 | .originalPrice(20490000) 63 | .build(), 64 | Product.builder() 65 | .id("3") 66 | .imageUrl( 67 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/g/i/gift-tang-kem.u2566.d20170321.t153838.40965.jpg") 68 | .name("Bột Giặt SURF Ngát Hương Chanh 6kg - 32012953") 69 | .price(18490000) 70 | .originalPrice(20490000) 71 | .build(), 72 | Product.builder() 73 | .id("4") 74 | .imageUrl( 75 | "https://vcdn.tikicdn.com/cache/w250/media/catalog/product/z/c/zc553klgold_1.u504.d20161125.t163659.671549.jpg") 76 | .name("Asus ZenFone 3 Max ZC553KL 32GB RAM 3GB - Vàng") 77 | .price(4150000) 78 | .originalPrice(4990000) 79 | .build())).build() 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/manager/MockManager.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.manager; 2 | 3 | import android.support.v4.util.ArrayMap; 4 | import android.support.v4.util.Pair; 5 | import java.util.Map; 6 | import vn.tale.architecture.model.User; 7 | 8 | import static vn.tale.architecture.model.User.user; 9 | 10 | /** 11 | * Created by Giang Nguyen on 3/27/17. 12 | */ 13 | 14 | class MockManager { 15 | static final Map, User> USER_MAP; 16 | 17 | static { 18 | USER_MAP = new ArrayMap<>(); 19 | USER_MAP.put(new Pair<>("foo@tiki.vn", "foo123"), user("foo@tiki.vn", "Mr. Foo")); 20 | USER_MAP.put(new Pair<>("bar@tiki.vn", "bar123"), user("bar@tiki.vn", "Mr. Bar")); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/model/manager/UserModel.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.model.manager; 2 | 3 | import android.support.v4.util.Pair; 4 | import io.reactivex.Completable; 5 | import io.reactivex.Observable; 6 | import io.reactivex.Single; 7 | import io.reactivex.subjects.BehaviorSubject; 8 | import vn.tale.architecture.model.User; 9 | import vn.tale.architecture.model.error.AuthenticateError; 10 | 11 | /** 12 | * Created by Giang Nguyen on 2/21/17. 13 | */ 14 | 15 | public class UserModel { 16 | 17 | public static final User ANNOYMOUS = User.user("annoymous@tale.vn", "annoymous"); 18 | 19 | private final BehaviorSubject userSubject = BehaviorSubject.createDefault(ANNOYMOUS); 20 | 21 | public Observable user() { 22 | return userSubject; 23 | } 24 | 25 | public Single login(final String email, final String password) { 26 | final Pair authInfo = new Pair<>(email, password); 27 | return Single.fromCallable(() -> { 28 | Thread.sleep(500); 29 | if (MockManager.USER_MAP.containsKey(authInfo)) { 30 | return MockManager.USER_MAP.get(authInfo); 31 | } 32 | throw new AuthenticateError(); 33 | }).doOnSuccess(userSubject::onNext); 34 | } 35 | 36 | public Completable logout() { 37 | return Completable.fromAction(() -> userSubject.onNext(ANNOYMOUS)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/util/DisplayUtil.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.util; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.WindowManager; 6 | 7 | public class DisplayUtil { 8 | 9 | public static int getScreenWidth(Context context) { 10 | DisplayMetrics displayMetrics = new DisplayMetrics(); 11 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 12 | wm.getDefaultDisplay().getMetrics(displayMetrics); 13 | return displayMetrics.widthPixels; 14 | } 15 | 16 | public static int getScreenHeight(Context context) { 17 | DisplayMetrics displayMetrics = new DisplayMetrics(); 18 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 19 | wm.getDefaultDisplay().getMetrics(displayMetrics); 20 | return displayMetrics.heightPixels; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/vn/tale/architecture/util/FormatUtil.java: -------------------------------------------------------------------------------- 1 | package vn.tale.architecture.util; 2 | 3 | import java.text.NumberFormat; 4 | import java.util.Locale; 5 | 6 | public class FormatUtil { 7 | 8 | private FormatUtil(){ 9 | 10 | } 11 | public static String getFormattedCurrency(int price) { 12 | Locale locale = new Locale("vi", "VN"); 13 | NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); 14 | return currencyFormatter.format(price); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_device_hub_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fork_24dp.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logout_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_placeholder_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_public_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/nav_item_color_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_counter.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 |