├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_kg.webp
│ │ │ │ ├── ic_bell.webp
│ │ │ │ ├── ic_call.webp
│ │ │ │ ├── ic_sort.webp
│ │ │ │ ├── ic_time.webp
│ │ │ │ ├── ic_address.webp
│ │ │ │ ├── ic_dropdown.webp
│ │ │ │ ├── ic_password.webp
│ │ │ │ ├── ic_personal.webp
│ │ │ │ ├── ic_arrow_back.webp
│ │ │ │ ├── ic_phone_number.webp
│ │ │ │ └── ic_default_avatar.webp
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ └── ic_github.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── drawable
│ │ │ │ ├── shape_green_view.xml
│ │ │ │ ├── shape_order_background_color.xml
│ │ │ │ ├── shape_common_edit_text_background.xml
│ │ │ │ ├── ic_circle_c.xml
│ │ │ │ ├── ic_circle_css.xml
│ │ │ │ ├── ic_circle_go.xml
│ │ │ │ ├── ic_circle_java.xml
│ │ │ │ ├── ic_circle_other.xml
│ │ │ │ ├── ic_circle_php.xml
│ │ │ │ ├── ic_circle_ruby.xml
│ │ │ │ ├── ic_circle_swift.xml
│ │ │ │ ├── ic_circle_kotlin.xml
│ │ │ │ ├── ic_circle_python.xml
│ │ │ │ ├── ic_circle_c_plus_plus.xml
│ │ │ │ ├── ic_circle_java_script.xml
│ │ │ │ ├── ic_star.xml
│ │ │ │ ├── selector_common_button_background_color.xml
│ │ │ │ ├── ic_fork.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── layout
│ │ │ │ ├── layout_divider_line.xml
│ │ │ │ ├── activity_register_and_login.xml
│ │ │ │ ├── activity_splash.xml
│ │ │ │ ├── layout_toolbar.xml
│ │ │ │ ├── layout_loading.xml
│ │ │ │ ├── fragment_repository.xml
│ │ │ │ ├── layout_error.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── fragment_login.xml
│ │ │ │ ├── activity_personal_center.xml
│ │ │ │ └── item_repository.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── anim
│ │ │ │ ├── switch_fade_in.xml
│ │ │ │ ├── switch_fade_out.xml
│ │ │ │ ├── switch_in_replaced.xml
│ │ │ │ ├── switch_in_bottom.xml
│ │ │ │ ├── switch_in_left.xml
│ │ │ │ ├── switch_in_right.xml
│ │ │ │ ├── switch_in_top.xml
│ │ │ │ ├── switch_out_bottom.xml
│ │ │ │ ├── switch_out_hidden.xml
│ │ │ │ ├── switch_out_left.xml
│ │ │ │ ├── switch_out_replaced.xml
│ │ │ │ ├── switch_out_right.xml
│ │ │ │ └── switch_out_top.xml
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── tanjiajun
│ │ │ │ └── androidgenericframework
│ │ │ │ ├── ui
│ │ │ │ ├── NoViewModel.kt
│ │ │ │ ├── recyclerview
│ │ │ │ │ ├── BaseViewHolder.kt
│ │ │ │ │ ├── NoDataViewType.kt
│ │ │ │ │ ├── BaseViewType.kt
│ │ │ │ │ ├── BaseDataBindingAdapter.kt
│ │ │ │ │ └── MultiViewTypeDataBindingAdapter.kt
│ │ │ │ ├── user
│ │ │ │ │ ├── activity
│ │ │ │ │ │ ├── RegisterAndLoginActivity.kt
│ │ │ │ │ │ └── PersonalCenterActivity.kt
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ │ ├── PersonalCenterViewModel.kt
│ │ │ │ │ │ └── LoginViewModel.kt
│ │ │ │ │ └── fragment
│ │ │ │ │ │ └── LoginFragment.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ │ ├── SplashViewModel.kt
│ │ │ │ │ │ └── MainViewModel.kt
│ │ │ │ │ └── activity
│ │ │ │ │ │ ├── SplashActivity.kt
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── repository
│ │ │ │ │ ├── adapter
│ │ │ │ │ │ └── RepositoryAdapter.kt
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ │ └── RepositoryViewModel.kt
│ │ │ │ │ └── fragment
│ │ │ │ │ │ └── RepositoryFragment.kt
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ └── BaseViewModel.kt
│ │ │ │ ├── AndroidGenericFrameworkFragmentTag.kt
│ │ │ │ ├── AndroidGenericFrameworkExtra.kt
│ │ │ │ ├── utils
│ │ │ │ ├── DateUtils.kt
│ │ │ │ ├── FragmentExt.kt
│ │ │ │ ├── ActivityExt.kt
│ │ │ │ ├── ToastExt.kt
│ │ │ │ ├── BooleanExt.kt
│ │ │ │ ├── GsonExt.kt
│ │ │ │ ├── SingleLiveEvent.kt
│ │ │ │ ├── OnTabSelectedListenerBuilder.kt
│ │ │ │ ├── BindingAdapters.kt
│ │ │ │ ├── Language.kt
│ │ │ │ └── Preferences.kt
│ │ │ │ ├── data
│ │ │ │ ├── model
│ │ │ │ │ ├── user
│ │ │ │ │ │ ├── response
│ │ │ │ │ │ │ ├── UserAccessTokenData.kt
│ │ │ │ │ │ │ └── UserInfoData.kt
│ │ │ │ │ │ └── request
│ │ │ │ │ │ │ └── LoginRequestData.kt
│ │ │ │ │ ├── ListData.kt
│ │ │ │ │ └── repository
│ │ │ │ │ │ └── Repository.kt
│ │ │ │ ├── remote
│ │ │ │ │ ├── ResponseThrowable.kt
│ │ │ │ │ ├── user
│ │ │ │ │ │ └── UserRemoteDataSource.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ └── RepositoryRemoteDataSource.kt
│ │ │ │ │ ├── BasicAuthInterceptor.kt
│ │ │ │ │ └── ExceptionHandler.kt
│ │ │ │ ├── local
│ │ │ │ │ └── user
│ │ │ │ │ │ └── UserLocalDataSource.kt
│ │ │ │ └── repository
│ │ │ │ │ ├── GitHubRepository.kt
│ │ │ │ │ └── UserInfoRepository.kt
│ │ │ │ ├── AndroidGenericFrameworkConfiguration.kt
│ │ │ │ ├── AndroidGenericFrameworkAppGlideModule.kt
│ │ │ │ ├── AndroidGenericFrameworkApplication.kt
│ │ │ │ └── di
│ │ │ │ ├── GitHubRepositoryModule.kt
│ │ │ │ ├── ApplicationComponent.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ ├── MainModule.kt
│ │ │ │ ├── ApplicationModule.kt
│ │ │ │ ├── UserModule.kt
│ │ │ │ ├── NetworkModule.kt
│ │ │ │ └── ViewModelFactory.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── tanjiajun
│ │ │ └── androidgenericframework
│ │ │ ├── utils
│ │ │ ├── LanguageTest.kt
│ │ │ └── GsonExtTest.kt
│ │ │ ├── viewmodel
│ │ │ ├── PersonalCenterViewModelTest.kt
│ │ │ ├── SplashViewModelTest.kt
│ │ │ ├── RepositoryViewModelTest.kt
│ │ │ ├── LoginViewModelTest.kt
│ │ │ └── MainViewModelTest.kt
│ │ │ └── data
│ │ │ ├── RepositoryRemoteDataSourceTest.kt
│ │ │ ├── UserRemoteDataSourceTest.kt
│ │ │ └── FakeDataSource.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── tanjiajun
│ │ └── androidgenericframework
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .gitignore
├── screenshot
├── data.png
├── test.png
├── ui.png
├── diKoin.png
├── utils.png
├── LoginPage.png
├── MainPage.png
├── diDagger2.png
├── PersonalCenterPage.png
└── PrefixAndroidGenericFrameworkFile.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | local.properties
4 | build
5 | .idea
--------------------------------------------------------------------------------
/screenshot/data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/data.png
--------------------------------------------------------------------------------
/screenshot/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/test.png
--------------------------------------------------------------------------------
/screenshot/ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/ui.png
--------------------------------------------------------------------------------
/screenshot/diKoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/diKoin.png
--------------------------------------------------------------------------------
/screenshot/utils.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/utils.png
--------------------------------------------------------------------------------
/screenshot/LoginPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/LoginPage.png
--------------------------------------------------------------------------------
/screenshot/MainPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/MainPage.png
--------------------------------------------------------------------------------
/screenshot/diDagger2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/diDagger2.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/screenshot/PersonalCenterPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/PersonalCenterPage.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_kg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_kg.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_bell.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_bell.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_call.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_call.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_sort.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_sort.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_time.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_time.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_address.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_address.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_dropdown.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_dropdown.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_password.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_password.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_personal.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_personal.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_github.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxxhdpi/ic_github.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/screenshot/PrefixAndroidGenericFrameworkFile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/screenshot/PrefixAndroidGenericFrameworkFile.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_arrow_back.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_arrow_back.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_phone_number.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_phone_number.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_default_avatar.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanJiaJunBeyond/AndroidGenericFramework/HEAD/app/src/main/res/drawable-xxhdpi/ic_default_avatar.webp
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/NoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui
2 |
3 | /**
4 | * Created by TanJiaJun on 2020-02-13.
5 | */
6 | class NoViewModel : BaseViewModel()
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_green_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_order_background_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_common_edit_text_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Sep 07 02:33:28 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_divider_line.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/AndroidGenericFrameworkFragmentTag.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | /**
4 | * Created by TanJiaJun on 2019-07-29.
5 | */
6 | const val FRAGMENT_TAG_LOGIN = "login_fragment"
7 | const val FRAGMENT_TAG_REPOSITORY = "repository_fragment"
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/AndroidGenericFrameworkExtra.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | /**
4 | * Created by TanJiaJun on 2019-08-24.
5 | */
6 | // 用户
7 | const val EXTRA_LOGOUT = "EXTRA_LOGOUT"
8 |
9 | // 仓库
10 | const val EXTRA_LANGUAGE = "EXTRA_LANGUAGE"
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/DateUtils.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import java.time.format.DateTimeFormatter
4 |
5 | /**
6 | * Created by TanJiaJun on 2020-02-07.
7 | */
8 | fun dateFormatForRepository(): DateTimeFormatter =
9 | DateTimeFormatter.ofPattern("yyyy-MM-dd")
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_register_and_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/model/user/response/UserAccessTokenData.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.model.user.response
2 |
3 | /**
4 | * Created by TanJiaJun on 2020-02-02.
5 | */
6 | data class UserAccessTokenData(
7 | var id: Int,
8 | var token: String,
9 | var url: String
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/remote/ResponseThrowable.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.remote
2 |
3 | /**
4 | * Created by TanJiaJun on 2020-02-04.
5 | */
6 | class ResponseThrowable(
7 | var errorCode: Int,
8 | var errorMessage: String,
9 | throwable: Throwable
10 | ) : Exception(throwable)
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/FragmentExt.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import androidx.fragment.app.Fragment
4 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
5 |
6 | /**
7 | * Created by TanJiaJun on 2019-08-07.
8 | */
9 | inline fun > Fragment.startActivity() =
10 | startActivity(android.content.Intent(context, T::class.java))
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/ActivityExt.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
6 |
7 | /**
8 | * Created by TanJiaJun on 2019-08-12.
9 | */
10 | inline fun > Activity.startActivity() =
11 | startActivity(Intent(this, T::class.java))
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/model/ListData.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | /**
6 | * Created by TanJiaJun on 2020-02-06.
7 | */
8 | data class ListData(
9 | @SerializedName("total_count") val totalCount: Int? = null,
10 | @SerializedName("incomplete_results") val incompleteResults: Boolean? = null,
11 | var items: List? = null
12 | )
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_in_replaced.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_in_bottom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_in_top.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_bottom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_hidden.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_replaced.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/switch_out_top.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_c.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_css.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_go.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_java.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_other.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_php.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_ruby.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_swift.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_kotlin.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_python.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_c_plus_plus.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_circle_java_script.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/ToastExt.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import android.widget.Toast
4 | import com.tanjiajun.androidgenericframework.AndroidGenericFrameworkApplication
5 |
6 | /**
7 | * Created by TanJiaJun on 2020-02-13.
8 | */
9 | fun toastShort(text: String) =
10 | Toast.makeText(AndroidGenericFrameworkApplication.instance, text, Toast.LENGTH_SHORT).show()
11 |
12 | fun toastLong(text: String) =
13 | Toast.makeText(AndroidGenericFrameworkApplication.instance, text, Toast.LENGTH_LONG).show()
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/AndroidGenericFrameworkConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | /**
4 | * Created by TanJiaJun on 2019-07-30.
5 | */
6 | object AndroidGenericFrameworkConfiguration {
7 |
8 | // MMKV
9 | const val MMKV_ID = "android_generic_framework_configuration_mmkv_id"
10 | const val MMKV_CRYPT_KEY = "android_generic_framework_configuration_crypt_key"
11 |
12 | // Retrofit
13 | const val CONNECT_TIMEOUT = 20000L
14 | const val READ_TIMEOUT = 20000L
15 | const val HOST = "api.github.com"
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_star.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/recyclerview/BaseViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.recyclerview
2 |
3 | import androidx.databinding.ViewDataBinding
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.tanjiajun.androidgenericframework.BR
6 |
7 | /**
8 | * Created by TanJiaJun on 2019-08-25.
9 | */
10 | class BaseViewHolder(val binding: ViewDataBinding)
11 | : RecyclerView.ViewHolder(binding.root) {
12 |
13 | fun bind(data: Any) =
14 | with(binding) {
15 | setVariable(BR.data, data)
16 | executePendingBindings()
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_common_button_background_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/utils/LanguageTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Assert.assertNotEquals
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.junit.runners.JUnit4
8 |
9 | /**
10 | * Created by TanJiaJun on 2020/5/28.
11 | */
12 | @RunWith(JUnit4::class)
13 | class LanguageTest {
14 |
15 | @Test
16 | fun getLanguage_success() {
17 | assertEquals(Language.KOTLIN, Language.of("Kotlin"))
18 | }
19 |
20 | @Test
21 | fun getLanguage_failure() {
22 | assertNotEquals(Language.JAVA, Language.of("Kotlin"))
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/BooleanExt.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | /**
4 | * Created by TanJiaJun on 2020-01-28.
5 | */
6 | sealed class BooleanExt
7 |
8 | class TransferData(val data: T) : BooleanExt()
9 | object Otherwise : BooleanExt()
10 |
11 | inline fun Boolean.yes(block: () -> T): BooleanExt =
12 | when {
13 | this -> TransferData(block.invoke())
14 | else -> Otherwise
15 | }
16 |
17 | inline fun BooleanExt.otherwise(block: () -> T): T =
18 | when (this) {
19 | is Otherwise -> block()
20 | is TransferData -> data
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/GsonExt.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.JsonElement
5 | import com.google.gson.reflect.TypeToken
6 | import java.io.Reader
7 |
8 | /**
9 | * Created by TanJiaJun on 2019/5/3.
10 | */
11 | inline fun Gson.fromJson(string: String): T =
12 | fromJson(string, T::class.java)
13 |
14 | inline fun Gson.fromJsonByType(string: String): T =
15 | fromJson(string, object : TypeToken() {}.type)
16 |
17 | inline fun Gson.fromJson(reader: Reader): T =
18 | fromJson(reader, T::class.java)
19 |
20 | inline fun Gson.fromJson(jsonElement: JsonElement): T =
21 | fromJson(jsonElement, T::class.java)
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/AndroidGenericFrameworkAppGlideModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | import android.content.Context
4 | import com.bumptech.glide.GlideBuilder
5 | import com.bumptech.glide.annotation.GlideModule
6 | import com.bumptech.glide.load.DecodeFormat
7 | import com.bumptech.glide.module.AppGlideModule
8 | import com.bumptech.glide.request.RequestOptions
9 |
10 | /**
11 | * Created by TanJiaJun on 2019-09-13.
12 | */
13 | @GlideModule
14 | class AndroidGenericFrameworkAppGlideModule : AppGlideModule() {
15 |
16 | override fun applyOptions(context: Context, builder: GlideBuilder) {
17 | super.applyOptions(context, builder)
18 | builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #ffffff
5 | #000000
6 |
7 | #5e616d
8 |
9 | #f8f9fb
10 |
11 | #f2f3f5
12 |
13 | #000913
14 | #a8acbf
15 | #5e616d
16 |
17 | #cfd6e0
18 | #5e616d
19 | #a8acbf
20 |
21 | #a8acbf
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/tanjiajun/androidgenericframework/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getTargetContext()
20 | assertEquals("com.tanjiajun.androidgenericframework", appContext.packageName)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/AndroidGenericFrameworkApplication.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework
2 |
3 | import com.tanjiajun.androidgenericframework.di.DaggerApplicationComponent
4 | import com.tencent.mmkv.MMKV
5 | import dagger.android.AndroidInjector
6 | import dagger.android.DaggerApplication
7 |
8 | /**
9 | * Created by TanJiaJun on 2019-07-28.
10 | */
11 | class AndroidGenericFrameworkApplication : DaggerApplication() {
12 |
13 | override fun applicationInjector(): AndroidInjector =
14 | DaggerApplicationComponent.factory().create(applicationContext)
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | instance = this
19 | MMKV.initialize(this)
20 | }
21 |
22 | companion object {
23 | lateinit var instance: AndroidGenericFrameworkApplication
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/GitHubRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.tanjiajun.androidgenericframework.ui.repository.fragment.RepositoryFragment
5 | import com.tanjiajun.androidgenericframework.ui.repository.viewmodel.RepositoryViewModel
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.android.ContributesAndroidInjector
9 | import dagger.multibindings.IntoMap
10 |
11 | /**
12 | * Created by TanJiaJun on 2020/3/8.
13 | */
14 | @Suppress("unused")
15 | @Module
16 | abstract class GitHubRepositoryModule {
17 |
18 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
19 | internal abstract fun contributeRepositoryFragment(): RepositoryFragment
20 |
21 | @Binds
22 | @IntoMap
23 | @ViewModelKey(RepositoryViewModel::class)
24 | internal abstract fun bindRepositoryViewModel(viewModel: RepositoryViewModel): ViewModel
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/model/user/request/LoginRequestData.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.model.user.request
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import com.tanjiajun.androidgenericframework.BuildConfig
5 |
6 | /**
7 | * Created by TanJiaJun on 2019-08-02.
8 | */
9 | data class LoginRequestData(
10 | val scopes: List,
11 | val note: String,
12 | @SerializedName("client_id") val clientId: String,
13 | @SerializedName("client_secret") val clientSecret: String
14 | ) {
15 |
16 | companion object {
17 | fun generate(): LoginRequestData =
18 | LoginRequestData(
19 | scopes = listOf("user", "repo", "gist", "notifications"),
20 | note = BuildConfig.APPLICATION_ID,
21 | clientId = BuildConfig.GITHUB_CLIENT_ID,
22 | clientSecret = BuildConfig.GITHUB_CLIENT_SECRET
23 | )
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AndroidGenericFramework
4 |
5 |
6 | %1$s://%2$s/
7 | GitHub图标
8 |
9 |
10 | 出错了!请点击重试。
11 | 重试
12 |
13 |
14 | 添加
15 |
16 |
17 | 请输入用户名
18 | 请输入密码
19 | 登录
20 | 取消
21 |
22 | 个人中心
23 | 头像
24 | 名字
25 | 退出登录
26 |
27 |
28 | 语言图标
29 | star图标
30 | fork图标
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/user/activity/RegisterAndLoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.user.activity
2 |
3 | import android.os.Bundle
4 | import com.tanjiajun.androidgenericframework.R
5 | import com.tanjiajun.androidgenericframework.databinding.ActivityRegisterAndLoginBinding
6 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
7 | import com.tanjiajun.androidgenericframework.ui.NoViewModel
8 | import com.tanjiajun.androidgenericframework.ui.user.fragment.LoginFragment
9 |
10 | /**
11 | * Created by TanJiaJun on 2019-07-28.
12 | */
13 | class RegisterAndLoginActivity : BaseActivity() {
14 |
15 | override val layoutRes: Int = R.layout.activity_register_and_login
16 | override val viewModel = NoViewModel()
17 | override val containId: Int = R.id.fl_content
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | addFragment(LoginFragment.newInstance())
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/utils/GsonExtTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import com.google.gson.Gson
4 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserInfoData
5 | import com.tanjiajun.androidgenericframework.data.userAccessTokenData
6 | import com.tanjiajun.androidgenericframework.data.userInfoData
7 | import com.tanjiajun.androidgenericframework.data.userInfoDataJson
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Assert.assertNotEquals
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.junit.runners.JUnit4
13 |
14 | /**
15 | * Created by TanJiaJun on 2020/5/28.
16 | */
17 | @RunWith(JUnit4::class)
18 | class GsonExtTest {
19 |
20 | @Test
21 | fun fromJson_success() {
22 | assertEquals(userInfoData.id, Gson().fromJson(userInfoDataJson).id)
23 | }
24 |
25 | @Test
26 | fun fromJson_failure() {
27 | assertNotEquals(userAccessTokenData.id, Gson().fromJson(userInfoDataJson).id)
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/main/viewmodel/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.main.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
7 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
8 | import kotlinx.coroutines.delay
9 | import kotlinx.coroutines.launch
10 | import javax.inject.Inject
11 |
12 | /**
13 | * Created by TanJiaJun on 2019-08-12.
14 | */
15 | class SplashViewModel @Inject constructor(
16 | private val repository: UserInfoRepository
17 | ) : BaseViewModel() {
18 |
19 | private val _isNavigateToMainActivity = MutableLiveData()
20 | var isNavigateToMainActivity: LiveData = _isNavigateToMainActivity
21 |
22 | fun navigateToPage() =
23 | viewModelScope.launch {
24 | delay(1000)
25 | _isNavigateToMainActivity.value = repository.isLogin()
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/ApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import android.content.Context
4 | import com.tanjiajun.androidgenericframework.AndroidGenericFrameworkApplication
5 | import dagger.BindsInstance
6 | import dagger.Component
7 | import dagger.android.AndroidInjector
8 | import dagger.android.support.AndroidSupportInjectionModule
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * Created by TanJiaJun on 2020/3/4.
13 | */
14 | @Singleton
15 | @Component(
16 | modules = [
17 | AndroidSupportInjectionModule::class,
18 | ApplicationModule::class,
19 | NetworkModule::class,
20 | RepositoryModule::class,
21 | MainModule::class,
22 | UserModule::class,
23 | GitHubRepositoryModule::class
24 | ]
25 | )
26 | interface ApplicationComponent : AndroidInjector {
27 |
28 | @Component.Factory
29 | interface Factory {
30 |
31 | fun create(@BindsInstance applicationContext: Context): ApplicationComponent
32 |
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
18 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | android.databinding.enableV2=true
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
20 |
21 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/recyclerview/NoDataViewType.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.recyclerview
2 |
3 | /**
4 | * Created by TanJiaJun on 2019-08-31.
5 | */
6 | abstract class NoDataViewType : BaseViewType() {
7 |
8 | abstract fun bind(holder: BaseViewHolder,
9 | position: Int)
10 |
11 | abstract fun isMatchViewType(position: Int,
12 | headerCount: Int,
13 | itemCount: Int,
14 | footerCount: Int): Boolean
15 |
16 | override fun bind(holder: BaseViewHolder, data: T, position: Int) {
17 | // no implementation
18 | }
19 |
20 | override fun bind(holder: BaseViewHolder, data: T,
21 | position: Int,
22 | payLoads: List) {
23 | // no implementation
24 | }
25 |
26 | override fun isMatchViewType(any: Any): Boolean = false
27 |
28 | fun bind(holder: BaseViewHolder,
29 | position: Int,
30 | payLoads: List) =
31 | with(holder) {
32 | bind(this, position)
33 | binding.executePendingBindings()
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import com.tanjiajun.androidgenericframework.data.local.user.UserLocalDataSource
4 | import com.tanjiajun.androidgenericframework.data.remote.repository.RepositoryRemoteDataSource
5 | import com.tanjiajun.androidgenericframework.data.remote.user.UserRemoteDataSource
6 | import com.tanjiajun.androidgenericframework.data.repository.GitHubRepository
7 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
8 | import dagger.Module
9 | import dagger.Provides
10 | import javax.inject.Singleton
11 |
12 | /**
13 | * Created by TanJiaJun on 2020/5/6.
14 | */
15 | @Suppress("unused")
16 | @Module
17 | open class RepositoryModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideUserInfoRepository(remoteDataSource: UserRemoteDataSource,
22 | localDataSource: UserLocalDataSource): UserInfoRepository =
23 | UserInfoRepository(remoteDataSource, localDataSource)
24 |
25 | @Provides
26 | @Singleton
27 | fun provideGitHubRepository(remoteDataSource: RepositoryRemoteDataSource): GitHubRepository =
28 | GitHubRepository(remoteDataSource)
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/user/viewmodel/PersonalCenterViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.user.viewmodel
2 |
3 | import android.view.View
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
7 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Created by TanJiaJun on 2019-08-24.
12 | */
13 | class PersonalCenterViewModel @Inject constructor(
14 | private val repository: UserInfoRepository
15 | ) : BaseViewModel() {
16 |
17 | private val _avatarUrl = MutableLiveData().apply {
18 | value = repository.getAvatarUrl()
19 | }
20 | val avatarUrl: LiveData = _avatarUrl
21 |
22 | private val _name = MutableLiveData().apply {
23 | value = repository.getName()
24 | }
25 | val name: LiveData = _name
26 |
27 | fun showTitle(title: String) {
28 | _title.value = title
29 | }
30 |
31 | fun logout() =
32 | repository.logout()
33 |
34 | interface Handlers : BaseViewModel.Handlers {
35 |
36 | fun onLogoutClick(view: View)
37 |
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/repository/adapter/RepositoryAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.repository.adapter
2 |
3 | import com.tanjiajun.androidgenericframework.BR
4 | import com.tanjiajun.androidgenericframework.R
5 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryData
6 | import com.tanjiajun.androidgenericframework.ui.recyclerview.BaseViewHolder
7 | import com.tanjiajun.androidgenericframework.ui.recyclerview.BaseViewType
8 | import com.tanjiajun.androidgenericframework.ui.recyclerview.MultiViewTypeDataBindingAdapter
9 |
10 | /**
11 | * Created by TanJiaJun on 2020-02-07.
12 | */
13 | class RepositoryAdapter : MultiViewTypeDataBindingAdapter() {
14 |
15 | init {
16 | addViewType(RepositoryViewType())
17 | }
18 |
19 | }
20 |
21 | class RepositoryViewType : BaseViewType() {
22 |
23 | override fun getItemLayoutRes(): Int = R.layout.item_repository
24 |
25 | override fun isMatchViewType(any: Any): Boolean =
26 | any is RepositoryData
27 |
28 | override fun bind(holder: BaseViewHolder, data: RepositoryData, position: Int) {
29 | holder.binding.setVariable(BR.data, data)
30 | }
31 |
32 | override fun getSpanSize(): Int = 0
33 |
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/SingleLiveEvent.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import android.util.Log
4 | import androidx.annotation.MainThread
5 | import androidx.lifecycle.LifecycleOwner
6 | import androidx.lifecycle.MutableLiveData
7 | import androidx.lifecycle.Observer
8 | import java.util.concurrent.atomic.AtomicBoolean
9 |
10 | /**
11 | * Created by TanJiaJun on 2020-02-03.
12 | */
13 | class SingleLiveEvent : MutableLiveData() {
14 |
15 | private val pending = AtomicBoolean(false)
16 |
17 | @MainThread
18 | override fun observe(owner: LifecycleOwner, observer: Observer) {
19 | if (hasActiveObservers()) {
20 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
21 | }
22 | super.observe(owner, Observer {
23 | if (pending.compareAndSet(true, false)) {
24 | observer.onChanged(it)
25 | }
26 | })
27 | }
28 |
29 | @MainThread
30 | override fun setValue(value: T?) {
31 | pending.set(true)
32 | super.setValue(value)
33 | }
34 |
35 | @MainThread
36 | fun call() {
37 | value = null
38 | }
39 |
40 | private companion object {
41 | const val TAG = "SingleLiveEvent"
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/recyclerview/BaseViewType.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.recyclerview
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.annotation.LayoutRes
6 | import androidx.databinding.DataBindingUtil
7 |
8 | /**
9 | * Created by TanJiaJun on 2019-08-31.
10 | */
11 | abstract class BaseViewType {
12 |
13 | @LayoutRes
14 | abstract fun getItemLayoutRes(): Int
15 |
16 | abstract fun isMatchViewType(any: Any): Boolean
17 |
18 | abstract fun bind(holder: BaseViewHolder,
19 | data: T,
20 | position: Int)
21 |
22 | abstract fun getSpanSize(): Int
23 |
24 | fun onCreateDataBindingViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder =
25 | BaseViewHolder(DataBindingUtil.inflate(
26 | LayoutInflater.from(parent.context),
27 | viewType,
28 | parent,
29 | false
30 | ))
31 |
32 | open fun bind(holder: BaseViewHolder,
33 | data: T,
34 | position: Int,
35 | payLoads: List) =
36 | with(holder) {
37 | bind(this, data, position)
38 | binding.executePendingBindings()
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_repository.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
28 |
29 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/MainModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.tanjiajun.androidgenericframework.ui.main.activity.MainActivity
5 | import com.tanjiajun.androidgenericframework.ui.main.activity.SplashActivity
6 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.MainViewModel
7 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.SplashViewModel
8 | import dagger.Binds
9 | import dagger.Module
10 | import dagger.android.ContributesAndroidInjector
11 | import dagger.multibindings.IntoMap
12 |
13 | /**
14 | * Created by TanJiaJun on 2020/5/3.
15 | */
16 | @Suppress("unused")
17 | @Module
18 | abstract class MainModule {
19 |
20 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
21 | internal abstract fun contributeSplashActivity(): SplashActivity
22 |
23 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
24 | internal abstract fun contributeMainActivity(): MainActivity
25 |
26 | @Binds
27 | @IntoMap
28 | @ViewModelKey(SplashViewModel::class)
29 | internal abstract fun bindSplashViewModel(viewModel: SplashViewModel): ViewModel
30 |
31 | @Binds
32 | @IntoMap
33 | @ViewModelKey(MainViewModel::class)
34 | internal abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/remote/user/UserRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.remote.user
2 |
3 | import com.tanjiajun.androidgenericframework.data.model.user.request.LoginRequestData
4 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserAccessTokenData
5 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserInfoData
6 | import retrofit2.Retrofit
7 | import retrofit2.http.Body
8 | import retrofit2.http.GET
9 | import retrofit2.http.Headers
10 | import retrofit2.http.POST
11 | import javax.inject.Inject
12 |
13 | /**
14 | * Created by TanJiaJun on 2020/4/4.
15 | */
16 | class UserRemoteDataSource @Inject constructor(
17 | retrofit: Retrofit
18 | ) {
19 |
20 | private val service: Service = retrofit.create(Service::class.java)
21 |
22 | suspend fun authorizations(): UserAccessTokenData =
23 | service.authorizations(LoginRequestData.generate())
24 |
25 | suspend fun fetchUserInfo(): UserInfoData =
26 | service.fetchUserInfo()
27 |
28 | interface Service {
29 |
30 | @POST("authorizations")
31 | @Headers("Accept: application/json")
32 | suspend fun authorizations(@Body loginRequestData: LoginRequestData): UserAccessTokenData
33 |
34 | @GET("user")
35 | suspend fun fetchUserInfo(): UserInfoData
36 |
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/model/repository/Repository.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.model.repository
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import com.tanjiajun.androidgenericframework.utils.Language
5 |
6 | /**
7 | * Created by TanJiaJun on 2020-02-06.
8 | */
9 | data class RepositoryResponseData(
10 | val id: Int,
11 | val name: String? = null,
12 | val description: String? = null,
13 | val language: String? = null,
14 | @SerializedName("stargazers_count") val stargazersCount: Int? = null,
15 | @SerializedName("forks_count") val forksCount: Int? = null
16 | )
17 |
18 | data class RepositoryData(
19 | val id: Int,
20 | val name: String,
21 | val description: String,
22 | val language: Language,
23 | val starCount: Int,
24 | val forkCount: Int
25 | )
26 |
27 | object RepositoryMapper {
28 |
29 | fun toRepositoryData(data: RepositoryResponseData): RepositoryData =
30 | RepositoryData(
31 | id = data.id,
32 | name = data.name ?: "",
33 | description = data.description ?: "",
34 | language = Language.of(data.language ?: ""),
35 | starCount = data.stargazersCount ?: 0,
36 | forkCount = data.forksCount ?: 0
37 | )
38 |
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/recyclerview/BaseDataBindingAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.recyclerview
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.recyclerview.widget.RecyclerView
7 |
8 | /**
9 | * Created by TanJiaJun on 2019-08-29.
10 | */
11 | abstract class BaseDataBindingAdapter
12 | : RecyclerView.Adapter() {
13 |
14 | protected abstract fun getLayoutResByPosition(position: Int): Int
15 |
16 | protected abstract fun getItemByPosition(position: Int): T?
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder =
19 | BaseViewHolder(DataBindingUtil.inflate(
20 | LayoutInflater.from(parent.context),
21 | viewType,
22 | parent,
23 | false
24 | ))
25 |
26 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
27 | getItemByPosition(position)?.let { holder.bind(it) }
28 | }
29 |
30 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int, payloads: MutableList) {
31 | getItemByPosition(position)?.let { holder.bind(it) }
32 | }
33 |
34 | override fun getItemViewType(position: Int): Int =
35 | getLayoutResByPosition(position)
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/OnTabSelectedListenerBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import com.google.android.material.tabs.TabLayout
4 |
5 | /**
6 | * Created by TanJiaJun on 2019-09-07.
7 | */
8 | private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit
9 |
10 | class OnTabSelectedListenerBuilder : TabLayout.OnTabSelectedListener {
11 |
12 | private var onTabReselectedCallback: OnTabCallback? = null
13 | private var onTabUnselectedCallback: OnTabCallback? = null
14 | private var onTabSelectedCallback: OnTabCallback? = null
15 |
16 | override fun onTabReselected(tab: TabLayout.Tab?) =
17 | onTabReselectedCallback?.invoke(tab) ?: Unit
18 |
19 | override fun onTabUnselected(tab: TabLayout.Tab?) =
20 | onTabUnselectedCallback?.invoke(tab) ?: Unit
21 |
22 | override fun onTabSelected(tab: TabLayout.Tab?) =
23 | onTabSelectedCallback?.invoke(tab) ?: Unit
24 |
25 | fun onTabReselected(callback: OnTabCallback) {
26 | onTabReselectedCallback = callback
27 | }
28 |
29 | fun onTabUnselected(callback: OnTabCallback) {
30 | onTabUnselectedCallback = callback
31 | }
32 |
33 | fun onTabSelected(callback: OnTabCallback) {
34 | onTabSelectedCallback = callback
35 | }
36 |
37 | }
38 |
39 | fun registerOnTabSelectedListener(function: OnTabSelectedListenerBuilder.() -> Unit) =
40 | OnTabSelectedListenerBuilder().also(function)
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import com.tanjiajun.androidgenericframework.AndroidGenericFrameworkConfiguration
4 | import com.tanjiajun.androidgenericframework.data.local.user.UserLocalDataSource
5 | import com.tanjiajun.androidgenericframework.data.remote.repository.RepositoryRemoteDataSource
6 | import com.tanjiajun.androidgenericframework.data.remote.user.UserRemoteDataSource
7 | import com.tencent.mmkv.MMKV
8 | import dagger.Module
9 | import dagger.Provides
10 | import retrofit2.Retrofit
11 | import javax.inject.Singleton
12 |
13 | /**
14 | * Created by TanJiaJun on 2020/3/4.
15 | */
16 | @Suppress("unused")
17 | @Module
18 | open class ApplicationModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideUserLocalDataSource(): UserLocalDataSource =
23 | UserLocalDataSource(MMKV.mmkvWithID(
24 | AndroidGenericFrameworkConfiguration.MMKV_ID,
25 | MMKV.SINGLE_PROCESS_MODE,
26 | AndroidGenericFrameworkConfiguration.MMKV_CRYPT_KEY
27 | ))
28 |
29 | @Provides
30 | @Singleton
31 | fun provideUserRemoteDataSource(retrofit: Retrofit): UserRemoteDataSource =
32 | UserRemoteDataSource(retrofit)
33 |
34 | @Provides
35 | @Singleton
36 | fun provideRepositoryRemoteDataSource(retrofit: Retrofit): RepositoryRemoteDataSource =
37 | RepositoryRemoteDataSource(retrofit)
38 |
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import android.graphics.drawable.Drawable
4 | import android.view.View
5 | import android.widget.ImageView
6 | import androidx.annotation.DrawableRes
7 | import androidx.appcompat.widget.Toolbar
8 | import androidx.databinding.BindingAdapter
9 | import com.bumptech.glide.Glide
10 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
11 | import com.bumptech.glide.request.transition.DrawableCrossFadeFactory
12 | import com.tanjiajun.androidgenericframework.R
13 |
14 | /**
15 | * Created by TanJiaJun on 2019-08-25.
16 | */
17 | @BindingAdapter("android:src")
18 | fun ImageView.setImageSource(@DrawableRes resId: Int) =
19 | setImageResource(resId)
20 |
21 | @BindingAdapter("onNavigationIconClick")
22 | fun Toolbar.setOnNavigationIconClickListener(listener: View.OnClickListener) =
23 | setNavigationOnClickListener(listener)
24 |
25 | @BindingAdapter(value = ["url", "placeholder", "error"], requireAll = false)
26 | fun ImageView.loadImage(url: String?, placeholder: Drawable?, error: Drawable?) =
27 | Glide
28 | .with(context)
29 | .load(url)
30 | .placeholder(placeholder ?: context.getDrawable(R.mipmap.ic_launcher))
31 | .error(error ?: context.getDrawable(R.mipmap.ic_launcher))
32 | .transition(DrawableTransitionOptions.withCrossFade(DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()))
33 | .into(this)
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/repository/viewmodel/RepositoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.repository.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryData
6 | import com.tanjiajun.androidgenericframework.data.repository.GitHubRepository
7 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
8 | import com.tanjiajun.androidgenericframework.ui.UIState
9 | import javax.inject.Inject
10 |
11 | /**
12 | * Created by TanJiaJun on 2020-02-07.
13 | */
14 | class RepositoryViewModel @Inject constructor(
15 | private val repository: GitHubRepository
16 | ) : BaseViewModel() {
17 |
18 | private val _isShowRepositoryView = MutableLiveData()
19 | val isShowRepositoryView: LiveData = _isShowRepositoryView
20 |
21 | private val _repositories = MutableLiveData>()
22 | val repositories: LiveData> = _repositories
23 |
24 | fun getRepositories(languageName: String) =
25 | launch(
26 | uiState = UIState(isShowLoadingView = true, isShowErrorView = true),
27 | block = { repository.getRepositories(languageName) },
28 | success = {
29 | if (it.isNotEmpty()) {
30 | _repositories.value = it
31 | _isShowRepositoryView.value = true
32 | }
33 | }
34 | )
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/local/user/UserLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.local.user
2 |
3 | import com.tanjiajun.androidgenericframework.utils.int
4 | import com.tanjiajun.androidgenericframework.utils.string
5 | import com.tencent.mmkv.MMKV
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Created by TanJiaJun on 2019-08-08.
10 | */
11 | class UserLocalDataSource @Inject constructor(
12 | private val mmkv: MMKV
13 | ) {
14 |
15 | var accessToken by mmkv.string("user_access_token", "")
16 | var userId by mmkv.int("user_id", -1)
17 | var username by mmkv.string("username", "")
18 | var password by mmkv.string("password", "")
19 | var name by mmkv.string("name", "")
20 | var avatarUrl by mmkv.string("avatar_url", "")
21 |
22 | fun clearUserInfoCache() =
23 | mmkv.removeValuesForKeys(arrayOf(
24 | "user_access_token",
25 | "user_id",
26 | "username",
27 | "password",
28 | "name",
29 | "avatar_url"
30 | ))
31 |
32 | fun cacheUserId(userId: Int) {
33 | this.userId = userId
34 | }
35 |
36 | fun cacheUsername(username: String) {
37 | this.username = username
38 | }
39 |
40 | fun cachePassword(password: String) {
41 | this.password = password
42 | }
43 |
44 | fun cacheName(name: String) {
45 | this.name = name
46 | }
47 |
48 | fun cacheAvatarUrl(avatarUrl: String) {
49 | this.avatarUrl = avatarUrl
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/main/activity/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.main.activity
2 |
3 | import android.os.Bundle
4 | import androidx.activity.viewModels
5 | import androidx.lifecycle.Observer
6 | import com.tanjiajun.androidgenericframework.R
7 | import com.tanjiajun.androidgenericframework.databinding.ActivitySplashBinding
8 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
9 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.SplashViewModel
10 | import com.tanjiajun.androidgenericframework.ui.user.activity.RegisterAndLoginActivity
11 | import com.tanjiajun.androidgenericframework.utils.otherwise
12 | import com.tanjiajun.androidgenericframework.utils.startActivity
13 | import com.tanjiajun.androidgenericframework.utils.yes
14 |
15 | /**
16 | * Created by TanJiaJun on 2019-08-09.
17 | */
18 | class SplashActivity : BaseActivity() {
19 |
20 | override val layoutRes: Int = R.layout.activity_splash
21 | override val viewModel by viewModels { viewModelFactory }
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | binding.lifecycleOwner = this
26 | with(viewModel) {
27 | navigateToPage()
28 | isNavigateToMainActivity.observe(this@SplashActivity, Observer {
29 | it
30 | .yes { startActivity() }
31 | .otherwise { startActivity() }
32 | finish()
33 | })
34 | }
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/UserModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.tanjiajun.androidgenericframework.ui.user.activity.PersonalCenterActivity
5 | import com.tanjiajun.androidgenericframework.ui.user.activity.RegisterAndLoginActivity
6 | import com.tanjiajun.androidgenericframework.ui.user.fragment.LoginFragment
7 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.LoginViewModel
8 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.PersonalCenterViewModel
9 | import dagger.Binds
10 | import dagger.Module
11 | import dagger.android.ContributesAndroidInjector
12 | import dagger.multibindings.IntoMap
13 |
14 | /**
15 | * Created by TanJiaJun on 2020/3/8.
16 | */
17 | @Suppress("unused")
18 | @Module
19 | abstract class UserModule {
20 |
21 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
22 | internal abstract fun contributeRegisterAndLoginActivity(): RegisterAndLoginActivity
23 |
24 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
25 | internal abstract fun contributeLoginFragment(): LoginFragment
26 |
27 | @ContributesAndroidInjector(modules = [ViewModelBuilder::class])
28 | internal abstract fun contributePersonalCenterActivity(): PersonalCenterActivity
29 |
30 | @Binds
31 | @IntoMap
32 | @ViewModelKey(LoginViewModel::class)
33 | internal abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel
34 |
35 | @Binds
36 | @IntoMap
37 | @ViewModelKey(PersonalCenterViewModel::class)
38 | internal abstract fun bindPersonalCenterViewModel(viewModel: PersonalCenterViewModel): ViewModel
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/repository/GitHubRepository.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.repository
2 |
3 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryData
4 | import com.tanjiajun.androidgenericframework.data.remote.repository.RepositoryRemoteDataSource
5 | import com.tanjiajun.androidgenericframework.utils.Language
6 | import java.time.LocalDateTime
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Created by TanJiaJun on 2020-02-08.
11 | */
12 | class GitHubRepository @Inject constructor(
13 | private val remoteDataSource: RepositoryRemoteDataSource
14 | ) {
15 |
16 | fun getDefaultLanguageNames(): List =
17 | listOf(
18 | Language.KOTLIN.languageName,
19 | Language.JAVA.languageName,
20 | Language.SWIFT.languageName,
21 | Language.JAVA_SCRIPT.languageName,
22 | Language.PYTHON.languageName,
23 | Language.GO.languageName,
24 | Language.CSS.languageName
25 | )
26 |
27 | fun getMoreLanguageNames(): List =
28 | listOf(
29 | Language.PHP.languageName,
30 | Language.RUBY.languageName,
31 | Language.C_PLUS_PLUS.languageName,
32 | Language.C.languageName,
33 | Language.OTHER.languageName
34 | )
35 |
36 | suspend fun getRepositories(languageName: String): List =
37 | remoteDataSource.fetchRepositories(
38 | languageName = languageName,
39 | fromDateTime = LocalDateTime.now().minusMonths(1)
40 | )
41 |
42 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_fork.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
20 |
21 |
27 |
28 |
34 |
35 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import com.tanjiajun.androidgenericframework.AndroidGenericFrameworkConfiguration
4 | import com.tanjiajun.androidgenericframework.data.local.user.UserLocalDataSource
5 | import com.tanjiajun.androidgenericframework.data.remote.BasicAuthInterceptor
6 | import dagger.Module
7 | import dagger.Provides
8 | import okhttp3.OkHttpClient
9 | import retrofit2.Retrofit
10 | import retrofit2.converter.gson.GsonConverterFactory
11 | import retrofit2.converter.scalars.ScalarsConverterFactory
12 | import java.util.concurrent.TimeUnit
13 | import javax.inject.Singleton
14 |
15 | /**
16 | * Created by TanJiaJun on 2020/4/4.
17 | */
18 | @Suppress("unused")
19 | @Module
20 | open class NetworkModule {
21 |
22 | @Provides
23 | @Singleton
24 | fun provideOkHttpClient(localDataSource: UserLocalDataSource): OkHttpClient =
25 | OkHttpClient.Builder()
26 | .connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
27 | .readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
28 | .addInterceptor(BasicAuthInterceptor(localDataSource))
29 | .build()
30 |
31 | @Provides
32 | @Singleton
33 | fun provideRetrofit(client: OkHttpClient): Retrofit =
34 | Retrofit.Builder()
35 | .client(client)
36 | .addConverterFactory(ScalarsConverterFactory.create())
37 | .addConverterFactory(GsonConverterFactory.create())
38 | .baseUrl(String.format("%1\$s://%2\$s/", "https", AndroidGenericFrameworkConfiguration.HOST))
39 | .build()
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/remote/repository/RepositoryRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.remote.repository
2 |
3 | import com.tanjiajun.androidgenericframework.data.model.ListData
4 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryData
5 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryMapper
6 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryResponseData
7 | import com.tanjiajun.androidgenericframework.utils.dateFormatForRepository
8 | import retrofit2.Retrofit
9 | import retrofit2.http.GET
10 | import retrofit2.http.Query
11 | import java.time.LocalDateTime
12 | import javax.inject.Inject
13 |
14 | /**
15 | * Created by TanJiaJun on 2020/4/4.
16 | */
17 | class RepositoryRemoteDataSource @Inject constructor(
18 | retrofit: Retrofit
19 | ) {
20 |
21 | private val service: Service = retrofit.create(Service::class.java)
22 |
23 | suspend fun fetchRepositories(languageName: String,
24 | fromDateTime: LocalDateTime): List =
25 | service
26 | .fetchRepositories(
27 | query = "language:${languageName} created:>${fromDateTime.format(dateFormatForRepository())}",
28 | sort = "stars"
29 | )
30 | .items
31 | ?.map { RepositoryMapper.toRepositoryData(it) }
32 | ?: emptyList()
33 |
34 | interface Service {
35 |
36 | @GET("search/repositories")
37 | suspend fun fetchRepositories(@Query("q") query: String,
38 | @Query("sort") sort: String): ListData
39 |
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/user/activity/PersonalCenterActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.user.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import com.tanjiajun.androidgenericframework.EXTRA_LOGOUT
8 | import com.tanjiajun.androidgenericframework.R
9 | import com.tanjiajun.androidgenericframework.databinding.ActivityPersonalCenterBinding
10 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
11 | import com.tanjiajun.androidgenericframework.ui.main.activity.MainActivity
12 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.PersonalCenterViewModel
13 |
14 | /**
15 | * Created by TanJiaJun on 2019-08-24.
16 | */
17 | class PersonalCenterActivity
18 | : BaseActivity(), PersonalCenterViewModel.Handlers {
19 |
20 | override val layoutRes: Int = R.layout.activity_personal_center
21 | override val viewModel by viewModels { viewModelFactory }
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | with(binding) {
26 | lifecycleOwner = this@PersonalCenterActivity
27 | viewModel = this@PersonalCenterActivity.viewModel
28 | handlers = this@PersonalCenterActivity
29 | }
30 | viewModel.showTitle(getString(R.string.personal_center))
31 | }
32 |
33 | override fun onNavigationIconClick(view: View) =
34 | finish()
35 |
36 | override fun onLogoutClick(view: View) {
37 | viewModel.logout()
38 | startActivity(Intent(this, MainActivity::class.java).apply {
39 | putExtra(EXTRA_LOGOUT, true)
40 | })
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/repository/UserInfoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.repository
2 |
3 | import com.tanjiajun.androidgenericframework.data.local.user.UserLocalDataSource
4 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserAccessTokenData
5 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserInfoData
6 | import com.tanjiajun.androidgenericframework.data.remote.user.UserRemoteDataSource
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Created by TanJiaJun on 2019-07-31.
11 | */
12 | class UserInfoRepository @Inject constructor(
13 | private val remoteDataSource: UserRemoteDataSource,
14 | private val localDataSource: UserLocalDataSource
15 | ) {
16 |
17 | fun isLogin(): Boolean =
18 | localDataSource.userId != -1
19 |
20 | fun cacheUsername(username: String) =
21 | localDataSource.cacheUsername(username)
22 |
23 | fun cachePassword(password: String) =
24 | localDataSource.cachePassword(password)
25 |
26 | suspend fun authorizations(): UserAccessTokenData =
27 | remoteDataSource.authorizations()
28 |
29 | suspend fun getUserInfo(): UserInfoData =
30 | remoteDataSource.fetchUserInfo()
31 |
32 | fun cacheUserId(userId: Int) =
33 | localDataSource.cacheUserId(userId)
34 |
35 | fun getName(): String =
36 | localDataSource.name
37 |
38 | fun cacheName(name: String) =
39 | localDataSource.cacheName(name)
40 |
41 | fun getAvatarUrl(): String =
42 | localDataSource.avatarUrl
43 |
44 | fun cacheAvatarUrl(avatarUrl: String) =
45 | localDataSource.cacheAvatarUrl(avatarUrl)
46 |
47 | fun logout() =
48 | localDataSource.clearUserInfoCache()
49 |
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/Language.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import androidx.annotation.DrawableRes
4 | import com.tanjiajun.androidgenericframework.R
5 |
6 | /**
7 | * Created by TanJiaJun on 2020-02-07.
8 | */
9 | enum class Language(val languageName: String, @DrawableRes val iconRes: Int) {
10 |
11 | KOTLIN("Kotlin", R.drawable.ic_circle_kotlin),
12 |
13 | JAVA("Java", R.drawable.ic_circle_java),
14 |
15 | SWIFT("Swift", R.drawable.ic_circle_kotlin),
16 |
17 | JAVA_SCRIPT("JavaScript", R.drawable.ic_circle_java_script),
18 |
19 | PYTHON("Python", R.drawable.ic_circle_python),
20 |
21 | GO("Go", R.drawable.ic_circle_go),
22 |
23 | CSS("CSS", R.drawable.ic_circle_css),
24 |
25 | PHP("PHP", R.drawable.ic_circle_php),
26 |
27 | RUBY("Ruby", R.drawable.ic_circle_ruby),
28 |
29 | C_PLUS_PLUS("C++", R.drawable.ic_circle_c_plus_plus),
30 |
31 | C("C", R.drawable.ic_circle_c),
32 |
33 | OTHER("Other", R.drawable.ic_circle_other);
34 |
35 | companion object {
36 | fun of(language: String): Language =
37 | when (language) {
38 | KOTLIN.languageName -> KOTLIN
39 | JAVA.languageName -> JAVA
40 | SWIFT.languageName -> SWIFT
41 | JAVA_SCRIPT.languageName -> JAVA_SCRIPT
42 | PYTHON.languageName -> PYTHON
43 | GO.languageName -> GO
44 | CSS.languageName -> CSS
45 | PHP.languageName -> PHP
46 | RUBY.languageName -> RUBY
47 | C_PLUS_PLUS.languageName -> C_PLUS_PLUS
48 | C.languageName -> C
49 | OTHER.languageName -> OTHER
50 | else -> OTHER
51 | }
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/viewmodel/PersonalCenterViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.Observer
5 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
6 | import com.tanjiajun.androidgenericframework.data.userInfoData
7 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.PersonalCenterViewModel
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.every
10 | import io.mockk.impl.annotations.MockK
11 | import io.mockk.mockk
12 | import io.mockk.verify
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import org.junit.runner.RunWith
17 | import org.junit.runners.JUnit4
18 |
19 | /**
20 | * Created by TanJiaJun on 2020/5/28.
21 | */
22 | @RunWith(JUnit4::class)
23 | class PersonalCenterViewModelTest {
24 |
25 | @get:Rule
26 | var instantTaskExecutorRule = InstantTaskExecutorRule()
27 |
28 | @MockK
29 | private lateinit var repository: UserInfoRepository
30 |
31 | private lateinit var viewModel: PersonalCenterViewModel
32 |
33 | @Before
34 | fun setUp() {
35 | MockKAnnotations.init(this)
36 | every { repository.getAvatarUrl() } returns userInfoData.avatarUrl
37 | every { repository.getName() } returns userInfoData.login
38 | viewModel = PersonalCenterViewModel(repository)
39 | }
40 |
41 | @Test
42 | fun showTitle_success() {
43 | viewModel.showTitle("个人中心")
44 | val observer = mockk>(relaxed = true)
45 | viewModel.title.observeForever(observer)
46 | verify { observer.onChanged(match { it == "个人中心" }) }
47 | }
48 |
49 | @Test
50 | fun showTitle_failure() {
51 | viewModel.showTitle("标题")
52 | val observer = mockk>(relaxed = true)
53 | viewModel.title.observeForever(observer)
54 | verify { observer.onChanged(match { it != "个人中心" }) }
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/remote/BasicAuthInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.remote
2 |
3 | import android.util.Base64
4 | import com.tanjiajun.androidgenericframework.data.local.user.UserLocalDataSource
5 | import com.tanjiajun.androidgenericframework.utils.otherwise
6 | import com.tanjiajun.androidgenericframework.utils.yes
7 | import okhttp3.Interceptor
8 | import okhttp3.Response
9 |
10 | /**
11 | * Created by TanJiaJun on 2020-02-01.
12 | */
13 | class BasicAuthInterceptor(
14 | private val localDataSource: UserLocalDataSource
15 | ) : Interceptor {
16 |
17 | override fun intercept(chain: Interceptor.Chain): Response =
18 | with(chain) {
19 | var request = request()
20 | val authorization = getAuthorization()
21 | (authorization.isNotEmpty())
22 | .yes {
23 | request = request
24 | .newBuilder()
25 | .addHeader("Authorization", authorization)
26 | .url(request.url().toString())
27 | .build()
28 | }
29 | proceed(request)
30 | }
31 |
32 | private fun getAuthorization(): String =
33 | with(localDataSource) {
34 | (accessToken.isBlank())
35 | .yes {
36 | (username.isNotBlank() && password.isNotBlank())
37 | .yes {
38 | "basic " + Base64.encodeToString((
39 | "$username:$password").toByteArray(),
40 | Base64.NO_WRAP
41 | )
42 | }
43 | .otherwise { "" }
44 | }
45 | .otherwise { "token $accessToken" }
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/di/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import dagger.Binds
6 | import dagger.MapKey
7 | import dagger.Module
8 | import javax.inject.Inject
9 | import javax.inject.Provider
10 | import kotlin.reflect.KClass
11 |
12 | /**
13 | * Created by TanJiaJun on 2019-08-07.
14 | */
15 | class AndroidGenericFrameworkViewModelFactory @Inject constructor(
16 | private val creators: @JvmSuppressWildcards Map, Provider>
17 | ) : ViewModelProvider.Factory {
18 |
19 | override fun create(modelClass: Class): T =
20 | creators[modelClass]
21 | ?.let {
22 | creators[modelClass]
23 | ?.let { getViewModel(it) }
24 | ?: throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
25 | }
26 | ?: creators.entries
27 | .find { modelClass.isAssignableFrom(it.key) }
28 | ?.value
29 | ?.let { getViewModel(it) }
30 | ?: throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
31 |
32 | private fun getViewModel(provider: Provider): T =
33 | try {
34 | @Suppress("UNCHECKED_CAST")
35 | provider.get() as T
36 | } catch (e: Exception) {
37 | throw RuntimeException(e)
38 | }
39 |
40 | }
41 |
42 | @Module
43 | internal abstract class ViewModelBuilder {
44 |
45 | @Binds
46 | internal abstract fun bindViewModelFactory(
47 | factory: AndroidGenericFrameworkViewModelFactory
48 | ): ViewModelProvider.Factory
49 |
50 | }
51 |
52 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
53 | @Retention(AnnotationRetention.RUNTIME)
54 | @MapKey
55 | annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
47 |
48 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/model/user/response/UserInfoData.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.model.user.response
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | /**
6 | * Created by TanJiaJun on 2019-07-31.
7 | */
8 | data class UserInfoData(
9 | val id: Int,
10 | val login: String,
11 | @SerializedName("node_id") val nodeId: String,
12 | @SerializedName("avatar_url") val avatarUrl: String,
13 | @SerializedName("gravatar_id") val gravatarId: String,
14 | val url: String,
15 | @SerializedName("html_url") val htmlUrl: String,
16 | @SerializedName("followers_url") val followersUrl: String,
17 | @SerializedName("following_url") val followingUrl: String,
18 | @SerializedName("gists_url") val gistsUrl: String,
19 | @SerializedName("starred_url") val starredUrl: String,
20 | @SerializedName("subscriptions_url") val subscriptionsUrl: String,
21 | @SerializedName("organizations_url") val organizationsUrl: String,
22 | @SerializedName("repos_url") val reposUrl: String,
23 | @SerializedName("events_url") val eventsUrl: String,
24 | @SerializedName("received_events_url") val receivedEventsUrl: String,
25 | val type: String,
26 | @SerializedName("site_admin") val siteAdmin: Boolean,
27 | val name: String,
28 | val company: String,
29 | val blog: String,
30 | val location: String,
31 | val email: String,
32 | val hireable: String,
33 | val bio: String,
34 | @SerializedName("public_repos") val publicRepos: Int,
35 | @SerializedName("public_gists") val publicGists: Int,
36 | val followers: Int,
37 | val following: Int,
38 | @SerializedName("created_at") val createdAt: String,
39 | @SerializedName("updated_at") val updatedAt: String,
40 | @SerializedName("private_gists") val privateGists: Int,
41 | @SerializedName("total_private_repos") val totalPrivateRepos: Int,
42 | @SerializedName("owned_private_repos") val ownedPrivateRepos: Int,
43 | @SerializedName("disk_usage") val diskUsage: Int,
44 | val collaborators: Int,
45 | @SerializedName("two_factor_authentication") val twoFactorAuthentication: Boolean
46 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/data/remote/ExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data.remote
2 |
3 | import android.util.MalformedJsonException
4 | import com.google.gson.JsonParseException
5 | import org.json.JSONException
6 | import retrofit2.HttpException
7 | import java.net.ConnectException
8 | import java.net.SocketTimeoutException
9 | import java.net.UnknownHostException
10 | import java.text.ParseException
11 | import javax.net.ssl.SSLException
12 |
13 | /**
14 | * Created by TanJiaJun on 2020-02-04.
15 | */
16 | object ExceptionHandler {
17 |
18 | fun handleException(throwable: Throwable): ResponseThrowable =
19 | when (throwable) {
20 | is JsonParseException ->
21 | ResponseThrowable(errorCode = 0, errorMessage = "JsonParseException", throwable = throwable)
22 |
23 | is JSONException ->
24 | ResponseThrowable(errorCode = 0, errorMessage = "JSONException", throwable = throwable)
25 |
26 | is ParseException ->
27 | ResponseThrowable(errorCode = 0, errorMessage = "ParseException", throwable = throwable)
28 |
29 | is MalformedJsonException ->
30 | ResponseThrowable(errorCode = 0, errorMessage = "MalformedJsonException", throwable = throwable)
31 |
32 | is ConnectException ->
33 | ResponseThrowable(errorCode = 0, errorMessage = "ConnectException", throwable = throwable)
34 |
35 | is HttpException ->
36 | ResponseThrowable(errorCode = throwable.code(), errorMessage = throwable.message(), throwable = throwable)
37 |
38 | is SSLException ->
39 | ResponseThrowable(errorCode = 0, errorMessage = "SSLException", throwable = throwable)
40 |
41 | is SocketTimeoutException ->
42 | ResponseThrowable(errorCode = 0, errorMessage = "SocketTimeoutException", throwable = throwable)
43 |
44 | is UnknownHostException ->
45 | ResponseThrowable(errorCode = 0, errorMessage = "UnknownHostException", throwable = throwable)
46 |
47 | else ->
48 | ResponseThrowable(errorCode = 0, errorMessage = "UnknownError", throwable = throwable)
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/data/RepositoryRemoteDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import com.tanjiajun.androidgenericframework.data.remote.repository.RepositoryRemoteDataSource
5 | import kotlinx.coroutines.runBlocking
6 | import okhttp3.mockwebserver.MockResponse
7 | import okhttp3.mockwebserver.MockWebServer
8 | import org.junit.Before
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.junit.runners.JUnit4
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import retrofit2.converter.scalars.ScalarsConverterFactory
16 | import java.time.LocalDateTime
17 |
18 | /**
19 | * Created by TanJiaJun on 2020/5/26.
20 | */
21 | @RunWith(JUnit4::class)
22 | class RepositoryRemoteDataSourceTest {
23 |
24 | @get:Rule
25 | val mockWebServer = MockWebServer()
26 |
27 | private lateinit var remoteDataSource: RepositoryRemoteDataSource
28 |
29 | @Before
30 | fun setUp() {
31 | remoteDataSource = RepositoryRemoteDataSource(
32 | Retrofit.Builder()
33 | .addConverterFactory(ScalarsConverterFactory.create())
34 | .addConverterFactory(GsonConverterFactory.create())
35 | .baseUrl(mockWebServer.url("/").toString())
36 | .build()
37 | )
38 | }
39 |
40 | @Test
41 | fun fetchRepositories() {
42 | runBlocking {
43 | mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(repositoryDataJson))
44 | remoteDataSource.fetchRepositories(
45 | languageName = "Kotlin",
46 | fromDateTime = LocalDateTime.now().minusMonths(1)
47 | ).first().run {
48 | assertThat(id).isEqualTo(repositoryData.id)
49 | assertThat(name).isEqualTo(repositoryData.name)
50 | assertThat(description).isEqualTo(repositoryData.description)
51 | assertThat(language).isEqualTo(repositoryData.language)
52 | assertThat(starCount).isEqualTo(repositoryData.starCount)
53 | assertThat(forkCount).isEqualTo(repositoryData.forkCount)
54 | }
55 | }
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
24 |
25 |
37 |
38 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/user/fragment/LoginFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.user.fragment
2 |
3 | import android.os.Bundle
4 | import android.text.Editable
5 | import android.view.View
6 | import androidx.fragment.app.viewModels
7 | import androidx.lifecycle.Observer
8 | import com.tanjiajun.androidgenericframework.FRAGMENT_TAG_LOGIN
9 | import com.tanjiajun.androidgenericframework.R
10 | import com.tanjiajun.androidgenericframework.databinding.FragmentLoginBinding
11 | import com.tanjiajun.androidgenericframework.ui.BaseFragment
12 | import com.tanjiajun.androidgenericframework.ui.main.activity.MainActivity
13 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.LoginViewModel
14 | import com.tanjiajun.androidgenericframework.utils.startActivity
15 | import com.tanjiajun.androidgenericframework.utils.yes
16 | import kotlinx.coroutines.ExperimentalCoroutinesApi
17 | import kotlinx.coroutines.FlowPreview
18 |
19 | /**
20 | * Created by TanJiaJun on 2019-07-29.
21 | */
22 | class LoginFragment private constructor()
23 | : BaseFragment(), LoginViewModel.Handlers {
24 |
25 | override val layoutRes: Int = R.layout.fragment_login
26 | override val viewModel by viewModels { viewModelFactory }
27 | override val transactionTag: String = FRAGMENT_TAG_LOGIN
28 |
29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
30 | with(binding) {
31 | lifecycleOwner = this@LoginFragment
32 | viewModel = this@LoginFragment.viewModel
33 | handlers = this@LoginFragment
34 | }.also {
35 | registerLoadingProgressBarEvent()
36 | registerSnackbarEvent()
37 | observe()
38 | }
39 |
40 | private fun observe() =
41 | viewModel.isLoginSuccess.observe(this@LoginFragment, Observer {
42 | it.yes {
43 | startActivity()
44 | activity?.finish()
45 | }
46 | })
47 |
48 | override fun onUsernameAfterTextChanged(editable: Editable) =
49 | viewModel.checkLoginEnable()
50 |
51 | override fun onPasswordAfterTextChanged(editable: Editable) =
52 | viewModel.checkLoginEnable()
53 |
54 | @ExperimentalCoroutinesApi
55 | @FlowPreview
56 | override fun onLoginClick(view: View) {
57 | viewModel.login()
58 | }
59 |
60 | companion object {
61 | fun newInstance() = LoginFragment()
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/main/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.main.viewmodel
2 |
3 | import android.view.View
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.tanjiajun.androidgenericframework.data.repository.GitHubRepository
7 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
8 | import com.tanjiajun.androidgenericframework.utils.otherwise
9 | import com.tanjiajun.androidgenericframework.utils.yes
10 | import javax.inject.Inject
11 |
12 | /**
13 | * Created by TanJiaJun on 2019-08-24.
14 | */
15 | class MainViewModel @Inject constructor(
16 | val repository: GitHubRepository
17 | ) : BaseViewModel() {
18 |
19 | private val _languageNames = MutableLiveData>().apply {
20 | value = mutableListOf().apply { addAll(repository.getDefaultLanguageNames()) }
21 | }
22 | val languageNames: LiveData> = _languageNames
23 |
24 | private var _isShowAdd = MutableLiveData().apply { value = true }
25 | val isShowAdd: LiveData = _isShowAdd
26 |
27 | var index = 0
28 |
29 | fun getDefaultLanguageNames(): List =
30 | repository.getDefaultLanguageNames()
31 |
32 | fun getDefaultLanguageNamesCount(): Int =
33 | repository.getDefaultLanguageNames().size
34 |
35 | private fun getAllLanguageNames(): Int =
36 | repository.getDefaultLanguageNames().size + repository.getMoreLanguageNames().size
37 |
38 | fun getLastLanguageNameIndex(): Int =
39 | _languageNames.value?.lastIndex ?: 0
40 |
41 | fun getLastLanguageName(): String =
42 | _languageNames.value
43 | ?.let {
44 | it[it.lastIndex]
45 | }
46 | ?: ""
47 |
48 | fun addLanguageName() =
49 | _languageNames.value?.let {
50 | it.add(repository.getMoreLanguageNames()[index])
51 | index++
52 | notifyLanguageNamesUpdate(it)
53 | (it.size == getAllLanguageNames())
54 | .yes { _isShowAdd.value = false }
55 | .otherwise { _isShowAdd.value = true }
56 | }
57 |
58 | private fun notifyLanguageNamesUpdate(languageNames: MutableList) {
59 | _languageNames.value = languageNames
60 | }
61 |
62 | interface Handlers {
63 |
64 | fun onPersonalCenterClick(view: View)
65 |
66 | fun onAddClick(view: View)
67 |
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/viewmodel/SplashViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.Observer
5 | import androidx.lifecycle.viewModelScope
6 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
7 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.SplashViewModel
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.every
10 | import io.mockk.impl.annotations.MockK
11 | import io.mockk.mockk
12 | import io.mockk.verify
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.ExperimentalCoroutinesApi
15 | import kotlinx.coroutines.Job
16 | import kotlinx.coroutines.runBlocking
17 | import kotlinx.coroutines.test.resetMain
18 | import kotlinx.coroutines.test.setMain
19 | import org.junit.After
20 | import org.junit.Before
21 | import org.junit.Rule
22 | import org.junit.Test
23 | import org.junit.runner.RunWith
24 | import org.junit.runners.JUnit4
25 |
26 | /**
27 | * Created by TanJiaJun on 2020/5/26.
28 | */
29 | @RunWith(JUnit4::class)
30 | class SplashViewModelTest {
31 |
32 | @get:Rule
33 | var instantTaskExecutorRule = InstantTaskExecutorRule()
34 |
35 | @MockK
36 | private lateinit var repository: UserInfoRepository
37 |
38 | private lateinit var viewModel: SplashViewModel
39 |
40 | @ExperimentalCoroutinesApi
41 | @Before
42 | fun setUp() {
43 | MockKAnnotations.init(this)
44 | Dispatchers.setMain(Dispatchers.Unconfined)
45 | viewModel = SplashViewModel(repository)
46 | }
47 |
48 | @ExperimentalCoroutinesApi
49 | @After
50 | fun tearDown() {
51 | Dispatchers.resetMain()
52 | }
53 |
54 | @Test
55 | fun navigateToPage_success() {
56 | runBlocking {
57 | every { repository.isLogin() } returns true
58 | viewModel.navigateToPage()
59 | val observer = mockk>(relaxed = true)
60 | viewModel.isNavigateToMainActivity.observeForever(observer)
61 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
62 | verify { observer.onChanged(match { it }) }
63 | }
64 | }
65 |
66 | @Test
67 | fun navigateToPage_failure() {
68 | runBlocking {
69 | every { repository.isLogin() } returns false
70 | viewModel.navigateToPage()
71 | val observer = mockk>(relaxed = true)
72 | viewModel.isNavigateToMainActivity.observeForever(observer)
73 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
74 | verify { observer.onChanged(match { !it }) }
75 | }
76 | }
77 |
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/user/viewmodel/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.user.viewmodel
2 |
3 | import android.text.Editable
4 | import android.view.View
5 | import androidx.lifecycle.LiveData
6 | import androidx.lifecycle.MutableLiveData
7 | import com.tanjiajun.androidgenericframework.data.remote.ExceptionHandler
8 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
9 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.FlowPreview
13 | import kotlinx.coroutines.flow.*
14 | import javax.inject.Inject
15 |
16 | /**
17 | * Created by TanJiaJun on 2019-08-02.
18 | */
19 | class LoginViewModel @Inject constructor(
20 | private val repository: UserInfoRepository
21 | ) : BaseViewModel() {
22 |
23 | val username = MutableLiveData()
24 | val password = MutableLiveData()
25 |
26 | private val _isLoginEnable = MutableLiveData()
27 | val isLoginEnable: LiveData = _isLoginEnable
28 |
29 | val isLoginSuccess = MutableLiveData()
30 |
31 | fun checkLoginEnable() {
32 | _isLoginEnable.value = !username.value.isNullOrEmpty() && !password.value.isNullOrEmpty()
33 | }
34 |
35 | @ExperimentalCoroutinesApi
36 | @FlowPreview
37 | fun login() =
38 | launchUI {
39 | launchFlow {
40 | repository.run {
41 | cacheUsername(username.value ?: "")
42 | cachePassword(password.value ?: "")
43 | authorizations()
44 | }
45 | }
46 | .flatMapMerge {
47 | launchFlow { repository.getUserInfo() }
48 | }
49 | .flowOn(Dispatchers.IO)
50 | .onStart { uiLiveEvent.showLoadingProgressBarEvent.call() }
51 | .catch {
52 | val responseThrowable = ExceptionHandler.handleException(it)
53 | uiLiveEvent.showSnackbarEvent.value = "${responseThrowable.errorCode}:${responseThrowable.errorMessage}"
54 | }
55 | .onCompletion { uiLiveEvent.dismissLoadingProgressBarEvent.call() }
56 | .collect {
57 | repository.run {
58 | cacheUserId(it.id)
59 | cacheName(it.login)
60 | cacheAvatarUrl(it.avatarUrl)
61 | }
62 | isLoginSuccess.value = true
63 | }
64 | }
65 |
66 | interface Handlers {
67 |
68 | fun onUsernameAfterTextChanged(editable: Editable)
69 |
70 | fun onPasswordAfterTextChanged(editable: Editable)
71 |
72 | fun onLoginClick(view: View)
73 |
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/viewmodel/RepositoryViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.Observer
5 | import androidx.lifecycle.viewModelScope
6 | import com.tanjiajun.androidgenericframework.data.repository.GitHubRepository
7 | import com.tanjiajun.androidgenericframework.data.repositoryData
8 | import com.tanjiajun.androidgenericframework.ui.repository.viewmodel.RepositoryViewModel
9 | import io.mockk.MockKAnnotations
10 | import io.mockk.coEvery
11 | import io.mockk.impl.annotations.MockK
12 | import io.mockk.mockk
13 | import io.mockk.verify
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.Job
17 | import kotlinx.coroutines.runBlocking
18 | import kotlinx.coroutines.test.TestCoroutineDispatcher
19 | import kotlinx.coroutines.test.resetMain
20 | import kotlinx.coroutines.test.setMain
21 | import org.junit.After
22 | import org.junit.Before
23 | import org.junit.Rule
24 | import org.junit.Test
25 | import org.junit.runner.RunWith
26 | import org.junit.runners.JUnit4
27 |
28 | /**
29 | * Created by TanJiaJun on 2020/5/28.
30 | */
31 | @RunWith(JUnit4::class)
32 | class RepositoryViewModelTest {
33 |
34 | @get:Rule
35 | var instantTaskExecutorRule = InstantTaskExecutorRule()
36 |
37 | @MockK
38 | private lateinit var repository: GitHubRepository
39 |
40 | private lateinit var viewModel: RepositoryViewModel
41 |
42 | @ExperimentalCoroutinesApi
43 | @Before
44 | fun setUp() {
45 | MockKAnnotations.init(this)
46 | Dispatchers.setMain(TestCoroutineDispatcher())
47 | viewModel = RepositoryViewModel(repository)
48 | }
49 |
50 | @ExperimentalCoroutinesApi
51 | @After
52 | fun tearDown() {
53 | Dispatchers.resetMain()
54 | }
55 |
56 | @Test
57 | fun getRepositories_success() {
58 | runBlocking {
59 | val languageName = "Kotlin"
60 | coEvery { repository.getRepositories(languageName) } returns listOf(repositoryData)
61 | viewModel.getRepositories(languageName)
62 | val observer = mockk>(relaxed = true)
63 | viewModel.isShowRepositoryView.observeForever(observer)
64 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
65 | verify { observer.onChanged(match { it }) }
66 | }
67 | }
68 |
69 | @Test
70 | fun getRepositories_failure() {
71 | runBlocking {
72 | val languageName = "Kotlin"
73 | coEvery { repository.getRepositories(languageName) } throws Throwable("UnknownError")
74 | viewModel.getRepositories(languageName)
75 | val observer = mockk>(relaxed = true)
76 | viewModel.isShowErrorView.observeForever(observer)
77 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
78 | verify { observer.onChanged(match { it }) }
79 | }
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/utils/Preferences.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.utils
2 |
3 | import android.os.Parcelable
4 | import com.tencent.mmkv.MMKV
5 | import kotlin.properties.ReadWriteProperty
6 | import kotlin.reflect.KProperty
7 |
8 | /**
9 | * Created by TanJiaJun on 2020-01-11.
10 | */
11 | private inline fun MMKV.delegate(
12 | key: String? = null,
13 | defaultValue: T,
14 | crossinline getter: MMKV.(String, T) -> T,
15 | crossinline setter: MMKV.(String, T) -> Boolean
16 | ): ReadWriteProperty =
17 | object : ReadWriteProperty {
18 | override fun getValue(thisRef: Any, property: KProperty<*>): T =
19 | getter(key ?: property.name, defaultValue)
20 |
21 | override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
22 | setter(key ?: property.name, value)
23 | }
24 | }
25 |
26 | fun MMKV.boolean(
27 | key: String? = null,
28 | defaultValue: Boolean = false
29 | ): ReadWriteProperty =
30 | delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode)
31 |
32 | fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty =
33 | delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode)
34 |
35 | fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty =
36 | delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode)
37 |
38 | fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty =
39 | delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode)
40 |
41 | fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty =
42 | delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode)
43 |
44 | private inline fun MMKV.nullableDefaultValueDelegate(
45 | key: String? = null,
46 | defaultValue: T?,
47 | crossinline getter: MMKV.(String, T?) -> T,
48 | crossinline setter: MMKV.(String, T) -> Boolean
49 | ): ReadWriteProperty =
50 | object : ReadWriteProperty {
51 | override fun getValue(thisRef: Any, property: KProperty<*>): T =
52 | getter(key ?: property.name, defaultValue)
53 |
54 | override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
55 | setter(key ?: property.name, value)
56 | }
57 | }
58 |
59 | fun MMKV.byteArray(
60 | key: String? = null,
61 | defaultValue: ByteArray? = null
62 | ): ReadWriteProperty =
63 | nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode)
64 |
65 | fun MMKV.string(key: String? = null, defaultValue: String? = null): ReadWriteProperty =
66 | nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeString, MMKV::encode)
67 |
68 | fun MMKV.stringSet(
69 | key: String? = null,
70 | defaultValue: Set? = null
71 | ): ReadWriteProperty> =
72 | nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode)
73 |
74 | inline fun MMKV.parcelable(
75 | key: String? = null,
76 | defaultValue: T? = null
77 | ): ReadWriteProperty =
78 | object : ReadWriteProperty {
79 | override fun getValue(thisRef: Any, property: KProperty<*>): T =
80 | decodeParcelable(key ?: property.name, T::class.java, defaultValue)
81 |
82 | override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
83 | encode(key ?: property.name, value)
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui
2 |
3 | import android.os.Bundle
4 | import android.view.Gravity
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.FrameLayout
9 | import android.widget.ProgressBar
10 | import androidx.annotation.LayoutRes
11 | import androidx.core.content.ContextCompat
12 | import androidx.databinding.DataBindingUtil
13 | import androidx.databinding.ViewDataBinding
14 | import androidx.lifecycle.Observer
15 | import androidx.lifecycle.ViewModelProvider
16 | import com.google.android.material.snackbar.Snackbar
17 | import com.tanjiajun.androidgenericframework.R
18 | import com.tanjiajun.androidgenericframework.utils.toastShort
19 | import dagger.android.support.DaggerFragment
20 | import javax.inject.Inject
21 |
22 | /**
23 | * Created by TanJiaJun on 2019-07-28.
24 | */
25 | abstract class BaseFragment : DaggerFragment() {
26 |
27 | lateinit var binding: T
28 |
29 | @Inject
30 | lateinit var viewModelFactory: ViewModelProvider.Factory
31 |
32 | @get:LayoutRes
33 | abstract val layoutRes: Int
34 |
35 | abstract val viewModel: VM
36 |
37 | abstract val transactionTag: String
38 |
39 | open val enterAnimation: Int =
40 | R.anim.switch_in_right
41 |
42 | open val exitAnimation: Int =
43 | R.anim.switch_out_left
44 |
45 | open val popEnterAnimation: Int =
46 | R.anim.switch_in_left
47 |
48 | open val popExitAnimation: Int =
49 | R.anim.switch_out_right
50 |
51 | open val enableAnimation: Boolean = true
52 |
53 | private var progressBar: ProgressBar? = null
54 |
55 | override fun onCreateView(inflater: LayoutInflater,
56 | container: ViewGroup?,
57 | savedInstanceState: Bundle?): View? =
58 | DataBindingUtil.inflate(inflater, layoutRes, container, false)
59 | .also { binding = it }
60 | .root
61 |
62 | protected fun registerToastEvent() =
63 | viewModel.uiLiveEvent.showToastEvent.observe(this, Observer { toastShort(it) })
64 |
65 | protected fun registerLoadingProgressBarEvent() =
66 | with(viewModel.uiLiveEvent) {
67 | showLoadingProgressBarEvent.observe(this@BaseFragment, Observer {
68 | activity?.findViewById(android.R.id.content)?.addView(
69 | ProgressBar(context)
70 | .apply {
71 | layoutParams = FrameLayout.LayoutParams(
72 | FrameLayout.LayoutParams.WRAP_CONTENT,
73 | FrameLayout.LayoutParams.WRAP_CONTENT
74 | ).also { it.gravity = Gravity.CENTER }
75 | }
76 | .also { progressBar = it }
77 | )
78 | })
79 | dismissLoadingProgressBarEvent.observe(this@BaseFragment, Observer {
80 | progressBar?.let { activity?.findViewById(android.R.id.content)?.removeView(it) }
81 | })
82 | }
83 |
84 | protected fun registerSnackbarEvent() =
85 | viewModel.uiLiveEvent.showSnackbarEvent.observe(this, Observer {
86 | activity?.let { activity ->
87 | context?.let { context ->
88 | Snackbar
89 | .make(activity.window.decorView, it, Snackbar.LENGTH_SHORT)
90 | .setActionTextColor(ContextCompat.getColor(context, R.color.white))
91 | .show()
92 | }
93 | }
94 | })
95 |
96 | open fun onHandleGoBack() {
97 | // no implementation
98 | }
99 |
100 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
23 |
24 |
34 |
35 |
43 |
44 |
55 |
56 |
68 |
69 |
77 |
78 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/viewmodel/LoginViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.Observer
5 | import androidx.lifecycle.viewModelScope
6 | import com.tanjiajun.androidgenericframework.data.repository.UserInfoRepository
7 | import com.tanjiajun.androidgenericframework.data.userAccessTokenData
8 | import com.tanjiajun.androidgenericframework.data.userInfoData
9 | import com.tanjiajun.androidgenericframework.ui.user.viewmodel.LoginViewModel
10 | import io.mockk.MockKAnnotations
11 | import io.mockk.coEvery
12 | import io.mockk.impl.annotations.MockK
13 | import io.mockk.mockk
14 | import io.mockk.verify
15 | import kotlinx.coroutines.*
16 | import kotlinx.coroutines.test.TestCoroutineDispatcher
17 | import kotlinx.coroutines.test.resetMain
18 | import kotlinx.coroutines.test.setMain
19 | import org.junit.After
20 | import org.junit.Before
21 | import org.junit.Rule
22 | import org.junit.Test
23 | import org.junit.runner.RunWith
24 | import org.junit.runners.JUnit4
25 |
26 | /**
27 | * Created by TanJiaJun on 2020/5/27.
28 | */
29 | @RunWith(JUnit4::class)
30 | class LoginViewModelTest {
31 |
32 | @get:Rule
33 | var instantTaskExecutorRule = InstantTaskExecutorRule()
34 |
35 | @MockK(relaxed = true)
36 | private lateinit var repository: UserInfoRepository
37 |
38 | private lateinit var viewModel: LoginViewModel
39 |
40 | @ExperimentalCoroutinesApi
41 | @Before
42 | fun setUp() {
43 | MockKAnnotations.init(this)
44 | Dispatchers.setMain(TestCoroutineDispatcher())
45 | viewModel = LoginViewModel(repository)
46 | }
47 |
48 | @ExperimentalCoroutinesApi
49 | @After
50 | fun tearDown() {
51 | Dispatchers.resetMain()
52 | }
53 |
54 | @Test
55 | fun checkLoginEnable_success() {
56 | viewModel.username.value = "1120571286@qq.com"
57 | viewModel.password.value = "password"
58 | viewModel.checkLoginEnable()
59 | val observer = mockk>(relaxed = true)
60 | viewModel.isLoginEnable.observeForever(observer)
61 | verify { observer.onChanged(match { it }) }
62 | }
63 |
64 | @Test
65 | fun checkLoginEnable_failure() {
66 | viewModel.username.value = null
67 | viewModel.password.value = null
68 | viewModel.checkLoginEnable()
69 | val observer = mockk>(relaxed = true)
70 | viewModel.isLoginEnable.observeForever(observer)
71 | verify { observer.onChanged(match { !it }) }
72 | }
73 |
74 | @ExperimentalCoroutinesApi
75 | @FlowPreview
76 | @Test
77 | fun login_success() {
78 | runBlocking {
79 | viewModel.username.value = "1120571286@qq.com"
80 | viewModel.password.value = "password"
81 | coEvery { repository.authorizations() } returns userAccessTokenData
82 | coEvery { repository.getUserInfo() } returns userInfoData
83 | viewModel.login()
84 | val observer = mockk>(relaxed = true)
85 | viewModel.isLoginSuccess.observeForever(observer)
86 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
87 | verify { observer.onChanged(match { it }) }
88 | }
89 | }
90 |
91 | @ExperimentalCoroutinesApi
92 | @FlowPreview
93 | @Test
94 | fun login_failure() {
95 | runBlocking {
96 | viewModel.username.value = "1120571286@qq.com"
97 | viewModel.password.value = "password"
98 | coEvery { repository.authorizations() } returns userAccessTokenData
99 | coEvery { repository.getUserInfo() } throws Throwable("UnknownError")
100 | viewModel.login()
101 | val observer = mockk>(relaxed = true)
102 | viewModel.uiLiveEvent.showSnackbarEvent.observeForever(observer)
103 | viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
104 | verify { observer.onChanged(match { it == "0:UnknownError" }) }
105 | }
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/viewmodel/MainViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.Observer
5 | import com.tanjiajun.androidgenericframework.data.repository.GitHubRepository
6 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.MainViewModel
7 | import com.tanjiajun.androidgenericframework.utils.Language
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.every
10 | import io.mockk.impl.annotations.MockK
11 | import io.mockk.mockk
12 | import io.mockk.verify
13 | import org.junit.Assert.assertEquals
14 | import org.junit.Assert.assertNotEquals
15 | import org.junit.Before
16 | import org.junit.Rule
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import org.junit.runners.JUnit4
20 |
21 | /**
22 | * Created by TanJiaJun on 2020/5/26.
23 | */
24 | @RunWith(JUnit4::class)
25 | class MainViewModelTest {
26 |
27 | @get:Rule
28 | var instantTaskExecutorRule = InstantTaskExecutorRule()
29 |
30 | @MockK
31 | private lateinit var repository: GitHubRepository
32 |
33 | private lateinit var viewModel: MainViewModel
34 |
35 | @Before
36 | fun setUp() {
37 | MockKAnnotations.init(this)
38 | every { repository.getDefaultLanguageNames() } returns listOf(
39 | Language.KOTLIN.languageName,
40 | Language.JAVA.languageName,
41 | Language.SWIFT.languageName,
42 | Language.JAVA_SCRIPT.languageName,
43 | Language.PYTHON.languageName,
44 | Language.GO.languageName,
45 | Language.CSS.languageName
46 | )
47 | every { repository.getMoreLanguageNames() } returns listOf(
48 | Language.PHP.languageName,
49 | Language.RUBY.languageName,
50 | Language.C_PLUS_PLUS.languageName,
51 | Language.C.languageName,
52 | Language.OTHER.languageName
53 | )
54 | viewModel = MainViewModel(repository)
55 | }
56 |
57 | @Test
58 | fun getDefaultLanguageNames_success() {
59 | assertEquals(Language.KOTLIN.languageName, viewModel.getDefaultLanguageNames()[0])
60 | }
61 |
62 | @Test
63 | fun getDefaultLanguageNames_failure() {
64 | assertNotEquals(Language.JAVA.languageName, viewModel.getDefaultLanguageNames()[0])
65 | }
66 |
67 | @Test
68 | fun getDefaultLanguageNamesCount_success() {
69 | assertEquals(7, viewModel.getDefaultLanguageNamesCount())
70 | }
71 |
72 | @Test
73 | fun getDefaultLanguageNamesCount_failure() {
74 | assertNotEquals(0, viewModel.getDefaultLanguageNamesCount())
75 | }
76 |
77 | @Test
78 | fun getLastLanguageNameIndex_success() {
79 | assertEquals(6, viewModel.getLastLanguageNameIndex())
80 | }
81 |
82 | @Test
83 | fun getLastLanguageNameIndex_failure() {
84 | assertNotEquals(0, viewModel.getLastLanguageNameIndex())
85 | }
86 |
87 | @Test
88 | fun addLanguageName_showAdd() {
89 | viewModel.languageNames.value?.addAll(listOf(
90 | Language.PHP.languageName,
91 | Language.RUBY.languageName,
92 | Language.C_PLUS_PLUS.languageName
93 | ))
94 | viewModel.index = 3
95 | viewModel.addLanguageName()
96 | val observer = mockk>(relaxed = true)
97 | viewModel.isShowAdd.observeForever(observer)
98 | verify { observer.onChanged(match { it }) }
99 | }
100 |
101 | @Test
102 | fun addLanguageName_doNotShowAdd() {
103 | viewModel.languageNames.value?.addAll(listOf(
104 | Language.PHP.languageName,
105 | Language.RUBY.languageName,
106 | Language.C_PLUS_PLUS.languageName,
107 | Language.C.languageName
108 | ))
109 | viewModel.index = 4
110 | viewModel.addLanguageName()
111 | val observer = mockk>(relaxed = true)
112 | viewModel.isShowAdd.observeForever(observer)
113 | verify { observer.onChanged(match { !it }) }
114 | }
115 |
116 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/repository/fragment/RepositoryFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.repository.fragment
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.fragment.app.viewModels
8 | import androidx.lifecycle.Observer
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.tanjiajun.androidgenericframework.EXTRA_LANGUAGE
11 | import com.tanjiajun.androidgenericframework.FRAGMENT_TAG_REPOSITORY
12 | import com.tanjiajun.androidgenericframework.R
13 | import com.tanjiajun.androidgenericframework.databinding.FragmentRepositoryBinding
14 | import com.tanjiajun.androidgenericframework.databinding.LayoutErrorBinding
15 | import com.tanjiajun.androidgenericframework.ui.BaseFragment
16 | import com.tanjiajun.androidgenericframework.ui.BaseViewModel
17 | import com.tanjiajun.androidgenericframework.ui.repository.adapter.RepositoryAdapter
18 | import com.tanjiajun.androidgenericframework.ui.repository.viewmodel.RepositoryViewModel
19 |
20 | /**
21 | * Created by TanJiaJun on 2020-02-07.
22 | */
23 | class RepositoryFragment private constructor()
24 | : BaseFragment(), BaseViewModel.Handlers {
25 |
26 | override val layoutRes: Int = R.layout.fragment_repository
27 | override val viewModel by viewModels { viewModelFactory }
28 | override val transactionTag: String = FRAGMENT_TAG_REPOSITORY
29 |
30 | private lateinit var language: String
31 | private val adapter = RepositoryAdapter()
32 | private var errorView: View? = null
33 |
34 | override fun onAttach(context: Context) {
35 | super.onAttach(context)
36 | language = getLanguageFromArgs()
37 | }
38 |
39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
40 | initUI()
41 | initObservers()
42 | initData()
43 | }
44 |
45 | override fun onRetryClick(view: View) {
46 | viewModel.getRepositories(language)
47 | }
48 |
49 | private fun getLanguageFromArgs(): String =
50 | arguments?.run { getString(EXTRA_LANGUAGE, "") } ?: ""
51 |
52 | private fun initUI() {
53 | with(binding) {
54 | lifecycleOwner = this@RepositoryFragment
55 | viewModel = this@RepositoryFragment.viewModel
56 | }
57 | initRecyclerView()
58 | initErrorView()
59 | }
60 |
61 | private fun initRecyclerView() {
62 | with(binding.rvRepository) {
63 | layoutManager = LinearLayoutManager(context)
64 | adapter = this@RepositoryFragment.adapter
65 | }
66 | }
67 |
68 | private fun initErrorView() {
69 | binding.vsError.setOnInflateListener { _, inflated ->
70 | DataBindingUtil.bind(inflated)?.run {
71 | lifecycleOwner = this@RepositoryFragment
72 | viewModel = this@RepositoryFragment.viewModel
73 | handlers = this@RepositoryFragment
74 | }
75 | }
76 | }
77 |
78 | private fun initObservers() {
79 | viewModel.isShowErrorView.observe(viewLifecycleOwner, Observer {
80 | handleErrorView(it)
81 | })
82 | }
83 |
84 | private fun handleErrorView(isShowErrorView: Boolean) {
85 | if (isShowErrorView) {
86 | errorView
87 | ?.run { visibility = View.VISIBLE }
88 | ?: binding.vsError.viewStub?.inflate()?.also { errorView = it }
89 | } else {
90 | errorView?.visibility = View.GONE
91 | }
92 | }
93 |
94 | private fun initData() =
95 | with(viewModel) {
96 | getRepositories(language)
97 | repositories.observe(viewLifecycleOwner, Observer {
98 | adapter.setItems(it)
99 | })
100 | }
101 |
102 | companion object {
103 | fun newInstance(language: String): RepositoryFragment =
104 | RepositoryFragment().apply {
105 | arguments = Bundle().apply {
106 | putString(EXTRA_LANGUAGE, language)
107 | }
108 | }
109 | }
110 |
111 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion rootProject.compileSdkVersion
8 |
9 | defaultConfig {
10 | applicationId "com.tanjiajun.androidgenericframework"
11 | minSdkVersion rootProject.minSdkVersion
12 | targetSdkVersion rootProject.targetSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 |
17 | def properties = new Properties()
18 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
19 | buildConfigField("String", "GITHUB_CLIENT_ID", properties.getProperty("github_client_id"))
20 | buildConfigField("String", "GITHUB_CLIENT_SECRET", properties.getProperty("github_client_secret"))
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 |
30 | androidExtensions {
31 | experimental = true
32 | }
33 |
34 | dataBinding {
35 | enabled = true
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility JavaVersion.VERSION_1_8
40 | targetCompatibility JavaVersion.VERSION_1_8
41 | }
42 |
43 | kotlinOptions {
44 | jvmTarget = '1.8'
45 | }
46 |
47 | packagingOptions {
48 | pickFirst 'META-INF/kotlinx-io.kotlin_module'
49 | pickFirst 'META-INF/atomicfu.kotlin_module'
50 | pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'
51 | }
52 | }
53 |
54 | dependencies {
55 | implementation fileTree(dir: 'libs', include: ['*.jar'])
56 |
57 | // AppCompat
58 | implementation "androidx.appcompat:appcompat:$appcompatVersion"
59 |
60 | // ConstraintLayout
61 | implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion"
62 |
63 | // MultiDex
64 | implementation "com.android.support:multidex:$multidexVersion"
65 |
66 | // Kotlin
67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
68 |
69 | // Kotlin Coroutines
70 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion"
71 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion"
72 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleViewmodelKtxVersion"
73 |
74 | // Retrofit
75 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
76 | implementation "com.squareup.retrofit2:converter-gson:$retrofitConverterGsonVersion"
77 | implementation "com.squareup.retrofit2:converter-scalars:$retrofitConverterScalarsVersion"
78 |
79 | // Glide
80 | implementation "com.github.bumptech.glide:glide:$glideVersion"
81 | kapt "com.github.bumptech.glide:compiler:$glideVersion"
82 |
83 | // Lifecycle Extensions
84 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensionsVersion"
85 |
86 | // Navigation
87 | implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
88 | implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
89 |
90 | // ViewPager2
91 | implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
92 |
93 | // MMKV
94 | implementation "com.tencent:mmkv:$mmkvVersion"
95 |
96 | // Dagger2
97 | implementation "com.google.dagger:dagger:$daggerVersion"
98 | kapt "com.google.dagger:dagger-compiler:$daggerVersion"
99 | implementation "com.google.dagger:dagger-android-support:$daggerVersion"
100 | kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
101 |
102 | // JUnit(test)
103 | testImplementation "junit:junit:$junitVersion"
104 |
105 | // OkHttp(test)
106 | testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
107 |
108 | // MockK(test)
109 | testImplementation "io.mockk:mockk:$mockkVersion"
110 |
111 | // Truth(test)
112 | testImplementation "com.google.truth:truth:$truthVersion"
113 |
114 | // Kotlin Coroutines(test)
115 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion"
116 |
117 | // Core Testing(test)
118 | testImplementation "android.arch.core:core-testing:$coreTestingVersion"
119 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/data/UserRemoteDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import com.tanjiajun.androidgenericframework.data.remote.user.UserRemoteDataSource
5 | import kotlinx.coroutines.runBlocking
6 | import okhttp3.mockwebserver.MockResponse
7 | import okhttp3.mockwebserver.MockWebServer
8 | import org.junit.Before
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.junit.runners.JUnit4
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import retrofit2.converter.scalars.ScalarsConverterFactory
16 |
17 | /**
18 | * Created by TanJiaJun on 2020/5/26.
19 | */
20 | @RunWith(JUnit4::class)
21 | class UserRemoteDataSourceTest {
22 |
23 | @get:Rule
24 | val mockWebServer = MockWebServer()
25 |
26 | private lateinit var remoteDataSource: UserRemoteDataSource
27 |
28 | @Before
29 | fun setUp() {
30 | remoteDataSource = UserRemoteDataSource(
31 | Retrofit.Builder()
32 | .addConverterFactory(ScalarsConverterFactory.create())
33 | .addConverterFactory(GsonConverterFactory.create())
34 | .baseUrl(mockWebServer.url("/").toString())
35 | .build()
36 | )
37 | }
38 |
39 | @Test
40 | fun authorizations() {
41 | runBlocking {
42 | mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(userAccessTokenDataJson))
43 | remoteDataSource.authorizations().run {
44 | assertThat(id).isEqualTo(userAccessTokenData.id)
45 | assertThat(token).isEqualTo(userAccessTokenData.token)
46 | assertThat(url).isEqualTo(userAccessTokenData.url)
47 | }
48 | }
49 | }
50 |
51 | @Test
52 | fun fetchUserInfo() {
53 | runBlocking {
54 | mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(userInfoDataJson))
55 | remoteDataSource.fetchUserInfo().run {
56 | assertThat(avatarUrl).isEqualTo(userInfoData.avatarUrl)
57 | assertThat(blog).isEqualTo(userInfoData.blog)
58 | assertThat(collaborators).isEqualTo(userInfoData.collaborators)
59 | assertThat(createdAt).isEqualTo(userInfoData.createdAt)
60 | assertThat(diskUsage).isEqualTo(userInfoData.diskUsage)
61 | assertThat(email).isEqualTo(userInfoData.email)
62 | assertThat(eventsUrl).isEqualTo(userInfoData.eventsUrl)
63 | assertThat(followers).isEqualTo(userInfoData.followers)
64 | assertThat(followersUrl).isEqualTo(userInfoData.followersUrl)
65 | assertThat(following).isEqualTo(userInfoData.following)
66 | assertThat(followingUrl).isEqualTo(userInfoData.followingUrl)
67 | assertThat(gistsUrl).isEqualTo(userInfoData.gistsUrl)
68 | assertThat(gravatarId).isEqualTo(userInfoData.gravatarId)
69 | assertThat(htmlUrl).isEqualTo(userInfoData.htmlUrl)
70 | assertThat(id).isEqualTo(userInfoData.id)
71 | assertThat(location).isEqualTo(userInfoData.location)
72 | assertThat(login).isEqualTo(userInfoData.login)
73 | assertThat(name).isEqualTo(userInfoData.name)
74 | assertThat(nodeId).isEqualTo(userInfoData.nodeId)
75 | assertThat(organizationsUrl).isEqualTo(userInfoData.organizationsUrl)
76 | assertThat(ownedPrivateRepos).isEqualTo(userInfoData.ownedPrivateRepos)
77 | assertThat(privateGists).isEqualTo(userInfoData.privateGists)
78 | assertThat(publicGists).isEqualTo(userInfoData.publicGists)
79 | assertThat(publicRepos).isEqualTo(userInfoData.publicRepos)
80 | assertThat(receivedEventsUrl).isEqualTo(userInfoData.receivedEventsUrl)
81 | assertThat(reposUrl).isEqualTo(userInfoData.reposUrl)
82 | assertThat(siteAdmin).isEqualTo(userInfoData.siteAdmin)
83 | assertThat(starredUrl).isEqualTo(userInfoData.starredUrl)
84 | assertThat(subscriptionsUrl).isEqualTo(userInfoData.subscriptionsUrl)
85 | assertThat(totalPrivateRepos).isEqualTo(userInfoData.totalPrivateRepos)
86 | assertThat(twoFactorAuthentication).isEqualTo(userInfoData.twoFactorAuthentication)
87 | assertThat(type).isEqualTo(userInfoData.type)
88 | assertThat(updatedAt).isEqualTo(userInfoData.updatedAt)
89 | assertThat(url).isEqualTo(userInfoData.url)
90 | }
91 | }
92 | }
93 |
94 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
16 |
17 |
21 |
22 |
39 |
40 |
60 |
61 |
80 |
81 |
91 |
92 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/main/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.main.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.FragmentActivity
9 | import androidx.lifecycle.Observer
10 | import androidx.viewpager2.adapter.FragmentStateAdapter
11 | import androidx.viewpager2.widget.ViewPager2
12 | import com.google.android.material.tabs.TabLayout
13 | import com.tanjiajun.androidgenericframework.EXTRA_LOGOUT
14 | import com.tanjiajun.androidgenericframework.R
15 | import com.tanjiajun.androidgenericframework.databinding.ActivityMainBinding
16 | import com.tanjiajun.androidgenericframework.ui.BaseActivity
17 | import com.tanjiajun.androidgenericframework.ui.main.viewmodel.MainViewModel
18 | import com.tanjiajun.androidgenericframework.ui.repository.fragment.RepositoryFragment
19 | import com.tanjiajun.androidgenericframework.ui.user.activity.PersonalCenterActivity
20 | import com.tanjiajun.androidgenericframework.ui.user.activity.RegisterAndLoginActivity
21 | import com.tanjiajun.androidgenericframework.utils.registerOnTabSelectedListener
22 | import com.tanjiajun.androidgenericframework.utils.startActivity
23 | import com.tanjiajun.androidgenericframework.utils.yes
24 |
25 | /**
26 | * Created by TanJiaJun on 2019-07-22.
27 | */
28 | class MainActivity : BaseActivity(), MainViewModel.Handlers {
29 |
30 | override val layoutRes: Int = R.layout.activity_main
31 | override val viewModel by viewModels { viewModelFactory }
32 |
33 | private lateinit var tlRepository: TabLayout
34 | private lateinit var vpRepository: ViewPager2
35 |
36 | private lateinit var repositoryFragments: MutableList
37 | private lateinit var adapter: OrderFragmentStateAdapter
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | with(binding) {
42 | lifecycleOwner = this@MainActivity
43 | viewModel = this@MainActivity.viewModel
44 | handlers = this@MainActivity
45 | }
46 |
47 | initView()
48 | initData()
49 | }
50 |
51 | private fun initView() {
52 | tlRepository = binding.tlRepository
53 | vpRepository = binding.vpRepository
54 |
55 | repositoryFragments = mutableListOf().apply {
56 | viewModel.getDefaultLanguageNames().forEach { add(RepositoryFragment.newInstance(it)) }
57 | }
58 | adapter = OrderFragmentStateAdapter(this, repositoryFragments)
59 | viewModel.getDefaultLanguageNames().forEach {
60 | tlRepository.addTab(tlRepository.newTab().setText(it))
61 | }
62 | tlRepository.addOnTabSelectedListener(registerOnTabSelectedListener {
63 | onTabSelected { vpRepository.currentItem = it?.position ?: 0 }
64 | })
65 | vpRepository.adapter = adapter
66 | vpRepository.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
67 | override fun onPageSelected(position: Int) =
68 | tlRepository.setScrollPosition(position, 0.0F, true)
69 | })
70 | }
71 |
72 | private fun initData() =
73 | with(viewModel) {
74 | languageNames.observe(this@MainActivity, Observer {
75 | if (it.size > getDefaultLanguageNamesCount()) {
76 | tlRepository.addTab(tlRepository.newTab().setText(getLastLanguageName()))
77 | repositoryFragments.add(RepositoryFragment.newInstance(getLastLanguageName()))
78 | adapter.notifyItemInserted(getLastLanguageNameIndex())
79 | vpRepository.currentItem = getLastLanguageNameIndex()
80 | }
81 | })
82 | }
83 |
84 | override fun onNewIntent(intent: Intent?) {
85 | super.onNewIntent(intent)
86 | intent
87 | ?.getBooleanExtra(EXTRA_LOGOUT, false)
88 | ?.yes {
89 | startActivity()
90 | finish()
91 | }
92 | }
93 |
94 | override fun onPersonalCenterClick(view: View) =
95 | startActivity()
96 |
97 | override fun onAddClick(view: View) {
98 | viewModel.addLanguageName()
99 | }
100 |
101 | }
102 |
103 | class OrderFragmentStateAdapter(fragmentActivity: FragmentActivity,
104 | private val repositoryFragments: List)
105 | : FragmentStateAdapter(fragmentActivity) {
106 |
107 | override fun createFragment(position: Int): Fragment =
108 | repositoryFragments[position]
109 |
110 | override fun getItemCount(): Int = repositoryFragments.size
111 |
112 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_personal_center.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
18 |
23 |
24 |
38 |
39 |
50 |
51 |
62 |
63 |
75 |
76 |
84 |
85 |
93 |
94 |
103 |
104 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/test/java/com/tanjiajun/androidgenericframework/data/FakeDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.data
2 |
3 | import com.tanjiajun.androidgenericframework.data.model.repository.RepositoryData
4 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserAccessTokenData
5 | import com.tanjiajun.androidgenericframework.data.model.user.response.UserInfoData
6 | import com.tanjiajun.androidgenericframework.utils.Language
7 |
8 | /**
9 | * Created by TanJiaJun on 2020/5/28.
10 | */
11 | val userAccessTokenDataJson = "{\n" +
12 | "\"id\": 432604074,\n" +
13 | "\"token\": \"\",\n" +
14 | "\"url\": \"https://api.github.com/authorizations/432604074\"\n" +
15 | "}"
16 |
17 | val userInfoDataJson = "{\n" +
18 | "\"avatar_url\": \"https://avatars1.githubusercontent.com/u/25838119?v=4\",\n" +
19 | "\"blog\": \"\",\n" +
20 | "\"collaborators\": 0,\n" +
21 | "\"created_at\": \"2017-02-17T06:28:25Z\",\n" +
22 | "\"disk_usage\": 46380,\n" +
23 | "\"email\": \"1120571286@qq.com\",\n" +
24 | "\"events_url\": \"https://api.github.com/users/TanJiaJunBeyond/events{/privacy}\",\n" +
25 | "\"followers\": 15,\n" +
26 | "\"followers_url\": \"https://api.github.com/users/TanJiaJunBeyond/followers\",\n" +
27 | "\"following\": 5,\n" +
28 | "\"following_url\": \"https://api.github.com/users/TanJiaJunBeyond/following{/other_user}\",\n" +
29 | "\"gists_url\": \"https://api.github.com/users/TanJiaJunBeyond/gists{/gist_id}\",\n" +
30 | "\"gravatar_id\": \"\",\n" +
31 | "\"html_url\": \"https://github.com/TanJiaJunBeyond\",\n" +
32 | "\"id\": 25838119,\n" +
33 | "\"location\": \"中国广东省广州市\",\n" +
34 | "\"login\": \"TanJiaJunBeyond\",\n" +
35 | "\"name\": \"TanJiaJun\",\n" +
36 | "\"node_id\": \"MDQ6VXNlcjI1ODM4MTE5\",\n" +
37 | "\"organizations_url\": \"https://api.github.com/users/TanJiaJunBeyond/orgs\",\n" +
38 | "\"owned_private_repos\": 0,\n" +
39 | "\"private_gists\": 0,\n" +
40 | "\"public_gists\": 0,\n" +
41 | "\"public_repos\": 11,\n" +
42 | "\"received_events_url\": \"https://api.github.com/users/TanJiaJunBeyond/received_events\",\n" +
43 | "\"repos_url\": \"https://api.github.com/users/TanJiaJunBeyond/repos\",\n" +
44 | "\"site_admin\": \"false\",\n" +
45 | "\"starred_url\": \"https://api.github.com/users/TanJiaJunBeyond/starred{/owner}{/repo}\",\n" +
46 | "\"subscriptions_url\": \"https://api.github.com/users/TanJiaJunBeyond/subscriptions\",\n" +
47 | "\"total_private_repos\": 0,\n" +
48 | "\"two_factor_authentication\": false,\n" +
49 | "\"type\": \"User\",\n" +
50 | "\"updated_at\": \"2020-05-26T07:19:39Z\",\n" +
51 | "\"url\": \"https://api.github.com/users/TanJiaJunBeyond\"\n" +
52 | "}"
53 |
54 | val repositoryDataJson = "{\n" +
55 | "\"total_count\": 1,\n" +
56 | "\"incomplete_results\": false,\n" +
57 | "\"items\": [\n" +
58 | "{\n" +
59 | "\"id\": 0,\n" +
60 | "\"name\": \"谭嘉俊\",\n" +
61 | "\"description\": \"描述\",\n" +
62 | "\"language\": \"Kotlin\",\n" +
63 | "\"starCount\": 0,\n" +
64 | "\"forkCount\": 0\n" +
65 | "}\n" +
66 | "]\n" +
67 | "}"
68 |
69 | val userAccessTokenData = UserAccessTokenData(
70 | id = 432604074,
71 | token = "",
72 | url = "https://api.github.com/authorizations/432604074"
73 | )
74 |
75 | val userInfoData = UserInfoData(
76 | id = 25838119,
77 | login = "TanJiaJunBeyond",
78 | nodeId = "MDQ6VXNlcjI1ODM4MTE5",
79 | avatarUrl = "https://avatars1.githubusercontent.com/u/25838119?v=4",
80 | gravatarId = "",
81 | url = "https://api.github.com/users/TanJiaJunBeyond",
82 | htmlUrl = "https://github.com/TanJiaJunBeyond",
83 | followersUrl = "https://api.github.com/users/TanJiaJunBeyond/followers",
84 | followingUrl = "https://api.github.com/users/TanJiaJunBeyond/following{/other_user}",
85 | gistsUrl = "https://api.github.com/users/TanJiaJunBeyond/gists{/gist_id}",
86 | starredUrl = "https://api.github.com/users/TanJiaJunBeyond/starred{/owner}{/repo}",
87 | subscriptionsUrl = "https://api.github.com/users/TanJiaJunBeyond/subscriptions",
88 | organizationsUrl = "https://api.github.com/users/TanJiaJunBeyond/orgs",
89 | reposUrl = "https://api.github.com/users/TanJiaJunBeyond/repos",
90 | eventsUrl = "https://api.github.com/users/TanJiaJunBeyond/events{/privacy}",
91 | receivedEventsUrl = "https://api.github.com/users/TanJiaJunBeyond/received_events",
92 | type = "User",
93 | siteAdmin = false,
94 | name = "TanJiaJun",
95 | company = "",
96 | blog = "",
97 | location = "中国广东省广州市",
98 | email = "1120571286@qq.com",
99 | hireable = "",
100 | bio = "",
101 | publicRepos = 11,
102 | publicGists = 0,
103 | followers = 15,
104 | following = 5,
105 | createdAt = "2017-02-17T06:28:25Z",
106 | updatedAt = "2020-05-26T07:19:39Z",
107 | privateGists = 0,
108 | totalPrivateRepos = 0,
109 | ownedPrivateRepos = 0,
110 | diskUsage = 46380,
111 | collaborators = 0,
112 | twoFactorAuthentication = false
113 | )
114 |
115 | val repositoryData = RepositoryData(
116 | id = 0,
117 | name = "谭嘉俊",
118 | description = "描述",
119 | language = Language.KOTLIN,
120 | starCount = 0,
121 | forkCount = 0
122 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui
2 |
3 | import android.view.View
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.tanjiajun.androidgenericframework.data.remote.ExceptionHandler
9 | import com.tanjiajun.androidgenericframework.data.remote.ResponseThrowable
10 | import com.tanjiajun.androidgenericframework.utils.SingleLiveEvent
11 | import kotlinx.coroutines.*
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.flow
14 |
15 | /**
16 | * Created by TanJiaJun on 2019-08-02.
17 | */
18 | private typealias CommonCallback = suspend CoroutineScope.() -> Unit
19 | private typealias ErrorCallback = suspend CoroutineScope.(ResponseThrowable) -> Unit
20 |
21 | abstract class BaseViewModel : ViewModel() {
22 |
23 | // 标题
24 | protected val _title = MutableLiveData()
25 | val title: LiveData = _title
26 |
27 | // 是否显示加载中页面
28 | protected val _isShowLoadingView = MutableLiveData()
29 | val isShowLoadingView: LiveData = _isShowLoadingView
30 |
31 | // 是否显示失败页面
32 | protected val _isShowErrorView = MutableLiveData()
33 | val isShowErrorView: LiveData = _isShowErrorView
34 |
35 | // UI事件
36 | val uiLiveEvent by lazy { UILiveEvent() }
37 |
38 | fun launchUI(block: suspend CoroutineScope.() -> Unit) =
39 | viewModelScope.launch { block() }
40 |
41 | fun launchFlow(block: suspend () -> T): Flow =
42 | flow { emit(block()) }
43 |
44 | /**
45 | * 处理逻辑
46 | *
47 | * @param block 请求块
48 | * @param success 成功回调
49 | * @param error 失败回调
50 | * @param complete 完成回调(成功或者失败都会回调)
51 | */
52 | private suspend fun handle(block: suspend CoroutineScope.() -> T,
53 | success: suspend CoroutineScope.(T) -> Unit,
54 | error: ErrorCallback,
55 | complete: CommonCallback) =
56 | coroutineScope {
57 | try {
58 | success(block())
59 | } catch (throwable: Throwable) {
60 | error(ExceptionHandler.handleException(throwable))
61 | } finally {
62 | complete()
63 | }
64 | }
65 |
66 | /**
67 | * 处理网络请求
68 | *
69 | * @param uiState 处理UI状态
70 | * @param block 请求块
71 | * @param success 成功回调
72 | * @param error 失败回调
73 | * @param complete 完成回调(成功或者失败都会回调)
74 | */
75 | fun launch(uiState: UIState = UIState(),
76 | block: suspend CoroutineScope.() -> T,
77 | success: (suspend CoroutineScope.(T) -> Unit)? = null,
78 | error: (ErrorCallback)? = null,
79 | complete: (CommonCallback)? = null) =
80 | with(uiState) {
81 | if (isShowLoadingProgressBar) uiLiveEvent.showLoadingProgressBarEvent.call()
82 | if (isShowLoadingView) _isShowLoadingView.value = true
83 | if (isShowErrorView) _isShowErrorView.value = false
84 | launchUI {
85 | handle(
86 | block = withContext(Dispatchers.IO) { block },
87 | success = { withContext(Dispatchers.Main) { success?.invoke(this, it) } },
88 | error = {
89 | withContext(Dispatchers.Main) {
90 | if (isShowErrorToast) uiLiveEvent.showToastEvent.postValue("${it.errorCode}:${it.errorMessage}")
91 | if (isShowErrorView) _isShowErrorView.value = true
92 | error?.invoke(this, it)
93 | }
94 | },
95 | complete = {
96 | withContext(Dispatchers.Main) {
97 | if (isShowLoadingProgressBar) uiLiveEvent.dismissLoadingProgressBarEvent.call()
98 | if (isShowLoadingView) _isShowLoadingView.value = false
99 | complete?.invoke(this)
100 | }
101 | }
102 | )
103 | }
104 | }
105 |
106 | inner class UILiveEvent {
107 |
108 | val showToastEvent by lazy { SingleLiveEvent() }
109 | val showLoadingProgressBarEvent by lazy { SingleLiveEvent() }
110 | val dismissLoadingProgressBarEvent by lazy { SingleLiveEvent() }
111 | val showSnackbarEvent by lazy { SingleLiveEvent() }
112 |
113 | }
114 |
115 | interface Handlers {
116 |
117 | @JvmDefault
118 | fun onNavigationIconClick(view: View) {
119 | // no implementation
120 | }
121 |
122 | @JvmDefault
123 | fun onRetryClick(view: View) {
124 | // no implementation
125 | }
126 |
127 | }
128 |
129 | }
130 |
131 | /**
132 | * UI状态
133 | *
134 | * @param isShowLoadingProgressBar 是否显示加载中ProgressBar
135 | * @param isShowLoadingView 是否显示加载中页面
136 | * @param isShowErrorToast 是否弹出错误Toast
137 | * @param isShowErrorView 是否显示错误页面
138 | */
139 | data class UIState(
140 | val isShowLoadingProgressBar: Boolean = false,
141 | val isShowLoadingView: Boolean = false,
142 | val isShowErrorToast: Boolean = false,
143 | val isShowErrorView: Boolean = false
144 | )
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_repository.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
18 |
19 |
33 |
34 |
48 |
49 |
62 |
63 |
75 |
76 |
90 |
91 |
101 |
102 |
116 |
117 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tanjiajun/androidgenericframework/ui/recyclerview/MultiViewTypeDataBindingAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.tanjiajun.androidgenericframework.ui.recyclerview
2 |
3 | import android.util.SparseArray
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.GridLayoutManager
6 | import androidx.recyclerview.widget.RecyclerView
7 |
8 | /**
9 | * Created by TanJiaJun on 2019-08-31.
10 | */
11 | abstract class MultiViewTypeDataBindingAdapter
12 | : BaseDataBindingAdapter() {
13 |
14 | private val items = mutableListOf()
15 | private val viewTypes = SparseArray>()
16 | private val noDataViewTypes = SparseArray>()
17 | private var headerCount = 0
18 | private var footerCount = 0
19 |
20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder =
21 | viewTypes[viewType, noDataViewTypes[viewType]]
22 | .onCreateDataBindingViewHolder(parent, viewType)
23 |
24 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
25 | super.onBindViewHolder(holder, position)
26 | bindViewType(holder, position)
27 | }
28 |
29 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int, payloads: MutableList) {
30 | super.onBindViewHolder(holder, position, payloads)
31 | payloads
32 | .takeIf { it.isNotEmpty() }
33 | ?.let {
34 | bindViewType(holder, position, payloads)
35 | }
36 | }
37 |
38 | override fun getItemCount(): Int =
39 | headerCount + items.size + footerCount
40 |
41 | override fun getItemByPosition(position: Int): T? =
42 | items[position]
43 |
44 | override fun getLayoutResByPosition(position: Int): Int =
45 | getViewTypeByPosition(position).getItemLayoutRes()
46 |
47 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
48 | recyclerView.layoutManager
49 | ?.let {
50 | if (it is GridLayoutManager) {
51 | it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
52 | override fun getSpanSize(position: Int): Int =
53 | getViewTypeByPosition(position).getSpanSize()
54 | }
55 | }
56 | }
57 | }
58 |
59 | private fun bindViewType(holder: BaseViewHolder,
60 | position: Int) =
61 | viewTypes[holder.itemViewType]
62 | ?.run {
63 | bind(holder, items[getItemPositionByPosition(position)], position)
64 | }
65 | ?: noDataViewTypes[holder.itemViewType].bind(holder, position)
66 |
67 | private fun bindViewType(holder: BaseViewHolder,
68 | position: Int,
69 | payloads: MutableList) =
70 | viewTypes[holder.itemViewType]
71 | ?.run {
72 | bind(holder, items[getItemPositionByPosition(position)], position, payloads)
73 | }
74 | ?: noDataViewTypes[holder.itemViewType].bind(holder, position, payloads)
75 |
76 | private fun getItemPositionByPosition(position: Int): Int =
77 | position - headerCount
78 |
79 | fun getViewTypeByPosition(position: Int): BaseViewType {
80 | val noDataViewTypesSize = noDataViewTypes.size()
81 |
82 | for (i in 0 until noDataViewTypesSize) {
83 | val noDataViewType = noDataViewTypes.valueAt(i)
84 |
85 | if (noDataViewType.isMatchViewType(position, headerCount, items.size, footerCount)) {
86 | return noDataViewType
87 | }
88 | }
89 |
90 | val viewTypesSize = viewTypes.size()
91 |
92 | for (i in 0 until viewTypesSize) {
93 | val viewType = viewTypes.valueAt(i)
94 |
95 | if (viewType.isMatchViewType(items[getItemPositionByPosition(position)])) {
96 | return viewType
97 | }
98 | }
99 |
100 | throw IllegalStateException("View type not find.")
101 | }
102 |
103 | fun addHeaderViewType(viewType: NoDataViewType) {
104 | noDataViewTypes.put(viewType.getItemLayoutRes(), viewType)
105 | headerCount++
106 | }
107 |
108 | fun addViewType(viewType: BaseViewType) =
109 | viewTypes.put(viewType.getItemLayoutRes(), viewType)
110 |
111 | fun addFooterViewType(viewType: NoDataViewType) {
112 | noDataViewTypes.put(viewType.getItemLayoutRes(), viewType)
113 | footerCount++
114 | }
115 |
116 | fun removeHeaderViewType(key: Int): Boolean =
117 | noDataViewTypes
118 | .takeIf { it[key] != null }
119 | ?.let {
120 | it.remove(key)
121 | headerCount--
122 | true
123 | }
124 | ?: false
125 |
126 | private fun findItemsHasMatchViewType(items: List): List =
127 | mutableListOf()
128 | .apply {
129 | val viewTypesSize = viewTypes.size()
130 |
131 | items.forEach {
132 | for (i in 0 until viewTypesSize) {
133 | if (viewTypes.valueAt(i).isMatchViewType(it)) {
134 | add(it)
135 | }
136 | }
137 | }
138 | }
139 |
140 | fun setItems(items: List) =
141 | with(this.items) {
142 | if (isNotEmpty()) clear()
143 | addAll(findItemsHasMatchViewType(items))
144 | notifyDataSetChanged()
145 | }
146 |
147 | fun addItems(items: List) =
148 | with(this.items) {
149 | addAll(findItemsHasMatchViewType(items))
150 | notifyItemRangeInserted(headerCount + size, items.size)
151 | }
152 |
153 | }
--------------------------------------------------------------------------------