├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── hana053 │ │ └── micropost │ │ ├── pages │ │ ├── login │ │ │ └── LoginActivityTest.kt │ │ ├── main │ │ │ └── MainActivityTest.kt │ │ ├── micropostnew │ │ │ └── MicropostNewActivityTest.kt │ │ ├── relateduserlist │ │ │ └── RelatedUserListActivityTest.kt │ │ ├── signup │ │ │ └── SignupActivityTest.kt │ │ ├── top │ │ │ └── TopActivityTest.kt │ │ └── usershow │ │ │ └── UserShowActivityTest.kt │ │ └── testing │ │ ├── AppTestRunner.kt │ │ ├── EspressoHelpers.kt │ │ └── InjectableTestImpl.kt │ ├── debug │ └── kotlin │ │ └── com │ │ └── hana053 │ │ └── micropost │ │ └── Application.kt │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── hana053 │ │ │ └── micropost │ │ │ ├── AppModule.kt │ │ │ ├── BaseApplication.kt │ │ │ ├── Extensions.kt │ │ │ ├── domain │ │ │ ├── Avatar.kt │ │ │ ├── Micropost.kt │ │ │ ├── RelatedUser.kt │ │ │ ├── User.kt │ │ │ └── UserStats.kt │ │ │ ├── interactor │ │ │ ├── FeedInteractor.kt │ │ │ ├── InteractorModule.kt │ │ │ ├── LoginInteractor.kt │ │ │ ├── MicropostInteractor.kt │ │ │ ├── RelatedUserListInteractor.kt │ │ │ ├── RelationshipInteractor.kt │ │ │ ├── UserInteractor.kt │ │ │ └── UserMicropostInteractor.kt │ │ │ ├── pages │ │ │ ├── Presenter.kt │ │ │ ├── ViewWrapper.kt │ │ │ ├── login │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginModule.kt │ │ │ │ ├── LoginPresenter.kt │ │ │ │ └── LoginView.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainModule.kt │ │ │ │ ├── MainPresenter.kt │ │ │ │ ├── MainService.kt │ │ │ │ └── MainView.kt │ │ │ ├── micropostnew │ │ │ │ ├── MIcropostNewModule.kt │ │ │ │ ├── MicropostNewActivity.kt │ │ │ │ ├── MicropostNewNavigator.kt │ │ │ │ ├── MicropostNewPresenter.kt │ │ │ │ ├── MicropostNewService.kt │ │ │ │ └── MicropostNewView.kt │ │ │ ├── relateduserlist │ │ │ │ ├── RelatedUserListActivity.kt │ │ │ │ ├── RelatedUserListAdapter.kt │ │ │ │ ├── RelatedUserListModule.kt │ │ │ │ ├── RelatedUserListPresenter.kt │ │ │ │ ├── RelatedUserListService.kt │ │ │ │ ├── RelatedUserListView.kt │ │ │ │ ├── followerlist │ │ │ │ │ └── FollowerListService.kt │ │ │ │ └── followinglist │ │ │ │ │ └── FollowingListService.kt │ │ │ ├── signup │ │ │ │ ├── SignupActivity.kt │ │ │ │ ├── SignupModule.kt │ │ │ │ ├── SignupNavigator.kt │ │ │ │ ├── SignupNavigatorImpl.kt │ │ │ │ ├── SignupService.kt │ │ │ │ ├── SignupServiceImpl.kt │ │ │ │ ├── SignupState.kt │ │ │ │ ├── email │ │ │ │ │ ├── SignupEmailFragment.kt │ │ │ │ │ ├── SignupEmailPresenter.kt │ │ │ │ │ ├── SignupEmailView.kt │ │ │ │ │ └── SignupFullNameModule.kt │ │ │ │ ├── fullname │ │ │ │ │ ├── SignupFullNameFragment.kt │ │ │ │ │ ├── SignupFullNameModule.kt │ │ │ │ │ ├── SignupFullNamePresenter.kt │ │ │ │ │ └── SignupFullNameView.kt │ │ │ │ └── password │ │ │ │ │ ├── SignupPasswordFragment.kt │ │ │ │ │ ├── SignupPasswordModule.kt │ │ │ │ │ ├── SignupPasswordPresenter.kt │ │ │ │ │ └── SignupPasswordView.kt │ │ │ ├── top │ │ │ │ ├── TopActivity.kt │ │ │ │ ├── TopModule.kt │ │ │ │ ├── TopPresenter.kt │ │ │ │ └── TopView.kt │ │ │ └── usershow │ │ │ │ ├── UserShowActivity.kt │ │ │ │ ├── UserShowModule.kt │ │ │ │ ├── detail │ │ │ │ ├── UserShowDetailModule.kt │ │ │ │ ├── UserShowDetailPresenter.kt │ │ │ │ ├── UserShowDetailService.kt │ │ │ │ └── UserShowDetailView.kt │ │ │ │ └── posts │ │ │ │ ├── UserShowPostsModule.kt │ │ │ │ ├── UserShowPostsPresenter.kt │ │ │ │ ├── UserShowPostsService.kt │ │ │ │ └── UserShowPostsView.kt │ │ │ ├── repository │ │ │ ├── AuthTokenRepository.kt │ │ │ ├── AuthTokenRepositoryImpl.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── service │ │ │ ├── AuthService.kt │ │ │ ├── AuthServiceImpl.kt │ │ │ ├── HttpErrorHandler.kt │ │ │ ├── HttpErrorHandlerImpl.kt │ │ │ ├── LoginService.kt │ │ │ ├── LoginServiceImpl.kt │ │ │ ├── Navigator.kt │ │ │ ├── NavigatorImpl.kt │ │ │ └── ServiceModule.kt │ │ │ ├── shared │ │ │ ├── avatar │ │ │ │ └── AvatarView.kt │ │ │ ├── followbtn │ │ │ │ ├── FollowBtnModule.kt │ │ │ │ ├── FollowBtnService.kt │ │ │ │ └── FollowBtnView.kt │ │ │ └── posts │ │ │ │ └── PostListAdapter.kt │ │ │ └── system │ │ │ └── SystemServicesModule.kt │ └── res │ │ ├── anim │ │ └── slide_in_up.xml │ │ ├── drawable │ │ ├── bg_top.xml │ │ ├── border_bottom_on_white.xml │ │ ├── border_top.xml │ │ └── ic_create_white_36dp.png │ │ ├── layout │ │ ├── _user_detail.xml │ │ ├── _user_posts.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_micropost_new.xml │ │ ├── activity_related_user_list.xml │ │ ├── activity_signup.xml │ │ ├── activity_top.xml │ │ ├── activity_user_show.xml │ │ ├── fragment_signup_email.xml │ │ ├── fragment_signup_full_name.xml │ │ ├── fragment_signup_password.xml │ │ ├── item_posts.xml │ │ └── item_related_users.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── release │ └── kotlin │ │ └── com │ │ └── hana053 │ │ └── micropost │ │ └── Application.kt │ ├── test │ └── kotlin │ │ └── com │ │ └── hana053 │ │ └── micropost │ │ ├── pages │ │ ├── main │ │ │ └── MainServiceTest.kt │ │ ├── micropostnew │ │ │ └── MicropostNewServiceTest.kt │ │ ├── relateduserlist │ │ │ ├── followerlist │ │ │ │ └── FollowerListServiceTest.kt │ │ │ └── followinglist │ │ │ │ └── FollowingListServiceTest.kt │ │ ├── signup │ │ │ └── SignupServiceTest.kt │ │ └── usershow │ │ │ ├── detail │ │ │ └── UserShowDetailServiceTest.kt │ │ │ └── posts │ │ │ └── UserShowPostsServiceTest.kt │ │ ├── repository │ │ └── AuthTokenRepositoryTest.kt │ │ ├── service │ │ ├── AuthServiceTest.kt │ │ ├── HttpErrorHandlerTest.kt │ │ ├── LoginServiceTest.kt │ │ └── NavigatorTest.kt │ │ ├── shared │ │ └── followbtn │ │ │ └── FollowBtnServiceTest.kt │ │ └── testing │ │ ├── RobolectricBaseTest.kt │ │ └── TestSchedulerProxy.kt │ └── testShared │ └── kotlin │ └── com │ └── hana053 │ └── micropost │ └── testing │ ├── BlockedInteractorModule.kt │ ├── EmptyResponseBody.kt │ ├── InjectableTest.kt │ └── TestUtil.kt ├── build.gradle ├── gradle.properties.ci.example ├── gradle.properties.example ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 2 | *.iml 3 | 4 | ## Directory-based project format: 5 | .idea/ 6 | 7 | .gradle 8 | /local.properties 9 | .DS_Store 10 | /build 11 | /captures 12 | 13 | # release key 14 | app/myreleasekey.jks 15 | 16 | gradle.properties 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: 3 | - oraclejdk8 4 | env: 5 | global: 6 | - GRADLE_OPTS="-Xmx512m -XX:MaxPermSize=512m" 7 | - MALLOC_ARENA_MAX=2 8 | android: 9 | components: 10 | - platform-tools 11 | - tools 12 | - build-tools-25.0.2 13 | - android-22 14 | - android-25 15 | - extra-android-support 16 | - extra-google-m2repository 17 | - extra-android-m2repository 18 | - sys-img-armeabi-v7a-android-22 19 | 20 | before_script: 21 | - cp gradle.properties.ci.example gradle.properties 22 | # Emulator Management: Create, Start and Wait 23 | - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a 24 | - emulator -avd test -no-audio -no-window & 25 | - android-wait-for-emulator 26 | - adb shell input keyevent 82 & 27 | 28 | script: 29 | - ./gradlew clean connectedAndroidTest 30 | - ./gradlew testDebugUnitTest 31 | 32 | before_cache: 33 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 34 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 35 | cache: 36 | directories: 37 | - $HOME/.gradle/caches/ 38 | - $HOME/.gradle/wrapper/ 39 | - $HOME/.android/build-cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Akira Sosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android micropost app 2 | 3 | This repository is an example Android application based on Rails tutorial app. 4 | 5 | [![Build Status](https://travis-ci.org/springboot-angular2-tutorial/android-app.svg?branch=master)](https://travis-ci.org/springboot-angular2-tutorial/android-app) 6 | 7 | * Kotlin 100% without kapt 8 | * [Kodein](https://github.com/SalomonBrys/Kodein) as Dependency Injection 9 | * [RxJava](https://github.com/ReactiveX/RxJava) / [RxAndroid](https://github.com/ReactiveX/RxAndroid) 10 | * [RxBinding](https://github.com/JakeWharton/RxBinding) 11 | * [Retrofit](https://github.com/square/retrofit) 12 | * [Robolectric](http://robolectric.org/) for testing model layer 13 | * [Espresso](https://google.github.io/android-testing-support-library/docs/espresso/) for testing presentation layer 14 | 15 | ## Getting Started 16 | 17 | Prepare backend app. 18 | 19 | ``` 20 | git clone https://github.com/springboot-angular2-tutorial/boot-app.git 21 | cd boot-app 22 | mvn spring-boot:run 23 | ``` 24 | 25 | Configure API URL. 26 | 27 | ``` 28 | cp gradle.properties.example gradle.properties 29 | ``` 30 | 31 | ``` 32 | # gradle.properties 33 | micropost.apiUrl="Your API HERE" 34 | ``` 35 | 36 | Then, just run from Android Studio. 37 | 38 | Testing. 39 | 40 | ``` 41 | ./gradlew testDebugUnitTest 42 | ./gradlew connectedAndroidTest 43 | ``` 44 | 45 | Release build. 46 | 47 | ``` 48 | cp yourkey app/myreleasekey.jks 49 | export KSTOREPWD=yourpass 50 | export KEYPWD=yourpass 51 | ./gradlew assembleRelease 52 | ``` 53 | 54 | ## Tutorial 55 | 56 | Under construction... 57 | 58 | ## License 59 | 60 | [MIT](/LICENSE) 61 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 25 7 | buildToolsVersion '25.0.2' 8 | 9 | defaultConfig { 10 | applicationId "com.hana053.micropost" 11 | minSdkVersion 19 12 | targetSdkVersion 25 13 | versionCode 1 14 | versionName "1.0.0" 15 | testInstrumentationRunner "com.hana053.micropost.testing.AppTestRunner" 16 | multiDexEnabled = true 17 | buildConfigField('String', 'API_URL', getProperty("micropost.apiUrl")) 18 | } 19 | signingConfigs { 20 | release { 21 | storeFile file("myreleasekey.jks") 22 | keyAlias "MyReleaseKey" 23 | storePassword System.getenv("KSTOREPWD") 24 | keyPassword System.getenv("KEYPWD") 25 | } 26 | } 27 | buildTypes { 28 | debug { 29 | applicationIdSuffix ".debug" 30 | testCoverageEnabled true 31 | } 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | signingConfig signingConfigs.release 36 | } 37 | } 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | debug.java.srcDirs += 'src/debug/kotlin' 41 | release.java.srcDirs += 'src/release/kotlin' 42 | test.java.srcDirs += 'src/test/kotlin' 43 | test.java.srcDirs += 'src/testShared/kotlin' 44 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 45 | androidTest.java.srcDirs += 'src/testShared/kotlin' 46 | } 47 | configurations.all { 48 | resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.1' 49 | } 50 | } 51 | 52 | dependencies { 53 | compile "com.android.support:appcompat-v7:$android_support_version" 54 | compile "com.android.support:design:$android_support_version" 55 | compile "com.android.support:support-annotations:$android_support_version" 56 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 57 | compile "com.github.salomonbrys.kodein:kodein:$kodein_version" 58 | compile "com.github.salomonbrys.kodein:kodein-android:$kodein_version" 59 | compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbinding_version" 60 | compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbinding_version" 61 | compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbinding_version" 62 | compile "com.trello:rxlifecycle:$rxlifecycle_version" 63 | compile "com.trello:rxlifecycle-android:$rxlifecycle_version" 64 | compile "com.trello:rxlifecycle-components:$rxlifecycle_version" 65 | compile "com.trello:rxlifecycle-kotlin:$rxlifecycle_version" 66 | compile "com.squareup.retrofit2:retrofit:$retrofit_version" 67 | compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" 68 | compile "com.squareup.retrofit2:converter-moshi:$retrofit_version" 69 | compile "com.squareup.okhttp3:okhttp:$okhttp_version" 70 | compile "com.squareup.picasso:picasso:$picasso_version" 71 | compile "com.jakewharton.timber:timber:$timber_version" 72 | compile "com.github.curioustechizen.android-ago:library:$android_ago_version" 73 | compile "com.nimbusds:nimbus-jose-jwt:$jose_jwt_version" 74 | 75 | debugCompile "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" 76 | 77 | testCompile 'junit:junit:4.12' 78 | testCompile "org.hamcrest:hamcrest-all:$hamcrest_version" 79 | testCompile "org.mockito:mockito-core:$mockito_version" 80 | testCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version" 81 | testCompile "org.robolectric:robolectric:$robolectric_version" 82 | testCompile "org.robolectric:shadows-support-v4:$robolectric_version" 83 | testCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanary_version" 84 | testCompile group: 'org.assertj', name: 'assertj-core', version: assertj_version 85 | 86 | androidTestCompile "com.android.support.test:runner:$android_support_test_version" 87 | androidTestCompile "com.android.support.test:rules:$android_support_test_version" 88 | androidTestCompile "com.android.support.test.espresso:espresso-core:$espresso_version", { 89 | exclude group: 'com.android.support', module: 'support-annotations' 90 | } 91 | androidTestCompile "com.android.support:support-annotations:$android_support_version" 92 | androidTestCompile "org.mockito:mockito-core:$mockito_version" 93 | androidTestCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version" 94 | androidTestCompile "com.linkedin.dexmaker:dexmaker-mockito:$dexmaker_mockito_version" 95 | androidTestCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanary_version" 96 | androidTestCompile "com.linkedin.testbutler:test-butler-library:$testbutler_version" 97 | androidTestCompile "com.squareup.assertj:assertj-android:$assertj_android_version" 98 | } 99 | repositories { 100 | mavenCentral() 101 | } 102 | -------------------------------------------------------------------------------- /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 /usr/local/opt/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/kotlin/com/hana053/micropost/pages/login/LoginActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.login 2 | 3 | import android.support.test.espresso.Espresso.onView 4 | import android.support.test.espresso.action.ViewActions.* 5 | import android.support.test.espresso.assertion.ViewAssertions 6 | import android.support.test.espresso.assertion.ViewAssertions.matches 7 | import android.support.test.espresso.matcher.ViewMatchers.* 8 | import android.support.test.filters.LargeTest 9 | import android.support.test.rule.ActivityTestRule 10 | import android.support.test.runner.AndroidJUnit4 11 | import android.view.View 12 | import com.github.salomonbrys.kodein.bind 13 | import com.github.salomonbrys.kodein.instance 14 | import com.hana053.micropost.R 15 | import com.hana053.micropost.service.LoginService 16 | import com.hana053.micropost.service.Navigator 17 | import com.hana053.micropost.testing.InjectableTest 18 | import com.hana053.micropost.testing.InjectableTestImpl 19 | import com.nhaarman.mockito_kotlin.any 20 | import com.nhaarman.mockito_kotlin.doReturn 21 | import com.nhaarman.mockito_kotlin.mock 22 | import com.nhaarman.mockito_kotlin.verify 23 | import org.hamcrest.CoreMatchers.not 24 | import org.hamcrest.Matcher 25 | import org.junit.Rule 26 | import org.junit.Test 27 | import org.junit.runner.RunWith 28 | import rx.Observable 29 | 30 | 31 | @RunWith(AndroidJUnit4::class) 32 | @LargeTest 33 | class LoginActivityTest : InjectableTest by InjectableTestImpl() { 34 | 35 | @Rule @JvmField 36 | val activityRule = ActivityTestRule(LoginActivity::class.java, false, false) 37 | 38 | val loginBtn: Matcher = withId(R.id.btn_login) 39 | val emailEditText: Matcher = withId(R.id.et_email) 40 | val passwordEditText: Matcher = withId(R.id.et_password) 41 | 42 | @Test 43 | fun shouldBeOpened() { 44 | activityRule.launchActivity(null) 45 | onView(withText(R.string.log_in_to_micropost)).check(matches(isDisplayed())) 46 | } 47 | 48 | @Test 49 | fun shouldDisableOrEnableBtn() { 50 | activityRule.launchActivity(null) 51 | 52 | onView(loginBtn).check(ViewAssertions.matches(not(isEnabled()))) 53 | 54 | onView(emailEditText).perform(typeText("test@test.com"), closeSoftKeyboard()) 55 | onView(passwordEditText).perform(typeText("secret123"), closeSoftKeyboard()) 56 | onView(loginBtn).check(ViewAssertions.matches(isEnabled())) 57 | 58 | onView(passwordEditText).perform(clearText(), closeSoftKeyboard()) 59 | onView(loginBtn).check(ViewAssertions.matches(not(isEnabled()))) 60 | 61 | onView(emailEditText).perform(clearText(), closeSoftKeyboard()) 62 | onView(passwordEditText).perform(typeText("secret123"), closeSoftKeyboard()) 63 | onView(loginBtn).check(ViewAssertions.matches(not(isEnabled()))) 64 | } 65 | 66 | @Test 67 | fun shouldNavigateToMainWhenEmailAndPasswordIsValid() { 68 | val navigator = mock() 69 | overrideAppBindings { 70 | bind(overrides = true) with instance(mock { 71 | on { login(any(), any()) } doReturn Observable.just(null) 72 | }) 73 | bind(overrides = true) with instance(navigator) 74 | } 75 | activityRule.launchActivity(null) 76 | 77 | onView(emailEditText).perform(typeText("test@test.com"), closeSoftKeyboard()) 78 | onView(passwordEditText).perform(typeText("secret123"), closeSoftKeyboard()) 79 | onView(loginBtn).perform(click()) 80 | 81 | verify(navigator).navigateToMain() 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import org.assertj.android.api.Assertions.assertThat 4 | import android.support.test.espresso.Espresso.onView 5 | import android.support.test.espresso.action.ViewActions.* 6 | import android.support.test.espresso.assertion.ViewAssertions.matches 7 | import android.support.test.espresso.matcher.ViewMatchers.* 8 | import android.support.test.filters.LargeTest 9 | import android.support.test.rule.ActivityTestRule 10 | import android.support.test.runner.AndroidJUnit4 11 | import com.github.salomonbrys.kodein.bind 12 | import com.github.salomonbrys.kodein.instance 13 | import com.hana053.micropost.R 14 | import com.hana053.micropost.interactor.MicropostInteractor 15 | import com.hana053.micropost.service.Navigator 16 | import com.hana053.micropost.testing.InjectableTest 17 | import com.hana053.micropost.testing.InjectableTestImpl 18 | import com.hana053.micropost.testing.TestMicropost 19 | import com.hana053.micropost.testing.fakeAuthToken 20 | import com.nhaarman.mockito_kotlin.any 21 | import com.nhaarman.mockito_kotlin.doReturn 22 | import com.nhaarman.mockito_kotlin.mock 23 | import com.nhaarman.mockito_kotlin.verify 24 | import org.hamcrest.CoreMatchers.not 25 | import org.junit.Rule 26 | import org.junit.Test 27 | import org.junit.runner.RunWith 28 | import rx.Observable 29 | 30 | 31 | @RunWith(AndroidJUnit4::class) 32 | @LargeTest 33 | class MicropostNewActivityTest : InjectableTest by InjectableTestImpl() { 34 | 35 | @Rule @JvmField 36 | val activityRule = ActivityTestRule(MicropostNewActivity::class.java, false, false) 37 | 38 | @Test 39 | fun shouldBeOpenedWhenAuthenticated() { 40 | overrideAppBindings { fakeAuthToken() } 41 | activityRule.launchActivity(null) 42 | onView(withText(R.string.post)).check(matches(isDisplayed())) 43 | } 44 | 45 | @Test 46 | fun shouldNotBeOpenedWhenNotAuthenticated() { 47 | val navigator = mock() 48 | overrideAppBindings { 49 | bind(overrides = true) with instance(navigator) 50 | } 51 | activityRule.launchActivity(null) 52 | verify(navigator).navigateToTop() 53 | } 54 | 55 | @Test 56 | fun shouldDisableOrEnableBtn() { 57 | overrideAppBindings { fakeAuthToken() } 58 | activityRule.launchActivity(null) 59 | 60 | onView(withId(R.id.btn_post)).check(matches(not(isEnabled()))) 61 | 62 | onView(withId(R.id.et_post)).perform(typeText("a")) 63 | onView(withId(R.id.btn_post)).check(matches(isEnabled())) 64 | } 65 | 66 | @Test 67 | fun shouldPostAndNavigateToMain() { 68 | val micropostInteractor: MicropostInteractor = mock { 69 | on { create(any()) } doReturn Observable.just(TestMicropost) 70 | } 71 | overrideAppBindings { 72 | fakeAuthToken() 73 | bind(overrides = true) with instance(micropostInteractor) 74 | } 75 | activityRule.launchActivity(null) 76 | 77 | onView(withId(R.id.et_post)).perform(typeText("a")) 78 | onView(withId(R.id.btn_post)).perform(closeSoftKeyboard(), click()) 79 | 80 | verify(micropostInteractor).create(MicropostInteractor.MicropostRequest("a")) 81 | assertThat(activityRule.activity).isFinishing 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/hana053/micropost/pages/top/TopActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.top 2 | 3 | import android.support.test.espresso.Espresso.onView 4 | import android.support.test.espresso.action.ViewActions.click 5 | import android.support.test.espresso.assertion.ViewAssertions.matches 6 | import android.support.test.espresso.matcher.ViewMatchers.* 7 | import android.support.test.filters.LargeTest 8 | import android.support.test.rule.ActivityTestRule 9 | import android.support.test.runner.AndroidJUnit4 10 | import com.github.salomonbrys.kodein.bind 11 | import com.github.salomonbrys.kodein.instance 12 | import com.hana053.micropost.R 13 | import com.hana053.micropost.service.Navigator 14 | import com.hana053.micropost.testing.InjectableTest 15 | import com.hana053.micropost.testing.InjectableTestImpl 16 | import com.nhaarman.mockito_kotlin.mock 17 | import com.nhaarman.mockito_kotlin.verify 18 | import org.junit.Rule 19 | import org.junit.Test 20 | import org.junit.runner.RunWith 21 | 22 | 23 | @RunWith(AndroidJUnit4::class) 24 | @LargeTest 25 | class TopActivityTest : InjectableTest by InjectableTestImpl() { 26 | 27 | @Rule @JvmField 28 | val activityRule = ActivityTestRule(TopActivity::class.java, false, false) 29 | 30 | @Test 31 | fun shouldBeOpened() { 32 | activityRule.launchActivity(null) 33 | 34 | onView(withText(R.string.welcome_to_micropost)).check(matches(isDisplayed())) 35 | } 36 | 37 | @Test 38 | fun shouldNavigateToSignup() { 39 | val navigator = mock() 40 | overrideAppBindings { 41 | bind(overrides = true) with instance(navigator) 42 | } 43 | 44 | activityRule.launchActivity(null) 45 | onView(withId(R.id.btn_signup)).perform(click()) 46 | 47 | verify(navigator).navigateToSignup() 48 | } 49 | 50 | @Test 51 | fun shouldNavigateToLogin() { 52 | val navigator = mock() 53 | overrideAppBindings { 54 | bind(overrides = true) with instance(navigator) 55 | } 56 | 57 | activityRule.launchActivity(null) 58 | onView(withId(R.id.btn_login)).perform(click()) 59 | 60 | verify(navigator).navigateToLogin() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/hana053/micropost/testing/AppTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.testing 2 | 3 | import android.os.Bundle 4 | import android.support.test.InstrumentationRegistry 5 | import android.support.test.runner.AndroidJUnitRunner 6 | import com.linkedin.android.testbutler.TestButler 7 | 8 | 9 | class AppTestRunner : AndroidJUnitRunner() { 10 | 11 | override fun onStart() { 12 | TestButler.setup(InstrumentationRegistry.getTargetContext()) 13 | super.onStart() 14 | } 15 | 16 | override fun finish(resultCode: Int, results: Bundle) { 17 | TestButler.teardown(InstrumentationRegistry.getTargetContext()) 18 | super.finish(resultCode, results) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/hana053/micropost/testing/EspressoHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.testing 2 | 3 | import android.support.test.espresso.matcher.BoundedMatcher 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | 10 | fun atPositionOnView( 11 | position: Int, 12 | matcher: Matcher, 13 | targetId: Int 14 | ) = object : BoundedMatcher(RecyclerView::class.java) { 15 | 16 | override fun matchesSafely(recyclerView: RecyclerView) = 17 | recyclerView.findViewHolderForAdapterPosition(position) 18 | .itemView 19 | .findViewById(targetId) 20 | .let { matcher.matches(it) } 21 | 22 | override fun describeTo(description: Description?) { 23 | description?.appendText("has view id $matcher at position $position") 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/hana053/micropost/testing/InjectableTestImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.testing 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import com.hana053.micropost.BaseApplication 5 | 6 | 7 | class InjectableTestImpl : InjectableTest { 8 | override val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as BaseApplication 9 | } -------------------------------------------------------------------------------- /app/src/debug/kotlin/com/hana053/micropost/Application.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost 2 | 3 | import android.os.StrictMode 4 | import com.squareup.leakcanary.LeakCanary 5 | import timber.log.Timber 6 | 7 | class Application : BaseApplication() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | LeakCanary.install(this) 12 | 13 | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().penaltyDeathOnNetwork().build()) 14 | StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().detectActivityLeaks().penaltyLog().build()) 15 | 16 | Timber.plant(Timber.DebugTree()) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.hana053.micropost.service.serviceModule 5 | import com.hana053.micropost.interactor.interactorModule 6 | import com.hana053.micropost.repository.repositoryModule 7 | import com.hana053.micropost.system.systemModule 8 | 9 | fun appModule() = Kodein.Module { 10 | 11 | import(systemModule()) 12 | import(repositoryModule()) 13 | import(interactorModule()) 14 | import(serviceModule()) 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost 2 | 3 | import android.app.Application 4 | import android.support.annotation.VisibleForTesting 5 | import com.github.salomonbrys.kodein.Kodein 6 | import com.github.salomonbrys.kodein.KodeinAware 7 | import com.github.salomonbrys.kodein.android.androidActivityScope 8 | import com.hana053.micropost.pages.login.LoginActivity 9 | import com.hana053.micropost.pages.login.loginModule 10 | import com.hana053.micropost.pages.main.MainActivity 11 | import com.hana053.micropost.pages.main.mainModule 12 | import com.hana053.micropost.pages.micropostnew.MicropostNewActivity 13 | import com.hana053.micropost.pages.micropostnew.micropostNewModule 14 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity 15 | import com.hana053.micropost.pages.relateduserlist.relatedUserListModule 16 | import com.hana053.micropost.pages.signup.SignupActivity 17 | import com.hana053.micropost.pages.signup.signupModule 18 | import com.hana053.micropost.pages.top.TopActivity 19 | import com.hana053.micropost.pages.top.topModule 20 | import com.hana053.micropost.pages.usershow.UserShowActivity 21 | import com.hana053.micropost.pages.usershow.userShowModule 22 | 23 | abstract class BaseApplication : Application(), KodeinAware { 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | registerActivityLifecycleCallbacks(androidActivityScope.lifecycleManager) 28 | } 29 | 30 | private var _kodein = Kodein { 31 | import(appModule()) 32 | } 33 | 34 | override val kodein: Kodein 35 | get() = _kodein 36 | 37 | @VisibleForTesting 38 | fun setKodein(kodein: Kodein) { 39 | _kodein = kodein 40 | } 41 | 42 | private val overridingModules = mutableMapOf, Kodein.Module>( 43 | Pair(TopActivity::class.java, topModule()), 44 | Pair(LoginActivity::class.java, loginModule()), 45 | Pair(MainActivity::class.java, mainModule()), 46 | Pair(SignupActivity::class.java, signupModule()), 47 | Pair(UserShowActivity::class.java, userShowModule()), 48 | Pair(MicropostNewActivity::class.java, micropostNewModule()), 49 | Pair(RelatedUserListActivity::class.java, relatedUserListModule()) 50 | ) 51 | 52 | fun getOverridingModule(clazz: Class<*>) = overridingModules[clazz]!! 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost 2 | 3 | import android.app.Activity 4 | 5 | fun Activity.getOverridingModule() = 6 | (application as BaseApplication).getOverridingModule(javaClass) 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/domain/Avatar.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.domain 2 | 3 | 4 | interface Avatar { 5 | val avatarHash: String 6 | 7 | fun avatarUrl(size: Int = 72) = "https://secure.gravatar.com/avatar/$avatarHash?s=$size" 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/domain/Micropost.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.domain 2 | 3 | 4 | data class Micropost( 5 | val id: Long, 6 | val content: String, 7 | val createdAt: Long, 8 | val user: User 9 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/domain/RelatedUser.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.domain 2 | 3 | 4 | data class RelatedUser( 5 | val id: Long, 6 | val name: String, 7 | val email: String?, 8 | override val avatarHash: String, 9 | val isFollowedByMe: Boolean, 10 | val userStats: UserStats, 11 | val relationshipId: Long 12 | ) : Avatar { 13 | 14 | fun toUser() = User(id, name, email, avatarHash, isFollowedByMe, userStats) 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/domain/User.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.domain 2 | 3 | data class User( 4 | val id: Long, 5 | val name: String, 6 | val email: String?, 7 | override val avatarHash: String, 8 | val isFollowedByMe: Boolean? = null, 9 | val userStats: UserStats 10 | ) : Avatar 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/domain/UserStats.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.domain 2 | 3 | 4 | data class UserStats( 5 | val micropostCnt: Int, 6 | val followingCnt: Int, 7 | val followerCnt: Int 8 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/FeedInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.myapp.interactors 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | import rx.Observable 7 | 8 | interface FeedInteractor { 9 | 10 | @GET("feed?count=20") 11 | fun loadNextFeed(@Query("sinceId") sinceId: Long?): Observable> 12 | 13 | @GET("feed?count=20") 14 | fun loadPrevFeed(@Query("maxId") maxId: Long?): Observable> 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/InteractorModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.github.salomonbrys.kodein.bind 5 | import com.github.salomonbrys.kodein.instance 6 | import com.github.salomonbrys.kodein.singleton 7 | import com.hana053.micropost.BuildConfig 8 | import com.hana053.micropost.repository.AuthTokenRepository 9 | import com.hana053.myapp.interactors.FeedInteractor 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory 13 | import retrofit2.converter.moshi.MoshiConverterFactory 14 | 15 | fun interactorModule() = Kodein.Module { 16 | 17 | bind() with singleton { 18 | val authTokenRepository = instance() 19 | 20 | OkHttpClient().newBuilder().addInterceptor { chain -> 21 | val original = chain.request() 22 | val modified = original.newBuilder().apply { 23 | authTokenRepository.get()?.let { 24 | header("authorization", "Bearer $it") 25 | } 26 | }.method(original.method(), original.body()).build() 27 | 28 | chain.proceed(modified) 29 | }.build() 30 | } 31 | 32 | bind() with singleton { 33 | val okHttpClient = instance() 34 | 35 | Retrofit.Builder() 36 | .baseUrl(BuildConfig.API_URL) 37 | .addConverterFactory(MoshiConverterFactory.create()) 38 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 39 | .client(okHttpClient) 40 | .build() 41 | } 42 | 43 | bind() with singleton { 44 | instance().create() 45 | } 46 | 47 | bind() with singleton { 48 | instance().create() 49 | } 50 | 51 | bind() with singleton { 52 | instance().create() 53 | } 54 | 55 | bind() with singleton { 56 | instance().create() 57 | } 58 | 59 | bind() with singleton { 60 | instance().create() 61 | } 62 | 63 | bind() with singleton { 64 | instance().create() 65 | } 66 | 67 | bind() with singleton { 68 | instance().create() 69 | } 70 | } 71 | 72 | inline fun Retrofit.create(): T = create(T::class.java) 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/LoginInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import retrofit2.http.Body 4 | import retrofit2.http.POST 5 | import rx.Observable 6 | 7 | interface LoginInteractor { 8 | 9 | @POST("auth") 10 | fun login(@Body request: LoginRequest): Observable 11 | 12 | data class LoginRequest(val email: String, val password: String) 13 | data class LoginResponse(val token: String) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/MicropostInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import retrofit2.http.Body 5 | import retrofit2.http.POST 6 | import rx.Observable 7 | 8 | 9 | interface MicropostInteractor { 10 | 11 | @POST("microposts") 12 | fun create(@Body request: MicropostRequest): Observable 13 | 14 | data class MicropostRequest(val content: String) 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/RelatedUserListInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import com.hana053.micropost.domain.RelatedUser 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | import retrofit2.http.Query 7 | import rx.Observable 8 | 9 | 10 | interface RelatedUserListInteractor { 11 | 12 | @GET("users/{id}/followings") 13 | fun listFollowings( 14 | @Path("id") userId: Long, 15 | @Query("maxId") maxId: Long? 16 | ): Observable> 17 | 18 | @GET("users/{id}/followers") 19 | fun listFollowers( 20 | @Path("id") userId: Long, 21 | @Query("maxId") maxId: Long? 22 | ): Observable> 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/RelationshipInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import retrofit2.http.DELETE 4 | import retrofit2.http.POST 5 | import retrofit2.http.Path 6 | import rx.Observable 7 | 8 | interface RelationshipInteractor { 9 | 10 | @POST("relationships/to/{followerId}") 11 | fun follow(@Path("followerId") followerId: Long): Observable 12 | 13 | @DELETE("relationships/to/{followerId}") 14 | fun unfollow(@Path("followerId") followerId: Long): Observable 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/UserInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import com.hana053.micropost.domain.User 4 | import retrofit2.http.Body 5 | import retrofit2.http.GET 6 | import retrofit2.http.POST 7 | import retrofit2.http.Path 8 | import rx.Observable 9 | 10 | interface UserInteractor { 11 | 12 | @GET("users/{id}") 13 | fun get(@Path("id") userId: Long): Observable 14 | 15 | @POST("users") 16 | fun create(@Body request: SignupRequest): Observable 17 | 18 | data class SignupRequest( 19 | val name: String, 20 | val email: String, 21 | val password: String 22 | ) 23 | } 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/interactor/UserMicropostInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.interactor 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | import retrofit2.http.Query 7 | import rx.Observable 8 | 9 | 10 | interface UserMicropostInteractor { 11 | 12 | @GET("users/{userId}/microposts?count=20") 13 | fun loadPrevPosts(@Path("userId") userId: Long, @Query("maxId") maxId: Long?): Observable> 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/Presenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages 2 | 3 | import android.view.Gravity 4 | import android.view.View 5 | import android.widget.ProgressBar 6 | import android.widget.RelativeLayout 7 | import com.trello.rxlifecycle.kotlin.bindToLifecycle 8 | import rx.Observable 9 | 10 | 11 | interface Presenter { 12 | 13 | val view: T 14 | 15 | fun bind() 16 | 17 | fun Observable.withProgressDialog(): Observable = Observable.using({ 18 | val progressBar = ProgressBar(view.content.context, null, android.R.attr.progressBarStyle).apply { 19 | isIndeterminate = true 20 | visibility = View.VISIBLE 21 | } 22 | val rl = RelativeLayout(view.content.context).apply { 23 | gravity = Gravity.CENTER 24 | addView(progressBar) 25 | } 26 | val layoutParams = RelativeLayout.LayoutParams( 27 | RelativeLayout.LayoutParams.MATCH_PARENT, 28 | RelativeLayout.LayoutParams.MATCH_PARENT 29 | ) 30 | view.content.addView(rl, layoutParams) 31 | progressBar 32 | }, { this }, { 33 | it.visibility = View.GONE 34 | }) 35 | 36 | fun Observable.bindToLifecycle(): Observable = bindToLifecycle(view.content) 37 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/ViewWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages 2 | 3 | import android.content.Context 4 | import android.view.ViewGroup 5 | 6 | 7 | interface ViewWrapper { 8 | val content: ViewGroup 9 | 10 | fun context(): Context = content.context 11 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.login 2 | 3 | import android.os.Bundle 4 | import com.github.salomonbrys.kodein.KodeinInjector 5 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 6 | import com.github.salomonbrys.kodein.instance 7 | import com.hana053.micropost.R 8 | import com.hana053.micropost.getOverridingModule 9 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 10 | 11 | class LoginActivity : RxAppCompatActivity(), AppCompatActivityInjector { 12 | 13 | override val injector: KodeinInjector = KodeinInjector() 14 | 15 | private val presenter: LoginPresenter by instance() 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_login) 20 | initializeInjector() 21 | 22 | presenter.bind() 23 | } 24 | 25 | override fun onDestroy() { 26 | super.onDestroy() 27 | destroyInjector() 28 | } 29 | 30 | override fun provideOverridingModule() = getOverridingModule() 31 | } 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/login/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.login 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.activity_login.* 10 | 11 | fun loginModule() = Kodein.Module { 12 | 13 | bind() with autoScopedSingleton(androidActivityScope) { 14 | LoginPresenter(instance(), instance(), instance()) 15 | } 16 | 17 | bind() with autoScopedSingleton(androidActivityScope) { 18 | instance().activity_login.let(::LoginView) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/login/LoginPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.login 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.service.LoginService 5 | import com.hana053.micropost.service.Navigator 6 | import rx.Observable 7 | 8 | class LoginPresenter( 9 | override val view: LoginView, 10 | private val loginService: LoginService, 11 | private val navigator: Navigator 12 | ) : Presenter { 13 | 14 | override fun bind() { 15 | val emailChanges = view.emailChanges.share() 16 | val passwordChanges = view.passwordChanges.share() 17 | 18 | val emailAndPassword = Observable.combineLatest(emailChanges, passwordChanges, ::EmailAndPassword) 19 | 20 | emailAndPassword 21 | .bindToLifecycle() 22 | .map { it.isValid() } 23 | .subscribe { view.loginEnabled.call(it) } 24 | 25 | view.loginClicks 26 | .bindToLifecycle() 27 | .withLatestFrom(emailAndPassword, { click, emailAndPassword -> 28 | emailAndPassword 29 | }) 30 | .flatMap { 31 | loginService.login(it.email, it.password) 32 | .withProgressDialog() 33 | } 34 | .subscribe { navigator.navigateToMain() } 35 | } 36 | 37 | data class EmailAndPassword(val email: String, val password: String) { 38 | fun isValid() = email.isNotBlank() && password.isNotBlank() 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/login/LoginView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.login 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import com.jakewharton.rxbinding.view.enabled 7 | import com.jakewharton.rxbinding.widget.textChanges 8 | import kotlinx.android.synthetic.main.activity_login.view.* 9 | import rx.Observable 10 | 11 | class LoginView(override val content: ViewGroup) : ViewWrapper { 12 | 13 | // Events 14 | val emailChanges: Observable = content.et_email 15 | .textChanges() 16 | .map { it.toString() } 17 | val passwordChanges: Observable = content.et_password 18 | .textChanges() 19 | .map { it.toString() } 20 | val loginClicks = content.btn_login.clicks() 21 | 22 | // Props 23 | val loginEnabled = content.btn_login.enabled() 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.main 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import com.github.salomonbrys.kodein.KodeinInjector 9 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 10 | import com.github.salomonbrys.kodein.instance 11 | import com.hana053.micropost.R 12 | import com.hana053.micropost.getOverridingModule 13 | import com.hana053.micropost.service.AuthService 14 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 15 | 16 | 17 | class MainActivity : RxAppCompatActivity(), AppCompatActivityInjector { 18 | 19 | override val injector: KodeinInjector = KodeinInjector() 20 | 21 | private val authService: AuthService by instance() 22 | private val presenter: MainPresenter by instance() 23 | 24 | companion object { 25 | val REQUEST_POST = 1 26 | } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_main) 31 | initializeInjector() 32 | 33 | if (!authService.auth()) return 34 | 35 | presenter.bind() 36 | } 37 | 38 | override fun onDestroy() { 39 | super.onDestroy() 40 | destroyInjector() 41 | } 42 | 43 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 44 | super.onActivityResult(requestCode, resultCode, data) 45 | if (requestCode == REQUEST_POST && resultCode == Activity.RESULT_OK) { 46 | presenter.newPostSubmittedSubject.onNext(null) 47 | } 48 | } 49 | 50 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 51 | menuInflater.inflate(R.menu.menu_main, menu) 52 | return true 53 | } 54 | 55 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 56 | when (item.itemId) { 57 | R.id.action_logout -> { 58 | authService.logout() 59 | return true 60 | } 61 | } 62 | return super.onOptionsItemSelected(item) 63 | } 64 | 65 | override fun provideOverridingModule() = getOverridingModule() 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/main/MainModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.main 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import com.hana053.micropost.shared.posts.PostListAdapter 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | 12 | fun mainModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | PostListAdapter() 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | MainPresenter(instance(), instance(), instance(), instance()) 20 | } 21 | 22 | bind() with autoScopedSingleton(androidActivityScope) { 23 | instance().activity_main 24 | .let { MainView(it, instance()) } 25 | } 26 | 27 | bind() with autoScopedSingleton(androidActivityScope) { 28 | MainService(instance(), instance(), instance()) 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/main/MainPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.main 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.service.Navigator 5 | import com.hana053.micropost.shared.posts.PostListAdapter 6 | import rx.subjects.PublishSubject 7 | 8 | 9 | class MainPresenter( 10 | override val view: MainView, 11 | private val mainService: MainService, 12 | private val postListAdapter: PostListAdapter, 13 | private val navigator: Navigator 14 | ) : Presenter { 15 | 16 | val newPostSubmittedSubject: PublishSubject = PublishSubject.create() 17 | 18 | override fun bind() { 19 | mainService.loadNextFeed() 20 | .bindToLifecycle() 21 | .withProgressDialog() 22 | .subscribe() 23 | 24 | view.swipeRefreshes 25 | .bindToLifecycle() 26 | .flatMap { mainService.loadNextFeed() } 27 | .subscribe { view.swipeRefreshing.call(false) } 28 | 29 | view.scrollsToBottom 30 | .bindToLifecycle() 31 | .flatMap { 32 | mainService.loadPrevFeed() 33 | .withProgressDialog() 34 | } 35 | .subscribe() 36 | 37 | view.newMicropostClicks 38 | .bindToLifecycle() 39 | .subscribe { navigator.navigateToMicropostNew() } 40 | 41 | postListAdapter.avatarClicksSubject 42 | .bindToLifecycle() 43 | .subscribe { navigator.navigateToUserShow(it.id) } 44 | 45 | newPostSubmittedSubject 46 | .bindToLifecycle() 47 | .flatMap { 48 | mainService.loadNextFeed() 49 | .withProgressDialog() 50 | } 51 | .subscribe() 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/main/MainService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.main 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import com.hana053.micropost.service.HttpErrorHandler 5 | import com.hana053.micropost.shared.posts.PostListAdapter 6 | import com.hana053.myapp.interactors.FeedInteractor 7 | import rx.Observable 8 | import rx.android.schedulers.AndroidSchedulers 9 | import rx.schedulers.Schedulers 10 | 11 | 12 | class MainService( 13 | private val feedInteractor: FeedInteractor, 14 | private val postListAdapter: PostListAdapter, 15 | private val httpErrorHandler: HttpErrorHandler 16 | ) { 17 | 18 | fun loadNextFeed(): Observable> { 19 | val sinceId = postListAdapter.getFirstItemId() 20 | 21 | return feedInteractor.loadNextFeed(sinceId) 22 | .subscribeOn(Schedulers.newThread()) 23 | .observeOn(AndroidSchedulers.mainThread()) 24 | .doOnNext { postListAdapter.addAll(0, it) } 25 | .doOnError { httpErrorHandler.handleError(it) } 26 | .onErrorResumeNext(Observable.empty()) 27 | } 28 | 29 | fun loadPrevFeed(): Observable> { 30 | val maxId = postListAdapter.getLastItemId() 31 | val itemCount = postListAdapter.itemCount 32 | 33 | return feedInteractor.loadPrevFeed(maxId) 34 | .subscribeOn(Schedulers.newThread()) 35 | .observeOn(AndroidSchedulers.mainThread()) 36 | .doOnNext { postListAdapter.addAll(itemCount, it) } 37 | .doOnError { httpErrorHandler.handleError(it) } 38 | .onErrorResumeNext(Observable.empty()) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/main/MainView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.main 2 | 3 | import android.support.v7.widget.LinearLayoutManager 4 | import android.view.ViewGroup 5 | import com.hana053.micropost.pages.ViewWrapper 6 | import com.hana053.micropost.shared.posts.PostListAdapter 7 | import com.jakewharton.rxbinding.support.v4.widget.refreshes 8 | import com.jakewharton.rxbinding.support.v4.widget.refreshing 9 | import com.jakewharton.rxbinding.support.v7.widget.RecyclerViewScrollEvent 10 | import com.jakewharton.rxbinding.support.v7.widget.scrollEvents 11 | import com.jakewharton.rxbinding.view.clicks 12 | import kotlinx.android.synthetic.main.activity_main.view.* 13 | import rx.Observable 14 | 15 | class MainView( 16 | override val content: ViewGroup, 17 | postListAdapter: PostListAdapter 18 | ) : ViewWrapper { 19 | 20 | private val listPost = content.list_post 21 | private val swipeRefresh = content.swipe_refresh 22 | 23 | // Events 24 | val swipeRefreshes = swipeRefresh.refreshes() 25 | val scrollsToBottom: Observable = listPost.scrollEvents() 26 | .filter { !listPost.canScrollVertically(1) } 27 | val newMicropostClicks = content.btn_new_micropost.clicks() 28 | 29 | // Props 30 | val swipeRefreshing = swipeRefresh.refreshing() 31 | 32 | init { 33 | listPost.layoutManager = LinearLayoutManager(context()) 34 | listPost.adapter = postListAdapter 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MIcropostNewModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.activity_micropost_new.* 10 | 11 | 12 | fun micropostNewModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | MicropostNewPresenter(instance(), instance(), instance()) 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | instance().activity_micropost_new.let(::MicropostNewView) 20 | } 21 | 22 | bind() with autoScopedSingleton(androidActivityScope) { 23 | MicropostNewService(instance(), instance()) 24 | } 25 | 26 | bind() with autoScopedSingleton(androidActivityScope) { 27 | MicropostNewNavigator(instance()) 28 | } 29 | 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import android.os.Bundle 4 | import com.github.salomonbrys.kodein.KodeinInjector 5 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 6 | import com.github.salomonbrys.kodein.instance 7 | import com.hana053.micropost.R 8 | import com.hana053.micropost.getOverridingModule 9 | import com.hana053.micropost.service.AuthService 10 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 11 | 12 | 13 | class MicropostNewActivity : RxAppCompatActivity(), AppCompatActivityInjector { 14 | 15 | override val injector: KodeinInjector = KodeinInjector() 16 | 17 | private val authService: AuthService by instance() 18 | private val presenter: MicropostNewPresenter by instance() 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_micropost_new) 23 | initializeInjector() 24 | 25 | if (!authService.auth()) return 26 | 27 | presenter.bind() 28 | } 29 | 30 | override fun onDestroy() { 31 | super.onDestroy() 32 | destroyInjector() 33 | } 34 | 35 | override fun provideOverridingModule() = getOverridingModule() 36 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import android.app.Activity 4 | 5 | 6 | class MicropostNewNavigator( 7 | private val activity: Activity 8 | ) { 9 | fun finishWithPost() { 10 | activity.setResult(Activity.RESULT_OK) 11 | activity.finish() 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | 5 | 6 | class MicropostNewPresenter( 7 | override val view: MicropostNewView, 8 | private val micropostNewService: MicropostNewService, 9 | private val micropostNewNavigator: MicropostNewNavigator 10 | ) : Presenter { 11 | 12 | override fun bind() { 13 | val postTextChanges = view.postTextChanges.share() 14 | 15 | postTextChanges 16 | .bindToLifecycle() 17 | .map { it.isNotBlank() } 18 | .subscribe { view.postBtnEnabled.call(it) } 19 | 20 | view.postBtnClicks 21 | .bindToLifecycle() 22 | .withLatestFrom(postTextChanges, { click, text -> text }) 23 | .flatMap { 24 | micropostNewService.createPost(it.toString()) 25 | .withProgressDialog() 26 | } 27 | .subscribe { micropostNewNavigator.finishWithPost() } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import com.hana053.micropost.interactor.MicropostInteractor 5 | import com.hana053.micropost.service.HttpErrorHandler 6 | import rx.Observable 7 | import rx.android.schedulers.AndroidSchedulers 8 | import rx.schedulers.Schedulers 9 | 10 | 11 | class MicropostNewService( 12 | private val micropostInteractor: MicropostInteractor, 13 | private val httpErrorHandler: HttpErrorHandler 14 | ) { 15 | 16 | fun createPost(content: String): Observable { 17 | return micropostInteractor 18 | .create(MicropostInteractor.MicropostRequest(content)) 19 | .subscribeOn(Schedulers.newThread()) 20 | .observeOn(AndroidSchedulers.mainThread()) 21 | .doOnError { httpErrorHandler.handleError(it) } 22 | .onErrorResumeNext(Observable.empty()) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/micropostnew/MicropostNewView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.micropostnew 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import com.jakewharton.rxbinding.view.enabled 7 | import com.jakewharton.rxbinding.widget.textChanges 8 | import kotlinx.android.synthetic.main.activity_micropost_new.view.* 9 | 10 | 11 | class MicropostNewView(override val content: ViewGroup) : ViewWrapper { 12 | 13 | // Events 14 | val postTextChanges = content.et_post.textChanges() 15 | val postBtnClicks = content.btn_post.clicks() 16 | 17 | // Props 18 | val postBtnEnabled = content.btn_post.enabled() 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import com.github.salomonbrys.kodein.KodeinInjector 6 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 7 | import com.github.salomonbrys.kodein.instance 8 | import com.hana053.micropost.R 9 | import com.hana053.micropost.getOverridingModule 10 | import com.hana053.micropost.service.AuthService 11 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 12 | 13 | 14 | class RelatedUserListActivity : RxAppCompatActivity(), AppCompatActivityInjector { 15 | 16 | override val injector: KodeinInjector = KodeinInjector() 17 | 18 | private val authService: AuthService by instance() 19 | private val presenter: RelatedUserListPresenter by instance() 20 | private val relatedUserListService: RelatedUserListService by instance() 21 | 22 | enum class ListType { 23 | FOLLOWER, FOLLOWING 24 | } 25 | 26 | companion object { 27 | val KEY_USER_ID = "KEY_USER_ID" 28 | val KEY_LIST_TYPE = "KEY_LIST_TYPE" 29 | } 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | setContentView(R.layout.activity_related_user_list) 34 | initializeInjector() 35 | 36 | if (!authService.auth()) return 37 | 38 | presenter.bind() 39 | 40 | title = relatedUserListService.title() 41 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 42 | } 43 | 44 | 45 | override fun onDestroy() { 46 | super.onDestroy() 47 | destroyInjector() 48 | } 49 | 50 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 51 | when (item.itemId) { 52 | android.R.id.home -> { 53 | finish() 54 | return true 55 | } 56 | } 57 | return super.onOptionsItemSelected(item) 58 | } 59 | 60 | override fun provideOverridingModule() = getOverridingModule() 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Button 8 | import android.widget.ImageView 9 | import android.widget.LinearLayout 10 | import android.widget.TextView 11 | import com.hana053.micropost.R 12 | import com.hana053.micropost.domain.RelatedUser 13 | import com.hana053.micropost.domain.User 14 | import com.hana053.micropost.shared.avatar.AvatarView 15 | import com.hana053.micropost.shared.followbtn.FollowBtnView 16 | import kotlinx.android.synthetic.main.item_related_users.view.* 17 | import rx.subjects.PublishSubject 18 | 19 | 20 | class RelatedUserListAdapter( 21 | private val users: MutableList = mutableListOf() 22 | ) : RecyclerView.Adapter() { 23 | 24 | val followBtnClicksSubject: PublishSubject = PublishSubject.create() 25 | val avatarClicksSubject: PublishSubject = PublishSubject.create() 26 | 27 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 28 | val container: LinearLayout = view.container 29 | val avatar: ImageView = view.img_avatar 30 | val userName: TextView = view.tv_user_name 31 | val followBtn: Button = view.btn_follow 32 | } 33 | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 35 | LayoutInflater.from(parent.context) 36 | .inflate(R.layout.item_related_users, parent, false) 37 | .let(::ViewHolder) 38 | 39 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 40 | val item = users[position] 41 | 42 | holder.apply { 43 | container.tag = item 44 | userName.text = item.name 45 | 46 | AvatarView(avatar).render(item.toUser()) 47 | avatar.setOnClickListener { 48 | avatarClicksSubject.onNext(item.toUser()) 49 | } 50 | 51 | FollowBtnView(followBtn).apply { 52 | render(item.toUser()) 53 | followBtn.setOnClickListener { 54 | followBtnClicksSubject.onNext(this) 55 | } 56 | } 57 | } 58 | } 59 | 60 | override fun getItemCount() = users.size 61 | 62 | fun getLastItemId() = users.map { it.relationshipId }.lastOrNull() 63 | 64 | fun addAll(location: Int, users: List): Boolean { 65 | if (this.users.addAll(location, users)) { 66 | notifyItemRangeInserted(location, users.size) 67 | return true 68 | } 69 | return false 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.Companion.KEY_LIST_TYPE 10 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.Companion.KEY_USER_ID 11 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.ListType.FOLLOWER 12 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.ListType.FOLLOWING 13 | import com.hana053.micropost.pages.relateduserlist.followerlist.FollowerListService 14 | import com.hana053.micropost.pages.relateduserlist.followinglist.FollowingListService 15 | import com.hana053.micropost.shared.followbtn.followBtnModule 16 | import kotlinx.android.synthetic.main.activity_related_user_list.* 17 | 18 | 19 | fun relatedUserListModule() = Kodein.Module { 20 | 21 | bind() with autoScopedSingleton(androidActivityScope) { 22 | RelatedUserListAdapter() 23 | } 24 | 25 | bind() with autoScopedSingleton(androidActivityScope) { 26 | instance().activity_related_user_list 27 | .let { RelatedUserListView(it, instance()) } 28 | } 29 | 30 | bind() with autoScopedSingleton(androidActivityScope) { 31 | RelatedUserListPresenter(instance(), instance(KEY_USER_ID), instance(), instance(), instance(), instance()) 32 | } 33 | 34 | bind() with autoScopedSingleton(androidActivityScope) { 35 | when (instance()) { 36 | FOLLOWER -> FollowerListService(instance(), instance(), instance(), instance()) 37 | FOLLOWING -> FollowingListService(instance(), instance(), instance(), instance()) 38 | } 39 | } 40 | 41 | bind(KEY_USER_ID) with autoScopedSingleton(androidActivityScope) { 42 | extras().getLong(KEY_USER_ID) 43 | } 44 | 45 | bind() with autoScopedSingleton(androidActivityScope) { 46 | extras().getSerializable(KEY_LIST_TYPE) as RelatedUserListActivity.ListType 47 | } 48 | 49 | import(followBtnModule()) 50 | } 51 | 52 | private fun Kodein.extras() = instance().intent.extras 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.service.Navigator 5 | import com.hana053.micropost.shared.followbtn.FollowBtnService 6 | 7 | 8 | class RelatedUserListPresenter( 9 | override val view: RelatedUserListView, 10 | private val userId: Long, 11 | private val relatedUserListService: RelatedUserListService, 12 | private val relatedUserListAdapter: RelatedUserListAdapter, 13 | private val followBtnService: FollowBtnService, 14 | private val navigator: Navigator 15 | ) : Presenter { 16 | 17 | override fun bind() { 18 | 19 | relatedUserListService.listUsers(userId) 20 | .bindToLifecycle() 21 | .withProgressDialog() 22 | .subscribe() 23 | 24 | view.scrollsToBottom 25 | .bindToLifecycle() 26 | .flatMap { 27 | relatedUserListService.listUsers(userId) 28 | .withProgressDialog() 29 | } 30 | .subscribe() 31 | 32 | relatedUserListAdapter.followBtnClicksSubject 33 | .bindToLifecycle() 34 | .flatMap { followBtnService.handleFollowBtnClicks(it) } 35 | .subscribe() 36 | 37 | relatedUserListAdapter.avatarClicksSubject 38 | .bindToLifecycle() 39 | .subscribe { navigator.navigateToUserShow(it.id) } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import com.hana053.micropost.domain.RelatedUser 4 | import rx.Observable 5 | 6 | 7 | interface RelatedUserListService { 8 | fun listUsers(userId: Long): Observable> 9 | fun title(): String 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/RelatedUserListView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist 2 | 3 | import android.support.v7.widget.LinearLayoutManager 4 | import android.view.ViewGroup 5 | import com.hana053.micropost.pages.ViewWrapper 6 | import com.jakewharton.rxbinding.support.v7.widget.RecyclerViewScrollEvent 7 | import com.jakewharton.rxbinding.support.v7.widget.scrollEvents 8 | import kotlinx.android.synthetic.main.activity_related_user_list.view.* 9 | import rx.Observable 10 | 11 | 12 | class RelatedUserListView( 13 | override val content: ViewGroup, 14 | relatedUserListAdapter: RelatedUserListAdapter 15 | ) : ViewWrapper { 16 | 17 | private val listUser = content.list_user 18 | 19 | // Events 20 | val scrollsToBottom: Observable = listUser.scrollEvents() 21 | .filter { !listUser.canScrollVertically(1) } 22 | 23 | init { 24 | listUser.layoutManager = LinearLayoutManager(context()) 25 | listUser.adapter = relatedUserListAdapter 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/followerlist/FollowerListService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist.followerlist 2 | 3 | import android.content.Context 4 | import com.hana053.micropost.R 5 | import com.hana053.micropost.domain.RelatedUser 6 | import com.hana053.micropost.interactor.RelatedUserListInteractor 7 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListAdapter 8 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListService 9 | import com.hana053.micropost.service.HttpErrorHandler 10 | import rx.Observable 11 | import rx.android.schedulers.AndroidSchedulers 12 | import rx.schedulers.Schedulers 13 | 14 | 15 | class FollowerListService( 16 | private val interactor: RelatedUserListInteractor, 17 | private val adapter: RelatedUserListAdapter, 18 | private val httpErrorHandler: HttpErrorHandler, 19 | context: Context 20 | ) : RelatedUserListService { 21 | 22 | private val context = context.applicationContext 23 | 24 | override fun listUsers(userId: Long): Observable> { 25 | val maxId = adapter.getLastItemId() 26 | val itemCount = adapter.itemCount 27 | 28 | return interactor.listFollowers(userId, maxId) 29 | .subscribeOn(Schedulers.newThread()) 30 | .observeOn(AndroidSchedulers.mainThread()) 31 | .doOnNext { adapter.addAll(itemCount, it) } 32 | .doOnError { httpErrorHandler.handleError(it) } 33 | .onErrorResumeNext { Observable.empty() } 34 | } 35 | 36 | override fun title(): String = context.getString(R.string.Followers) 37 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/relateduserlist/followinglist/FollowingListService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.relateduserlist.followinglist 2 | 3 | import android.content.Context 4 | import com.hana053.micropost.R 5 | import com.hana053.micropost.domain.RelatedUser 6 | import com.hana053.micropost.interactor.RelatedUserListInteractor 7 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListAdapter 8 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListService 9 | import com.hana053.micropost.service.HttpErrorHandler 10 | import rx.Observable 11 | import rx.android.schedulers.AndroidSchedulers 12 | import rx.schedulers.Schedulers 13 | 14 | 15 | class FollowingListService( 16 | private val interactor: RelatedUserListInteractor, 17 | private val adapter: RelatedUserListAdapter, 18 | private val httpErrorHandler: HttpErrorHandler, 19 | context: Context 20 | ) : RelatedUserListService { 21 | 22 | private val context = context.applicationContext 23 | 24 | override fun listUsers(userId: Long): Observable> { 25 | val maxId = adapter.getLastItemId() 26 | val itemCount = adapter.itemCount 27 | 28 | return interactor.listFollowings(userId, maxId) 29 | .subscribeOn(Schedulers.newThread()) 30 | .observeOn(AndroidSchedulers.mainThread()) 31 | .doOnNext { adapter.addAll(itemCount, it) } 32 | .doOnError { httpErrorHandler.handleError(it) } 33 | .onErrorResumeNext { Observable.empty() } 34 | } 35 | 36 | override fun title(): String = context.getString(R.string.Followings) 37 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import android.os.Bundle 4 | import com.github.salomonbrys.kodein.KodeinInjector 5 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 6 | import com.github.salomonbrys.kodein.instance 7 | import com.hana053.micropost.R 8 | import com.hana053.micropost.getOverridingModule 9 | import com.hana053.micropost.pages.signup.fullname.SignupFullNameFragment 10 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 11 | 12 | 13 | class SignupActivity : RxAppCompatActivity(), AppCompatActivityInjector { 14 | 15 | override val injector: KodeinInjector = KodeinInjector() 16 | 17 | private val signupNavigator: SignupNavigator by instance() 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_signup) 22 | 23 | initializeInjector() 24 | 25 | if (savedInstanceState == null) { 26 | supportFragmentManager 27 | .beginTransaction() 28 | .replace(R.id.container, SignupFullNameFragment()) 29 | .commit() 30 | } 31 | } 32 | 33 | override fun onDestroy() { 34 | super.onDestroy() 35 | destroyInjector() 36 | } 37 | 38 | override fun onBackPressed() { 39 | signupNavigator.navigateToPrev() 40 | } 41 | 42 | override fun provideOverridingModule() = getOverridingModule() 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.github.salomonbrys.kodein.android.androidActivityScope 5 | import com.github.salomonbrys.kodein.autoScopedSingleton 6 | import com.github.salomonbrys.kodein.bind 7 | import com.github.salomonbrys.kodein.instance 8 | import com.hana053.micropost.pages.signup.email.signupEmailModule 9 | import com.hana053.micropost.pages.signup.fullname.signupFullNameModule 10 | import com.hana053.micropost.pages.signup.password.signupPasswordModule 11 | 12 | fun signupModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | SignupState() 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | SignupNavigatorImpl(instance()) 20 | } 21 | 22 | bind() with autoScopedSingleton(androidActivityScope) { 23 | SignupServiceImpl(instance(), instance(), instance(), instance(), instance()) 24 | } 25 | 26 | import(signupFullNameModule()) 27 | import(signupEmailModule()) 28 | import(signupPasswordModule()) 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | 4 | interface SignupNavigator { 5 | 6 | fun navigateToEmail() 7 | fun navigateToPassword() 8 | fun navigateToPrev() 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupNavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import android.support.v4.app.FragmentActivity 4 | import com.hana053.micropost.R 5 | import com.hana053.micropost.pages.signup.email.SignupEmailFragment 6 | import com.hana053.micropost.pages.signup.password.SignupPasswordFragment 7 | 8 | 9 | class SignupNavigatorImpl( 10 | private val activity: FragmentActivity 11 | ) : SignupNavigator { 12 | 13 | override fun navigateToEmail() { 14 | activity.supportFragmentManager 15 | .beginTransaction() 16 | .replace(R.id.container, SignupEmailFragment()) 17 | .addToBackStack(null) 18 | .commit() 19 | } 20 | 21 | override fun navigateToPassword() { 22 | activity.supportFragmentManager 23 | .beginTransaction() 24 | .replace(R.id.container, SignupPasswordFragment()) 25 | .addToBackStack(null) 26 | .commit() 27 | } 28 | 29 | override fun navigateToPrev() { 30 | activity.supportFragmentManager.backStackEntryCount.let { 31 | when (it) { 32 | 0 -> activity.finish() 33 | else -> activity.supportFragmentManager.popBackStack() 34 | } 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import com.hana053.micropost.interactor.UserInteractor 4 | import rx.Observable 5 | 6 | 7 | interface SignupService { 8 | 9 | fun signup(request: UserInteractor.SignupRequest): Observable 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import com.hana053.micropost.interactor.UserInteractor 6 | import com.hana053.micropost.service.HttpErrorHandler 7 | import com.hana053.micropost.service.LoginService 8 | import retrofit2.HttpException 9 | import rx.Observable 10 | import rx.android.schedulers.AndroidSchedulers 11 | import rx.schedulers.Schedulers 12 | 13 | 14 | class SignupServiceImpl( 15 | private val userInteractor: UserInteractor, 16 | private val loginService: LoginService, 17 | private val signupNavigator: SignupNavigator, 18 | private val httpErrorHandler: HttpErrorHandler, 19 | context: Context 20 | ) : SignupService { 21 | 22 | private val context = context.applicationContext 23 | 24 | override fun signup(request: UserInteractor.SignupRequest): Observable = 25 | userInteractor.create(request) 26 | .subscribeOn(Schedulers.newThread()) 27 | .observeOn(AndroidSchedulers.mainThread()) 28 | .doOnError { 29 | when { 30 | isEmailAlreadyTaken(it) -> { 31 | Toast.makeText(context, "This email is already taken.", Toast.LENGTH_LONG).show() 32 | signupNavigator.navigateToPrev() 33 | } 34 | else -> httpErrorHandler.handleError(it) 35 | } 36 | } 37 | .onErrorResumeNext { Observable.empty() } 38 | .flatMap { 39 | loginService.login(request.email, request.password) 40 | } 41 | 42 | private fun isEmailAlreadyTaken(e: Throwable) = e is HttpException && e.code() == 400 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/SignupState.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup 2 | 3 | import com.hana053.micropost.interactor.UserInteractor 4 | 5 | 6 | data class SignupState( 7 | var fullName: String = "", 8 | var email: String = "", 9 | var password: String = "" 10 | ) { 11 | 12 | fun toSignupRequest() = UserInteractor.SignupRequest(fullName, email, password) 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/email/SignupEmailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.email 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.github.salomonbrys.kodein.KodeinInjector 8 | import com.github.salomonbrys.kodein.android.SupportFragmentInjector 9 | import com.github.salomonbrys.kodein.instance 10 | import com.hana053.micropost.R 11 | import com.trello.rxlifecycle.components.support.RxFragment 12 | 13 | 14 | class SignupEmailFragment : RxFragment(), SupportFragmentInjector { 15 | 16 | override val injector: KodeinInjector = KodeinInjector() 17 | 18 | private val presenter: SignupEmailPresenter by instance() 19 | 20 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 21 | inflater.inflate(R.layout.fragment_signup_email, container, false) 22 | 23 | override fun onActivityCreated(savedInstanceState: Bundle?) { 24 | super.onActivityCreated(savedInstanceState) 25 | initializeInjector() 26 | 27 | presenter.bind() 28 | } 29 | 30 | override fun onDestroyView() { 31 | super.onDestroyView() 32 | destroyInjector() 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/email/SignupEmailPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.email 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.pages.signup.SignupNavigator 5 | import com.hana053.micropost.pages.signup.SignupState 6 | 7 | 8 | class SignupEmailPresenter( 9 | override val view: SignupEmailView, 10 | private val signupState: SignupState, 11 | private val signupNavigator: SignupNavigator 12 | ) : Presenter { 13 | 14 | override fun bind() { 15 | val emailChanges = view.emailChanges.share() 16 | 17 | emailChanges 18 | .bindToLifecycle() 19 | .map { isFormValid(it) } 20 | .subscribe { view.nextBtnEnabled.call(it) } 21 | 22 | emailChanges 23 | .bindToLifecycle() 24 | .map { !isFormValid(it) && it.isNotBlank() } 25 | .subscribe { view.emailInvalidVisibility.call(it) } 26 | 27 | view.nextBtnClicks 28 | .bindToLifecycle() 29 | .withLatestFrom(emailChanges, { click, email -> email }) 30 | .subscribe { 31 | signupState.email = it.toString() 32 | signupNavigator.navigateToPassword() 33 | } 34 | } 35 | 36 | private fun isFormValid(email: CharSequence) = 37 | "^[^0-9][a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)*@\\[?([\\d\\w\\.-]+)]?$" 38 | .let(String::toRegex) 39 | .let { email.matches(it) } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/email/SignupEmailView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.email 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import com.jakewharton.rxbinding.view.enabled 7 | import com.jakewharton.rxbinding.view.visibility 8 | import com.jakewharton.rxbinding.widget.textChanges 9 | import kotlinx.android.synthetic.main.fragment_signup_email.view.* 10 | 11 | 12 | class SignupEmailView(override val content: ViewGroup) : ViewWrapper { 13 | 14 | // Events 15 | val emailChanges = content.et_email.textChanges() 16 | val nextBtnClicks = content.btn_next.clicks() 17 | 18 | // Props 19 | val nextBtnEnabled = content.btn_next.enabled() 20 | val emailInvalidVisibility = content.tv_email_invalid.visibility() 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/email/SignupFullNameModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.email 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.fragment_signup_email.* 10 | 11 | 12 | fun signupEmailModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | SignupEmailPresenter(instance(), instance(), instance()) 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | instance().fragment_signup_email.let(::SignupEmailView) 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/fullname/SignupFullNameFragment.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.fullname 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.github.salomonbrys.kodein.KodeinInjector 8 | import com.github.salomonbrys.kodein.android.SupportFragmentInjector 9 | import com.github.salomonbrys.kodein.instance 10 | import com.hana053.micropost.R 11 | import com.trello.rxlifecycle.components.support.RxFragment 12 | 13 | 14 | class SignupFullNameFragment : RxFragment(), SupportFragmentInjector { 15 | 16 | override val injector: KodeinInjector = KodeinInjector() 17 | 18 | private val presenter: SignupFullNamePresenter by instance() 19 | 20 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 21 | inflater.inflate(R.layout.fragment_signup_full_name, container, false) 22 | 23 | override fun onActivityCreated(savedInstanceState: Bundle?) { 24 | super.onActivityCreated(savedInstanceState) 25 | initializeInjector() 26 | 27 | presenter.bind() 28 | } 29 | 30 | override fun onDestroy() { 31 | super.onDestroy() 32 | destroyInjector() 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/fullname/SignupFullNameModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.fullname 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.fragment_signup_full_name.* 10 | 11 | 12 | fun signupFullNameModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | SignupFullNamePresenter(instance(), instance(), instance()) 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | instance().fragment_signup_full_name.let(::SignupFullNameView) 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/fullname/SignupFullNamePresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.fullname 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.pages.signup.SignupNavigator 5 | import com.hana053.micropost.pages.signup.SignupState 6 | 7 | 8 | class SignupFullNamePresenter( 9 | override val view: SignupFullNameView, 10 | private val signupState: SignupState, 11 | private val signupNavigator: SignupNavigator 12 | ) : Presenter { 13 | 14 | override fun bind() { 15 | val fullNameChanges = view.fullNameChanges.share() 16 | 17 | fullNameChanges 18 | .bindToLifecycle() 19 | .map { isFormValid(it) } 20 | .subscribe { view.nextBtnEnabled.call(it) } 21 | 22 | fullNameChanges 23 | .bindToLifecycle() 24 | .map { !isFormValid(it) && it.isNotBlank() } 25 | .subscribe { view.fullNameInvalidVisibility.call(it) } 26 | 27 | view.nextBtnClicks 28 | .bindToLifecycle() 29 | .withLatestFrom(fullNameChanges, { click, fullName -> fullName }) 30 | .subscribe { 31 | signupState.fullName = it.toString() 32 | signupNavigator.navigateToEmail() 33 | } 34 | } 35 | 36 | private fun isFormValid(fullName: CharSequence) = fullName.length >= 4 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/fullname/SignupFullNameView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.fullname 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import com.jakewharton.rxbinding.view.enabled 7 | import com.jakewharton.rxbinding.view.visibility 8 | import com.jakewharton.rxbinding.widget.textChanges 9 | import kotlinx.android.synthetic.main.fragment_signup_full_name.view.* 10 | 11 | 12 | class SignupFullNameView(override val content: ViewGroup) : ViewWrapper { 13 | 14 | // Events 15 | val fullNameChanges = content.et_full_name.textChanges() 16 | val nextBtnClicks = content.btn_next.clicks() 17 | 18 | // Props 19 | val nextBtnEnabled = content.btn_next.enabled() 20 | val fullNameInvalidVisibility = content.tv_full_name_invalid.visibility() 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/password/SignupPasswordFragment.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.password 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.github.salomonbrys.kodein.KodeinInjector 8 | import com.github.salomonbrys.kodein.android.SupportFragmentInjector 9 | import com.github.salomonbrys.kodein.instance 10 | import com.hana053.micropost.R 11 | import com.trello.rxlifecycle.components.support.RxFragment 12 | 13 | 14 | class SignupPasswordFragment : RxFragment(), SupportFragmentInjector { 15 | 16 | override val injector: KodeinInjector = KodeinInjector() 17 | 18 | private val presenter: SignupPasswordPresenter by instance() 19 | 20 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 21 | inflater.inflate(R.layout.fragment_signup_password, container, false) 22 | 23 | override fun onActivityCreated(savedInstanceState: Bundle?) { 24 | super.onActivityCreated(savedInstanceState) 25 | initializeInjector() 26 | 27 | presenter.bind() 28 | } 29 | 30 | override fun onDestroyView() { 31 | super.onDestroyView() 32 | destroyInjector() 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/password/SignupPasswordModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.password 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.fragment_signup_password.* 10 | 11 | 12 | fun signupPasswordModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | SignupPasswordPresenter(instance(), instance(), instance(), instance()) 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | instance().fragment_signup_password.let(::SignupPasswordView) 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/password/SignupPasswordPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.password 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.pages.signup.SignupService 5 | import com.hana053.micropost.pages.signup.SignupState 6 | import com.hana053.micropost.service.Navigator 7 | 8 | 9 | class SignupPasswordPresenter( 10 | override val view: SignupPasswordView, 11 | private val signupState: SignupState, 12 | private val signupService: SignupService, 13 | private val navigator: Navigator 14 | ) : Presenter { 15 | 16 | override fun bind() { 17 | val passwordChanges = view.passwordChanges.share() 18 | val nextBtnClicks = view.nextBtnClicks.share() 19 | 20 | passwordChanges 21 | .bindToLifecycle() 22 | .map { isFormValid(it) } 23 | .subscribe { view.nextBtnEnabled.call(it) } 24 | 25 | passwordChanges 26 | .bindToLifecycle() 27 | .map { !isFormValid(it) && it.isNotBlank() } 28 | .subscribe { view.passwordInvalidVisibility.call(it) } 29 | 30 | nextBtnClicks 31 | .bindToLifecycle() 32 | .withLatestFrom(passwordChanges, { click, password -> password }) 33 | .map { 34 | signupState.password = it.toString() 35 | signupState.toSignupRequest() 36 | } 37 | .flatMap { 38 | signupService.signup(it) 39 | .withProgressDialog() 40 | } 41 | .subscribe { navigator.navigateToMain() } 42 | } 43 | 44 | private fun isFormValid(password: CharSequence): Boolean = password.length >= 8 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/signup/password/SignupPasswordView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.signup.password 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import com.jakewharton.rxbinding.view.enabled 7 | import com.jakewharton.rxbinding.view.visibility 8 | import com.jakewharton.rxbinding.widget.textChanges 9 | import kotlinx.android.synthetic.main.fragment_signup_password.view.* 10 | 11 | 12 | class SignupPasswordView(override val content: ViewGroup) : ViewWrapper { 13 | 14 | // Events 15 | val passwordChanges = content.et_password.textChanges() 16 | val nextBtnClicks = content.btn_next.clicks() 17 | 18 | // Props 19 | val nextBtnEnabled = content.btn_next.enabled() 20 | val passwordInvalidVisibility = content.tv_password_invalid.visibility() 21 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/top/TopActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.top 2 | 3 | import android.os.Bundle 4 | import com.github.salomonbrys.kodein.KodeinInjector 5 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 6 | import com.github.salomonbrys.kodein.instance 7 | import com.hana053.micropost.R 8 | import com.hana053.micropost.getOverridingModule 9 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 10 | 11 | 12 | class TopActivity : RxAppCompatActivity(), AppCompatActivityInjector { 13 | 14 | override val injector: KodeinInjector = KodeinInjector() 15 | 16 | private val presenter: TopPresenter by instance() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.activity_top) 21 | initializeInjector() 22 | 23 | presenter.bind() 24 | } 25 | 26 | override fun onDestroy() { 27 | super.onDestroy() 28 | destroyInjector() 29 | } 30 | 31 | override fun provideOverridingModule() = getOverridingModule() 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/top/TopModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.top 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import kotlinx.android.synthetic.main.activity_top.* 10 | 11 | fun topModule() = Kodein.Module { 12 | 13 | bind() with autoScopedSingleton(androidActivityScope) { 14 | TopPresenter(instance(), instance()) 15 | } 16 | 17 | bind() with autoScopedSingleton(androidActivityScope) { 18 | instance().activity_top.let(::TopView) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/top/TopPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.top 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | import com.hana053.micropost.service.Navigator 5 | 6 | 7 | class TopPresenter( 8 | override val view: TopView, 9 | private val navigator: Navigator 10 | ) : Presenter { 11 | 12 | override fun bind() { 13 | view.loginClicks 14 | .bindToLifecycle() 15 | .subscribe { navigator.navigateToLogin() } 16 | 17 | view.signupClicks 18 | .bindToLifecycle() 19 | .subscribe { navigator.navigateToSignup() } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/top/TopView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.top 2 | 3 | import android.view.ViewGroup 4 | import com.hana053.micropost.pages.ViewWrapper 5 | import com.jakewharton.rxbinding.view.clicks 6 | import kotlinx.android.synthetic.main.activity_top.view.* 7 | 8 | class TopView(override val content: ViewGroup) : ViewWrapper { 9 | 10 | val signupClicks = content.btn_signup.clicks() 11 | val loginClicks = content.btn_login.clicks() 12 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/UserShowActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import com.github.salomonbrys.kodein.KodeinInjector 6 | import com.github.salomonbrys.kodein.android.AppCompatActivityInjector 7 | import com.github.salomonbrys.kodein.instance 8 | import com.hana053.micropost.R 9 | import com.hana053.micropost.getOverridingModule 10 | import com.hana053.micropost.pages.usershow.detail.UserShowDetailPresenter 11 | import com.hana053.micropost.pages.usershow.posts.UserShowPostsPresenter 12 | import com.hana053.micropost.service.AuthService 13 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 14 | 15 | class UserShowActivity : RxAppCompatActivity(), AppCompatActivityInjector { 16 | 17 | override val injector: KodeinInjector = KodeinInjector() 18 | 19 | private val authService: AuthService by instance() 20 | private val detailPresenter: UserShowDetailPresenter by instance() 21 | private val postsPresenter: UserShowPostsPresenter by instance() 22 | 23 | companion object { 24 | val KEY_USER_ID = "KEY_USER_ID" 25 | } 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | setContentView(R.layout.activity_user_show) 30 | initializeInjector() 31 | 32 | if (!authService.auth()) return 33 | 34 | detailPresenter.bind() 35 | postsPresenter.bind() 36 | 37 | supportActionBar!!.setDisplayShowTitleEnabled(false) 38 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 39 | } 40 | 41 | override fun onDestroy() { 42 | super.onDestroy() 43 | destroyInjector() 44 | } 45 | 46 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 47 | when (item.itemId) { 48 | android.R.id.home -> { 49 | finish() 50 | return true 51 | } 52 | } 53 | return super.onOptionsItemSelected(item) 54 | } 55 | 56 | override fun provideOverridingModule() = getOverridingModule() 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/UserShowModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import com.hana053.micropost.pages.usershow.UserShowActivity.Companion.KEY_USER_ID 10 | import com.hana053.micropost.pages.usershow.detail.userShowDetailModule 11 | import com.hana053.micropost.pages.usershow.posts.userShowPostsModule 12 | import com.hana053.micropost.shared.followbtn.followBtnModule 13 | 14 | fun userShowModule() = Kodein.Module { 15 | 16 | bind(KEY_USER_ID) with autoScopedSingleton(androidActivityScope) { 17 | instance().intent.extras.getLong(KEY_USER_ID) 18 | } 19 | 20 | import(userShowDetailModule()) 21 | import(userShowPostsModule()) 22 | import(followBtnModule()) 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/detail/UserShowDetailModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.detail 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import com.hana053.micropost.pages.usershow.UserShowActivity.Companion.KEY_USER_ID 10 | import kotlinx.android.synthetic.main._user_detail.* 11 | 12 | fun userShowDetailModule() = Kodein.Module { 13 | 14 | bind() with autoScopedSingleton(androidActivityScope) { 15 | UserShowDetailService(instance(), instance()) 16 | } 17 | 18 | bind() with autoScopedSingleton(androidActivityScope) { 19 | instance()._user_detail.let(::UserShowDetailView) 20 | } 21 | 22 | bind() with autoScopedSingleton(androidActivityScope) { 23 | UserShowDetailPresenter(instance(), instance(KEY_USER_ID), instance(), instance(), instance()) 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/detail/UserShowDetailPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.detail 2 | 3 | import com.hana053.micropost.domain.User 4 | import com.hana053.micropost.pages.Presenter 5 | import com.hana053.micropost.service.Navigator 6 | import com.hana053.micropost.shared.followbtn.FollowBtnService 7 | import rx.Observable 8 | 9 | 10 | class UserShowDetailPresenter( 11 | override val view: UserShowDetailView, 12 | private val userId: Long, 13 | private val detailService: UserShowDetailService, 14 | private val followBtnService: FollowBtnService, 15 | private val navigator: Navigator 16 | ) : Presenter { 17 | 18 | override fun bind() { 19 | getUser() 20 | .bindToLifecycle() 21 | .subscribe() 22 | 23 | view.followClicks 24 | .bindToLifecycle() 25 | .flatMap { followBtnService.handleFollowBtnClicks(it) } 26 | .flatMap { getUser() } 27 | .subscribe() 28 | 29 | view.followersClicks 30 | .bindToLifecycle() 31 | .subscribe { navigator.navigateToFollowerList(userId) } 32 | 33 | view.followingsClicks 34 | .bindToLifecycle() 35 | .subscribe { navigator.navigateToFollowingList(userId) } 36 | } 37 | 38 | private fun getUser(): Observable { 39 | return detailService.getUser(userId) 40 | .withProgressDialog() 41 | .doOnNext { view.render(it) } 42 | } 43 | 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/detail/UserShowDetailService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.detail 2 | 3 | import com.hana053.micropost.domain.User 4 | import com.hana053.micropost.interactor.UserInteractor 5 | import com.hana053.micropost.service.HttpErrorHandler 6 | import rx.Observable 7 | import rx.android.schedulers.AndroidSchedulers 8 | import rx.schedulers.Schedulers 9 | 10 | 11 | class UserShowDetailService( 12 | private val userInteractor: UserInteractor, 13 | private val httpErrorHandler: HttpErrorHandler 14 | ) { 15 | 16 | fun getUser(userId: Long): Observable = 17 | userInteractor.get(userId) 18 | .subscribeOn(Schedulers.newThread()) 19 | .observeOn(AndroidSchedulers.mainThread()) 20 | .doOnError { httpErrorHandler.handleError(it) } 21 | .onErrorResumeNext { Observable.empty() } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/detail/UserShowDetailView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.detail 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.ViewGroup 5 | import com.hana053.micropost.R 6 | import com.hana053.micropost.domain.User 7 | import com.hana053.micropost.pages.ViewWrapper 8 | import com.hana053.micropost.shared.avatar.AvatarView 9 | import com.hana053.micropost.shared.followbtn.FollowBtnView 10 | import com.jakewharton.rxbinding.view.clicks 11 | import kotlinx.android.synthetic.main._user_detail.view.* 12 | 13 | class UserShowDetailView( 14 | override val content: ViewGroup 15 | ) : ViewWrapper { 16 | 17 | private val userName = content.tv_user_name 18 | private val followers = content.tv_followers 19 | private val followings = content.tv_followings 20 | 21 | // Sub View 22 | private val followBtnView = FollowBtnView(content.btn_follow) 23 | private val avatarView = AvatarView(content.img_avatar) 24 | 25 | // Events 26 | val followersClicks = followers.clicks() 27 | val followingsClicks = followings.clicks() 28 | val followClicks = followBtnView.clicks() 29 | 30 | @SuppressLint("SetTextI18n") 31 | fun render(user: User) { 32 | followBtnView.render(user) 33 | avatarView.render(user) 34 | userName.text = user.name 35 | followers.text = "${user.userStats.followerCnt} ${context().getString(R.string.followers)}" 36 | followings.text = "${user.userStats.followingCnt} ${context().getString(R.string.followings)}" 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/posts/UserShowPostsModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.posts 2 | 3 | import android.app.Activity 4 | import com.github.salomonbrys.kodein.Kodein 5 | import com.github.salomonbrys.kodein.android.androidActivityScope 6 | import com.github.salomonbrys.kodein.autoScopedSingleton 7 | import com.github.salomonbrys.kodein.bind 8 | import com.github.salomonbrys.kodein.instance 9 | import com.hana053.micropost.pages.usershow.UserShowActivity.Companion.KEY_USER_ID 10 | import com.hana053.micropost.shared.posts.PostListAdapter 11 | import kotlinx.android.synthetic.main._user_posts.* 12 | 13 | fun userShowPostsModule() = Kodein.Module { 14 | 15 | bind() with autoScopedSingleton(androidActivityScope) { 16 | PostListAdapter() 17 | } 18 | 19 | bind() with autoScopedSingleton(androidActivityScope) { 20 | UserShowPostsService(instance(), instance(), instance()) 21 | } 22 | 23 | bind() with autoScopedSingleton(androidActivityScope) { 24 | instance()._user_posts 25 | .let { UserShowPostsView(it, instance()) } 26 | } 27 | 28 | bind() with autoScopedSingleton(androidActivityScope) { 29 | UserShowPostsPresenter(instance(), instance(KEY_USER_ID), instance()) 30 | } 31 | 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/posts/UserShowPostsPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.posts 2 | 3 | import com.hana053.micropost.pages.Presenter 4 | 5 | 6 | class UserShowPostsPresenter( 7 | override val view: UserShowPostsView, 8 | private val userId: Long, 9 | private val service: UserShowPostsService 10 | ) : Presenter { 11 | 12 | override fun bind() { 13 | service.loadPosts(userId) 14 | .bindToLifecycle() 15 | .withProgressDialog() 16 | .subscribe() 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/posts/UserShowPostsService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.posts 2 | 3 | import com.hana053.micropost.domain.Micropost 4 | import com.hana053.micropost.interactor.UserMicropostInteractor 5 | import com.hana053.micropost.service.HttpErrorHandler 6 | import com.hana053.micropost.shared.posts.PostListAdapter 7 | import rx.Observable 8 | import rx.android.schedulers.AndroidSchedulers 9 | import rx.schedulers.Schedulers 10 | 11 | 12 | class UserShowPostsService( 13 | private val postListAdapter: PostListAdapter, 14 | private val userMicropostInteractor: UserMicropostInteractor, 15 | private val httpErrorHandler: HttpErrorHandler 16 | ) { 17 | 18 | fun loadPosts(userId: Long): Observable> { 19 | val maxId = postListAdapter.getLastItemId() 20 | val itemCount = postListAdapter.itemCount 21 | 22 | return userMicropostInteractor.loadPrevPosts(userId, maxId) 23 | .subscribeOn(Schedulers.newThread()) 24 | .observeOn(AndroidSchedulers.mainThread()) 25 | .doOnNext { postListAdapter.addAll(itemCount, it) } 26 | .doOnError { httpErrorHandler.handleError(it) } 27 | .onErrorResumeNext { Observable.empty() } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/pages/usershow/posts/UserShowPostsView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.pages.usershow.posts 2 | 3 | import android.support.v7.widget.LinearLayoutManager 4 | import android.view.ViewGroup 5 | import com.hana053.micropost.pages.ViewWrapper 6 | import com.hana053.micropost.shared.posts.PostListAdapter 7 | import kotlinx.android.synthetic.main._user_posts.view.* 8 | 9 | 10 | class UserShowPostsView( 11 | override val content: ViewGroup, 12 | postListAdapter: PostListAdapter 13 | ) : ViewWrapper { 14 | 15 | private val listPost = content.list_post 16 | 17 | init { 18 | with(listPost) { 19 | layoutManager = LinearLayoutManager(context()) 20 | adapter = postListAdapter 21 | isNestedScrollingEnabled = false 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/repository/AuthTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.repository 2 | 3 | 4 | interface AuthTokenRepository { 5 | fun get(): String? 6 | fun set(authToken: String) 7 | fun clear() 8 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/repository/AuthTokenRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.repository 2 | 3 | import android.content.SharedPreferences 4 | 5 | class AuthTokenRepositoryImpl( 6 | private val sharedPreferences: SharedPreferences 7 | ) : AuthTokenRepository { 8 | 9 | private val AUTH_TOKEN = "AUTH_TOKEN" 10 | 11 | override fun get(): String? = 12 | sharedPreferences.getString(AUTH_TOKEN, null) 13 | 14 | override fun set(authToken: String) = 15 | sharedPreferences.edit() 16 | .putString(AUTH_TOKEN, authToken) 17 | .apply() 18 | 19 | override fun clear() = 20 | sharedPreferences.edit() 21 | .putString(AUTH_TOKEN, null) 22 | .apply() 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/repository/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.repository 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.github.salomonbrys.kodein.bind 5 | import com.github.salomonbrys.kodein.instance 6 | import com.github.salomonbrys.kodein.singleton 7 | 8 | 9 | fun repositoryModule() = Kodein.Module { 10 | 11 | bind() with singleton { 12 | AuthTokenRepositoryImpl(instance()) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/AuthService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import com.hana053.micropost.domain.User 4 | 5 | 6 | interface AuthService { 7 | fun isMyself(user: User): Boolean 8 | fun logout() 9 | fun auth(): Boolean 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/AuthServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import com.hana053.micropost.domain.User 4 | import com.hana053.micropost.repository.AuthTokenRepository 5 | import com.nimbusds.jose.JWSObject 6 | 7 | 8 | class AuthServiceImpl( 9 | private val authTokenRepository: AuthTokenRepository, 10 | private val navigator: Navigator 11 | ) : AuthService { 12 | 13 | override fun isMyself(user: User): Boolean { 14 | val authToken = authTokenRepository.get() ?: return false 15 | return user.id == JWSObject.parse(authToken) 16 | .payload 17 | .toJSONObject()["sub"] 18 | .toString() 19 | .toLong() 20 | 21 | } 22 | 23 | override fun logout() { 24 | authTokenRepository.clear() 25 | navigator.navigateToTop() 26 | } 27 | 28 | override fun auth(): Boolean { 29 | if (authTokenRepository.get().isNullOrBlank()) { 30 | logout() 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/HttpErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | interface HttpErrorHandler { 4 | 5 | fun handleError(throwable: Throwable) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/HttpErrorHandlerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import retrofit2.HttpException 6 | import timber.log.Timber 7 | import java.net.ConnectException 8 | import java.net.SocketTimeoutException 9 | 10 | internal class HttpErrorHandlerImpl( 11 | private val authService: AuthService, 12 | context: Context 13 | ) : HttpErrorHandler { 14 | 15 | private val context: Context = context.applicationContext 16 | 17 | override fun handleError(throwable: Throwable) { 18 | when (throwable) { 19 | is SocketTimeoutException -> showToast("Connection timed out.") 20 | is ConnectException -> showToast("Cannot connect to server.") 21 | is HttpException -> { 22 | when { 23 | (throwable.code() == 401) -> { 24 | showToast("Please sign in.") 25 | authService.logout() 26 | } 27 | (throwable.code() >= 500) -> showToast("Something bad happened.") 28 | } 29 | } 30 | is Throwable -> { 31 | Timber.e(throwable, "handleHttpError: ${throwable.message}") 32 | showToast("Something bad happened.") 33 | } 34 | } 35 | } 36 | 37 | private fun showToast(msg: String) { 38 | Toast.makeText(context, msg, Toast.LENGTH_LONG).show() 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/LoginService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import rx.Observable 4 | 5 | 6 | interface LoginService { 7 | 8 | fun login(email: String, password: String): Observable 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/LoginServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import com.hana053.micropost.interactor.LoginInteractor 6 | import com.hana053.micropost.repository.AuthTokenRepository 7 | import retrofit2.HttpException 8 | import rx.Observable 9 | import rx.android.schedulers.AndroidSchedulers 10 | import rx.schedulers.Schedulers 11 | 12 | 13 | class LoginServiceImpl( 14 | private val loginInteractor: LoginInteractor, 15 | private val authTokenRepository: AuthTokenRepository, 16 | private val httpErrorHandler: HttpErrorHandler, 17 | context: Context 18 | ) : LoginService { 19 | 20 | private val context: Context = context.applicationContext 21 | 22 | override fun login(email: String, password: String): Observable { 23 | return loginInteractor 24 | .login(LoginInteractor.LoginRequest(email, password)) 25 | .subscribeOn(Schedulers.newThread()) 26 | .observeOn(AndroidSchedulers.mainThread()) 27 | .doOnNext { authTokenRepository.set(it.token) } 28 | .doOnError { 29 | when { 30 | it is HttpException && it.code() == 401 -> 31 | Toast.makeText(context, "Email or Password is wrong.", Toast.LENGTH_LONG).show() 32 | else -> httpErrorHandler.handleError(it) 33 | } 34 | } 35 | .onErrorResumeNext(Observable.empty()) 36 | .map { null } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/Navigator.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | interface Navigator { 4 | 5 | fun navigateToTop() 6 | fun navigateToMain() 7 | fun navigateToLogin() 8 | fun navigateToSignup() 9 | fun navigateToUserShow(userId: Long) 10 | fun navigateToFollowerList(userId: Long) 11 | fun navigateToFollowingList(userId: Long) 12 | fun navigateToMicropostNew() 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/NavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import com.hana053.micropost.R 6 | import com.hana053.micropost.pages.login.LoginActivity 7 | import com.hana053.micropost.pages.main.MainActivity 8 | import com.hana053.micropost.pages.main.MainActivity.Companion.REQUEST_POST 9 | import com.hana053.micropost.pages.micropostnew.MicropostNewActivity 10 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity 11 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.ListType.FOLLOWER 12 | import com.hana053.micropost.pages.relateduserlist.RelatedUserListActivity.ListType.FOLLOWING 13 | import com.hana053.micropost.pages.signup.SignupActivity 14 | import com.hana053.micropost.pages.top.TopActivity 15 | import com.hana053.micropost.pages.usershow.UserShowActivity 16 | 17 | class NavigatorImpl(private val activity: Activity) : Navigator { 18 | 19 | override fun navigateToTop() { 20 | val intent = Intent(activity, TopActivity::class.java) 21 | activity.finishAffinity() 22 | activity.startActivity(intent) 23 | } 24 | 25 | override fun navigateToMain() { 26 | val intent = Intent(activity, MainActivity::class.java) 27 | activity.finishAffinity() 28 | activity.startActivity(intent) 29 | } 30 | 31 | override fun navigateToLogin() { 32 | val intent = Intent(activity, LoginActivity::class.java) 33 | activity.startActivity(intent) 34 | } 35 | 36 | override fun navigateToSignup() { 37 | val intent = Intent(activity, SignupActivity::class.java) 38 | activity.startActivity(intent) 39 | } 40 | 41 | override fun navigateToUserShow(userId: Long) { 42 | val intent = Intent(activity, UserShowActivity::class.java) 43 | intent.putExtra(UserShowActivity.KEY_USER_ID, userId) 44 | activity.startActivity(intent) 45 | } 46 | 47 | override fun navigateToFollowerList(userId: Long) { 48 | val intent = Intent(activity, RelatedUserListActivity::class.java) 49 | intent.putExtra(RelatedUserListActivity.KEY_USER_ID, userId) 50 | intent.putExtra(RelatedUserListActivity.KEY_LIST_TYPE, FOLLOWER) 51 | activity.startActivity(intent) 52 | } 53 | 54 | override fun navigateToFollowingList(userId: Long) { 55 | val intent = Intent(activity, RelatedUserListActivity::class.java) 56 | intent.putExtra(RelatedUserListActivity.KEY_USER_ID, userId) 57 | intent.putExtra(RelatedUserListActivity.KEY_LIST_TYPE, FOLLOWING) 58 | activity.startActivity(intent) 59 | } 60 | 61 | override fun navigateToMicropostNew() { 62 | val intent = Intent(activity, MicropostNewActivity::class.java) 63 | activity.startActivityForResult(intent, REQUEST_POST) 64 | activity.overridePendingTransition(R.anim.slide_in_up, 0) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/service/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.service 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.github.salomonbrys.kodein.android.androidActivityScope 5 | import com.github.salomonbrys.kodein.autoScopedSingleton 6 | import com.github.salomonbrys.kodein.bind 7 | import com.github.salomonbrys.kodein.instance 8 | 9 | fun serviceModule() = Kodein.Module { 10 | 11 | bind() with autoScopedSingleton(androidActivityScope) { 12 | NavigatorImpl(instance()) 13 | } 14 | 15 | bind() with autoScopedSingleton(androidActivityScope) { 16 | HttpErrorHandlerImpl(instance(), instance()) 17 | } 18 | 19 | bind() with autoScopedSingleton(androidActivityScope) { 20 | AuthServiceImpl(instance(), instance()) 21 | } 22 | 23 | bind() with autoScopedSingleton(androidActivityScope) { 24 | LoginServiceImpl(instance(), instance(), instance(), instance()) 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/shared/avatar/AvatarView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.shared.avatar 2 | 3 | import android.widget.ImageView 4 | import com.hana053.micropost.domain.User 5 | import com.squareup.picasso.Picasso 6 | 7 | 8 | class AvatarView( 9 | private val content: ImageView, 10 | private val size: Int = 96 11 | 12 | ) { 13 | 14 | fun render(user: User) { 15 | Picasso.with(content.context) 16 | .load(user.avatarUrl(size)) 17 | .into(content) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/shared/followbtn/FollowBtnModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.shared.followbtn 2 | 3 | import com.github.salomonbrys.kodein.Kodein 4 | import com.github.salomonbrys.kodein.android.androidActivityScope 5 | import com.github.salomonbrys.kodein.autoScopedSingleton 6 | import com.github.salomonbrys.kodein.bind 7 | import com.github.salomonbrys.kodein.instance 8 | 9 | fun followBtnModule() = Kodein.Module { 10 | 11 | bind() with autoScopedSingleton(androidActivityScope) { 12 | FollowBtnService(instance(), instance()) 13 | } 14 | 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/shared/followbtn/FollowBtnService.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.shared.followbtn 2 | 3 | import com.hana053.micropost.domain.User 4 | import com.hana053.micropost.interactor.RelationshipInteractor 5 | import com.hana053.micropost.service.HttpErrorHandler 6 | import rx.Observable 7 | import rx.android.schedulers.AndroidSchedulers 8 | import rx.functions.Action1 9 | import rx.schedulers.Schedulers 10 | 11 | 12 | class FollowBtnService( 13 | private val relationshipInteractor: RelationshipInteractor, 14 | private val httpErrorHandler: HttpErrorHandler 15 | ) { 16 | fun handleFollowBtnClicks(view: FollowBtnView): Observable { 17 | val obs = if (view.isFollowState()) follow(view) else unfollow(view) 18 | return obs.withBtnDisabled(view.enabled) 19 | .doOnError { httpErrorHandler.handleError(it) } 20 | .onErrorResumeNext { Observable.empty() } 21 | .map { view.user } 22 | } 23 | 24 | private fun follow(view: FollowBtnView) = 25 | relationshipInteractor.follow(view.user.id) 26 | .subscribeOn(Schedulers.newThread()) 27 | .observeOn(AndroidSchedulers.mainThread()) 28 | .doOnNext { view.toUnfollow() } 29 | 30 | private fun unfollow(view: FollowBtnView) = 31 | relationshipInteractor.unfollow(view.user.id) 32 | .subscribeOn(Schedulers.newThread()) 33 | .observeOn(AndroidSchedulers.mainThread()) 34 | .doOnNext { view.toFollow() } 35 | 36 | private fun Observable.withBtnDisabled(enabled: Action1) = 37 | Observable.using({ 38 | enabled.call(false) 39 | }, { this }, { 40 | enabled.call(true) 41 | }) 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/shared/followbtn/FollowBtnView.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.shared.followbtn 2 | 3 | import android.widget.Button 4 | import com.github.salomonbrys.kodein.android.appKodein 5 | import com.github.salomonbrys.kodein.instance 6 | import com.hana053.micropost.R 7 | import com.hana053.micropost.domain.User 8 | import com.hana053.micropost.service.AuthService 9 | import com.jakewharton.rxbinding.view.clicks 10 | import com.jakewharton.rxbinding.view.enabled 11 | import com.jakewharton.rxbinding.view.visibility 12 | import rx.Observable 13 | 14 | 15 | class FollowBtnView( 16 | private val button: Button 17 | ) { 18 | 19 | private val FOLLOW = button.context.getString(R.string.Follow) 20 | private val UNFOLLOW = button.context.getString(R.string.Unfollow) 21 | 22 | // Props 23 | val enabled = button.enabled() 24 | val user: User 25 | get() = _user 26 | 27 | private lateinit var _user: User 28 | 29 | fun render(user: User) { 30 | val authService = button.context.appKodein().instance() 31 | 32 | button.visibility().call(!authService.isMyself(user)) 33 | user.isFollowedByMe?.let { if(it) toUnfollow() else toFollow()} 34 | this._user = user 35 | } 36 | 37 | fun clicks(): Observable = button.clicks().map { this } 38 | 39 | fun toFollow() { 40 | button.text = FOLLOW 41 | } 42 | 43 | fun toUnfollow() { 44 | button.text = UNFOLLOW 45 | } 46 | 47 | fun isFollowState() = button.text == FOLLOW 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/shared/posts/PostListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.shared.posts 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.LinearLayout 9 | import android.widget.TextView 10 | import com.github.curioustechizen.ago.RelativeTimeTextView 11 | import com.hana053.micropost.R 12 | import com.hana053.micropost.domain.Micropost 13 | import com.hana053.micropost.domain.User 14 | import com.hana053.micropost.shared.avatar.AvatarView 15 | import kotlinx.android.synthetic.main.item_posts.view.* 16 | import rx.subjects.PublishSubject 17 | 18 | 19 | class PostListAdapter( 20 | private val posts: MutableList = mutableListOf() 21 | ) : RecyclerView.Adapter() { 22 | 23 | val avatarClicksSubject: PublishSubject = PublishSubject.create() 24 | 25 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 26 | val container: LinearLayout = view.container 27 | val avatar: ImageView = view.img_avatar 28 | val userName: TextView = view.tv_post_user_name 29 | val createdAt: RelativeTimeTextView = view.tv_post_created_at 30 | val content: TextView = view.tv_post_content 31 | } 32 | 33 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 34 | LayoutInflater.from(parent.context) 35 | .inflate(R.layout.item_posts, parent, false) 36 | .let(::ViewHolder) 37 | 38 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 39 | val item = posts[position] 40 | 41 | holder.apply { 42 | container.tag = item 43 | userName.text = item.user.name 44 | createdAt.setReferenceTime(item.createdAt) 45 | content.text = item.content 46 | AvatarView(avatar).render(item.user) 47 | avatar.setOnClickListener { 48 | avatarClicksSubject.onNext(item.user) 49 | } 50 | } 51 | } 52 | 53 | override fun getItemCount(): Int = posts.size 54 | 55 | fun getFirstItemId(): Long? = posts.map { it.id }.firstOrNull() 56 | 57 | fun getLastItemId(): Long? = posts.map { it.id }.lastOrNull() 58 | 59 | fun addAll(location: Int, posts: List): Boolean { 60 | if (this.posts.addAll(location, posts)) { 61 | notifyItemRangeInserted(location, posts.size) 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/hana053/micropost/system/SystemServicesModule.kt: -------------------------------------------------------------------------------- 1 | package com.hana053.micropost.system 2 | 3 | import android.content.SharedPreferences 4 | import android.preference.PreferenceManager 5 | import com.github.salomonbrys.kodein.Kodein 6 | import com.github.salomonbrys.kodein.bind 7 | import com.github.salomonbrys.kodein.instance 8 | import com.github.salomonbrys.kodein.singleton 9 | 10 | 11 | fun systemModule() = Kodein.Module { 12 | 13 | bind() with singleton { 14 | PreferenceManager.getDefaultSharedPreferences(instance()) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/border_bottom_on_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/border_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_create_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springboot-angular2-tutorial/android-app/c609bfdaa867bd20a29fd4f64ab3a2b159e8c023/app/src/main/res/drawable/ic_create_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/layout/_user_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 23 | 24 | 27 | 28 | 37 | 38 | 44 | 45 |