├── .gitignore ├── LICENCE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── consistence │ │ │ └── pinyin │ │ │ ├── PinyinApplication.kt │ │ │ ├── PinyinApplicationComponent.kt │ │ │ ├── ViewModelFactory.kt │ │ │ ├── app │ │ │ ├── EntryActivity.kt │ │ │ ├── EntryActivityModule.kt │ │ │ ├── EntryIntent.kt │ │ │ ├── EntryRenderer.kt │ │ │ ├── EntryViewModel.kt │ │ │ ├── EntryViewState.kt │ │ │ ├── pinyin │ │ │ │ ├── PinyinActivity.kt │ │ │ │ ├── PinyinActivityModule.kt │ │ │ │ ├── PinyinFragmentManager.kt │ │ │ │ ├── PinyinIntent.kt │ │ │ │ ├── PinyinRenderer.kt │ │ │ │ ├── PinyinViewModel.kt │ │ │ │ ├── PinyinViewState.kt │ │ │ │ ├── detail │ │ │ │ │ ├── PinyinDetailActivity.kt │ │ │ │ │ ├── PinyinDetailActivityModule.kt │ │ │ │ │ ├── PinyinDetailIntent.kt │ │ │ │ │ ├── PinyinDetailRenderer.kt │ │ │ │ │ ├── PinyinDetailViewModel.kt │ │ │ │ │ ├── PinyinDetailViewState.kt │ │ │ │ │ └── PinyinParcel.kt │ │ │ │ └── list │ │ │ │ │ ├── PinyinListFragment.kt │ │ │ │ │ ├── PinyinListFragmentModule.kt │ │ │ │ │ ├── PinyinListIntent.kt │ │ │ │ │ ├── PinyinListRenderer.kt │ │ │ │ │ ├── PinyinListViewModel.kt │ │ │ │ │ ├── PinyinListViewState.kt │ │ │ │ │ ├── character │ │ │ │ │ ├── PinyinCharacterAdapter.kt │ │ │ │ │ ├── PinyinCharacterFragment.kt │ │ │ │ │ └── PinyinCharacterViewModel.kt │ │ │ │ │ ├── english │ │ │ │ │ ├── PinyinEnglishAdapter.kt │ │ │ │ │ ├── PinyinEnglishFragment.kt │ │ │ │ │ └── PinyinEnglishViewModel.kt │ │ │ │ │ └── phonetic │ │ │ │ │ ├── PinyinPhoneticAdapter.kt │ │ │ │ │ ├── PinyinPhoneticFragment.kt │ │ │ │ │ └── PinyinPhoneticViewModel.kt │ │ │ ├── study │ │ │ │ ├── CreateStudyActivity.kt │ │ │ │ ├── CreateStudyActivityModule.kt │ │ │ │ ├── CreateStudyIntent.kt │ │ │ │ ├── CreateStudyRenderer.kt │ │ │ │ ├── CreateStudyViewModel.kt │ │ │ │ ├── CreateStudyViewState.kt │ │ │ │ ├── StudyActivity.kt │ │ │ │ ├── StudyActivityModule.kt │ │ │ │ ├── StudyAdapter.kt │ │ │ │ ├── StudyCardView.kt │ │ │ │ ├── StudyIntent.kt │ │ │ │ ├── StudyRenderer.kt │ │ │ │ ├── StudyViewModel.kt │ │ │ │ └── StudyViewState.kt │ │ │ └── train │ │ │ │ ├── RandomPhraseActivity.kt │ │ │ │ ├── RandomPhraseActivityModule.kt │ │ │ │ ├── RandomPhraseIntent.kt │ │ │ │ ├── RandomPhraseRenderer.kt │ │ │ │ ├── RandomPhraseResultsAdapter.kt │ │ │ │ ├── RandomPhraseViewModel.kt │ │ │ │ ├── RandomPhraseViewState.kt │ │ │ │ ├── TrainPhraseActivity.kt │ │ │ │ ├── TrainPhraseActivityModule.kt │ │ │ │ ├── TrainPhraseIntent.kt │ │ │ │ ├── TrainPhraseRenderer.kt │ │ │ │ ├── TrainPhraseViewModel.kt │ │ │ │ └── TrainPhraseViewState.kt │ │ │ ├── audio │ │ │ ├── PinyinAudio.kt │ │ │ └── PlayPinyinAudio.kt │ │ │ ├── domain │ │ │ ├── Database.kt │ │ │ ├── Network.kt │ │ │ ├── SchedulerProvider.kt │ │ │ ├── pinyin │ │ │ │ ├── FetchAndSavePinyin.kt │ │ │ │ ├── Pinyin.kt │ │ │ │ ├── api │ │ │ │ │ ├── FetchPinyin.kt │ │ │ │ │ ├── PinyinApi.kt │ │ │ │ │ ├── PinyinJson.kt │ │ │ │ │ └── PinyinWrapper.kt │ │ │ │ └── db │ │ │ │ │ ├── CharacterSearch.kt │ │ │ │ │ ├── CountPinyin.kt │ │ │ │ │ ├── EnglishSearch.kt │ │ │ │ │ ├── GetPinyin.kt │ │ │ │ │ ├── PhoneticSearch.kt │ │ │ │ │ ├── PinyinDao.kt │ │ │ │ │ ├── PinyinEntity.kt │ │ │ │ │ └── SavePinyin.kt │ │ │ └── study │ │ │ │ ├── GetRandomStudy.kt │ │ │ │ ├── GetStudy.kt │ │ │ │ ├── Study.kt │ │ │ │ └── db │ │ │ │ ├── CountStudy.kt │ │ │ │ ├── DeleteStudy.kt │ │ │ │ ├── SaveStudy.kt │ │ │ │ ├── StudyDao.kt │ │ │ │ ├── StudyEntity.kt │ │ │ │ └── UpdateStudy.kt │ │ │ └── kit │ │ │ ├── Adapter.kt │ │ │ ├── AppCompatViews.kt │ │ │ ├── ErrorRetryView.kt │ │ │ ├── RxTabLayout2.kt │ │ │ ├── TabLayoutSelectionEventObservable2.kt │ │ │ └── View.kt │ └── res │ │ ├── anim │ │ ├── pinyin_list_transition_enter_bottom.xml │ │ ├── pinyin_list_transition_exit_left.xml │ │ └── pinyin_list_transition_exit_right.xml │ │ ├── drawable │ │ ├── ic_add.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_backspace.xml │ │ ├── ic_delete.xml │ │ ├── ic_edit_accent.xml │ │ ├── ic_home_up.xml │ │ ├── ic_play.xml │ │ ├── ic_study.xml │ │ ├── ic_study_accent.xml │ │ ├── view_button_primary_background.xml │ │ ├── view_button_primary_background_rounded.xml │ │ ├── view_button_secondary_background.xml │ │ └── view_button_secondary_background_rounded.xml │ │ ├── layout │ │ ├── entry_activity.xml │ │ ├── kit_error_retry.xml │ │ ├── pinyin_activity.xml │ │ ├── pinyin_character_fragment.xml │ │ ├── pinyin_character_list_item.xml │ │ ├── pinyin_detail_activity.xml │ │ ├── pinyin_english_fragment.xml │ │ ├── pinyin_english_list_item.xml │ │ ├── pinyin_phonetic_fragment.xml │ │ ├── pinyin_phonetic_list_item.xml │ │ ├── study_activity.xml │ │ ├── study_card_view.xml │ │ ├── study_create_activity.xml │ │ ├── study_list_item.xml │ │ ├── train_phrase_activity.xml │ │ ├── train_random_activity.xml │ │ └── train_random_results_list_item.xml │ │ ├── menu │ │ └── study_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ └── chinese.json │ │ └── values │ │ ├── anim_integers.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── ids.xml │ │ ├── pinyin_dimens.xml │ │ ├── pinyin_strings.xml │ │ ├── strings.xml │ │ ├── study_dimens.xml │ │ ├── study_strings.xml │ │ ├── styles.xml │ │ ├── train_phrase_strings.xml │ │ ├── typography_dimens.xml │ │ └── typography_styles.xml │ └── test │ ├── java │ └── com │ │ └── consistence │ │ └── pinyin │ │ ├── TestSchedulerProvider.kt │ │ └── app │ │ ├── EntryRenderTest.kt │ │ ├── EntryViewModelTest.kt │ │ ├── PinyinRenderTest.kt │ │ ├── PinyinViewModelTest.kt │ │ └── pinyin │ │ ├── detail │ │ ├── PinyinDetailRenderTest.kt │ │ └── PinyinDetailViewModelTest.kt │ │ └── list │ │ ├── PinyinCharacterViewModelTest.kt │ │ ├── PinyinEnglishViewModelTest.kt │ │ ├── PinyinListRenderTest.kt │ │ └── PinyinPhoneticViewModelTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── gradle.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | .idea 12 | package-lock.json 13 | gradle 14 | gradlew 15 | gradlew.bat 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### MVI Design pattern 2 | A production ready Kotlin example of the Android MVI (model view intent) pattern, a render/layout mechanism is used to provide high unit test coverage throughout the reactive plumbing. 3 | 4 | ### How to navigate the code 5 | - Start by reviewing the Model/Render unit tests in `src/test/java` 6 | - Get the big picture from the MVI pattern interfaces at: 7 | https://github.com/memtrip/mxandroid 8 | - See the pattern in action at `EntryActivity -> EntryModel -> EntryView` 9 | 10 | ### The app in Google play 11 | https://play.google.com/store/apps/details?id=com.consistence.pinyin 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'kotlin-android-extensions' 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "com.consistence.pinyin" 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | versionCode 5 13 | versionName "1.1.2" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | multiDexEnabled true 16 | } 17 | lintOptions { 18 | warningsAsErrors true 19 | disable 'ParcelCreator', 'OldTargetApi' 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | debug { 27 | applicationIdSuffix '.debug' 28 | versionNameSuffix '-DEBUG' 29 | } 30 | } 31 | packagingOptions { 32 | exclude 'META-INF/rxjava.properties' 33 | } 34 | androidExtensions { 35 | experimental = true 36 | } 37 | } 38 | 39 | dependencies { 40 | 41 | implementation 'androidx.multidex:multidex:2.0.0' 42 | 43 | /* */ 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 45 | 46 | /* */ 47 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' 48 | implementation 'androidx.cardview:cardview:1.0.0-rc01' 49 | implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-rc01' 50 | implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0-rc01' 51 | implementation 'com.google.android.material:material:1.0.0-rc01' 52 | 53 | /* */ 54 | implementation 'com.memtrip.mxandroid:mxandroid:1.0.0' 55 | implementation 'com.memtrip.exoeasy:exoeasy:1.0.1' 56 | 57 | /* */ 58 | implementation 'com.squareup.moshi:moshi-kotlin:1.6.0' 59 | kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.6.0' 60 | 61 | /* */ 62 | implementation 'com.squareup.retrofit2:retrofit:2.4.0' 63 | implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' 64 | implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' 65 | 66 | /* */ 67 | implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' 68 | 69 | /* */ 70 | implementation 'io.reactivex:rxkotlin:1.0.0' 71 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 72 | implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' 73 | implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' 74 | 75 | /* */ 76 | implementation 'com.google.dagger:dagger:2.16' 77 | kapt 'com.google.dagger:dagger-compiler:2.16' 78 | implementation 'com.google.dagger:dagger-android:2.16' 79 | implementation 'com.google.dagger:dagger-android-support:2.16' 80 | kapt 'com.google.dagger:dagger-android-processor:2.16' 81 | 82 | /* */ 83 | implementation 'androidx.room:room-runtime:2.0.0-rc01' 84 | kapt 'androidx.room:room-compiler:2.0.0-rc01' 85 | 86 | /* */ 87 | implementation 'com.airbnb.android:lottie:2.5.4' 88 | 89 | /* */ 90 | implementation 'com.google.android.exoplayer:exoplayer-core:r2.5.4' 91 | 92 | /* */ 93 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1' 94 | releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' 95 | 96 | /* */ 97 | implementation 'com.jakewharton.timber:timber:4.7.1' 98 | 99 | /* junit */ 100 | testImplementation 'junit:junit:4.12' 101 | testImplementation 'org.junit.platform:junit-platform-runner:1.0.1' 102 | 103 | /* spek */ 104 | testImplementation 'org.jetbrains.spek:spek-api:1.1.5' 105 | testImplementation 'org.jetbrains.spek:spek-junit-platform-engine:1.1.5' 106 | 107 | /* mockito */ 108 | testImplementation 'org.mockito:mockito-core:2.18.3' 109 | testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' 110 | 111 | /* mokk */ 112 | testImplementation "io.mockk:mockk:1.8.5" 113 | 114 | /* assertj */ 115 | testImplementation 'org.assertj:assertj-core:3.10.0' 116 | } 117 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -printmapping build/mapping.txt 2 | 3 | -repackageclasses '' 4 | -allowaccessmodification 5 | 6 | -dontwarn javax.annotation.** 7 | 8 | # Android architecture components 9 | -keep class android.arch.** { *; } 10 | 11 | # RxJava 12 | -dontwarn sun.misc.Unsafe 13 | 14 | # Okhttp 15 | -dontwarn okhttp3.** 16 | -dontwarn okio.** 17 | 18 | # Kotlin 19 | -dontwarn kotlin.** 20 | -dontwarn org.jetbrains.annotations.** 21 | -keep class kotlin.Metadata { *; } 22 | 23 | # Moshi 24 | # https://github.com/square/moshi/issues/402 25 | -keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl 26 | -keep class kotlin.Metadata { *; } 27 | -keepclassmembers class kotlin.Metadata { 28 | public ; 29 | } 30 | -keepclasseswithmembers class * { 31 | @com.squareup.moshi.* ; 32 | } 33 | 34 | -keepclassmembers class ** { 35 | @com.squareup.moshi.FromJson *; 36 | @com.squareup.moshi.ToJson *; 37 | } 38 | -keep @com.squareup.moshi.JsonQualifier interface * 39 | -keep class **JsonAdapter { 40 | (...); 41 | ; 42 | } 43 | 44 | -keepnames @com.squareup.moshi.JsonClass class * 45 | 46 | # Retrofit2 47 | -dontnote retrofit2.Platform 48 | -dontwarn retrofit2.Platform$Java8 49 | -keepattributes Signature 50 | -keepattributes Exceptions 51 | -keepclassmembers class com.consistence.pinyin.domain.** { 52 | (...); 53 | ; 54 | } 55 | -keep class retrofit.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 36 | 37 | 40 | 41 | 44 | 45 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/PinyinApplication.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin 2 | 3 | import android.app.Activity 4 | import androidx.fragment.app.Fragment 5 | import androidx.multidex.MultiDexApplication 6 | import com.squareup.leakcanary.LeakCanary 7 | import dagger.android.DispatchingAndroidInjector 8 | import dagger.android.HasActivityInjector 9 | import dagger.android.support.HasSupportFragmentInjector 10 | import timber.log.Timber 11 | import javax.inject.Inject 12 | 13 | class PinyinApplication : MultiDexApplication(), HasActivityInjector, HasSupportFragmentInjector { 14 | 15 | @Inject lateinit var activityInjector: DispatchingAndroidInjector 16 | @Inject lateinit var fragmentInjector: DispatchingAndroidInjector 17 | 18 | override fun activityInjector(): DispatchingAndroidInjector = activityInjector 19 | 20 | override fun supportFragmentInjector(): DispatchingAndroidInjector = fragmentInjector 21 | 22 | override fun onCreate() { 23 | super.onCreate() 24 | 25 | if (LeakCanary.isInAnalyzerProcess(this)) { return } 26 | 27 | LeakCanary.install(this) 28 | 29 | if (BuildConfig.DEBUG) { 30 | Timber.plant() 31 | } 32 | 33 | DaggerPinyinApplicationComponent 34 | .builder() 35 | .application(this) 36 | .build() 37 | .inject(this) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/PinyinApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.ApiModule 5 | import com.consistence.pinyin.domain.DatabaseModule 6 | import com.consistence.pinyin.domain.NetworkModule 7 | import com.consistence.pinyin.app.EntryActivityModule 8 | import com.consistence.pinyin.app.pinyin.PinyinActivityModule 9 | import com.consistence.pinyin.app.pinyin.detail.PinyinDetailActivityModule 10 | import com.consistence.pinyin.app.pinyin.list.PinyinCharacterFragmentModule 11 | import com.consistence.pinyin.app.pinyin.list.PinyinEnglishFragmentModule 12 | import com.consistence.pinyin.app.pinyin.list.PinyinPhoneticFragmentModule 13 | import com.consistence.pinyin.app.study.CreateStudyActivityModule 14 | import com.consistence.pinyin.app.study.StudyActivityModule 15 | import com.consistence.pinyin.app.train.RandomPhraseActivityModule 16 | import com.consistence.pinyin.app.train.TrainPhraseActivityModule 17 | import dagger.BindsInstance 18 | import dagger.Component 19 | 20 | import dagger.android.AndroidInjector 21 | import dagger.android.support.AndroidSupportInjectionModule 22 | import javax.inject.Singleton 23 | 24 | @Singleton 25 | @Component(modules = [ 26 | AndroidSupportInjectionModule::class, 27 | ApiModule::class, 28 | NetworkModule::class, 29 | DatabaseModule::class, 30 | EntryActivityModule::class, 31 | PinyinActivityModule::class, 32 | PinyinPhoneticFragmentModule::class, 33 | PinyinEnglishFragmentModule::class, 34 | PinyinCharacterFragmentModule::class, 35 | PinyinDetailActivityModule::class, 36 | StudyActivityModule::class, 37 | CreateStudyActivityModule::class, 38 | TrainPhraseActivityModule::class, 39 | RandomPhraseActivityModule::class 40 | ]) 41 | interface PinyinApplicationComponent : AndroidInjector { 42 | 43 | @Component.Builder 44 | interface Builder { 45 | 46 | @BindsInstance 47 | fun application(application: Application): Builder 48 | 49 | fun build(): PinyinApplicationComponent 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import dagger.MapKey 6 | import javax.inject.Inject 7 | import kotlin.reflect.KClass 8 | 9 | class ViewModelFactory @Inject constructor( 10 | private val viewModel: T 11 | ) : ViewModelProvider.Factory { 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | override fun create(modelClass: Class): T = viewModel as T 15 | } 16 | 17 | @MustBeDocumented 18 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 19 | @Retention(AnnotationRetention.RUNTIME) 20 | @MapKey 21 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryActivity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import android.os.Bundle 4 | import com.consistence.pinyin.R 5 | import com.consistence.pinyin.ViewModelFactory 6 | import com.consistence.pinyin.app.pinyin.PinyinActivity 7 | import com.consistence.pinyin.kit.gone 8 | import com.consistence.pinyin.kit.visible 9 | import com.jakewharton.rxbinding2.view.RxView 10 | import com.memtrip.mxandroid.MxViewActivity 11 | import dagger.android.AndroidInjection 12 | import io.reactivex.Observable 13 | import kotlinx.android.synthetic.main.entry_activity.* 14 | import kotlinx.android.synthetic.main.kit_error_retry.view.* 15 | import javax.inject.Inject 16 | 17 | class EntryActivity : MxViewActivity(), EntryLayout { 18 | 19 | @Inject lateinit var viewModelFactory: ViewModelFactory 20 | @Inject lateinit var render: EntryRenderer 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.entry_activity) 25 | } 26 | 27 | override fun intents(): Observable = Observable.merge( 28 | Observable.just(EntryIntent.Init), 29 | RxView.clicks(entry_activity_error.kit_error_retry_button).map { EntryIntent.Retry } 30 | ) 31 | 32 | override fun showProgress() { 33 | entry_activity_progress.visible() 34 | entry_activity_error.gone() 35 | } 36 | 37 | override fun navigateToPinyin() { 38 | entry_activity_progress.gone() 39 | startActivity(PinyinActivity.newIntent(this)) 40 | finish() 41 | } 42 | 43 | override fun showError() { 44 | entry_activity_progress.gone() 45 | entry_activity_error.visible() 46 | } 47 | 48 | override fun inject() { 49 | AndroidInjection.inject(this) 50 | } 51 | 52 | override fun layout(): EntryLayout = this 53 | 54 | override fun render(): EntryRenderer = render 55 | 56 | override fun model(): EntryViewModel = getViewModel(viewModelFactory) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class EntryActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesEntryActivity(): EntryActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.memtrip.mxandroid.MxViewIntent 4 | 5 | sealed class EntryIntent : MxViewIntent { 6 | object Init : EntryIntent() 7 | object Retry : EntryIntent() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.memtrip.mxandroid.MxRenderAction 4 | import com.memtrip.mxandroid.MxViewLayout 5 | import com.memtrip.mxandroid.MxViewRenderer 6 | import javax.inject.Inject 7 | 8 | sealed class EntryRenderAction : MxRenderAction { 9 | object OnProgress : EntryRenderAction() 10 | object OnError : EntryRenderAction() 11 | object OnPinyinLoaded : EntryRenderAction() 12 | } 13 | 14 | interface EntryLayout : MxViewLayout { 15 | fun showProgress() 16 | fun navigateToPinyin() 17 | fun showError() 18 | } 19 | 20 | class EntryRenderer @Inject internal constructor() : MxViewRenderer { 21 | override fun layout(layout: EntryLayout, state: EntryViewState) = when (state.view) { 22 | EntryViewState.View.OnProgress -> { 23 | layout.showProgress() 24 | } 25 | EntryViewState.View.OnPinyinLoaded -> { 26 | layout.navigateToPinyin() 27 | } 28 | EntryViewState.View.OnError -> { 29 | layout.showError() 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.pinyin.FetchAndSavePinyin 5 | import com.consistence.pinyin.domain.pinyin.db.CountPinyin 6 | import com.memtrip.mxandroid.MxViewModel 7 | import io.reactivex.Observable 8 | import io.reactivex.Single 9 | import javax.inject.Inject 10 | 11 | class EntryViewModel @Inject internal constructor( 12 | private val fetchAndSavePinyin: FetchAndSavePinyin, 13 | private val countPinyin: CountPinyin, 14 | application: Application 15 | ) : MxViewModel( 16 | EntryViewState(EntryViewState.View.OnProgress), 17 | application 18 | ) { 19 | 20 | private fun getPinyin(): Observable { 21 | return countPinyin.count() 22 | .flatMap { count -> 23 | if (count > 0) { 24 | Single.just(EntryRenderAction.OnPinyinLoaded) 25 | } else { 26 | fetchAndSavePinyin 27 | .save() 28 | .map { EntryRenderAction.OnPinyinLoaded } 29 | } 30 | } 31 | .onErrorReturnItem(EntryRenderAction.OnError) 32 | .toObservable() 33 | .startWith(EntryRenderAction.OnProgress) 34 | } 35 | 36 | override fun dispatcher(intent: EntryIntent): Observable = when (intent) { 37 | EntryIntent.Init, EntryIntent.Retry -> getPinyin() 38 | } 39 | 40 | override fun reducer(previousState: EntryViewState, renderAction: EntryRenderAction): EntryViewState = when (renderAction) { 41 | EntryRenderAction.OnProgress -> previousState.copy(view = EntryViewState.View.OnProgress) 42 | EntryRenderAction.OnError -> previousState.copy(view = EntryViewState.View.OnError) 43 | EntryRenderAction.OnPinyinLoaded -> previousState.copy(EntryViewState.View.OnPinyinLoaded) 44 | } 45 | 46 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 47 | intents.ofType(EntryIntent.Init.javaClass).take(1), 48 | intents.filter { 49 | !EntryIntent.Init.javaClass.isInstance(it) 50 | } 51 | ) 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/EntryViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.memtrip.mxandroid.MxViewState 4 | 5 | data class EntryViewState(val view: View) : MxViewState { 6 | sealed class View : MxViewState { 7 | object OnProgress : View() 8 | object OnError : View() 9 | object OnPinyinLoaded : View() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinActivity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.widget.SearchView 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.ViewModelFactory 9 | import com.consistence.pinyin.app.pinyin.list.PinyinListFragment 10 | import com.consistence.pinyin.app.pinyin.list.PinyinListIntent 11 | import com.consistence.pinyin.app.study.StudyActivity 12 | import com.consistence.pinyin.domain.pinyin.Pinyin 13 | import com.consistence.pinyin.kit.RxTabLayout2 14 | import com.consistence.pinyin.kit.invisible 15 | import com.consistence.pinyin.kit.visible 16 | import com.memtrip.mxandroid.MxViewActivity 17 | import dagger.android.AndroidInjection 18 | import io.reactivex.Observable 19 | import kotlinx.android.synthetic.main.pinyin_activity.* 20 | import javax.inject.Inject 21 | 22 | class PinyinActivity( 23 | override var currentSearchQuery: String = "", 24 | override val consumeSelection: Boolean = false, 25 | override val fullListStyle: Boolean = true 26 | ) : MxViewActivity(), PinyinLayout, PinyinListFragment.PinyinListDelegate { 27 | 28 | @Inject lateinit var viewModelFactory: ViewModelFactory 29 | @Inject lateinit var render: PinyinRenderer 30 | 31 | private lateinit var fragmentAdapter: PinyinFragmentAdapter 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.pinyin_activity) 36 | 37 | fragmentAdapter = PinyinFragmentAdapter( 38 | R.id.pinyin_activity_fragment_container, 39 | pinyin_activity_tablayout, 40 | supportFragmentManager, 41 | this) 42 | 43 | pinyin_activity_searchview.setOnQueryTextFocusChangeListener { _, hasFocus -> 44 | if (hasFocus) { 45 | pinyin_activity_searchview_label.invisible() 46 | } 47 | } 48 | 49 | pinyin_activity_searchview.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 50 | override fun onQueryTextSubmit(p: String): Boolean { return false } 51 | 52 | override fun onQueryTextChange(terms: String): Boolean { 53 | currentSearchQuery = terms 54 | sendSearchEvent(terms) 55 | return true 56 | } 57 | }) 58 | 59 | pinyin_activity_searchview.setOnCloseListener { 60 | sendSearchEvent() 61 | pinyin_activity_searchview_label.visible() 62 | false 63 | } 64 | 65 | pinyin_activity_searchview_label.setOnClickListener { 66 | pinyin_activity_searchview.isIconified = false 67 | } 68 | 69 | pinyin_activity_study_button.setOnClickListener { 70 | startActivity(StudyActivity.newIntent(this)) 71 | } 72 | } 73 | 74 | override fun intents(): Observable = Observable.merge( 75 | Observable.just(PinyinIntent.Init), 76 | RxTabLayout2 77 | .selectionEvents(pinyin_activity_tablayout) 78 | .map { PinyinIntent.TabSelected(Page.values()[it.tab().position]) } 79 | ) 80 | 81 | private fun sendSearchEvent(terms: String = "") { 82 | fragmentAdapter.sendIntent(PinyinListIntent.Search(terms)) 83 | } 84 | 85 | override fun pinyinSelection(pinyin: Pinyin) { 86 | } 87 | 88 | override fun inject() { 89 | AndroidInjection.inject(this) 90 | } 91 | 92 | override fun layout(): PinyinLayout = this 93 | 94 | override fun model(): PinyinViewModel = getViewModel(viewModelFactory) 95 | 96 | override fun render(): PinyinRenderer = render 97 | 98 | override fun updateSearchHint(hint: String) { 99 | pinyin_activity_searchview.queryHint = hint 100 | pinyin_activity_searchview_label.text = hint 101 | } 102 | 103 | companion object { fun newIntent(context: Context) = Intent(context, PinyinActivity::class.java) } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class PinyinActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesPinyinActivity(): PinyinActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import com.memtrip.mxandroid.MxViewIntent 4 | 5 | sealed class PinyinIntent : MxViewIntent { 6 | object Init : PinyinIntent() 7 | data class TabSelected(val page: Page) : PinyinIntent() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import com.memtrip.mxandroid.MxRenderAction 4 | import com.memtrip.mxandroid.MxViewLayout 5 | import com.memtrip.mxandroid.MxViewRenderer 6 | import javax.inject.Inject 7 | 8 | sealed class PinyinRenderAction : MxRenderAction { 9 | data class SearchHint(val hint: String) : PinyinRenderAction() 10 | } 11 | 12 | interface PinyinLayout : MxViewLayout { 13 | fun updateSearchHint(hint: String) 14 | } 15 | 16 | class PinyinRenderer @Inject internal constructor() : MxViewRenderer { 17 | 18 | override fun layout(layout: PinyinLayout, state: PinyinViewState) { 19 | layout.updateSearchHint(state.currentSearchQuery) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.R 5 | import com.memtrip.mxandroid.MxViewModel 6 | import io.reactivex.Observable 7 | import javax.inject.Inject 8 | 9 | class PinyinViewModel @Inject internal constructor( 10 | application: Application 11 | ) : MxViewModel( 12 | PinyinViewState(), 13 | application 14 | ) { 15 | 16 | override fun dispatcher(intent: PinyinIntent): Observable = when (intent) { 17 | PinyinIntent.Init -> 18 | observable(PinyinRenderAction.SearchHint( 19 | context().getString(R.string.pinyin_activity_search_phonetic_hint))) 20 | is PinyinIntent.TabSelected -> { 21 | observable( 22 | when (intent.page) { 23 | Page.PHONETIC -> PinyinRenderAction.SearchHint( 24 | context().getString(R.string.pinyin_activity_search_phonetic_hint)) 25 | Page.ENGLISH -> PinyinRenderAction.SearchHint( 26 | context().getString(R.string.pinyin_activity_search_english_hint)) 27 | Page.CHARACTER -> PinyinRenderAction.SearchHint( 28 | context().getString(R.string.pinyin_activity_search_character_hint)) 29 | } 30 | ) 31 | } 32 | } 33 | 34 | override fun reducer(previousState: PinyinViewState, renderAction: PinyinRenderAction) = when (renderAction) { 35 | is PinyinRenderAction.SearchHint -> PinyinViewState(renderAction.hint) 36 | } 37 | 38 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 39 | intents.ofType(PinyinIntent.Init.javaClass).take(1), 40 | intents.filter { 41 | !PinyinIntent.Init.javaClass.isInstance(it) 42 | } 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/PinyinViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin 2 | 3 | import com.memtrip.mxandroid.MxViewState 4 | 5 | data class PinyinViewState(val currentSearchQuery: String = "") : MxViewState -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.consistence.pinyin.R 7 | import com.consistence.pinyin.ViewModelFactory 8 | import com.consistence.pinyin.audio.PlayPinyAudioInPresenter 9 | import com.consistence.pinyin.domain.pinyin.Pinyin 10 | import com.consistence.pinyin.kit.visible 11 | import com.jakewharton.rxbinding2.view.RxView 12 | import com.memtrip.mxandroid.MxViewActivity 13 | import dagger.android.AndroidInjection 14 | import io.reactivex.Observable 15 | import kotlinx.android.synthetic.main.pinyin_detail_activity.* 16 | import javax.inject.Inject 17 | 18 | class PinyinDetailActivity : MxViewActivity(), PinyinDetailLayout { 19 | 20 | @Inject lateinit var viewModelFactory: ViewModelFactory 21 | @Inject lateinit var render: PinyinDetailRenderer 22 | 23 | private val pinyinAudio = PlayPinyAudioInPresenter() 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.pinyin_detail_activity) 28 | 29 | setSupportActionBar(pinyin_detail_activity_toolbar) 30 | 31 | supportActionBar!!.setHomeButtonEnabled(true) 32 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 33 | supportActionBar!!.setHomeAsUpIndicator(R.drawable.ic_home_up) 34 | } 35 | 36 | override fun onStart() { 37 | super.onStart() 38 | pinyinAudio.attach(this) 39 | } 40 | 41 | override fun onStop() { 42 | super.onStop() 43 | pinyinAudio.detach(this) 44 | } 45 | 46 | override fun intents(): Observable = Observable.merge( 47 | Observable.just(PinyinDetailIntent.Idle), 48 | RxView.clicks(pinyin_detail_activity_audio_button) 49 | .map({ PinyinDetailIntent.PlayAudio }) 50 | ) 51 | 52 | override fun inject() { 53 | AndroidInjection.inject(this) 54 | } 55 | 56 | override fun layout(): PinyinDetailLayout = this 57 | 58 | override fun model(): PinyinDetailViewModel = getViewModel(viewModelFactory) 59 | 60 | override fun render(): PinyinDetailRenderer = render 61 | 62 | override fun populate( 63 | phoneticScriptText: String, 64 | englishTranslationText: String, 65 | chineseCharacters: String 66 | ) { 67 | supportActionBar!!.title = phoneticScriptText 68 | pinyin_detail_activity_phonetic_script_value.text = phoneticScriptText 69 | pinyin_detail_activity_english_translation_value.text = englishTranslationText 70 | pinyin_detail_activity_chinese_character_value.text = chineseCharacters 71 | } 72 | 73 | override fun showAudioControl() { 74 | pinyin_detail_activity_audio_button.visible() 75 | } 76 | 77 | override fun playAudio(audioSrc: String) { 78 | model().publish(PinyinDetailIntent.Idle) 79 | pinyinAudio.playPinyinAudio(audioSrc, this) 80 | } 81 | 82 | companion object { 83 | fun newIntent(context: Context, Pinyin: Pinyin): Intent { 84 | val intent = Intent(context, PinyinDetailActivity::class.java) 85 | PinyinParcel.into(Pinyin, intent) 86 | return intent 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | abstract class PinyinDetailActivityModule { 9 | 10 | @ContributesAndroidInjector(modules = [PinyinParcelModule::class]) 11 | internal abstract fun contributesPinyinDetailActivity(): PinyinDetailActivity 12 | } 13 | 14 | @Module 15 | class PinyinParcelModule { 16 | 17 | @Provides 18 | fun bindPinyinParcel(activity: PinyinDetailActivity) = PinyinParcel.out(activity.intent) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import com.memtrip.mxandroid.MxViewIntent 4 | 5 | sealed class PinyinDetailIntent : MxViewIntent { 6 | object Idle : PinyinDetailIntent() 7 | object PlayAudio : PinyinDetailIntent() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import com.memtrip.mxandroid.MxRenderAction 4 | import com.memtrip.mxandroid.MxViewLayout 5 | import com.memtrip.mxandroid.MxViewRenderer 6 | import javax.inject.Inject 7 | 8 | sealed class PinyinDetailRenderAction : MxRenderAction { 9 | object Idle : PinyinDetailRenderAction() 10 | data class PlayAudio(val audioSrc: String) : PinyinDetailRenderAction() 11 | } 12 | 13 | interface PinyinDetailLayout : MxViewLayout { 14 | fun populate( 15 | phoneticScriptText: String, 16 | englishTranslationText: String, 17 | chineseCharacters: String 18 | ) 19 | fun showAudioControl() 20 | fun playAudio(audioSrc: String) 21 | } 22 | 23 | class PinyinDetailRenderer @Inject internal constructor() : MxViewRenderer { 24 | 25 | override fun layout(layout: PinyinDetailLayout, state: PinyinDetailViewState) { 26 | 27 | layout.populate( 28 | state.phoneticScriptText, 29 | state.englishTranslationText, 30 | state.chineseCharacters) 31 | 32 | if (state.audioSrc != null) { 33 | layout.showAudioControl() 34 | } 35 | 36 | renderAction(layout, state.action) 37 | } 38 | 39 | private fun renderAction(layout: PinyinDetailLayout, action: PinyinDetailViewState.Action): Unit = when (action) { 40 | PinyinDetailViewState.Action.None -> { 41 | } 42 | is PinyinDetailViewState.Action.PlayAudio -> { 43 | layout.playAudio(audioSrc = action.audioSrc) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import android.app.Application 4 | import com.memtrip.mxandroid.MxViewModel 5 | import io.reactivex.Observable 6 | import javax.inject.Inject 7 | 8 | class PinyinDetailViewModel @Inject internal constructor( 9 | private val pinyinParcel: PinyinParcel, 10 | application: Application 11 | ) : MxViewModel( 12 | PinyinDetailViewState( 13 | pinyinParcel.phoneticScriptText, 14 | pinyinParcel.englishTranslationText, 15 | pinyinParcel.chineseCharacters, 16 | pinyinParcel.audioSrc 17 | ), 18 | application 19 | ) { 20 | 21 | override fun dispatcher(intent: PinyinDetailIntent): Observable = when (intent) { 22 | is PinyinDetailIntent.Idle -> Observable.just(PinyinDetailRenderAction.Idle) 23 | PinyinDetailIntent.PlayAudio -> Observable.just(PinyinDetailRenderAction.PlayAudio(pinyinParcel.audioSrc!!)) 24 | } 25 | 26 | override fun reducer(previousState: PinyinDetailViewState, renderAction: PinyinDetailRenderAction): PinyinDetailViewState = when (renderAction) { 27 | PinyinDetailRenderAction.Idle -> previousState.copy(action = PinyinDetailViewState.Action.None) 28 | is PinyinDetailRenderAction.PlayAudio -> previousState.copy(action = PinyinDetailViewState.Action.PlayAudio(renderAction.audioSrc)) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import com.memtrip.mxandroid.MxViewState 4 | 5 | data class PinyinDetailViewState( 6 | val phoneticScriptText: String, 7 | val englishTranslationText: String, 8 | val chineseCharacters: String, 9 | val audioSrc: String? = null, 10 | val action: Action = Action.None 11 | ) : MxViewState { 12 | 13 | sealed class Action { 14 | object None : Action() 15 | data class PlayAudio(val audioSrc: String) : Action() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/detail/PinyinParcel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import android.content.Intent 4 | import android.os.Parcelable 5 | import com.consistence.pinyin.domain.pinyin.Pinyin 6 | import kotlinx.android.parcel.Parcelize 7 | 8 | @Parcelize 9 | class PinyinParcel( 10 | val phoneticScriptText: String, 11 | val audioSrc: String?, 12 | val englishTranslationText: String, 13 | val chineseCharacters: String 14 | ) : Parcelable { 15 | 16 | companion object { 17 | 18 | private const val PINYIN_PARCEL = "PINYIN_PARCEL" 19 | 20 | fun into(entity: Pinyin, intent: Intent) { 21 | intent.putExtra(PINYIN_PARCEL, PinyinParcel( 22 | entity.phoneticScriptText, 23 | entity.audioSrc, 24 | entity.englishTranslationText, 25 | entity.chineseCharacters) 26 | ) 27 | } 28 | 29 | fun out(intent: Intent): PinyinParcel = intent.getParcelableExtra(PINYIN_PARCEL) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import android.os.Bundle 4 | import com.consistence.pinyin.app.pinyin.detail.PinyinDetailActivity 5 | import com.consistence.pinyin.audio.PlayPinyAudioInPresenter 6 | import com.consistence.pinyin.domain.pinyin.Pinyin 7 | import com.memtrip.mxandroid.MxViewFragment 8 | import io.reactivex.Observable 9 | import javax.inject.Inject 10 | 11 | abstract class PinyinListFragment 12 | : MxViewFragment(), PinyinListLayout { 13 | 14 | @Inject lateinit var render: PinyinListRenderer 15 | 16 | private val pinyinAudio = PlayPinyAudioInPresenter() 17 | 18 | internal val delegate: PinyinListDelegate by lazy { 19 | (context as PinyinListDelegate) 20 | } 21 | 22 | interface PinyinListDelegate { 23 | var currentSearchQuery: String 24 | val consumeSelection: Boolean 25 | val fullListStyle: Boolean 26 | fun pinyinSelection(pinyin: Pinyin) 27 | } 28 | 29 | override fun onActivityCreated(savedInstanceState: Bundle?) { 30 | super.onActivityCreated(savedInstanceState) 31 | model().publish(PinyinListIntent.Search(delegate.currentSearchQuery)) 32 | } 33 | 34 | override fun onStart() { 35 | super.onStart() 36 | context?.let { pinyinAudio.attach(it) } 37 | } 38 | 39 | override fun onStop() { 40 | super.onStop() 41 | context?.let { pinyinAudio.detach(it) } 42 | } 43 | 44 | override fun intents(): Observable = 45 | Observable.just(PinyinListIntent.Init(delegate.currentSearchQuery)) 46 | 47 | override fun render(): PinyinListRenderer = render 48 | 49 | override fun pinyinItemSelected(pinyin: Pinyin) { 50 | if (delegate.consumeSelection) { 51 | delegate.pinyinSelection(pinyin) 52 | } else { 53 | model().publish(PinyinListIntent.Idle) 54 | startActivity(PinyinDetailActivity.newIntent(context!!, pinyin)) 55 | } 56 | } 57 | 58 | override fun playAudio(audioSrc: String) { 59 | context?.let { pinyinAudio.playPinyinAudio(audioSrc, it) } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.app.pinyin.list.character.PinyinCharacterFragment 4 | import com.consistence.pinyin.app.pinyin.list.english.PinyinEnglishFragment 5 | import com.consistence.pinyin.app.pinyin.list.phonetic.PinyinPhoneticFragment 6 | import dagger.Module 7 | import dagger.android.ContributesAndroidInjector 8 | 9 | @Module 10 | abstract class PinyinPhoneticFragmentModule { 11 | @ContributesAndroidInjector 12 | internal abstract fun contributesPinyinPhoneticFragment(): PinyinPhoneticFragment 13 | } 14 | 15 | @Module 16 | abstract class PinyinEnglishFragmentModule { 17 | @ContributesAndroidInjector 18 | internal abstract fun contributesPinyinEnglishFragment(): PinyinEnglishFragment 19 | } 20 | 21 | @Module 22 | abstract class PinyinCharacterFragmentModule { 23 | @ContributesAndroidInjector 24 | internal abstract fun contributesPinyinCharacterFragment(): PinyinCharacterFragment 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.memtrip.mxandroid.MxViewIntent 5 | 6 | sealed class PinyinListIntent : MxViewIntent { 7 | data class Init(val terms: String) : PinyinListIntent() 8 | data class Search(val terms: String) : PinyinListIntent() 9 | data class PlayAudio(val audioSrc: String) : PinyinListIntent() 10 | data class SelectItem(val pinyin: Pinyin) : PinyinListIntent() 11 | object Idle : PinyinListIntent() 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.memtrip.mxandroid.MxRenderAction 5 | import com.memtrip.mxandroid.MxViewLayout 6 | import com.memtrip.mxandroid.MxViewRenderer 7 | import javax.inject.Inject 8 | 9 | sealed class PinyinListRenderAction : MxRenderAction { 10 | data class Populate(val pinyinList: List) : PinyinListRenderAction() 11 | object OnError : PinyinListRenderAction() 12 | data class PlayAudio(val audioSrc: String) : PinyinListRenderAction() 13 | data class SelectItem(val pinyin: Pinyin) : PinyinListRenderAction() 14 | object Idle : PinyinListRenderAction() 15 | } 16 | 17 | interface PinyinListLayout : MxViewLayout { 18 | fun populate(pinyin: List) 19 | fun pinyinItemSelected(pinyin: Pinyin) 20 | fun showError() 21 | fun playAudio(audioSrc: String) 22 | } 23 | 24 | class PinyinListRenderer @Inject internal constructor() : MxViewRenderer { 25 | override fun layout(layout: PinyinListLayout, state: PinyinListViewState) = when (state.view) { 26 | is PinyinListViewState.View.Populate -> { 27 | layout.populate(state.view.pinyin) 28 | } 29 | PinyinListViewState.View.OnError -> { 30 | layout.showError() 31 | } 32 | is PinyinListViewState.View.PlayAudio -> { 33 | layout.playAudio(state.view.audioSrc) 34 | } 35 | is PinyinListViewState.View.SelectItem -> { 36 | layout.pinyinItemSelected(state.view.pinyin) 37 | } 38 | PinyinListViewState.View.Idle -> { 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import com.memtrip.mxandroid.MxViewModel 6 | import io.reactivex.Observable 7 | import io.reactivex.Single 8 | 9 | abstract class PinyinListViewModel( 10 | application: Application 11 | ) : MxViewModel( 12 | PinyinListViewState(view = PinyinListViewState.View.Idle), 13 | application 14 | ) { 15 | 16 | abstract fun search(terms: String = defaultSearch): Single> 17 | 18 | abstract val defaultSearch: String 19 | 20 | private fun searchQuery(terms: String = defaultSearch): Observable { 21 | return search(if (terms.isEmpty()) defaultSearch else terms) 22 | .map { PinyinListRenderAction.Populate(it) } 23 | .onErrorReturnItem(PinyinListRenderAction.OnError) 24 | .toObservable() 25 | } 26 | 27 | override fun dispatcher(intent: PinyinListIntent): Observable = when (intent) { 28 | is PinyinListIntent.Init -> searchQuery(intent.terms) 29 | is PinyinListIntent.Search -> searchQuery(intent.terms) 30 | is PinyinListIntent.PlayAudio -> Observable.just(PinyinListRenderAction.PlayAudio(intent.audioSrc)) 31 | is PinyinListIntent.SelectItem -> Observable.just(PinyinListRenderAction.SelectItem(intent.pinyin)) 32 | PinyinListIntent.Idle -> Observable.just(PinyinListRenderAction.Idle) 33 | } 34 | 35 | override fun reducer(previousState: PinyinListViewState, renderAction: PinyinListRenderAction) = when (renderAction) { 36 | is PinyinListRenderAction.Populate -> previousState.copy(view = PinyinListViewState.View.Populate(renderAction.pinyinList)) 37 | PinyinListRenderAction.OnError -> previousState.copy(view = PinyinListViewState.View.OnError) 38 | is PinyinListRenderAction.PlayAudio -> previousState.copy(view = PinyinListViewState.View.PlayAudio(renderAction.audioSrc)) 39 | is PinyinListRenderAction.SelectItem -> previousState.copy(view = PinyinListViewState.View.SelectItem(renderAction.pinyin)) 40 | PinyinListRenderAction.Idle -> previousState.copy(view = PinyinListViewState.View.Idle) 41 | } 42 | 43 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 44 | intents.ofType(PinyinListIntent.Init::class.java).take(1), 45 | intents.filter { 46 | !PinyinListIntent.Init::class.java.isInstance(it) 47 | } 48 | ) 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/PinyinListViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.memtrip.mxandroid.MxViewState 5 | import com.memtrip.mxandroid.MxViewState.Companion.id 6 | 7 | data class PinyinListViewState(val view: View) : MxViewState { 8 | 9 | sealed class View { 10 | object Idle : View() 11 | data class Populate(val pinyin: List) : View() 12 | object OnError : View() 13 | data class PlayAudio(val audioSrc: String, val eventId: Int = id()) : View() 14 | data class SelectItem(val pinyin: Pinyin, val eventId: Int = id()) : View() 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/character/PinyinCharacterAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.character 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.consistence.pinyin.R 7 | import com.consistence.pinyin.domain.pinyin.Pinyin 8 | import com.consistence.pinyin.kit.Interaction 9 | import com.consistence.pinyin.kit.SimpleAdapter 10 | import com.consistence.pinyin.kit.SimpleAdapterViewHolder 11 | import com.consistence.pinyin.kit.visible 12 | import com.consistence.pinyin.kit.gone 13 | import com.jakewharton.rxbinding2.view.RxView 14 | import io.reactivex.subjects.PublishSubject 15 | import kotlinx.android.synthetic.main.pinyin_character_list_item.view.* 16 | 17 | class PinyinCharacterAdapter( 18 | context: Context, 19 | private val fullListStyle: Boolean, 20 | interaction: PublishSubject> 21 | ) : SimpleAdapter(context, interaction) { 22 | 23 | override fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder { 24 | val viewHolder = PinyinCharacterViewHolder(inflater.inflate( 25 | R.layout.pinyin_character_list_item, parent, false), fullListStyle) 26 | 27 | RxView.clicks(viewHolder.itemView.pinyin_list_audio_button).map { 28 | Interaction(viewHolder.itemView.pinyin_list_audio_button.id, data[viewHolder.adapterPosition]) 29 | }.subscribe(interaction) 30 | 31 | return viewHolder 32 | } 33 | } 34 | 35 | class PinyinCharacterViewHolder( 36 | itemView: View, 37 | private val fullListStyle: Boolean 38 | ) : SimpleAdapterViewHolder(itemView) { 39 | 40 | override fun populate(position: Int, value: Pinyin) { 41 | itemView.pinyin_character_list_item_value.text = value.chineseCharacters 42 | itemView.pinyin_character_list_item_english_translation_value.text = value.englishTranslationText 43 | itemView.pinyin_character_list_item_phonetic_translation_value.text = value.phoneticScriptText 44 | 45 | if (fullListStyle) { 46 | itemView.pinyin_character_list_item_phonetic_translation.visible() 47 | itemView.pinyin_character_list_item_phonetic_translation_value.visible() 48 | itemView.pinyin_character_list_item_english_translation.visible() 49 | itemView.pinyin_character_list_item_english_translation_value.visible() 50 | 51 | 52 | value.audioSrc?.let { itemView.pinyin_list_audio_button.visible() } 53 | ?: itemView.pinyin_list_audio_button.gone() 54 | } else { 55 | itemView.pinyin_character_list_item_phonetic_translation.gone() 56 | itemView.pinyin_character_list_item_phonetic_translation_value.gone() 57 | itemView.pinyin_character_list_item_english_translation.visible() 58 | itemView.pinyin_character_list_item_english_translation_value.visible() 59 | itemView.pinyin_list_audio_button.gone() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/character/PinyinCharacterFragment.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.character 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.ViewModelFactory 9 | import com.consistence.pinyin.app.pinyin.list.PinyinListFragment 10 | import com.consistence.pinyin.app.pinyin.list.PinyinListIntent 11 | import com.consistence.pinyin.app.pinyin.list.PinyinListLayout 12 | import com.consistence.pinyin.domain.pinyin.Pinyin 13 | import com.consistence.pinyin.kit.Interaction 14 | import dagger.android.support.AndroidSupportInjection 15 | import io.reactivex.Observable 16 | import io.reactivex.subjects.PublishSubject 17 | import kotlinx.android.synthetic.main.pinyin_character_fragment.view.* 18 | import javax.inject.Inject 19 | 20 | class PinyinCharacterFragment : PinyinListFragment() { 21 | 22 | @Inject lateinit var model: ViewModelFactory 23 | 24 | private lateinit var adapter: PinyinCharacterAdapter 25 | 26 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 27 | val view = inflater.inflate(R.layout.pinyin_character_fragment, container, false) 28 | 29 | val adapterInteraction: PublishSubject> = PublishSubject.create() 30 | adapter = PinyinCharacterAdapter(context!!, delegate.fullListStyle, adapterInteraction) 31 | view.pinyin_character_fragment_recyclerview.adapter = adapter 32 | 33 | return view 34 | } 35 | 36 | override fun intents(): Observable = Observable.merge( 37 | super.intents(), 38 | adapter.interaction.map { 39 | when (it.id) { 40 | R.id.pinyin_list_audio_button -> PinyinListIntent.PlayAudio(it.data.audioSrc!!) 41 | else -> PinyinListIntent.SelectItem(it.data) 42 | } 43 | } 44 | ) 45 | 46 | override fun inject() { 47 | AndroidSupportInjection.inject(this) 48 | } 49 | 50 | override fun layout(): PinyinListLayout = this 51 | 52 | override fun model(): PinyinCharacterViewModel = getViewModel(model) 53 | 54 | override fun populate(pinyin: List) { 55 | adapter.clear() 56 | adapter.populate(pinyin) 57 | } 58 | 59 | override fun showError() { 60 | } 61 | 62 | companion object { 63 | fun newInstance() = PinyinCharacterFragment() 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/character/PinyinCharacterViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.character 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.app.pinyin.list.PinyinListViewModel 5 | import com.consistence.pinyin.domain.pinyin.db.CharacterSearch 6 | import javax.inject.Inject 7 | 8 | class PinyinCharacterViewModel @Inject internal constructor( 9 | private val characterSearch: CharacterSearch, 10 | application: Application 11 | ) : PinyinListViewModel(application) { 12 | 13 | override val defaultSearch = "拼音" 14 | 15 | override fun search(terms: String) = characterSearch.search(terms) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/english/PinyinEnglishAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.english 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.domain.pinyin.Pinyin 9 | import com.consistence.pinyin.kit.Interaction 10 | import com.consistence.pinyin.kit.SimpleAdapter 11 | import com.consistence.pinyin.kit.SimpleAdapterViewHolder 12 | import com.consistence.pinyin.kit.visible 13 | import com.consistence.pinyin.kit.gone 14 | import com.jakewharton.rxbinding2.view.RxView 15 | import io.reactivex.subjects.PublishSubject 16 | import kotlinx.android.synthetic.main.pinyin_english_list_item.view.* 17 | 18 | class PinyinEnglishAdapter( 19 | context: Context, 20 | private val fullListStyle: Boolean, 21 | interaction: PublishSubject> 22 | ) : SimpleAdapter(context, interaction) { 23 | 24 | override fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder { 25 | val viewHolder = PinyinEnglishViewHolder(inflater.inflate( 26 | R.layout.pinyin_english_list_item, parent, false), fullListStyle) 27 | 28 | RxView.clicks(viewHolder.itemView.pinyin_list_audio_button).map { 29 | Interaction(viewHolder.itemView.pinyin_list_audio_button.id, data[viewHolder.adapterPosition]) 30 | }.subscribe(interaction) 31 | 32 | return viewHolder 33 | } 34 | } 35 | 36 | class PinyinEnglishViewHolder( 37 | itemView: View, 38 | private val fullListStyle: Boolean 39 | ) : SimpleAdapterViewHolder(itemView) { 40 | 41 | override fun populate(position: Int, value: Pinyin) { 42 | itemView.pinyin_english_list_item_value.text = value.englishTranslationText 43 | itemView.pinyin_english_list_item_phonetic_translation_value.text = value.phoneticScriptText 44 | itemView.pinyin_english_list_item_chinese_character_value.text = value.chineseCharacters 45 | 46 | if (fullListStyle) { 47 | itemView.pinyin_english_list_item_phonetic_translation.visible() 48 | itemView.pinyin_english_list_item_phonetic_translation_value.visible() 49 | itemView.pinyin_english_list_item_chinese_character.visible() 50 | itemView.pinyin_english_list_item_chinese_character_value.visible() 51 | value.audioSrc?.let { itemView.pinyin_list_audio_button.visible() } 52 | ?: itemView.pinyin_list_audio_button.gone() 53 | } else { 54 | itemView.pinyin_english_list_item_phonetic_translation.gone() 55 | itemView.pinyin_english_list_item_phonetic_translation_value.gone() 56 | itemView.pinyin_english_list_item_chinese_character.visible() 57 | itemView.pinyin_english_list_item_chinese_character_value.visible() 58 | itemView.pinyin_list_audio_button.gone() 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/english/PinyinEnglishFragment.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.english 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.ViewModelFactory 9 | import com.consistence.pinyin.app.pinyin.list.PinyinListFragment 10 | import com.consistence.pinyin.app.pinyin.list.PinyinListIntent 11 | import com.consistence.pinyin.app.pinyin.list.PinyinListLayout 12 | import com.consistence.pinyin.domain.pinyin.Pinyin 13 | import com.consistence.pinyin.kit.Interaction 14 | import dagger.android.support.AndroidSupportInjection 15 | import io.reactivex.Observable 16 | import io.reactivex.subjects.PublishSubject 17 | import kotlinx.android.synthetic.main.pinyin_english_fragment.view.* 18 | import javax.inject.Inject 19 | 20 | class PinyinEnglishFragment : PinyinListFragment() { 21 | 22 | @Inject lateinit var viewModelFactory: ViewModelFactory 23 | 24 | private lateinit var adapter: PinyinEnglishAdapter 25 | 26 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 27 | val view = inflater.inflate(R.layout.pinyin_english_fragment, container, false) 28 | 29 | val adapterInteraction: PublishSubject> = PublishSubject.create() 30 | adapter = PinyinEnglishAdapter(context!!, delegate.fullListStyle, adapterInteraction) 31 | view.pinyin_english_fragment_recyclerview.adapter = adapter 32 | 33 | return view 34 | } 35 | 36 | override fun intents(): Observable = Observable.merge( 37 | super.intents(), 38 | adapter.interaction.map { 39 | when (it.id) { 40 | R.id.pinyin_list_audio_button -> PinyinListIntent.PlayAudio(it.data.audioSrc!!) 41 | else -> PinyinListIntent.SelectItem(it.data) 42 | } 43 | } 44 | ) 45 | 46 | override fun inject() { 47 | AndroidSupportInjection.inject(this) 48 | } 49 | 50 | override fun layout(): PinyinListLayout = this 51 | 52 | override fun model(): PinyinEnglishViewModel = getViewModel(viewModelFactory) 53 | 54 | override fun populate(pinyin: List) { 55 | adapter.clear() 56 | adapter.populate(pinyin) 57 | } 58 | 59 | override fun showError() { 60 | } 61 | 62 | companion object { fun newInstance() = PinyinEnglishFragment() } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/english/PinyinEnglishViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.english 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.app.pinyin.list.PinyinListViewModel 5 | import com.consistence.pinyin.domain.pinyin.db.EnglishSearch 6 | import javax.inject.Inject 7 | 8 | class PinyinEnglishViewModel @Inject internal constructor( 9 | private val englishSearch: EnglishSearch, 10 | application: Application 11 | ) : PinyinListViewModel(application) { 12 | 13 | override val defaultSearch = "pinyin" 14 | 15 | override fun search(terms: String) = englishSearch.search(terms) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/phonetic/PinyinPhoneticAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.phonetic 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.domain.pinyin.Pinyin 9 | import com.consistence.pinyin.kit.Interaction 10 | import com.consistence.pinyin.kit.SimpleAdapter 11 | import com.consistence.pinyin.kit.SimpleAdapterViewHolder 12 | import com.consistence.pinyin.kit.visible 13 | import com.consistence.pinyin.kit.gone 14 | import com.jakewharton.rxbinding2.view.RxView 15 | import io.reactivex.subjects.PublishSubject 16 | import kotlinx.android.synthetic.main.pinyin_phonetic_list_item.view.* 17 | 18 | class PinyinPhoneticAdapter( 19 | context: Context, 20 | private val fullListStyle: Boolean, 21 | interaction: PublishSubject> 22 | ) : SimpleAdapter(context, interaction) { 23 | 24 | override fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder { 25 | val viewHolder = PinyinPhoneticViewHolder(inflater.inflate( 26 | R.layout.pinyin_phonetic_list_item, parent, false), fullListStyle) 27 | 28 | RxView.clicks(viewHolder.itemView.pinyin_list_audio_button).map { 29 | Interaction(viewHolder.itemView.pinyin_list_audio_button.id, data[viewHolder.adapterPosition]) 30 | }.subscribe(interaction) 31 | 32 | return viewHolder 33 | } 34 | } 35 | 36 | class PinyinPhoneticViewHolder( 37 | itemView: View, 38 | private val fullListStyle: Boolean 39 | ) : SimpleAdapterViewHolder(itemView) { 40 | 41 | override fun populate(position: Int, value: Pinyin) { 42 | itemView.pinyin_phonetic_list_item_value.text = value.phoneticScriptText 43 | itemView.pinyin_phonetic_list_item_english_translation_value.text = value.englishTranslationText 44 | itemView.pinyin_phonetic_list_item_chinese_character_value.text = value.chineseCharacters 45 | 46 | if (fullListStyle) { 47 | itemView.pinyin_phonetic_list_item_english_translation.visible() 48 | itemView.pinyin_phonetic_list_item_english_translation_value.visible() 49 | itemView.pinyin_phonetic_list_item_chinese_character.visible() 50 | itemView.pinyin_phonetic_list_item_chinese_character_value.visible() 51 | value.audioSrc?.let { itemView.pinyin_list_audio_button.visible() } 52 | ?: itemView.pinyin_list_audio_button.gone() 53 | } else { 54 | itemView.pinyin_phonetic_list_item_english_translation.visible() 55 | itemView.pinyin_phonetic_list_item_english_translation_value.visible() 56 | itemView.pinyin_phonetic_list_item_chinese_character.gone() 57 | itemView.pinyin_phonetic_list_item_chinese_character_value.gone() 58 | itemView.pinyin_list_audio_button.gone() 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/phonetic/PinyinPhoneticFragment.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.phonetic 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.ViewModelFactory 9 | import com.consistence.pinyin.app.pinyin.list.PinyinListFragment 10 | import com.consistence.pinyin.app.pinyin.list.PinyinListIntent 11 | import com.consistence.pinyin.app.pinyin.list.PinyinListLayout 12 | import com.consistence.pinyin.domain.pinyin.Pinyin 13 | import com.consistence.pinyin.kit.Interaction 14 | import dagger.android.support.AndroidSupportInjection 15 | import io.reactivex.Observable 16 | import io.reactivex.subjects.PublishSubject 17 | import kotlinx.android.synthetic.main.pinyin_phonetic_fragment.view.* 18 | import javax.inject.Inject 19 | 20 | class PinyinPhoneticFragment : PinyinListFragment() { 21 | 22 | @Inject lateinit var viewModelFactory: ViewModelFactory 23 | 24 | private lateinit var adapter: PinyinPhoneticAdapter 25 | 26 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 27 | val view = inflater.inflate(R.layout.pinyin_phonetic_fragment, container, false) 28 | 29 | val adapterInteraction: PublishSubject> = PublishSubject.create() 30 | adapter = PinyinPhoneticAdapter(context!!, delegate.fullListStyle, adapterInteraction) 31 | view.pinyin_phonetic_fragment_recyclerview.adapter = adapter 32 | 33 | return view 34 | } 35 | 36 | override fun intents(): Observable = Observable.merge( 37 | super.intents(), 38 | adapter.interaction.map { 39 | when (it.id) { 40 | R.id.pinyin_list_audio_button -> PinyinListIntent.PlayAudio(it.data.audioSrc!!) 41 | else -> PinyinListIntent.SelectItem(it.data) 42 | } 43 | } 44 | ) 45 | 46 | override fun inject() { 47 | AndroidSupportInjection.inject(this) 48 | } 49 | 50 | override fun layout(): PinyinListLayout = this 51 | 52 | override fun model(): PinyinPhoneticViewModel = getViewModel(viewModelFactory) 53 | 54 | override fun populate(pinyin: List) { 55 | adapter.clear() 56 | adapter.populate(pinyin) 57 | } 58 | 59 | override fun showError() { 60 | } 61 | 62 | companion object { fun newInstance() = PinyinPhoneticFragment() } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/pinyin/list/phonetic/PinyinPhoneticViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list.phonetic 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.app.pinyin.list.PinyinListViewModel 5 | import com.consistence.pinyin.domain.pinyin.Pinyin 6 | import com.consistence.pinyin.domain.pinyin.db.PhoneticSearch 7 | import io.reactivex.Single 8 | import javax.inject.Inject 9 | 10 | class PinyinPhoneticViewModel @Inject internal constructor( 11 | private val phoneticSearch: PhoneticSearch, 12 | application: Application 13 | ) : PinyinListViewModel(application) { 14 | 15 | override val defaultSearch = "pinyin" 16 | 17 | override fun search(terms: String): Single> = phoneticSearch.search(terms) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/CreateStudyActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class CreateStudyActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesCreateStudyActivity(): CreateStudyActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/CreateStudyIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.consistence.pinyin.domain.study.Study 5 | import com.memtrip.mxandroid.MxViewIntent 6 | 7 | sealed class CreateStudyIntent : MxViewIntent { 8 | object Init : CreateStudyIntent() 9 | data class InitWithData(val study: Study) : CreateStudyIntent() 10 | object Idle : CreateStudyIntent() 11 | data class EnterEnglishTranslation(val englishTranslation: String) : CreateStudyIntent() 12 | object DeleteStudy : CreateStudyIntent() 13 | data class ConfirmDeleteStudy(val study: Study) : CreateStudyIntent() 14 | data class EnterChinesePhrase(val chinesePhrase: List) : CreateStudyIntent() 15 | data class AddPinyin(val pinyin: Pinyin) : CreateStudyIntent() 16 | object RemovePinyin : CreateStudyIntent() 17 | data class Confirm(val study: Study, val updateMode: Boolean) : CreateStudyIntent() 18 | object GoBack : CreateStudyIntent() 19 | object LoseChangesAndExit : CreateStudyIntent() 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/CreateStudyRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.consistence.pinyin.domain.study.Study 5 | import com.memtrip.mxandroid.MxRenderAction 6 | import com.memtrip.mxandroid.MxViewLayout 7 | import com.memtrip.mxandroid.MxViewRenderer 8 | import javax.inject.Inject 9 | 10 | sealed class CreateStudyRenderAction : MxRenderAction { 11 | object Init : CreateStudyRenderAction() 12 | data class InitWithData(val study: Study) : CreateStudyRenderAction() 13 | object Idle : CreateStudyRenderAction() 14 | data class EnterEnglishTranslation( 15 | val englishTranslation: String = "" 16 | ) : CreateStudyRenderAction() 17 | object DeleteStudy : CreateStudyRenderAction() 18 | object StudyDeleted : CreateStudyRenderAction() 19 | object DoneEnteringChinesePhrase : CreateStudyRenderAction() 20 | data class AddPinyin(val pinyin: Pinyin) : CreateStudyRenderAction() 21 | object RemovePinyin : CreateStudyRenderAction() 22 | object ConfirmPhrase : CreateStudyRenderAction() 23 | object GoBack : CreateStudyRenderAction() 24 | object LoseChangesAndExit : CreateStudyRenderAction() 25 | object Success : CreateStudyRenderAction() 26 | data class ValidationError(val message: String) : CreateStudyRenderAction() 27 | } 28 | 29 | interface CreateStudyLayout : MxViewLayout { 30 | fun enterEnglishTranslation(englishTranslation: String = "") 31 | fun confirmDeleteStudy() 32 | fun updateChinesePhrase(pinyin: List) 33 | fun confirmPhrase(englishTranslation: String, pinyin: List) 34 | fun exit() 35 | fun loseChanges() 36 | fun validationError(message: String) 37 | } 38 | 39 | class CreateStudyRenderer @Inject internal constructor() : MxViewRenderer { 40 | override fun layout(layout: CreateStudyLayout, state: CreateStudyViewState) = when (state.view) { 41 | CreateStudyViewState.View.Idle -> { 42 | } 43 | is CreateStudyViewState.View.EnglishTranslationForm -> { 44 | layout.enterEnglishTranslation(state.englishTranslation) 45 | } 46 | CreateStudyViewState.View.DeleteStudyConfirmation -> { 47 | layout.confirmDeleteStudy() 48 | } 49 | is CreateStudyViewState.View.ChinesePhraseForm -> { 50 | layout.updateChinesePhrase(state.pinyin) 51 | } 52 | is CreateStudyViewState.View.ConfirmPhrase -> { 53 | layout.confirmPhrase(state.englishTranslation, state.pinyin) 54 | } 55 | CreateStudyViewState.View.Exit -> { 56 | layout.exit() 57 | } 58 | CreateStudyViewState.View.LoseChangesConfirmation -> { 59 | layout.loseChanges() 60 | } 61 | CreateStudyViewState.View.Success -> { 62 | layout.exit() 63 | } 64 | is CreateStudyViewState.View.ValidationError -> { 65 | layout.validationError(state.view.message) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/CreateStudyViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.consistence.pinyin.domain.study.Study 5 | import com.memtrip.mxandroid.MxViewState 6 | import com.memtrip.mxandroid.MxViewState.Companion.id 7 | 8 | data class CreateStudyViewState( 9 | val view: View, 10 | val step: Step = Step.INITIAL, 11 | val englishTranslation: String = "", 12 | val pinyin: MutableList = mutableListOf(), 13 | val originalStudy: Study? = null // the study being updated 14 | ) : MxViewState { 15 | sealed class View : MxViewState { 16 | object Idle : View() 17 | object EnglishTranslationForm : View() 18 | object DeleteStudyConfirmation : View() 19 | data class ChinesePhraseForm(val unique: Int = id()) : View() 20 | object ConfirmPhrase : View() 21 | object Exit : View() 22 | object LoseChangesConfirmation : View() 23 | object Success : View() 24 | data class ValidationError(val message: String) : View() 25 | } 26 | 27 | enum class Step { 28 | INITIAL, 29 | ENGLISH_TRANSLATION, 30 | CHINESE_PHRASE, 31 | CONFIRM 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class StudyActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesStudyActivity(): StudyActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.consistence.pinyin.R 7 | import com.consistence.pinyin.domain.study.Study 8 | import com.consistence.pinyin.kit.Interaction 9 | import com.consistence.pinyin.kit.SimpleAdapter 10 | import com.consistence.pinyin.kit.SimpleAdapterViewHolder 11 | import com.jakewharton.rxbinding2.view.RxView 12 | import io.reactivex.subjects.PublishSubject 13 | import kotlinx.android.synthetic.main.study_card_view.view.* 14 | import kotlinx.android.synthetic.main.study_list_item.view.* 15 | 16 | class StudyAdapter( 17 | context: Context, 18 | interaction: PublishSubject> 19 | ) : SimpleAdapter(context, interaction) { 20 | 21 | override fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder { 22 | val viewHolder = StudyViewHolder(inflater.inflate( 23 | R.layout.study_list_item, parent, false)) 24 | 25 | RxView.clicks(viewHolder.itemView.study_list_card.study_card_item_edit).map { 26 | Interaction(viewHolder.itemView.study_list_card.study_card_item_edit.id, data[viewHolder.adapterPosition]) 27 | }.subscribe(interaction) 28 | 29 | return viewHolder 30 | } 31 | } 32 | 33 | class StudyViewHolder(itemView: View) : SimpleAdapterViewHolder(itemView) { 34 | 35 | override fun populate(position: Int, value: Study) { 36 | itemView.study_list_card.populate(value, true) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyCardView.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | 7 | import androidx.cardview.widget.CardView 8 | import androidx.core.content.ContextCompat 9 | import com.consistence.pinyin.R 10 | import com.consistence.pinyin.domain.pinyin.formatChineseCharacterString 11 | import com.consistence.pinyin.domain.pinyin.formatPinyinString 12 | import com.consistence.pinyin.domain.study.Study 13 | import com.consistence.pinyin.kit.gone 14 | import com.consistence.pinyin.kit.visible 15 | import kotlinx.android.synthetic.main.study_card_view.view.* 16 | 17 | class StudyCardView @JvmOverloads constructor( 18 | context: Context, 19 | attrs: AttributeSet? = null, 20 | defStyleAttr: Int = 0 21 | ) : CardView(context, attrs, defStyleAttr) { 22 | 23 | init { 24 | LayoutInflater.from(context).inflate(R.layout.study_card_view, this, true) 25 | setCardBackgroundColor(ContextCompat.getColor(context, R.color.cardBackgroundColor)) 26 | } 27 | 28 | fun populate(study: Study, showTrainIcon: Boolean = false) { 29 | study_card_item_chinese_phrase_value.text = study.pinyin.formatChineseCharacterString() 30 | study_card_item_english_translation_value.text = study.englishTranslation 31 | study_card_item_pinyin_translation_value.text = study.pinyin.formatPinyinString() 32 | 33 | if (showTrainIcon) { 34 | study_card_item_edit.visible() 35 | } else { 36 | study_card_item_edit.gone() 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxViewIntent 5 | 6 | sealed class StudyIntent : MxViewIntent { 7 | object Init : StudyIntent() 8 | object Idle : StudyIntent() 9 | object Refresh : StudyIntent() 10 | object Retry : StudyIntent() 11 | data class TrainPhrase(val study: Study) : StudyIntent() 12 | data class SelectStudy(val study: Study) : StudyIntent() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxRenderAction 5 | import com.memtrip.mxandroid.MxViewLayout 6 | import com.memtrip.mxandroid.MxViewRenderer 7 | import javax.inject.Inject 8 | 9 | sealed class StudyRenderAction : MxRenderAction { 10 | object Idle : StudyRenderAction() 11 | data class Populate(val study: List) : StudyRenderAction() 12 | object NoResults : StudyRenderAction() 13 | object Error : StudyRenderAction() 14 | data class NavigateToStudy(val study: Study) : StudyRenderAction() 15 | data class NavigateTrainPhrase(val study: Study) : StudyRenderAction() 16 | } 17 | 18 | interface StudyLayout : MxViewLayout { 19 | fun noResults() 20 | fun populate(study: List) 21 | fun navigateToStudy(study: Study) 22 | fun navigateToTrainPhrase(study: Study) 23 | } 24 | 25 | class StudyRenderer @Inject internal constructor() : MxViewRenderer { 26 | override fun layout(layout: StudyLayout, state: StudyViewState) = when (state.view) { 27 | StudyViewState.View.Idle -> { 28 | } 29 | is StudyViewState.View.Populate -> { 30 | layout.populate(state.view.study) 31 | } 32 | StudyViewState.View.NoResults -> { 33 | layout.noResults() 34 | } 35 | StudyViewState.View.Error -> { 36 | } 37 | is StudyViewState.View.NavigateToStudy -> { 38 | layout.navigateToStudy(state.view.study) 39 | } 40 | is StudyViewState.View.NavigateToTrainPhrase -> { 41 | layout.navigateToTrainPhrase(state.view.study) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.study.GetStudy 5 | import com.consistence.pinyin.domain.study.db.CountStudy 6 | import com.memtrip.mxandroid.MxViewModel 7 | import io.reactivex.Observable 8 | import io.reactivex.Single 9 | import javax.inject.Inject 10 | 11 | class StudyViewModel @Inject internal constructor( 12 | private val getStudy: GetStudy, 13 | private val countStudy: CountStudy, 14 | application: Application 15 | ) : MxViewModel( 16 | StudyViewState(StudyViewState.View.Idle), 17 | application 18 | ) { 19 | 20 | override fun dispatcher(intent: StudyIntent): Observable = when (intent) { 21 | StudyIntent.Init, StudyIntent.Retry, StudyIntent.Refresh -> getStudy() 22 | StudyIntent.Idle -> Observable.just(StudyRenderAction.Idle) 23 | is StudyIntent.TrainPhrase -> Observable.just(StudyRenderAction.NavigateTrainPhrase(intent.study)) 24 | is StudyIntent.SelectStudy -> Observable.just(StudyRenderAction.NavigateToStudy(intent.study)) 25 | } 26 | 27 | override fun reducer(previousState: StudyViewState, renderAction: StudyRenderAction): StudyViewState = when (renderAction) { 28 | StudyRenderAction.Idle -> previousState.copy(view = StudyViewState.View.Idle) 29 | is StudyRenderAction.Populate -> previousState.copy(view = StudyViewState.View.Populate(renderAction.study)) 30 | StudyRenderAction.NoResults -> previousState.copy(view = StudyViewState.View.NoResults) 31 | StudyRenderAction.Error -> previousState.copy(view = StudyViewState.View.Error) 32 | is StudyRenderAction.NavigateToStudy -> previousState.copy(view = StudyViewState.View.NavigateToStudy(renderAction.study)) 33 | is StudyRenderAction.NavigateTrainPhrase -> previousState.copy(view = StudyViewState.View.NavigateToTrainPhrase(renderAction.study)) 34 | } 35 | 36 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 37 | intents.ofType(StudyIntent.Init.javaClass).take(1), 38 | intents.filter { 39 | !StudyIntent.Init.javaClass.isInstance(it) 40 | } 41 | ) 42 | 43 | private fun getStudy(): Observable { 44 | return countStudy.count().flatMap { count -> 45 | if (count > 0) { 46 | getStudy.get().flatMap { 47 | Single.just(StudyRenderAction.Populate(it)) 48 | } 49 | } else { 50 | Single.just(StudyRenderAction.NoResults) 51 | } 52 | }.onErrorReturnItem(StudyRenderAction.Error).toObservable() 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/study/StudyViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.study 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxViewState 5 | 6 | data class StudyViewState(val view: View) : MxViewState { 7 | sealed class View : MxViewState { 8 | object Idle : View() 9 | data class Populate(val study: List) : View() 10 | object NoResults : View() 11 | object Error : View() 12 | data class NavigateToStudy(val study: Study) : View() 13 | data class NavigateToTrainPhrase(val study: Study) : View() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.consistence.pinyin.R 7 | import com.consistence.pinyin.ViewModelFactory 8 | import com.consistence.pinyin.domain.study.Study 9 | import com.consistence.pinyin.kit.gone 10 | import com.consistence.pinyin.kit.visible 11 | import com.jakewharton.rxbinding2.view.RxView 12 | import com.memtrip.mxandroid.MxViewActivity 13 | import dagger.android.AndroidInjection 14 | import io.reactivex.Observable 15 | import kotlinx.android.synthetic.main.train_random_activity.* 16 | import javax.inject.Inject 17 | 18 | class RandomPhraseActivity : MxViewActivity(), RandomPhraseLayout { 19 | 20 | @Inject 21 | lateinit var viewModelFactory: ViewModelFactory 22 | 23 | @Inject 24 | lateinit var render: RandomPhraseRenderer 25 | 26 | private lateinit var adapter: RandomPhraseResultsAdapter 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.train_random_activity) 31 | 32 | train_random_toolbar.title = getString(R.string.train_random_title) 33 | train_random_toolbar.setNavigationIcon(R.drawable.ic_arrow_back) 34 | train_random_toolbar.setNavigationOnClickListener { 35 | finish() 36 | } 37 | 38 | adapter = RandomPhraseResultsAdapter(this) 39 | train_random_results_recycler_view.adapter = adapter 40 | } 41 | 42 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 43 | super.onActivityResult(requestCode, resultCode, data) 44 | if (forceQuit(data)) { 45 | finish() 46 | } else { 47 | model().publish(RandomPhraseIntent.Result(result(data))) 48 | } 49 | } 50 | 51 | override fun intents(): Observable = Observable.merge( 52 | Observable.just(RandomPhraseIntent.Init), 53 | RxView.clicks(train_random_start_cta).map { 54 | RandomPhraseIntent.Start(studyLimit()) 55 | } 56 | ) 57 | 58 | // region RandomPhraseLayout 59 | override fun next(study: Study) { 60 | train_random_start_cta.gone() 61 | model().publish(RandomPhraseIntent.Idle) 62 | startActivityForResult(TrainPhraseActivity.newIntent(this, study), 0) 63 | } 64 | 65 | override fun finished(results: List>) { 66 | train_random_start_group.gone() 67 | train_random_results_recycler_view.visible() 68 | 69 | train_random_toolbar.title = getString(R.string.train_random_results_title, results.count { it.second }, results.size) 70 | adapter.populate(results) 71 | } 72 | // endregion 73 | 74 | private fun result(intent: Intent?): Boolean { 75 | return intent?.getBooleanExtra(TrainPhraseActivity.RESULT_STATUS, false) ?: false 76 | } 77 | 78 | private fun forceQuit(intent: Intent?): Boolean { 79 | return intent?.getBooleanExtra(TrainPhraseActivity.RESULT_FORCE_QUIT, false) ?: false 80 | } 81 | 82 | private fun studyLimit(): Int = when (train_random_start_limit_tab_layout.selectedTabPosition) { 83 | 0 -> 5 84 | 1 -> 10 85 | 2 -> 20 86 | 3 -> 50 87 | else -> 20 88 | } 89 | 90 | override fun inject() { 91 | AndroidInjection.inject(this) 92 | } 93 | 94 | override fun layout(): RandomPhraseLayout = this 95 | 96 | override fun render(): RandomPhraseRenderer = render 97 | 98 | override fun model(): RandomPhraseViewModel = getViewModel(viewModelFactory) 99 | 100 | companion object { 101 | 102 | fun newIntent(context: Context): Intent { 103 | return Intent(context, RandomPhraseActivity::class.java) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class RandomPhraseActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesRandomPhraseActivity(): RandomPhraseActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.memtrip.mxandroid.MxViewIntent 4 | 5 | sealed class RandomPhraseIntent : MxViewIntent { 6 | object Idle : RandomPhraseIntent() 7 | object Init : RandomPhraseIntent() 8 | data class Start(val limit: Int) : RandomPhraseIntent() 9 | data class Result(val correct: Boolean) : RandomPhraseIntent() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxRenderAction 5 | import com.memtrip.mxandroid.MxViewLayout 6 | import com.memtrip.mxandroid.MxViewRenderer 7 | import javax.inject.Inject 8 | 9 | sealed class RandomPhraseRenderAction : MxRenderAction { 10 | object Idle : RandomPhraseRenderAction() 11 | data class PickRandomPhrases(val study: List) : RandomPhraseRenderAction() 12 | data class Result(val correct: Boolean) : RandomPhraseRenderAction() 13 | } 14 | 15 | interface RandomPhraseLayout : MxViewLayout { 16 | fun next(study: Study) 17 | fun finished(results: List>) 18 | } 19 | 20 | class RandomPhraseRenderer @Inject internal constructor() : MxViewRenderer { 21 | override fun layout(layout: RandomPhraseLayout, state: RandomPhraseViewState) = when (state.view) { 22 | RandomPhraseViewState.View.Idle -> { 23 | } 24 | is RandomPhraseViewState.View.Next -> { 25 | layout.next(state.view.study) 26 | } 27 | is RandomPhraseViewState.View.Finished -> { 28 | layout.finished(state.results) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseResultsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.core.content.ContextCompat 7 | import com.consistence.pinyin.R 8 | import com.consistence.pinyin.domain.study.Study 9 | import com.consistence.pinyin.kit.SimpleAdapter 10 | import com.consistence.pinyin.kit.SimpleAdapterViewHolder 11 | import io.reactivex.subjects.PublishSubject 12 | 13 | import kotlinx.android.synthetic.main.train_random_results_list_item.view.* 14 | 15 | class RandomPhraseResultsAdapter( 16 | context: Context 17 | ) : SimpleAdapter>(context, PublishSubject.create()) { 18 | 19 | override fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder> { 20 | return StudyViewHolder(inflater.inflate( 21 | R.layout.train_random_results_list_item, parent, false)) 22 | } 23 | } 24 | 25 | class StudyViewHolder(itemView: View) : SimpleAdapterViewHolder>(itemView) { 26 | 27 | override fun populate(position: Int, value: Pair) { 28 | val (study, correct) = value 29 | 30 | itemView.train_random_results_list_card.populate(study, false) 31 | 32 | if (correct) { 33 | itemView.train_random_results_item.setBackgroundColor( 34 | ContextCompat.getColor(itemView.context, R.color.colorPositive)) 35 | } else { 36 | itemView.train_random_results_item.setBackgroundColor( 37 | ContextCompat.getColor(itemView.context, R.color.colorAccent)) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.study.GetRandomStudy 5 | import com.memtrip.mxandroid.MxViewModel 6 | import io.reactivex.Observable 7 | import javax.inject.Inject 8 | 9 | class RandomPhraseViewModel @Inject internal constructor( 10 | private val getRandomStudy: GetRandomStudy, 11 | application: Application 12 | ) : MxViewModel( 13 | RandomPhraseViewState(RandomPhraseViewState.View.Idle), 14 | application 15 | ) { 16 | override fun dispatcher(intent: RandomPhraseIntent): Observable = when (intent) { 17 | RandomPhraseIntent.Idle -> Observable.just(RandomPhraseRenderAction.Idle) 18 | RandomPhraseIntent.Init -> Observable.just(RandomPhraseRenderAction.Idle) 19 | is RandomPhraseIntent.Start -> randomStudy(intent.limit) 20 | is RandomPhraseIntent.Result -> Observable.just(RandomPhraseRenderAction.Result(intent.correct)) 21 | } 22 | 23 | override fun reducer(previousState: RandomPhraseViewState, renderAction: RandomPhraseRenderAction): RandomPhraseViewState = when (renderAction) { 24 | RandomPhraseRenderAction.Idle -> previousState.copy( 25 | view = RandomPhraseViewState.View.Idle 26 | ) 27 | is RandomPhraseRenderAction.PickRandomPhrases -> previousState.copy( 28 | view = RandomPhraseViewState.View.Next(renderAction.study[0]), 29 | study = renderAction.study 30 | ) 31 | is RandomPhraseRenderAction.Result -> previousState.copy( 32 | view = studyNextPhrase(previousState), 33 | results = previousState.results.plus( 34 | Pair(previousState.study[previousState.currentPosition], renderAction.correct)), 35 | currentPosition = incrementCurrentPosition(previousState) 36 | ) 37 | } 38 | 39 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 40 | intents.ofType(RandomPhraseIntent.Init.javaClass).take(1), 41 | intents.filter { 42 | !RandomPhraseIntent.Init.javaClass.isInstance(it) 43 | } 44 | ) 45 | 46 | private fun randomStudy(limit: Int): Observable { 47 | return getRandomStudy.random(limit).map { 48 | RandomPhraseRenderAction.PickRandomPhrases(it) 49 | }.toObservable() 50 | } 51 | 52 | private fun incrementCurrentPosition(state: RandomPhraseViewState): Int { 53 | return if (state.currentPosition == state.study.size - 1) { 54 | state.currentPosition 55 | } else { 56 | state.currentPosition.inc() 57 | } 58 | } 59 | 60 | private fun studyNextPhrase(state: RandomPhraseViewState): RandomPhraseViewState.View { 61 | return if (state.currentPosition == state.study.size - 1) { 62 | RandomPhraseViewState.View.Finished 63 | } else { 64 | RandomPhraseViewState.View.Next(state.study[state.currentPosition.inc()]) 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/RandomPhraseViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxViewState 5 | 6 | data class RandomPhraseViewState( 7 | val view: View, 8 | val study: List = listOf(), 9 | val currentPosition: Int = 0, 10 | val results: List> = listOf() 11 | ) : MxViewState { 12 | sealed class View : MxViewState { 13 | object Idle : View() 14 | data class Next(val study: Study) : View() 15 | object Finished : View() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/TrainPhraseActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class TrainPhraseActivityModule { 8 | @ContributesAndroidInjector 9 | internal abstract fun contributesTrainPhraseActivity(): TrainPhraseActivity 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/TrainPhraseIntent.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.consistence.pinyin.domain.study.Study 4 | import com.memtrip.mxandroid.MxViewIntent 5 | 6 | sealed class TrainPhraseIntent : MxViewIntent { 7 | data class Init(val study: Study) : TrainPhraseIntent() 8 | data class AnswerEnglishToChinese( 9 | val translation: String, 10 | val study: Study 11 | ) : TrainPhraseIntent() 12 | data class AnswerChineseToEnglish( 13 | val translation: String, 14 | val study: Study 15 | ) : TrainPhraseIntent() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/TrainPhraseRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.consistence.pinyin.domain.study.Study 5 | import com.memtrip.mxandroid.MxRenderAction 6 | import com.memtrip.mxandroid.MxViewLayout 7 | import com.memtrip.mxandroid.MxViewRenderer 8 | import javax.inject.Inject 9 | 10 | sealed class TrainPhraseRenderAction : MxRenderAction { 11 | data class EnglishQuestion( 12 | val englishQuestion: String 13 | ) : TrainPhraseRenderAction() 14 | data class ChineseQuestion( 15 | val chineseQuestion: List 16 | ) : TrainPhraseRenderAction() 17 | data class Correct( 18 | val study: Study 19 | ) : TrainPhraseRenderAction() 20 | data class IncorrectEnglish( 21 | val englishTranslation: String, 22 | val answer: Study 23 | ) : TrainPhraseRenderAction() 24 | data class IncorrectChinese( 25 | val chineseTranslation: String, 26 | val answer: Study 27 | ) : TrainPhraseRenderAction() 28 | } 29 | 30 | interface TrainPhraseLayout : MxViewLayout { 31 | fun englishQuestion(englishTranslation: String) 32 | fun chineseQuestion(chineseQuestion: List) 33 | fun correct(study: Study) 34 | fun incorrectEnglish(englishTranslation: String, answer: Study) 35 | fun incorrectChinese(chineseTranslation: String, answer: Study) 36 | } 37 | 38 | class TrainPhraseRenderer @Inject internal constructor() : MxViewRenderer { 39 | override fun layout(layout: TrainPhraseLayout, state: TrainPhraseViewState) = when (state.view) { 40 | TrainPhraseViewState.View.Idle -> { 41 | } 42 | is TrainPhraseViewState.View.ChineseQuestion -> { 43 | layout.chineseQuestion(state.view.chineseQuestion) 44 | } 45 | is TrainPhraseViewState.View.EnglishQuestion -> { 46 | layout.englishQuestion(state.view.englishQuestion) 47 | } 48 | is TrainPhraseViewState.View.Correct -> { 49 | layout.correct(state.view.study) 50 | } 51 | is TrainPhraseViewState.View.IncorrectEnglish -> { 52 | layout.incorrectEnglish(state.view.englishTranslation, state.view.answer) 53 | } 54 | is TrainPhraseViewState.View.IncorrectChinese -> { 55 | layout.incorrectChinese(state.view.chineseTranslation, state.view.answer) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/TrainPhraseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.domain.pinyin.formatChineseCharacterString 5 | import com.consistence.pinyin.domain.study.Study 6 | import com.memtrip.mxandroid.MxViewModel 7 | import io.reactivex.Observable 8 | import javax.inject.Inject 9 | 10 | class TrainPhraseViewModel @Inject internal constructor( 11 | application: Application 12 | ) : MxViewModel( 13 | TrainPhraseViewState(TrainPhraseViewState.View.Idle), 14 | application 15 | ) { 16 | override fun dispatcher(intent: TrainPhraseIntent): Observable = when (intent) { 17 | is TrainPhraseIntent.Init -> pickQuestion(intent.study) 18 | is TrainPhraseIntent.AnswerEnglishToChinese -> checkChineseAnswer(intent.translation, intent.study) 19 | is TrainPhraseIntent.AnswerChineseToEnglish -> checkEnglishAnswer(intent.translation, intent.study) 20 | } 21 | 22 | override fun reducer(previousState: TrainPhraseViewState, renderAction: TrainPhraseRenderAction): TrainPhraseViewState = when (renderAction) { 23 | is TrainPhraseRenderAction.EnglishQuestion -> previousState.copy( 24 | view = TrainPhraseViewState.View.EnglishQuestion(renderAction.englishQuestion) 25 | ) 26 | is TrainPhraseRenderAction.ChineseQuestion -> previousState.copy( 27 | view = TrainPhraseViewState.View.ChineseQuestion(renderAction.chineseQuestion) 28 | ) 29 | is TrainPhraseRenderAction.Correct -> previousState.copy( 30 | view = TrainPhraseViewState.View.Correct(renderAction.study) 31 | ) 32 | is TrainPhraseRenderAction.IncorrectEnglish -> previousState.copy( 33 | view = TrainPhraseViewState.View.IncorrectEnglish( 34 | renderAction.englishTranslation, renderAction.answer) 35 | ) 36 | is TrainPhraseRenderAction.IncorrectChinese -> previousState.copy( 37 | view = TrainPhraseViewState.View.IncorrectChinese( 38 | renderAction.chineseTranslation, renderAction.answer) 39 | ) 40 | } 41 | 42 | override fun filterIntents(intents: Observable): Observable = Observable.merge( 43 | intents.ofType(TrainPhraseIntent.Init::class.java).take(1), 44 | intents.filter { 45 | !TrainPhraseIntent.Init::class.java.isInstance(it) 46 | } 47 | ) 48 | 49 | private fun pickQuestion(study: Study): Observable { 50 | return Observable.just(if (useChinese()) { 51 | TrainPhraseRenderAction.ChineseQuestion(study.pinyin) 52 | } else { 53 | TrainPhraseRenderAction.EnglishQuestion(study.englishTranslation) 54 | }) 55 | } 56 | 57 | private fun useChinese(): Boolean { 58 | return (Math.random() * 50 + 1).toInt() % 2 == 0 59 | } 60 | 61 | private fun checkEnglishAnswer( 62 | englishTranslation: String, 63 | study: Study 64 | ): Observable { 65 | return Observable.just( 66 | if (englishTranslation.toLowerCase().trim() == study.englishTranslation.toLowerCase().trim()) { 67 | TrainPhraseRenderAction.Correct(study) 68 | } else { 69 | TrainPhraseRenderAction.IncorrectEnglish(englishTranslation, study) 70 | } 71 | ) 72 | } 73 | 74 | private fun checkChineseAnswer( 75 | chineseTranslation: String, 76 | study: Study 77 | ): Observable { 78 | return Observable.just( 79 | if (chineseTranslation == study.pinyin.formatChineseCharacterString()) { 80 | TrainPhraseRenderAction.Correct(study) 81 | } else { 82 | TrainPhraseRenderAction.IncorrectChinese( 83 | chineseTranslation, 84 | study) 85 | } 86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/app/train/TrainPhraseViewState.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.train 2 | 3 | import com.consistence.pinyin.domain.pinyin.Pinyin 4 | import com.consistence.pinyin.domain.study.Study 5 | import com.memtrip.mxandroid.MxViewState 6 | 7 | data class TrainPhraseViewState(val view: View) : MxViewState { 8 | sealed class View : MxViewState { 9 | object Idle : View() 10 | data class ChineseQuestion( 11 | val chineseQuestion: List 12 | ) : View() 13 | data class EnglishQuestion( 14 | val englishQuestion: String 15 | ) : View() 16 | data class Correct( 17 | val study: Study 18 | ) : View() 19 | data class IncorrectEnglish( 20 | val englishTranslation: String, 21 | val answer: Study 22 | ) : View() 23 | data class IncorrectChinese( 24 | val chineseTranslation: String, 25 | val answer: Study 26 | ) : View() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/audio/PinyinAudio.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.audio 2 | 3 | import android.content.Intent 4 | import com.consistence.pinyin.BuildConfig 5 | 6 | import com.memtrip.exoeasy.AudioResource 7 | import com.memtrip.exoeasy.AudioResourceIntent 8 | 9 | import com.memtrip.exoeasy.player.StreamingService 10 | 11 | data class PinyinAudio( 12 | override val url: String, 13 | override val userAgent: String = "${BuildConfig.VERSION_NAME}/${BuildConfig.VERSION_CODE}", 14 | override val trackProgress: Boolean = false 15 | ) : AudioResource 16 | 17 | class PinyinAudioIntent : AudioResourceIntent() { 18 | 19 | override fun get(intent: Intent): PinyinAudio { 20 | return PinyinAudio( 21 | intent.getStringExtra(HTTP_AUDIO_STREAM_URL), 22 | intent.getStringExtra(HTTP_AUDIO_STREAM_USER_AGENT), 23 | intent.getBooleanExtra(HTTP_AUDIO_STREAM_TRACK_PROGRESS, false) 24 | ) 25 | } 26 | } 27 | 28 | class PinyinStreamingService : StreamingService() { 29 | 30 | override fun audioResourceIntent(): PinyinAudioIntent = PinyinAudioIntent() 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/audio/PlayPinyinAudio.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.audio 2 | 3 | import android.content.Context 4 | 5 | import com.memtrip.exoeasy.AudioStreamController 6 | import com.memtrip.exoeasy.NotificationInfo 7 | import com.memtrip.exoeasy.broadcast.PlayBackState 8 | import com.memtrip.exoeasy.broadcast.PlayBackStateUpdates 9 | 10 | interface PlayPinyinAudio { 11 | fun attach(context: Context) 12 | fun detach(context: Context) 13 | fun playPinyinAudio(src: String, context: Context) 14 | } 15 | 16 | class PlayPinyAudioInPresenter : PlayPinyinAudio { 17 | 18 | private var playBackStateUpdates: PlayBackStateUpdates? = null 19 | private var pinyinAudioPlaying = false 20 | 21 | override fun attach(context: Context) { 22 | playBackStateUpdates?.register(context) 23 | } 24 | 25 | override fun detach(context: Context) { 26 | playBackStateUpdates?.unregister(context) 27 | } 28 | 29 | override fun playPinyinAudio(src: String, context: Context) { 30 | 31 | if (!pinyinAudioPlaying) { 32 | 33 | val pinyinAudio = PinyinAudio(src) 34 | 35 | playBackStateUpdates = PlayBackStateUpdates(pinyinAudio) 36 | 37 | playBackStateUpdates!!.playBackStateChanges().subscribe { 38 | pinyinAudioPlaying = isPlaying(it) 39 | } 40 | 41 | AudioStreamController( 42 | pinyinAudio, 43 | PinyinAudioIntent(), 44 | NotificationInfo("", "", null), 45 | PinyinStreamingService::class.java, 46 | context 47 | ).play() 48 | } 49 | } 50 | 51 | private fun isPlaying(state: PlayBackState): Boolean = when (state) { 52 | PlayBackState.Play -> true 53 | PlayBackState.Completed -> false 54 | PlayBackState.Buffering -> true 55 | PlayBackState.Pause -> false 56 | PlayBackState.Stop -> false 57 | is PlayBackState.Progress -> false 58 | is PlayBackState.BufferingError -> false 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/Database.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain 2 | 3 | import android.app.Application 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.consistence.pinyin.domain.pinyin.db.PinyinDao 8 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 9 | import com.consistence.pinyin.domain.study.db.StudyDao 10 | import com.consistence.pinyin.domain.study.db.StudyEntity 11 | import dagger.Module 12 | import dagger.Provides 13 | import javax.inject.Singleton 14 | 15 | @Database(entities = [PinyinEntity::class, StudyEntity::class], version = 3, exportSchema = false) 16 | abstract class AppDatabase : RoomDatabase() { 17 | abstract fun pinyinDao(): PinyinDao 18 | abstract fun studyDao(): StudyDao 19 | } 20 | 21 | @Module 22 | class DatabaseModule { 23 | 24 | @Provides @Singleton 25 | fun appDatabase(application: Application): AppDatabase { 26 | return Room.databaseBuilder(application, AppDatabase::class.java, "pingyin") 27 | .fallbackToDestructiveMigration() 28 | .build() 29 | } 30 | 31 | @Provides @Singleton 32 | fun pinyinDao(appDatabase: AppDatabase): PinyinDao { 33 | return appDatabase.pinyinDao() 34 | } 35 | 36 | @Provides @Singleton 37 | fun studoDao(appDatabase: AppDatabase): StudyDao { 38 | return appDatabase.studyDao() 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/Network.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain 2 | 3 | import com.consistence.pinyin.domain.pinyin.api.PinyinApi 4 | import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 5 | 6 | import com.squareup.moshi.Moshi 7 | 8 | import dagger.Module 9 | import dagger.Provides 10 | import io.reactivex.Scheduler 11 | import io.reactivex.android.schedulers.AndroidSchedulers 12 | import io.reactivex.schedulers.Schedulers 13 | import okhttp3.OkHttpClient 14 | import okhttp3.logging.HttpLoggingInterceptor 15 | import retrofit2.Converter 16 | import retrofit2.Retrofit 17 | import retrofit2.converter.moshi.MoshiConverterFactory 18 | import java.util.concurrent.TimeUnit 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | class NetworkModule { 23 | 24 | @Provides 25 | @Singleton 26 | fun schedules(): SchedulerProvider = object : SchedulerProvider { 27 | override fun main(): Scheduler { 28 | return AndroidSchedulers.mainThread() 29 | } 30 | 31 | override fun thread(): Scheduler { 32 | return Schedulers.io() 33 | } 34 | } 35 | 36 | @Singleton @Provides 37 | fun okhttpClient(): OkHttpClient { 38 | return OkHttpClient.Builder() 39 | .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) 40 | .connectTimeout(10, TimeUnit.SECONDS) 41 | .readTimeout(10, TimeUnit.SECONDS) 42 | .writeTimeout(10, TimeUnit.SECONDS) 43 | .build() 44 | } 45 | 46 | @Singleton @Provides 47 | fun moshi(): Moshi { 48 | return Moshi.Builder() 49 | .build() 50 | } 51 | 52 | @Singleton @Provides 53 | fun converterFactory(moshi: Moshi): Converter.Factory = MoshiConverterFactory.create(moshi) 54 | 55 | @Singleton @Provides 56 | fun retrofit(httpClient: OkHttpClient, converterFactory: Converter.Factory): Retrofit { 57 | return Retrofit.Builder() 58 | .baseUrl("http://pinyin.consistence.io/") 59 | .client(httpClient) 60 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 61 | .addConverterFactory(converterFactory) 62 | .build() 63 | } 64 | } 65 | 66 | @Module 67 | class ApiModule { 68 | 69 | @Singleton 70 | @Provides 71 | internal fun pinyinApi(retrofit: Retrofit): PinyinApi = retrofit.create(PinyinApi::class.java) 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/SchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain 2 | 3 | import io.reactivex.Scheduler 4 | 5 | interface SchedulerProvider { 6 | fun main(): Scheduler 7 | fun thread(): Scheduler 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/FetchAndSavePinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin 2 | 3 | import com.consistence.pinyin.domain.pinyin.api.FetchPinyin 4 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 5 | import com.consistence.pinyin.domain.pinyin.db.SavePinyin 6 | import io.reactivex.Single 7 | 8 | import javax.inject.Inject 9 | 10 | class FetchAndSavePinyin @Inject internal constructor( 11 | private val fetchPinyin: FetchPinyin, 12 | private val savePinyin: SavePinyin 13 | ) { 14 | 15 | fun save(): Single> { 16 | return fetchPinyin.values().flatMap { pinyinWrapper -> 17 | savePinyin.insert(pinyinWrapper.pinyin) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/Pinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class Pinyin( 8 | val uid: Int, 9 | val sourceUrl: String, 10 | val phoneticScriptText: String, 11 | val romanLetterText: String, 12 | val audioSrc: String?, 13 | val englishTranslationText: String, 14 | val chineseCharacters: String, 15 | val characterImageSrc: String 16 | ) : Parcelable 17 | 18 | fun List.formatChineseCharacterString(): String { 19 | return joinToString("") { 20 | it.chineseCharacters 21 | } 22 | } 23 | 24 | fun List.formatPinyinString(): String { 25 | return joinToString(" ") { 26 | it.phoneticScriptText 27 | } 28 | } 29 | 30 | fun List.pinyinUidForDatabase(): String { 31 | return joinToString("-") { 32 | it.uid.toString() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/api/FetchPinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.api 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import io.reactivex.Single 5 | import retrofit2.Retrofit 6 | import javax.inject.Inject 7 | 8 | class FetchPinyin @Inject internal constructor( 9 | retrofit: Retrofit, 10 | private val schedulerProvider: SchedulerProvider 11 | ) { 12 | 13 | private val api = retrofit.create(PinyinApi::class.java) 14 | 15 | fun values(): Single { 16 | return api.pinyin 17 | .subscribeOn(schedulerProvider.thread()) 18 | .observeOn(schedulerProvider.main()) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/api/PinyinApi.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.api 2 | 3 | import io.reactivex.Single 4 | import retrofit2.http.GET 5 | 6 | internal interface PinyinApi { 7 | 8 | @get:GET("/pinyin/") 9 | val pinyin: Single 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/api/PinyinJson.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.api 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class PinyinJson( 7 | val sourceUrl: String, 8 | val phoneticScriptText: String, 9 | val romanLetterText: String, 10 | val audioSrc: String?, 11 | val englishTranslationText: String, 12 | val chineseCharacters: String, 13 | val characterImageSrc: String 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/api/PinyinWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.api 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class PinyinWrapper(val pinyin: List) -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/CharacterSearch.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import io.reactivex.Single 6 | import javax.inject.Inject 7 | 8 | class CharacterSearch @Inject internal constructor( 9 | private val pinyinDao: PinyinDao, 10 | private val schedulerProvider: SchedulerProvider 11 | ) { 12 | 13 | fun search(terms: String): Single> { 14 | return Single.fromCallable { pinyinDao.characterSearch("$terms%") } 15 | .observeOn(schedulerProvider.main()) 16 | .subscribeOn(schedulerProvider.thread()) 17 | .map { entities -> 18 | entities.map { 19 | Pinyin( 20 | it.uid, 21 | it.sourceUrl, 22 | it.phoneticScriptText, 23 | it.romanLetterText, 24 | it.audioSrc, 25 | it.englishTranslationText, 26 | it.chineseCharacters, 27 | it.characterImageSrc 28 | ) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/CountPinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import io.reactivex.Single 5 | import javax.inject.Inject 6 | 7 | class CountPinyin @Inject internal constructor( 8 | private val pinyinDao: PinyinDao, 9 | private val schedulerProvider: SchedulerProvider 10 | ) { 11 | 12 | fun count(): Single { 13 | return Single.fromCallable { pinyinDao.count() } 14 | .observeOn(schedulerProvider.main()) 15 | .subscribeOn(schedulerProvider.thread()) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/EnglishSearch.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import io.reactivex.Single 6 | import javax.inject.Inject 7 | 8 | class EnglishSearch @Inject internal constructor( 9 | private val pinyinDao: PinyinDao, 10 | private val schedulerProvider: SchedulerProvider 11 | ) { 12 | 13 | fun search(terms: String): Single> { 14 | return Single.fromCallable { pinyinDao.englishSearch("%$terms%") } 15 | .observeOn(schedulerProvider.main()) 16 | .subscribeOn(schedulerProvider.thread()) 17 | .map { entities -> 18 | entities.map { 19 | Pinyin( 20 | it.uid, 21 | it.sourceUrl, 22 | it.phoneticScriptText, 23 | it.romanLetterText, 24 | it.audioSrc, 25 | it.englishTranslationText, 26 | it.chineseCharacters, 27 | it.characterImageSrc 28 | ) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/GetPinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import io.reactivex.Single 6 | import javax.inject.Inject 7 | 8 | class GetPinyin @Inject internal constructor( 9 | private val pinyinDao: PinyinDao, 10 | private val schedulerProvider: SchedulerProvider 11 | ) { 12 | 13 | fun byListOfUid(listOfUid: List): Single> { 14 | return Single.fromCallable { pinyinDao.getByUid(listOfUid) } 15 | .observeOn(schedulerProvider.main()) 16 | .subscribeOn(schedulerProvider.thread()) 17 | .map { entities -> 18 | entities.asSequence().map { 19 | Pinyin( 20 | it.uid, 21 | it.sourceUrl, 22 | it.phoneticScriptText, 23 | it.romanLetterText, 24 | it.audioSrc, 25 | it.englishTranslationText, 26 | it.chineseCharacters, 27 | it.characterImageSrc 28 | ) 29 | }.sortedWith(Comparator { left, right -> 30 | Integer.compare(listOfUid.indexOf(left.uid), listOfUid.indexOf(right.uid)) 31 | }).toList() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/PhoneticSearch.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import io.reactivex.Single 6 | import javax.inject.Inject 7 | 8 | class PhoneticSearch @Inject internal constructor( 9 | private val pinyinDao: PinyinDao, 10 | private val schedulerProvider: SchedulerProvider 11 | ) { 12 | 13 | fun search(terms: String): Single> { 14 | return Single.fromCallable { pinyinDao.phoneticSearch("$terms%") } 15 | .observeOn(schedulerProvider.main()) 16 | .subscribeOn(schedulerProvider.thread()) 17 | .map { entities -> 18 | entities.map { 19 | Pinyin( 20 | it.uid, 21 | it.sourceUrl, 22 | it.phoneticScriptText, 23 | it.romanLetterText, 24 | it.audioSrc, 25 | it.englishTranslationText, 26 | it.chineseCharacters, 27 | it.characterImageSrc 28 | ) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/PinyinDao.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | 7 | @Dao 8 | interface PinyinDao { 9 | 10 | @Query("SELECT * FROM Pinyin WHERE romanLetterText LIKE :terms ORDER BY romanLetterText ASC LIMIT 0, 100") 11 | fun phoneticSearch(terms: String): List 12 | 13 | @Query("SELECT * FROM Pinyin WHERE chineseCharacters LIKE :terms ORDER BY chineseCharacters ASC LIMIT 0, 100") 14 | fun characterSearch(terms: String): List 15 | 16 | @Query("SELECT * FROM Pinyin WHERE englishTranslationText LIKE :terms ORDER BY englishTranslationText ASC LIMIT 0, 100") 17 | fun englishSearch(terms: String): List 18 | 19 | @Query("SELECT * FROM Pinyin WHERE uid IN(:listOfUid)") 20 | fun getByUid(listOfUid: List): List 21 | 22 | @Insert 23 | fun insertAll(pinyin: List) 24 | 25 | @Query("SELECT COUNT(*) FROM Pinyin") 26 | fun count(): Int 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/PinyinEntity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "Pinyin") 8 | data class PinyinEntity( 9 | @ColumnInfo(name = "sourceUrl") val sourceUrl: String, 10 | @ColumnInfo(name = "phoneticScriptText") val phoneticScriptText: String, 11 | @ColumnInfo(name = "romanLetterText") val romanLetterText: String, 12 | @ColumnInfo(name = "audioSrc") val audioSrc: String?, 13 | @ColumnInfo(name = "englishTranslationText") val englishTranslationText: String, 14 | @ColumnInfo(name = "chineseCharacters") val chineseCharacters: String, 15 | @ColumnInfo(name = "characterImageSrc") val characterImageSrc: String, 16 | @PrimaryKey(autoGenerate = true) val uid: Int = 0 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/pinyin/db/SavePinyin.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.pinyin.db 2 | 3 | import com.consistence.pinyin.domain.pinyin.api.PinyinJson 4 | import com.consistence.pinyin.domain.SchedulerProvider 5 | import io.reactivex.Completable 6 | import io.reactivex.Single 7 | import javax.inject.Inject 8 | 9 | class SavePinyin @Inject internal constructor( 10 | private val pinyinDao: PinyinDao, 11 | private val schedulerProvider: SchedulerProvider 12 | ) { 13 | 14 | fun insert(pinyin: List): Single> { 15 | 16 | val pinyinEntities = pinyin.map { 17 | PinyinEntity( 18 | it.sourceUrl, 19 | it.phoneticScriptText, 20 | it.romanLetterText, 21 | it.audioSrc, 22 | it.englishTranslationText, 23 | it.chineseCharacters, 24 | it.characterImageSrc) 25 | } 26 | 27 | return Completable 28 | .fromAction { pinyinDao.insertAll(pinyinEntities) } 29 | .observeOn(schedulerProvider.main()) 30 | .subscribeOn(schedulerProvider.thread()) 31 | .toSingle { pinyinEntities } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/GetRandomStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.db.GetPinyin 5 | import com.consistence.pinyin.domain.study.db.StudyDao 6 | import com.consistence.pinyin.domain.study.db.withPinyin 7 | import io.reactivex.Observable 8 | import io.reactivex.Single 9 | import javax.inject.Inject 10 | 11 | class GetRandomStudy @Inject internal constructor( 12 | private val studyDao: StudyDao, 13 | private val getPinyin: GetPinyin, 14 | private val schedulerProvider: SchedulerProvider 15 | ) { 16 | 17 | fun random(limit: Int): Single> { 18 | return Observable.fromCallable { studyDao.randomStudy(limit) } 19 | .observeOn(schedulerProvider.main()) 20 | .subscribeOn(schedulerProvider.thread()) 21 | .withPinyin(getPinyin) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/GetStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.db.GetPinyin 5 | import com.consistence.pinyin.domain.study.db.StudyDao 6 | import com.consistence.pinyin.domain.study.db.withPinyin 7 | import io.reactivex.Observable 8 | import io.reactivex.Single 9 | import javax.inject.Inject 10 | 11 | class GetStudy @Inject internal constructor( 12 | private val studyDao: StudyDao, 13 | private val getPinyin: GetPinyin, 14 | private val schedulerProvider: SchedulerProvider 15 | ) { 16 | 17 | fun get(): Single> { 18 | return Observable.fromCallable { studyDao.studyOrderByDesc() } 19 | .observeOn(schedulerProvider.main()) 20 | .subscribeOn(schedulerProvider.thread()) 21 | .withPinyin(getPinyin) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/Study.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study 2 | 3 | import android.os.Parcelable 4 | import com.consistence.pinyin.domain.pinyin.Pinyin 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | data class Study( 9 | val englishTranslation: String, 10 | val pinyin: List, 11 | val uid: Int = -1 12 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/CountStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import io.reactivex.Single 5 | import javax.inject.Inject 6 | 7 | class CountStudy @Inject internal constructor( 8 | private val studyDao: StudyDao, 9 | private val schedulerProvider: SchedulerProvider 10 | ) { 11 | 12 | fun count(): Single { 13 | return Single.fromCallable { studyDao.count() } 14 | .observeOn(schedulerProvider.main()) 15 | .subscribeOn(schedulerProvider.thread()) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/DeleteStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.study.Study 5 | import io.reactivex.Completable 6 | import io.reactivex.Single 7 | import javax.inject.Inject 8 | 9 | class DeleteStudy @Inject internal constructor( 10 | private val studyDao: StudyDao, 11 | private val schedulerProvider: SchedulerProvider 12 | ) { 13 | 14 | fun remove(study: Study): Single { 15 | return Completable 16 | .fromAction { studyDao.deleteStudy(study.uid) } 17 | .observeOn(schedulerProvider.main()) 18 | .subscribeOn(schedulerProvider.thread()) 19 | .toSingle { true } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/SaveStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.pinyinUidForDatabase 5 | import com.consistence.pinyin.domain.study.Study 6 | import io.reactivex.Completable 7 | import io.reactivex.Single 8 | import javax.inject.Inject 9 | 10 | class SaveStudy @Inject internal constructor( 11 | private val studyDao: StudyDao, 12 | private val schedulerProvider: SchedulerProvider 13 | ) { 14 | 15 | fun insert(study: Study): Single { 16 | 17 | val studyEntity = StudyEntity( 18 | study.englishTranslation, 19 | study.pinyin.pinyinUidForDatabase(), 20 | 0, 21 | 0 22 | ) 23 | 24 | return Completable 25 | .fromAction { studyDao.insert(studyEntity) } 26 | .observeOn(schedulerProvider.main()) 27 | .subscribeOn(schedulerProvider.thread()) 28 | .toSingle { true } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/StudyDao.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | 7 | @Dao 8 | interface StudyDao { 9 | 10 | @Query("SELECT * FROM Study ORDER BY uid DESC LIMIT 0, 50") 11 | fun studyOrderByDesc(): List 12 | 13 | @Query("SELECT * FROM Study ORDER BY RANDOM() LIMIT :limit") 14 | fun randomStudy(limit: Int): List 15 | 16 | @Insert 17 | fun insert(study: StudyEntity) 18 | 19 | @Query("UPDATE Study SET englishTranslation = :englishTranslation, chineseSentence = :chineseSentence WHERE uid = :uid") 20 | fun update(englishTranslation: String, chineseSentence: String, uid: Int) 21 | 22 | @Query("DELETE FROM Study WHERE uid = :uid") 23 | fun deleteStudy(uid: Int) 24 | 25 | @Query("SELECT COUNT(*) FROM Study") 26 | fun count(): Int 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/StudyEntity.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.consistence.pinyin.domain.pinyin.Pinyin 7 | import com.consistence.pinyin.domain.pinyin.db.GetPinyin 8 | import com.consistence.pinyin.domain.study.Study 9 | import io.reactivex.Observable 10 | import io.reactivex.Single 11 | import io.reactivex.functions.BiFunction 12 | 13 | @Entity(tableName = "Study") 14 | data class StudyEntity( 15 | @ColumnInfo(name = "englishTranslation") val englishTranslation: String, 16 | @ColumnInfo(name = "chineseSentence") val chineseSentence: String, 17 | @ColumnInfo(name = "correct") val correct: Int, 18 | @ColumnInfo(name = "incorrect") val incorrect: Int, 19 | @PrimaryKey(autoGenerate = true) val uid: Int = 0 20 | ) 21 | 22 | fun String.listOfUid(): List = split("-").map { 23 | Integer.parseInt(it) 24 | } 25 | 26 | fun Observable>.withPinyin(getPinyin: GetPinyin): Single> { 27 | return flatMap { Observable.fromIterable(it) } 28 | .concatMap { studyEntity -> 29 | Observable.zip( 30 | Observable.just(studyEntity), 31 | getPinyin.byListOfUid(studyEntity.chineseSentence.listOfUid()).toObservable(), 32 | BiFunction, Study> { _, pinyin -> 33 | Study( 34 | studyEntity.englishTranslation, 35 | pinyin, 36 | studyEntity.uid 37 | ) 38 | }) 39 | } 40 | .toList() 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/domain/study/db/UpdateStudy.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.domain.study.db 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import com.consistence.pinyin.domain.pinyin.pinyinUidForDatabase 5 | import com.consistence.pinyin.domain.study.Study 6 | import io.reactivex.Completable 7 | import io.reactivex.Single 8 | import javax.inject.Inject 9 | 10 | class UpdateStudy @Inject internal constructor( 11 | private val studyDao: StudyDao, 12 | private val schedulerProvider: SchedulerProvider 13 | ) { 14 | 15 | fun update(study: Study): Single { 16 | 17 | val pinyinUidList = study.pinyin.pinyinUidForDatabase() 18 | 19 | return Completable 20 | .fromAction { studyDao.update(study.englishTranslation, pinyinUidList, study.uid) } 21 | .observeOn(schedulerProvider.main()) 22 | .subscribeOn(schedulerProvider.thread()) 23 | .toSingle { true } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/Adapter.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.IdRes 8 | import androidx.annotation.StringRes 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.jakewharton.rxbinding2.view.RxView 11 | import io.reactivex.subjects.PublishSubject 12 | 13 | abstract class SimpleAdapter( 14 | context: Context, 15 | val interaction: PublishSubject>, 16 | protected val inflater: LayoutInflater = LayoutInflater.from(context), 17 | internal val data: MutableList = ArrayList() 18 | ) : RecyclerView.Adapter>() { 19 | 20 | fun populate(items: List) { 21 | this.data.addAll(items) 22 | notifyDataSetChanged() 23 | } 24 | 25 | fun clear() { 26 | data.clear() 27 | notifyDataSetChanged() 28 | } 29 | 30 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleAdapterViewHolder { 31 | 32 | val viewHolder = createViewHolder(parent) 33 | 34 | RxView.clicks(viewHolder.itemView) 35 | .map({ Interaction(viewHolder.itemView.id, data[viewHolder.adapterPosition]) }) 36 | .subscribe(interaction) 37 | 38 | return viewHolder 39 | } 40 | 41 | override fun onBindViewHolder(viewHolder: SimpleAdapterViewHolder, position: Int) { 42 | viewHolder.populate(position, data[position]) 43 | } 44 | 45 | override fun getItemCount(): Int { 46 | return data.size 47 | } 48 | 49 | override fun getItemViewType(position: Int): Int { 50 | return DEFAULT_ITEM_TYPE 51 | } 52 | 53 | abstract fun createViewHolder(parent: ViewGroup): SimpleAdapterViewHolder 54 | 55 | companion object { 56 | private val DEFAULT_ITEM_TYPE = 0x100 57 | } 58 | } 59 | 60 | abstract class SimpleAdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 61 | 62 | abstract fun populate(position: Int, value: T) 63 | 64 | fun getString(@StringRes id: Int): String { 65 | return itemView.context.getString(id) 66 | } 67 | } 68 | 69 | data class Interaction(@IdRes val id: Int, val data: T) -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/AppCompatViews.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatButton 6 | import androidx.appcompat.widget.AppCompatTextView 7 | import com.consistence.pinyin.R 8 | 9 | class AppTextView @JvmOverloads constructor( 10 | context: Context, 11 | attrs: AttributeSet? = null, 12 | defStyleAttr: Int = 0 13 | ) : AppCompatTextView(context, attrs, defStyleAttr) 14 | 15 | class AppButton @JvmOverloads constructor( 16 | context: Context, 17 | attrs: AttributeSet? = null, 18 | defStyleAttr: Int = R.style.ButtonPrimary 19 | ) : AppCompatButton(context, attrs, defStyleAttr) { 20 | 21 | override fun drawableStateChanged() { 22 | super.drawableStateChanged() 23 | 24 | alpha = if (isEnabled) { 25 | 1.0f 26 | } else { 27 | 0.4f 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/ErrorRetryView.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import android.content.Context 4 | 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import androidx.constraintlayout.widget.ConstraintLayout 8 | import com.consistence.pinyin.R 9 | import kotlinx.android.synthetic.main.kit_error_retry.view.* 10 | 11 | class ErrorRetryView @JvmOverloads constructor( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | defStyleAttr: Int = 0 15 | ) : ConstraintLayout(context, attrs, defStyleAttr) { 16 | 17 | init { 18 | LayoutInflater.from(getContext()).inflate(R.layout.kit_error_retry, this) 19 | 20 | attrs?.let { 21 | val typedArray = context.obtainStyledAttributes(it, R.styleable.ErrorRetryView) 22 | kit_error_retry_message.text = resources.getText( 23 | typedArray.getResourceId( 24 | R.styleable.ErrorRetryView_ErrorRetryView_text, 25 | R.string.kit_error_retry_default_message)) 26 | typedArray.recycle() 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/RxTabLayout2.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import com.google.android.material.tabs.TabLayout 4 | import com.jakewharton.rxbinding2.support.design.widget.TabLayoutSelectionEvent 5 | import io.reactivex.Observable 6 | import io.reactivex.functions.Consumer 7 | 8 | class RxTabLayout2 private constructor() { 9 | 10 | init { 11 | throw AssertionError("No instances.") 12 | } 13 | 14 | companion object { 15 | 16 | fun selectionEvents(view: TabLayout): Observable { 17 | return TabLayoutSelectionEventObservable2(view) 18 | } 19 | 20 | fun select(view: TabLayout): Consumer { 21 | return Consumer { index -> 22 | if (index < 0 || index >= view.getTabCount()) { 23 | throw IllegalArgumentException("No tab for index " + index!!) 24 | } 25 | 26 | view.getTabAt(index)!!.select() 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/TabLayoutSelectionEventObservable2.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import android.os.Looper 4 | import com.google.android.material.tabs.TabLayout 5 | import com.jakewharton.rxbinding2.support.design.widget.TabLayoutSelectionEvent 6 | import com.jakewharton.rxbinding2.support.design.widget.TabLayoutSelectionReselectedEvent 7 | import com.jakewharton.rxbinding2.support.design.widget.TabLayoutSelectionSelectedEvent 8 | import com.jakewharton.rxbinding2.support.design.widget.TabLayoutSelectionUnselectedEvent 9 | import io.reactivex.Observable 10 | import io.reactivex.Observer 11 | import io.reactivex.android.MainThreadDisposable 12 | import io.reactivex.disposables.Disposables 13 | 14 | internal class TabLayoutSelectionEventObservable2(val view: TabLayout) : Observable() { 15 | 16 | override fun subscribeActual(observer: Observer) { 17 | if (checkMainThread(observer)) { 18 | val listener = Listener(this.view, observer) 19 | observer.onSubscribe(listener) 20 | this.view.addOnTabSelectedListener(listener) 21 | val index = this.view.selectedTabPosition 22 | if (index != -1) { 23 | observer.onNext(TabLayoutSelectionSelectedEvent.create(this.view, this.view.getTabAt(index)!!)) 24 | } 25 | } 26 | } 27 | 28 | internal inner class Listener(private val tabLayout: TabLayout, private val observer: Observer) : MainThreadDisposable(), TabLayout.OnTabSelectedListener { 29 | 30 | override fun onTabSelected(tab: TabLayout.Tab) { 31 | if (!this.isDisposed) { 32 | this.observer.onNext(TabLayoutSelectionSelectedEvent.create(view, tab)) 33 | } 34 | } 35 | 36 | override fun onTabUnselected(tab: TabLayout.Tab) { 37 | if (!this.isDisposed) { 38 | this.observer.onNext(TabLayoutSelectionUnselectedEvent.create(view, tab)) 39 | } 40 | } 41 | 42 | override fun onTabReselected(tab: TabLayout.Tab) { 43 | if (!this.isDisposed) { 44 | this.observer.onNext(TabLayoutSelectionReselectedEvent.create(view, tab)) 45 | } 46 | } 47 | 48 | override fun onDispose() { 49 | this.tabLayout.removeOnTabSelectedListener(this) 50 | } 51 | } 52 | 53 | fun checkMainThread(observer: Observer<*>): Boolean { 54 | if (Looper.myLooper() != Looper.getMainLooper()) { 55 | observer.onSubscribe(Disposables.empty()) 56 | observer.onError(IllegalStateException( 57 | "Expected to be called on the main thread but was " + Thread.currentThread().name)) 58 | return false 59 | } 60 | return true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/consistence/pinyin/kit/View.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.kit 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | import androidx.appcompat.app.AppCompatActivity 7 | 8 | fun View.visible() { 9 | visibility = View.VISIBLE 10 | } 11 | 12 | fun View.invisible() { 13 | visibility = View.INVISIBLE 14 | } 15 | 16 | fun View.gone() { 17 | visibility = View.GONE 18 | } 19 | 20 | fun AppCompatActivity.closeKeyboard(view: View) { 21 | val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 22 | imm.hideSoftInputFromWindow(view.windowToken, 0) 23 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/pinyin_list_transition_enter_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/anim/pinyin_list_transition_exit_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/anim/pinyin_list_transition_exit_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backspace.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_accent.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_up.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_study.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_study_accent.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_button_primary_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_button_primary_background_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_button_secondary_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/view_button_secondary_background_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/entry_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 32 | 33 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/kit_error_retry.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pinyin_character_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pinyin_english_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pinyin_phonetic_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/study_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 27 | 28 | 34 | 35 | 49 | 50 | 66 | 67 | 77 | 78 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/layout/study_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/train_random_results_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/study_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memtrip/android-mvi/7df0b28a89a1bf778b118e4c35274d89b0600161/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/anim_integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:integer/config_mediumAnimTime 4 | @android:integer/config_longAnimTime 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2D3047 4 | #1E2030 5 | #e94b35 6 | #8ABA10 7 | 8 | #2D3047 9 | 10 | @color/colorPrimary 11 | #DDFFFFFF 12 | 13 | #FFFFFF 14 | 15 | #FFFFFF 16 | 17 | 18 | @color/colorAccent 19 | #55FFFFFF 20 | @color/colorAccent 21 | #FFFFFF 22 | 23 | #33FFFFFF 24 | @color/colorTextInverse 25 | @color/colorTextInverse 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 12dp 6 | 16dp 7 | 8 | 9 | 116dp 10 | 36dp 11 | 32dp 12 | 3dp 13 | 1dp 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #CB4330 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/pinyin_dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52dp 4 | 64dp 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/pinyin_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Phonetic script 4 | English translation 5 | Chinese character 6 | 7 | Search phonetic pinyin 8 | Search english translations 9 | 寻找 汉语 10 | 11 | Pinyin 12 | English 13 | 汉语 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 拼音 7 | 8 | Retry 9 | 10 | Play audio 11 | 12 | 13 | Could not download Pinyin 14 | 15 | 16 | Sorry, something went wrong 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/study_dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 72dp 4 | 5 | 64dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/study_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vocabulary 5 | Start vocabulary training 6 | It\'s study time! 7 | 8 | You have not created any study vocabulary. Add your vocabulary to the app and test yourself everyday! 9 | 10 | Create vocabulary 11 | 12 | Chinese phrase 13 | English translation 14 | Pinyin 15 | 16 | 17 | Create Phrase 18 | Update Phrase 19 | Are you sure? 20 | You will lose your current changes if you exit now. 21 | Oops! 22 | 23 | Could not create study, unfortunately your database might corrupted. Please try again. 24 | 25 | 26 | Could not delete study, unfortunately your database might corrupted. Please try again. 27 | 28 | 29 | 30 | 31 | Let\'s memorise a phrase. 32 | 33 | 34 | e.g; I went shopping. 35 | 36 | Enter your phrase in English 37 | Next 38 | 39 | You must enter an english translation of your phrase. 40 | 41 | 42 | Your english translation cannot be more than 80 characters. 43 | 44 | Delete phrase 45 | Are you sure? 46 | 47 | Do you want to permanently delete this phrase? 48 | 49 | 50 | 51 | 52 | 53 | Search and select items 54 | 55 | Next 56 | 57 | You must enter a Chinese phrase! Use the search below to choose the characters and build 58 | your phrase. 59 | 60 | 61 | You must enter a Chinese phrase cannot be more than 80 characters. It\'s better to learn in 62 | smaller chunks! 63 | 64 | 65 | 66 | 67 | 68 | Check the phrase is correct! 69 | 70 | 71 | Confirm with a tutor if you are in doubt. 72 | 73 | 74 | Create 75 | Update 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 19 | 20 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values/train_phrase_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Train phrase 5 | Enter your answer 6 | Answer 7 | 8 | Translate the following into English: 9 | Translate the following into Chinese: 10 | 11 | Correct 12 | Incorrect 13 | 14 | Are you sure? 15 | You are about to exit your current training session 16 | 17 | 18 | 19 | Train your vocabulary 20 | 21 | We will prompt you with either an English or Chinese phrase to translate. 22 | 23 | How many phrases do you want to study? 24 | 5 25 | 10 26 | 20 27 | 50 28 | Start training 29 | Results (%1$d/%2$d) 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/typography_dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 24sp 3 | 20sp 4 | 16sp 5 | 14sp 6 | 12sp 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/typography_styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 11 | 12 | 16 | 17 | 21 | 22 | 25 | 26 | 30 | 31 | 34 | 35 | 39 | 40 | 44 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/TestSchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin 2 | 3 | import com.consistence.pinyin.domain.SchedulerProvider 4 | import io.reactivex.observers.TestObserver 5 | 6 | import io.reactivex.schedulers.Schedulers 7 | 8 | class TestSchedulerProvider : SchedulerProvider { 9 | override fun main() = Schedulers.trampoline() 10 | override fun thread() = Schedulers.trampoline() 11 | } 12 | 13 | fun TestObserver.get(index: Int): T = this.values()[index] 14 | 15 | fun TestObserver.first(): T = this.values()[0] -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/EntryRenderTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.verify 5 | 6 | import org.jetbrains.spek.api.Spek 7 | import org.jetbrains.spek.api.dsl.given 8 | import org.jetbrains.spek.api.dsl.it 9 | import org.jetbrains.spek.api.dsl.on 10 | 11 | import org.junit.platform.runner.JUnitPlatform 12 | import org.junit.runner.RunWith 13 | 14 | @RunWith(JUnitPlatform::class) 15 | class EntryRenderTest : Spek({ 16 | 17 | given("EntryRenderer") { 18 | 19 | on("EntryRenderAction.OnProgress") { 20 | val layout: StudyLayout = mock() 21 | val render = StudyRenderer() 22 | 23 | render.layout(layout, StudyViewState(view = StudyViewState.View.OnProgress)) 24 | 25 | it("shows progress indicator") { 26 | verify(layout).showProgress() 27 | } 28 | } 29 | 30 | on("EntryRenderAction.OnError") { 31 | val layout: StudyLayout = mock() 32 | val render = StudyRenderer() 33 | 34 | render.layout(layout, StudyViewState(view = StudyViewState.View.OnError)) 35 | 36 | it("hides layout indicator and shows the showError") { 37 | verify(layout).showError() 38 | } 39 | } 40 | 41 | on("EntryRenderAction.OnPinyinLoaded") { 42 | val layout: StudyLayout = mock() 43 | val render = StudyRenderer() 44 | 45 | render.layout(layout, StudyViewState(view = StudyViewState.View.OnPinyinLoaded)) 46 | 47 | it("hides layout indicator and navigate to pinyin list") { 48 | verify(layout).navigateToPinyin() 49 | } 50 | } 51 | } 52 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/EntryViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.consistence.pinyin.domain.pinyin.db.CountPinyin 4 | import com.consistence.pinyin.domain.pinyin.FetchAndSavePinyin 5 | import com.consistence.pinyin.get 6 | import com.nhaarman.mockito_kotlin.doReturn 7 | import com.nhaarman.mockito_kotlin.doThrow 8 | import com.nhaarman.mockito_kotlin.mock 9 | import com.nhaarman.mockito_kotlin.whenever 10 | import io.reactivex.Observable 11 | import io.reactivex.Single 12 | import org.jetbrains.spek.api.Spek 13 | import org.jetbrains.spek.api.dsl.given 14 | import org.jetbrains.spek.api.dsl.it 15 | import org.jetbrains.spek.api.dsl.on 16 | import org.junit.Assert.assertEquals 17 | import org.junit.platform.runner.JUnitPlatform 18 | import org.junit.runner.RunWith 19 | import java.util.Arrays.asList 20 | 21 | @RunWith(JUnitPlatform::class) 22 | class EntryViewModelTest : Spek({ 23 | 24 | given("a EntryViewModel") { 25 | 26 | val fetchAndSavePinyin by memoized { mock() } 27 | 28 | val countPinyin by memoized { mock() } 29 | 30 | val viewModel by memoized { StudyViewModel(fetchAndSavePinyin, countPinyin, mock()) } 31 | 32 | on("pinyin entries already exist") { 33 | 34 | whenever(countPinyin.count()).doReturn(Single.just(1)) 35 | 36 | val state = viewModel.states().test() 37 | 38 | viewModel.processIntents(intents = Observable.just(StudyIntent.Init)) 39 | 40 | it("should show pinyin loaded") { 41 | assertEquals(StudyViewState(view = StudyViewState.View.OnProgress), state.get(0)) 42 | assertEquals(StudyViewState(view = StudyViewState.View.OnPinyinLoaded), state.get(1)) 43 | } 44 | } 45 | 46 | on("failed to count pinyin") { 47 | 48 | whenever(countPinyin.count()).doReturn(Single.error(IllegalStateException())) 49 | 50 | val state = viewModel.states().test() 51 | 52 | viewModel.processIntents(intents = Observable.just(StudyIntent.Init)) 53 | 54 | it("should show an error") { 55 | assertEquals(StudyViewState(view = StudyViewState.View.OnProgress), state.get(0)) 56 | assertEquals(StudyViewState(view = StudyViewState.View.OnError), state.get(1)) 57 | } 58 | } 59 | 60 | on("fetch pinyin entries") { 61 | 62 | whenever(countPinyin.count()).doReturn(Single.just(0)) 63 | whenever(fetchAndSavePinyin.save()).doReturn(Single.just(asList(mock()))) 64 | 65 | val state = viewModel.states().test() 66 | 67 | viewModel.processIntents(intents = Observable.just(StudyIntent.Init)) 68 | 69 | it("should show pinyin loaded") { 70 | assertEquals(StudyViewState(view = StudyViewState.View.OnProgress), state.get(0)) 71 | assertEquals(StudyViewState(view = StudyViewState.View.OnPinyinLoaded), state.get(1)) 72 | } 73 | } 74 | 75 | on("failed to fetch pinyin entries") { 76 | 77 | whenever(countPinyin.count()).doReturn(Single.just(0)) 78 | whenever(fetchAndSavePinyin.save()).doThrow(IllegalStateException()) 79 | 80 | val state = viewModel.states().test() 81 | 82 | viewModel.processIntents(intents = Observable.just(StudyIntent.Init)) 83 | 84 | it("should show an error") { 85 | assertEquals(StudyViewState(view = StudyViewState.View.OnProgress), state.get(0)) 86 | assertEquals(StudyViewState(view = StudyViewState.View.OnError), state.get(1)) 87 | } 88 | } 89 | } 90 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/PinyinRenderTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import com.consistence.pinyin.app.pinyin.PinyinLayout 4 | import com.consistence.pinyin.app.pinyin.PinyinRenderer 5 | import com.consistence.pinyin.app.pinyin.PinyinViewState 6 | import com.nhaarman.mockito_kotlin.mock 7 | import com.nhaarman.mockito_kotlin.verify 8 | import org.jetbrains.spek.api.Spek 9 | import org.jetbrains.spek.api.dsl.given 10 | import org.jetbrains.spek.api.dsl.it 11 | import org.jetbrains.spek.api.dsl.on 12 | import org.junit.platform.runner.JUnitPlatform 13 | import org.junit.runner.RunWith 14 | 15 | @RunWith(JUnitPlatform::class) 16 | class PinyinRenderTest : Spek({ 17 | 18 | given("PinyinRenderer") { 19 | 20 | on("PinyinRenderAction.SearchHint") { 21 | val layout: PinyinLayout = mock() 22 | val render = PinyinRenderer() 23 | 24 | render.layout(layout, PinyinViewState("hello")) 25 | 26 | it("shows search hint") { 27 | verify(layout).updateSearchHint("hello") 28 | } 29 | } 30 | } 31 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/PinyinViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app 2 | 3 | import android.app.Application 4 | import com.consistence.pinyin.R 5 | import com.consistence.pinyin.app.pinyin.Page 6 | import com.consistence.pinyin.app.pinyin.PinyinIntent 7 | import com.consistence.pinyin.app.pinyin.PinyinViewModel 8 | import com.consistence.pinyin.app.pinyin.PinyinViewState 9 | import com.consistence.pinyin.get 10 | import com.nhaarman.mockito_kotlin.doReturn 11 | import com.nhaarman.mockito_kotlin.mock 12 | import io.reactivex.Observable 13 | import org.assertj.core.api.Assertions.assertThat 14 | import org.jetbrains.spek.api.Spek 15 | import org.jetbrains.spek.api.dsl.given 16 | import org.jetbrains.spek.api.dsl.it 17 | import org.jetbrains.spek.api.dsl.on 18 | import org.junit.platform.runner.JUnitPlatform 19 | import org.junit.runner.RunWith 20 | 21 | @RunWith(JUnitPlatform::class) 22 | class PinyinViewModelTest : Spek({ 23 | 24 | given("a PinyinViewModel") { 25 | 26 | val context: Application = mock { 27 | on { 28 | getString(R.string.pinyin_activity_search_phonetic_hint) 29 | } doReturn ("phonetic hint") 30 | 31 | on { 32 | getString(R.string.pinyin_activity_search_english_hint) 33 | } doReturn ("english hint") 34 | 35 | on { 36 | getString(R.string.pinyin_activity_search_character_hint) 37 | } doReturn ("character hint") 38 | } 39 | 40 | val viewModel by memoized { PinyinViewModel(context) } 41 | 42 | on("Phonetic tab selected") { 43 | 44 | viewModel.processIntents(intents = Observable.just(PinyinIntent.TabSelected(Page.PHONETIC))) 45 | 46 | val state = viewModel.states().test() 47 | 48 | it("should update the search bar with the phonetic hint") { 49 | assertThat(state.get(0)).isEqualTo(PinyinViewState("phonetic hint")) 50 | } 51 | } 52 | 53 | on("English tab selected") { 54 | 55 | viewModel.processIntents(intents = Observable.just(PinyinIntent.TabSelected(Page.ENGLISH))) 56 | 57 | val state = viewModel.states().test() 58 | 59 | it("should update the search bar with the english hint") { 60 | assertThat(state.get(0)).isEqualTo(PinyinViewState("english hint")) 61 | } 62 | } 63 | 64 | on("Character tab selected") { 65 | 66 | viewModel.processIntents(intents = Observable.just(PinyinIntent.TabSelected(Page.CHARACTER))) 67 | 68 | val state = viewModel.states().test() 69 | 70 | it("should update the search bar with the character hint") { 71 | assertThat(state.get(0)).isEqualTo(PinyinViewState("character hint")) 72 | } 73 | } 74 | } 75 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailRenderTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.never 5 | import com.nhaarman.mockito_kotlin.verify 6 | import org.jetbrains.spek.api.Spek 7 | import org.jetbrains.spek.api.dsl.given 8 | import org.jetbrains.spek.api.dsl.it 9 | import org.jetbrains.spek.api.dsl.on 10 | import org.junit.platform.runner.JUnitPlatform 11 | import org.junit.runner.RunWith 12 | 13 | @RunWith(JUnitPlatform::class) 14 | class PinyinDetailRenderTest : Spek({ 15 | 16 | given("PinyinDetailRenderer") { 17 | 18 | on("PinyinDetailRenderAction.Populate without audio") { 19 | val layout: PinyinDetailLayout = mock() 20 | val render = PinyinDetailRenderer() 21 | 22 | render.layout(layout, PinyinDetailViewState( 23 | "nĭ", 24 | "you", 25 | "你")) 26 | 27 | it("populates the details, but does not show audio controls") { 28 | verify(layout, never()).showAudioControl() 29 | verify(layout).populate( 30 | "nĭ", 31 | "you", 32 | "你") 33 | } 34 | } 35 | 36 | on("PinyinDetailRenderAction.Populate with audio") { 37 | val layout: PinyinDetailLayout = mock() 38 | val render = PinyinDetailRenderer() 39 | 40 | render.layout(layout, PinyinDetailViewState( 41 | "nĭ", 42 | "you", 43 | "你", 44 | "file://audio")) 45 | 46 | it("populates the details and show audio controls") { 47 | verify(layout).showAudioControl() 48 | verify(layout).populate( 49 | "nĭ", 50 | "you", 51 | "你") 52 | } 53 | } 54 | 55 | on("PinyinDetailRenderAction.PlayAudio") { 56 | val layout: PinyinDetailLayout = mock() 57 | val render = PinyinDetailRenderer() 58 | 59 | render.layout(layout, PinyinDetailViewState( 60 | "nĭ", 61 | "you", 62 | "你", 63 | "file://audio", 64 | action = PinyinDetailViewState.Action.PlayAudio("file://audio"))) 65 | 66 | it("populates the details and show audio controls") { 67 | verify(layout).playAudio("file://audio") 68 | } 69 | } 70 | } 71 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/detail/PinyinDetailViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.detail 2 | 3 | import com.consistence.pinyin.get 4 | import com.nhaarman.mockito_kotlin.doReturn 5 | import com.nhaarman.mockito_kotlin.mock 6 | import com.nhaarman.mockito_kotlin.whenever 7 | import io.reactivex.Observable 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.jetbrains.spek.api.Spek 10 | import org.jetbrains.spek.api.dsl.given 11 | import org.jetbrains.spek.api.dsl.it 12 | import org.jetbrains.spek.api.dsl.on 13 | import org.junit.platform.runner.JUnitPlatform 14 | import org.junit.runner.RunWith 15 | 16 | @RunWith(JUnitPlatform::class) 17 | class PinyinDetailViewModelTest : Spek({ 18 | 19 | given("a PinyinDetailsViewModel") { 20 | 21 | val pinyinParcel by memoized { mock() } 22 | 23 | val viewModel by memoized { PinyinDetailViewModel(pinyinParcel, mock()) } 24 | 25 | on("Populate pinyin details without audio") { 26 | 27 | whenever(pinyinParcel.phoneticScriptText).doReturn("nĭ") 28 | whenever(pinyinParcel.englishTranslationText).doReturn("you") 29 | whenever(pinyinParcel.chineseCharacters).doReturn("你") 30 | 31 | val states = viewModel.states().test() 32 | 33 | viewModel.processIntents(Observable.just(PinyinDetailIntent.Idle)) 34 | 35 | it("should return phoneticScriptText, englishTranslationText, chineseCharacters" + 36 | "with audioSrc null and with a None Action") { 37 | val element = states.get(0) 38 | assertThat(element.phoneticScriptText).isEqualTo("nĭ") 39 | assertThat(element.englishTranslationText).isEqualTo("you") 40 | assertThat(element.chineseCharacters).isEqualTo("你") 41 | assertThat(element.audioSrc).isNull() 42 | assertThat(element.action).isEqualTo(PinyinDetailViewState.Action.None) 43 | } 44 | } 45 | } 46 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/list/PinyinCharacterViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.db.CharacterSearch 4 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 5 | import com.consistence.pinyin.app.pinyin.list.character.PinyinCharacterViewModel 6 | import com.consistence.pinyin.get 7 | import com.memtrip.mxandroid.MxViewState 8 | import com.nhaarman.mockito_kotlin.doReturn 9 | import com.nhaarman.mockito_kotlin.mock 10 | import com.nhaarman.mockito_kotlin.whenever 11 | import io.mockk.every 12 | import io.mockk.mockkObject 13 | import io.reactivex.Observable 14 | import io.reactivex.Single 15 | import org.jetbrains.spek.api.Spek 16 | import org.jetbrains.spek.api.dsl.given 17 | import org.jetbrains.spek.api.dsl.it 18 | import org.jetbrains.spek.api.dsl.on 19 | import org.junit.Assert.assertEquals 20 | import org.junit.platform.runner.JUnitPlatform 21 | import org.junit.runner.RunWith 22 | import java.util.Arrays.asList 23 | 24 | @RunWith(JUnitPlatform::class) 25 | class PinyinCharacterViewModelTest : Spek({ 26 | 27 | mockkObject(MxViewState) 28 | every { 29 | MxViewState.id() 30 | } returns 0 31 | 32 | given("a PinyinCharacterViewModel") { 33 | 34 | val search by memoized { mock() } 35 | 36 | val viewModel by memoized { PinyinCharacterViewModel(search, mock()) } 37 | 38 | on("Pinyin entries exist for search query") { 39 | 40 | val pinyinList: List = asList(mock()) 41 | 42 | whenever(search.search("汉语")).doReturn(Single.just(pinyinList)) 43 | 44 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("汉语"))) 45 | 46 | val states = viewModel.states().test() 47 | 48 | it("should populate the view with the pinyin list") { 49 | assertEquals( 50 | PinyinListViewState(view = PinyinListViewState.View.Populate(pinyinList)), 51 | states.get(0)) 52 | } 53 | } 54 | 55 | on("Pinyin entries could not be fetched") { 56 | 57 | whenever(search.search("汉语")).doReturn(Single.error(IllegalStateException())) 58 | 59 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("汉语"))) 60 | 61 | val states = viewModel.states().test() 62 | 63 | it("should show an error") { 64 | assertEquals( 65 | PinyinListViewState(view = PinyinListViewState.View.OnError), 66 | states.get(0)) 67 | } 68 | } 69 | 70 | on("pinyin entry selected") { 71 | 72 | val pinyin: PinyinEntity = mock() 73 | 74 | viewModel.processIntents(Observable.just(PinyinListIntent.SelectItem(pinyin))) 75 | 76 | val states = viewModel.states().test() 77 | 78 | it("should navigate to pinyin details") { 79 | assertEquals( 80 | PinyinListViewState(view = PinyinListViewState.View.SelectItem(pinyin)), 81 | states.get(0)) 82 | } 83 | } 84 | 85 | on("play pinyin audio selected") { 86 | 87 | viewModel.processIntents(Observable.just(PinyinListIntent.PlayAudio("file://audio"))) 88 | 89 | val states = viewModel.states().test() 90 | 91 | it("should play the audio") { 92 | assertEquals( 93 | PinyinListViewState(view = PinyinListViewState.View.PlayAudio("file://audio", 0)), 94 | states.get(0)) 95 | } 96 | } 97 | } 98 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/list/PinyinEnglishViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.db.EnglishSearch 4 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 5 | import com.consistence.pinyin.app.pinyin.list.english.PinyinEnglishViewModel 6 | import com.memtrip.mxandroid.MxViewState 7 | import com.nhaarman.mockito_kotlin.doReturn 8 | import com.nhaarman.mockito_kotlin.mock 9 | import com.nhaarman.mockito_kotlin.whenever 10 | import io.mockk.every 11 | import io.mockk.mockkObject 12 | import io.reactivex.Observable 13 | import io.reactivex.Single 14 | import org.jetbrains.spek.api.Spek 15 | import org.jetbrains.spek.api.dsl.given 16 | import org.jetbrains.spek.api.dsl.it 17 | import org.jetbrains.spek.api.dsl.on 18 | import org.junit.Assert 19 | import org.junit.platform.runner.JUnitPlatform 20 | import org.junit.runner.RunWith 21 | import java.util.Arrays.asList 22 | 23 | @RunWith(JUnitPlatform::class) 24 | class PinyinEnglishViewModelTest : Spek({ 25 | 26 | mockkObject(MxViewState) 27 | every { 28 | MxViewState.id() 29 | } returns 0 30 | 31 | given("PinyinListIntent.Search") { 32 | 33 | val search by memoized { mock() } 34 | 35 | val viewModel by memoized { PinyinEnglishViewModel(search, mock()) } 36 | 37 | on("Pinyin entries exist for search query") { 38 | 39 | val pinyinList: List = asList(mock()) 40 | 41 | whenever(search.search("hello")).doReturn(Single.just(pinyinList)) 42 | 43 | val states = viewModel.states().blockingIterable().asSequence() 44 | 45 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("hello"))) 46 | 47 | it("should populate the view with the pinyin list") { 48 | Assert.assertEquals( 49 | PinyinListViewState(view = PinyinListViewState.View.Populate(pinyinList)), 50 | states.elementAt(0)) 51 | } 52 | } 53 | 54 | on("Pinyin entries could not be fetched") { 55 | 56 | whenever(search.search("hello")).doReturn(Single.error(IllegalStateException())) 57 | 58 | val states = viewModel.states().blockingIterable().asSequence() 59 | 60 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("hello"))) 61 | 62 | it("should show an error") { 63 | Assert.assertEquals( 64 | PinyinListViewState(view = PinyinListViewState.View.OnError), 65 | states.elementAt(0)) 66 | } 67 | } 68 | 69 | on("pinyin entry selected") { 70 | 71 | val pinyin: PinyinEntity = mock() 72 | 73 | val states = viewModel.states().blockingIterable().asSequence() 74 | 75 | viewModel.processIntents(Observable.just(PinyinListIntent.SelectItem(pinyin))) 76 | 77 | it("should navigate to pinyin details") { 78 | Assert.assertEquals( 79 | PinyinListViewState(view = PinyinListViewState.View.SelectItem(pinyin)), 80 | states.elementAt(0)) 81 | } 82 | } 83 | 84 | on("select to play pinyin audio") { 85 | 86 | val states = viewModel.states().blockingIterable().asSequence() 87 | 88 | viewModel.processIntents(Observable.just(PinyinListIntent.PlayAudio("file://audio"))) 89 | 90 | it("should play the audio") { 91 | Assert.assertEquals( 92 | PinyinListViewState(view = PinyinListViewState.View.PlayAudio("file://audio")), 93 | states.elementAt(0)) 94 | } 95 | } 96 | } 97 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/list/PinyinListRenderTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 4 | import com.nhaarman.mockito_kotlin.mock 5 | import com.nhaarman.mockito_kotlin.verify 6 | import org.jetbrains.spek.api.Spek 7 | import org.jetbrains.spek.api.dsl.given 8 | import org.jetbrains.spek.api.dsl.it 9 | import org.jetbrains.spek.api.dsl.on 10 | import org.junit.platform.runner.JUnitPlatform 11 | import org.junit.runner.RunWith 12 | import java.util.Arrays.asList 13 | 14 | @RunWith(JUnitPlatform::class) 15 | class PinyinListRenderTest : Spek({ 16 | 17 | given("PinyinListRenderer") { 18 | 19 | on("populate") { 20 | 21 | val layout: PinyinListLayout = mock() 22 | val render = PinyinListRenderer() 23 | 24 | val pinyinList: List = asList(mock()) 25 | 26 | render.layout(layout, PinyinListViewState(view = PinyinListViewState.View.Populate(pinyinList))) 27 | 28 | it("populates the pinyin list items") { 29 | verify(layout).populate(pinyinList) 30 | } 31 | } 32 | 33 | on("pinyinItemSelected") { 34 | 35 | val layout: PinyinListLayout = mock() 36 | val render = PinyinListRenderer() 37 | 38 | val pinyinItem: PinyinEntity = mock() 39 | 40 | render.layout(layout, PinyinListViewState(view = PinyinListViewState.View.SelectItem(pinyinItem))) 41 | 42 | it("navigates to pinyin details with the selected pinyin item") { 43 | verify(layout).pinyinItemSelected(pinyinItem) 44 | } 45 | } 46 | 47 | on("showError") { 48 | 49 | val layout: PinyinListLayout = mock() 50 | val render = PinyinListRenderer() 51 | 52 | render.layout(layout, PinyinListViewState(view = PinyinListViewState.View.OnError)) 53 | 54 | it("displays the showError") { 55 | verify(layout).showError() 56 | } 57 | } 58 | 59 | on("playAudio") { 60 | 61 | val layout: PinyinListLayout = mock() 62 | val render = PinyinListRenderer() 63 | 64 | render.layout(layout, PinyinListViewState(view = PinyinListViewState.View.PlayAudio("file://audio"))) 65 | 66 | it("plays the audio src") { 67 | verify(layout).playAudio("file://audio") 68 | } 69 | } 70 | } 71 | }) -------------------------------------------------------------------------------- /app/src/test/java/com/consistence/pinyin/app/pinyin/list/PinyinPhoneticViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.consistence.pinyin.app.pinyin.list 2 | 3 | import com.consistence.pinyin.domain.pinyin.db.PhoneticSearch 4 | import com.consistence.pinyin.domain.pinyin.db.PinyinEntity 5 | import com.consistence.pinyin.app.pinyin.list.phonetic.PinyinPhoneticViewModel 6 | import com.memtrip.mxandroid.MxViewState 7 | import com.nhaarman.mockito_kotlin.doReturn 8 | import com.nhaarman.mockito_kotlin.mock 9 | import com.nhaarman.mockito_kotlin.whenever 10 | import io.mockk.every 11 | import io.mockk.mockkObject 12 | import io.reactivex.Observable 13 | import io.reactivex.Single 14 | import org.jetbrains.spek.api.Spek 15 | import org.jetbrains.spek.api.dsl.given 16 | import org.jetbrains.spek.api.dsl.it 17 | import org.jetbrains.spek.api.dsl.on 18 | import org.junit.Assert 19 | import org.junit.platform.runner.JUnitPlatform 20 | import org.junit.runner.RunWith 21 | import java.util.Arrays.asList 22 | 23 | @RunWith(JUnitPlatform::class) 24 | class PinyinPhoneticViewModelTest : Spek({ 25 | 26 | mockkObject(MxViewState) 27 | every { 28 | MxViewState.id() 29 | } returns 0 30 | 31 | given("a PinyinPhoneticViewModel") { 32 | 33 | val search by memoized { mock() } 34 | 35 | val viewModel by memoized { PinyinPhoneticViewModel(search, mock()) } 36 | 37 | on("Pinyin entries exist for search query") { 38 | 39 | val pinyinList: List = asList(mock()) 40 | 41 | whenever(search.search("pinyin")).doReturn(Single.just(pinyinList)) 42 | 43 | val states = viewModel.states().blockingIterable().asSequence() 44 | 45 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("pinyin"))) 46 | 47 | it("should populate the view with the pinyin list") { 48 | Assert.assertEquals( 49 | PinyinListViewState(view = PinyinListViewState.View.Populate(pinyinList)), 50 | states.elementAt(0)) 51 | } 52 | } 53 | 54 | on("Pinyin entries could not be fetched") { 55 | 56 | whenever(search.search("pinyin")).doReturn(Single.error(IllegalStateException())) 57 | 58 | val states = viewModel.states().blockingIterable().asSequence() 59 | 60 | viewModel.processIntents(Observable.just(PinyinListIntent.Search("pinyin"))) 61 | 62 | it("should show an error") { 63 | Assert.assertEquals( 64 | PinyinListViewState(view = PinyinListViewState.View.OnError), 65 | states.elementAt(0)) 66 | } 67 | } 68 | 69 | on("pinyin entry selected") { 70 | 71 | val pinyin: PinyinEntity = mock() 72 | 73 | val states = viewModel.states().blockingIterable().asSequence() 74 | 75 | viewModel.processIntents(Observable.just(PinyinListIntent.SelectItem(pinyin))) 76 | 77 | it("should navigate to pinyin details") { 78 | Assert.assertEquals( 79 | PinyinListViewState(view = PinyinListViewState.View.SelectItem(pinyin)), 80 | states.elementAt(0)) 81 | } 82 | } 83 | 84 | on("select to play pinyin audio") { 85 | 86 | val states = viewModel.states().blockingIterable().asSequence() 87 | 88 | viewModel.processIntents(Observable.just(PinyinListIntent.PlayAudio("file://audio"))) 89 | 90 | it("should play the audio") { 91 | Assert.assertEquals( 92 | PinyinListViewState(view = PinyinListViewState.View.PlayAudio("file://audio")), 93 | states.elementAt(0)) 94 | } 95 | } 96 | } 97 | }) -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.60' 3 | repositories { 4 | google() 5 | jcenter() 6 | maven { 7 | url "https://plugins.gradle.org/m2/" 8 | } 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.3.0-alpha06' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | classpath 'gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:5.0.0' 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | mavenLocal() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | 29 | subprojects { 30 | apply plugin: "org.jlleitschuh.gradle.ktlint" 31 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------