├── .circleci └── config.yml ├── .editorconfig ├── .github └── workflows │ ├── build.yml │ ├── docs.yml │ └── issues-stale.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── king │ │ └── mvvmframe │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── king │ │ │ └── mvvmframe │ │ │ ├── App.kt │ │ │ ├── app │ │ │ ├── MainActivity.kt │ │ │ ├── adapter │ │ │ │ ├── BindingAdapter.kt │ │ │ │ ├── BindingHolder.kt │ │ │ │ └── SearchHistoryAdapter.kt │ │ │ ├── base │ │ │ │ └── BaseViewModel.kt │ │ │ ├── city │ │ │ │ ├── CityActivity.kt │ │ │ │ └── CityViewModel.kt │ │ │ ├── dictionary │ │ │ │ ├── DictionaryActivity.kt │ │ │ │ └── DictionaryViewModel.kt │ │ │ └── oil │ │ │ │ ├── OilPriceActivity.kt │ │ │ │ └── OilPriceViewModel.kt │ │ │ ├── config │ │ │ └── AppConfigModule.kt │ │ │ ├── constant │ │ │ └── Constants.kt │ │ │ ├── data │ │ │ ├── database │ │ │ │ ├── AppDatabase.kt │ │ │ │ └── dao │ │ │ │ │ └── SearchHistoryDao.kt │ │ │ ├── model │ │ │ │ ├── City.kt │ │ │ │ ├── DictionaryInfo.java │ │ │ │ ├── OilPrice.kt │ │ │ │ ├── Result.kt │ │ │ │ └── SearchHistory.kt │ │ │ ├── repository │ │ │ │ ├── CityRepository.kt │ │ │ │ ├── DictionaryRepository.kt │ │ │ │ └── OilPriceRepository.kt │ │ │ └── service │ │ │ │ ├── CityService.kt │ │ │ │ ├── DictionaryService.kt │ │ │ │ └── OilPriceService.kt │ │ │ └── util │ │ │ └── RandomUtil.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxhdpi │ │ ├── btn_delete_normal.png │ │ └── btn_delete_pressed.png │ │ ├── drawable │ │ ├── btn_delete_selector.xml │ │ ├── dialog_bg.xml │ │ ├── ic_launcher_background.xml │ │ ├── list_divider.xml │ │ ├── list_divider_8.xml │ │ ├── tag_bg_normal.xml │ │ ├── tag_bg_pressed.xml │ │ └── tag_bg_selector.xml │ │ ├── layout │ │ ├── city_activity.xml │ │ ├── dictionary_activity.xml │ │ ├── main_activity.xml │ │ ├── oil_price_activity.xml │ │ ├── rv_city_item.xml │ │ ├── rv_dictionary_item.xml │ │ ├── rv_oil_price_item.xml │ │ └── search_history_item.xml │ │ ├── menu │ │ └── search_view_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── king │ └── mvvmframe │ └── ExampleUnitTest.kt ├── art └── mvvm_architecture.jpg ├── build.gradle.kts ├── build_docs.sh ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── mkdocs.yml ├── mvvmframe-plugin ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ └── java │ └── com │ └── king │ └── frame │ └── mvvmframe │ └── plugin │ ├── MvvmFrameExtension.kt │ ├── MvvmFramePlugin.kt │ └── internal │ ├── DependencyExt.kt │ ├── PluginId.kt │ └── Version.kt ├── mvvmframe ├── .gitignore ├── build.gradle.kts ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── king │ │ └── frame │ │ └── mvvm │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── king │ │ │ └── frame │ │ │ └── mvvmframe │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseAndroidViewModel.kt │ │ │ ├── BaseApplication.kt │ │ │ ├── BaseDialogFragment.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── BaseProgressDialog.kt │ │ │ ├── BaseViewModel.kt │ │ │ ├── ContentActivity.kt │ │ │ ├── Ext.kt │ │ │ ├── IView.kt │ │ │ └── controller │ │ │ │ ├── CombinedController.kt │ │ │ │ ├── Controller.kt │ │ │ │ ├── DefaultCombinedController.kt │ │ │ │ ├── DefaultDialogController.kt │ │ │ │ ├── DefaultLoadingController.kt │ │ │ │ ├── DefaultToastController.kt │ │ │ │ ├── DialogController.kt │ │ │ │ ├── LoadingController.kt │ │ │ │ └── ToastController.kt │ │ │ ├── config │ │ │ ├── AppliesOptions.kt │ │ │ ├── Config.kt │ │ │ ├── FrameConfigModule.kt │ │ │ └── ManifestParser.kt │ │ │ ├── data │ │ │ ├── DataRepository.kt │ │ │ ├── Repository.kt │ │ │ └── datasource │ │ │ │ ├── DataSource.kt │ │ │ │ └── DefaultDataSource.kt │ │ │ ├── di │ │ │ └── module │ │ │ │ ├── ConfigModule.kt │ │ │ │ ├── DataSourceModule.kt │ │ │ │ ├── HttpModule.kt │ │ │ │ └── RepositoryModule.kt │ │ │ └── util │ │ │ └── JumpDebounce.kt │ └── res │ │ ├── anim │ │ ├── mvvmframe_dialog_in.xml │ │ └── mvvmframe_dialog_out.xml │ │ ├── drawable │ │ ├── mvvmframe_loading.xml │ │ └── mvvmframe_progress.xml │ │ ├── layout │ │ ├── mvvmframe_content_activity.xml │ │ ├── mvvmframe_loading.xml │ │ └── mvvmframe_progress_dialog.xml │ │ └── values │ │ ├── colors.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── king │ └── frame │ └── mvvm │ └── ExampleUnitTest.kt └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | android: circleci/android@2.4.0 5 | 6 | jobs: 7 | build: 8 | docker: 9 | - image: cimg/android:2023.08 10 | steps: 11 | - checkout 12 | - run: 13 | command: ./gradlew build 14 | 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{yml,yaml}] 11 | indent_size = 2 12 | 13 | [*.{kt, kts}] 14 | ij_kotlin_imports_layout = * -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | JAVA_VERSION: 17 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Configure JDK 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'zulu' 26 | java-version: ${{ env.JAVA_VERSION }} 27 | - name: Build with Gradle 28 | run: ./gradlew build 29 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | JAVA_VERSION: 17 10 | PYTHON_VERSION: 3.x 11 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" 12 | 13 | permissions: 14 | contents: write 15 | id-token: write 16 | pages: write 17 | 18 | jobs: 19 | docs: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | if: github.ref == 'refs/heads/master' 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Configure JDK 33 | uses: actions/setup-java@v4 34 | with: 35 | distribution: 'zulu' 36 | java-version: ${{ env.JAVA_VERSION }} 37 | 38 | - name: Install Python 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ env.PYTHON_VERSION }} 42 | 43 | - name: Install MkDocs Material 44 | run: pip install mkdocs-material 45 | 46 | - name: Generate Docs 47 | run: ./build_docs.sh 48 | 49 | - name: Upload to GitHub Pages 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: site 53 | 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /.github/workflows/issues-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 16 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 16 | days-before-stale: 30 17 | days-before-close: 5 18 | exempt-all-pr-milestones: true 19 | exempt-issue-labels: 'help wanted' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | dist: trusty 3 | jdk: oraclejdk8 4 | sudo: false 5 | 6 | env: 7 | global: 8 | - ANDROID_API_LEVEL=30 9 | - ANDROID_BUILD_TOOLS_VERSION=30.0.3 10 | - TRAVIS_SECURE_ENV_VARS=true 11 | 12 | before_install: 13 | - chmod +x gradlew 14 | - mkdir "$ANDROID_HOME/licenses" || true 15 | # Hack to accept Android licenses 16 | - yes | sdkmanager "platforms;android-$ANDROID_API_LEVEL" 17 | 18 | 19 | android: 20 | components: 21 | # The BuildTools version used by your project 22 | - tools 23 | - platform-tools 24 | - build-tools-$ANDROID_BUILD_TOOLS_VERSION 25 | # The SDK version used to compile your project 26 | - android-$ANDROID_API_LEVEL 27 | - extra-android-m2repository 28 | - extra-google-android-support 29 | 30 | script: 31 | - ./gradlew clean 32 | - ./gradlew assembleRelease -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 版本日志 2 | 3 | #### v3.1.0:2025-3-23 4 | * 优化插件配置 5 | * 优化一些细节 6 | * 更新gradle至v8.9 7 | * 更新kotlin至v1.9.24 8 | * 更新appcompat至v1.7.0 9 | * 更新core-ktx至v1.13.1 10 | * 更新fragment-ktx至v1.8.6 11 | * 更新lifecycle-ktx至v2.8.7 12 | * 更新hilt至v2.52 13 | * 更新retrofit至v2.11.0 14 | * 更新gson至v2.12.1 15 | 16 | #### v3.0.0:2024-3-3 17 | * 统一改为使用`kotlin`进行了重构 18 | * 移除所有`LiveData`相关代码改用`Flow` 19 | * 支持插件配置 20 | * 更新编译SDK至34 21 | * 更新Gradle至v8.0 22 | * 新增core-ktx依赖(v1.12.0) 23 | * 新增fragment-ktx依赖(v1.6.2) 24 | * 新增lifecycle-ktx相关依赖(v2.7.0) 25 | * 更新Okhttp至v4.12.0 26 | * 更新Hilt至v2.51 27 | * 更新Gson至v2.10.1 28 | * 更新Room至v2.6.1 29 | * 更新retrofit-helper至v1.1.0 30 | 31 | #### v2.2.1:2022-4-21 32 | * 更新Okhttp至v4.9.3 33 | * 更新Hilt至v2.41 34 | * 更新Gson至v2.9.0 35 | 36 | #### v2.2.0:2021-11-18 37 | * minSdk要求从 16+ 改为 21+ 38 | * 更新编译SDK至30 39 | * 更新Gradle至v6.7.1 40 | * 更新Okhttp至v4.9.2 41 | * 更新Hilt至v2.40.1 42 | * 更新Gson至v2.8.9 43 | * 更新Timber至v5.0.1 44 | 45 | #### v2.1.1:2021-6-29 46 | * 更新Hilt至v2.37 47 | * 更新Gson至v2.8.7 48 | * 优化细节 49 | 50 | #### v2.1.0:2021-4-28 (从v2.1.0开始不再发布至JCenter) 51 | * 更新Hilt至v2.35 52 | * 移除androidx.hilt:hilt-lifecycle-viewmodel [移除原因请查看Dagger v2.34更新说明](https://github.com/google/dagger/releases) 53 | * 更新Lifecycle至v2.3.1 54 | * 更新Room至v2.3.0 55 | * 更新RetrofitHelper至v1.0.1 56 | * 发布至Maven Central 57 | 58 | #### v2.0.0:2021-1-15 59 | * 使用Hilt简化Dagger依赖注入用法 60 | 61 | #### v1.1.4:2020-12-14 62 | * 优化细节 63 | * 更新Dagger至v2.30.1 64 | 65 | #### v1.1.3:2020-6-1 66 | * 支持配置多个BaseUrl,且支持动态改变(详情查看 [RetrofitHelper](https://github.com/jenly1314/RetrofitHelper)) 67 | * 对外暴露更多配置,(详情查看 FrameConfigModule) 68 | * 优化细节 69 | * 更新Retrofit至v2.9.0 70 | 71 | #### v1.1.2:2020-4-5 72 | * 优化细节 73 | * 更新Gradle至v5.6.4 74 | * 更新Lifecycle至v2.2.0 75 | * 更新Room至v2.2.5 76 | * 更新Dagger至v2.27 77 | * 更新Retrofit至v2.8.1 78 | 79 | #### v1.1.1:2019-11-4 80 | * 优化部分细节 81 | * 更新编译SDK至29 82 | * 更新Gradle至v5.4.1 83 | * 更新Lifecycle至v2.2.0-rc01 84 | * 更新Room至v2.2.1 85 | * 更新Dagger至v2.25.2 86 | * 更新Retrofit至v2.6.2 87 | * 更新Gson至v2.8.6 88 | 89 | #### v1.1.0:2019-7-22 90 | * 更新Dagger至v2.23.2 91 | * 更新Gradle至v5.1.1 92 | * 完全迁移至AndroidX版本 93 | 94 | #### v1.0.2:2019-7-22 95 | * 更新Dagger至v2.19 96 | * 为迁移至AndroidX做准备(下一版本将直接发布AndroidX版) 97 | 98 | #### v1.0.1:2019-7-9 99 | * Retrofit更新至v2.6.0 100 | 101 | #### v1.0.0:2018-12-12 102 | * MVVMFrame初始版本 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jenly Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kapt) 5 | alias(libs.plugins.ksp) 6 | alias(libs.plugins.mvvmframe) 7 | } 8 | 9 | android { 10 | namespace = "com.king.mvvmframe" 11 | compileSdk = libs.versions.compileSdk.get().toInt() 12 | 13 | defaultConfig { 14 | 15 | applicationId = "com.king.mvvmframe" 16 | minSdk = libs.versions.minSdk.get().toInt() 17 | targetSdk = libs.versions.targetSdk.get().toInt() 18 | versionCode = properties["VERSION_CODE"].toString().toInt() 19 | versionName = properties["VERSION_NAME"].toString() 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | 23 | vectorDrawables { 24 | useSupportLibrary = true 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | buildFeatures{ 39 | dataBinding = true 40 | buildConfig = true 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = JavaVersion.VERSION_17 45 | targetCompatibility = JavaVersion.VERSION_17 46 | } 47 | kotlinOptions { 48 | jvmTarget = JavaVersion.VERSION_17.toString() 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | // test 55 | testImplementation(libs.junit) 56 | androidTestImplementation(libs.androidx.junit) 57 | androidTestImplementation(libs.androidx.espresso) 58 | 59 | implementation(libs.material) 60 | implementation(libs.androidx.appcompat) 61 | implementation(libs.swiperefreshlayout) 62 | implementation(libs.recyclerview) 63 | implementation(libs.constraintlayout) 64 | 65 | implementation(libs.base.util) 66 | implementation(libs.base.adapter) 67 | 68 | implementation(libs.android.ktx) 69 | 70 | implementation(libs.flowlayout) 71 | 72 | implementation(libs.logx) 73 | 74 | //leakCanary 75 | debugImplementation(libs.leakcanary) 76 | 77 | ksp(libs.room.compiler) 78 | 79 | // MVVMFrame 80 | implementation(project(":mvvmframe")) 81 | 82 | } 83 | 84 | mvvmFrame { 85 | //.. 86 | } 87 | 88 | kapt { 89 | correctErrorTypes = true 90 | } 91 | -------------------------------------------------------------------------------- /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.kts. 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/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenly1314/MVVMFrame/e3d343fa4adf1fc5b073f9e36c5e5942be8352b5/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.king.mvvmframe", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 18, 15 | "versionName": "3.1.0", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File", 20 | "baselineProfiles": [ 21 | { 22 | "minApi": 28, 23 | "maxApi": 30, 24 | "baselineProfiles": [ 25 | "baselineProfiles/1/app-release.dm" 26 | ] 27 | }, 28 | { 29 | "minApi": 31, 30 | "maxApi": 2147483647, 31 | "baselineProfiles": [ 32 | "baselineProfiles/0/app-release.dm" 33 | ] 34 | } 35 | ], 36 | "minSdkVersionForDexing": 21 37 | } 38 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/king/mvvmframe/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.king.mvvmframe", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenly1314/MVVMFrame/e3d343fa4adf1fc5b073f9e36c5e5942be8352b5/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/App.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe 2 | 3 | import com.king.frame.mvvmframe.base.BaseApplication 4 | import com.king.logx.LogX 5 | import com.king.mvvmframe.constant.Constants 6 | import com.king.retrofit.retrofithelper.RetrofitHelper 7 | import dagger.hilt.android.HiltAndroidApp 8 | import timber.log.Timber 9 | 10 | /** 11 | * MVVMFrame 框架基于 Google 官方的 JetPack 构建,在使用 MVVMFrame 时,需遵循一些规范: 12 | * 13 | * 你需要参照如下方式添加 @HiltAndroidApp 注解 14 | * 15 | * ``` 16 | * // 示例 17 | * @HiltAndroidApp 18 | * class YourApplication : BaseApplication() { 19 | * 20 | * } 21 | * ``` 22 | * 23 | * @author Jenly 24 | *

25 | * Follow me 26 | */ 27 | @HiltAndroidApp 28 | class App : BaseApplication() { 29 | 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | initLog() 34 | //------------------------------ 35 | // 如果你没有使用FrameConfigModule中的第一中方式初始化BaseUrl,也可以通过第二种方式来设置BaseUrl(二选其一即可) 36 | // RetrofitHelper.getInstance().setBaseUrl(Constants.BASE_URL) 37 | // 设置动态BaseUrl 38 | RetrofitHelper.getInstance().apply { 39 | putDomain(Constants.DOMAIN_DICTIONARY, Constants.DICTIONARY_BASE_URL) 40 | putDomain(Constants.DOMAIN_JENLY, Constants.JENLY_BASE_URL) 41 | } 42 | } 43 | 44 | /** 45 | * 初始化 46 | */ 47 | private fun initLog() { 48 | // 初始化日志打印 49 | Timber.plant(object : Timber.DebugTree() { 50 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 51 | LogX.offset(4).log(priority, message) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.databinding.ViewDataBinding 6 | import com.king.android.ktx.activity.startActivity 7 | import com.king.frame.mvvmframe.base.BaseActivity 8 | import com.king.frame.mvvmframe.base.BaseViewModel 9 | import com.king.mvvmframe.R 10 | import com.king.mvvmframe.app.city.CityActivity 11 | import com.king.mvvmframe.app.dictionary.DictionaryActivity 12 | import com.king.mvvmframe.app.oil.OilPriceActivity 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | /** 16 | * MVVMFrame 框架基于 Google 官方的 JetPack 构建,在使用 MVVMFrame 时,需遵循一些规范: 17 | * 18 | * Hilt大幅简化了 Dagger2 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始, 19 | * Hilt一共支持6个入口点,分别是: 20 | * Application 21 | * Activity 22 | * Fragment 23 | * View 24 | * Service 25 | * BroadcastReceiver 26 | * 27 | * 其中,只有Application这个入口点是使用@HiltAndroidApp注解来声明,示例如下 28 | * 29 | * Example: Application 30 | * 31 | * ``` 32 | * @HiltAndroidApp 33 | * class YourApplication : Application() { 34 | * 35 | * } 36 | * ``` 37 | * 其他的入口点,都是用@AndroidEntryPoint注解来声明,示例如下 38 | * 39 | * Example: Activity 40 | * ``` 41 | * @AndroidEntryPoint 42 | * class YourActivity : BaseActivity() { 43 | * 44 | * } 45 | * ``` 46 | * Example: Fragment 47 | * ``` 48 | * @AndroidEntryPoint 49 | * class YourFragment : BaseFragment() { 50 | * 51 | * } 52 | * ``` 53 | * 主要示例请查看 [OilPriceActivity] 和 [OilPriceViewModel] 54 | * 55 | * @author Jenly 56 | *

57 | * Follow me 58 | */ 59 | @AndroidEntryPoint 60 | class MainActivity : BaseActivity() { 61 | 62 | override fun getLayoutId(): Int { 63 | return R.layout.main_activity 64 | } 65 | 66 | override fun isBinding(): Boolean { 67 | // 不覆写此方法时,默认返回true,这里返回false表示不使用DataBinding,因为当前界面比较简单,完全没有必要用 68 | return false 69 | } 70 | 71 | override fun initData(savedInstanceState: Bundle?) { 72 | 73 | } 74 | 75 | fun onClick(v: View) { 76 | when (v.id) { 77 | R.id.btn1 -> startActivity(OilPriceActivity::class.java) 78 | R.id.btn2 -> startActivity(DictionaryActivity::class.java) 79 | R.id.btn3 -> startActivity(CityActivity::class.java) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/adapter/BindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.databinding.ViewDataBinding 6 | import androidx.recyclerview.widget.DiffUtil 7 | import com.king.base.adapter.BaseRecyclerAdapter 8 | import com.king.mvvmframe.BR 9 | 10 | /** 11 | * @author Jenly 12 | *

13 | * Follow me 14 | */ 15 | class BindingAdapter : BaseRecyclerAdapter> { 16 | constructor(context: Context, layoutId: Int) : super(context, layoutId) 17 | constructor(context: Context, listData: MutableList?, layoutId: Int) : super( 18 | context, 19 | listData, 20 | layoutId 21 | ) 22 | 23 | override fun bindViewDatas(holder: BindingHolder, item: T, position: Int) { 24 | holder.binding?.also { 25 | it.setVariable(BR.data, item) 26 | it.executePendingBindings() 27 | } 28 | } 29 | 30 | fun getItem(position: Int): T? { 31 | return listData.getOrNull(position) 32 | } 33 | 34 | fun refreshData(list: List?) { 35 | val diffResult = DiffUtil.calculateDiff(ItemDiffCallback(listData, list.orEmpty())) 36 | if (list != null) { 37 | setListData(list) 38 | } else { 39 | listData.clear() 40 | } 41 | diffResult.dispatchUpdatesTo(this) 42 | } 43 | 44 | class ItemDiffCallback( 45 | private val oldList: List, 46 | private val newList: List, 47 | ) : DiffUtil.Callback() { 48 | override fun getOldListSize(): Int { 49 | return oldList.size 50 | } 51 | 52 | override fun getNewListSize(): Int { 53 | return newList.size 54 | } 55 | 56 | override fun areItemsTheSame(oldPosition: Int, newPosition: Int): Boolean { 57 | return oldList[oldPosition] == newList[newPosition] 58 | } 59 | 60 | override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean { 61 | return oldList[oldPosition] == newList[newPosition] 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/adapter/BindingHolder.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.adapter 2 | 3 | import android.view.View 4 | import androidx.databinding.DataBindingUtil 5 | import androidx.databinding.ViewDataBinding 6 | import com.king.base.adapter.holder.ViewHolder 7 | 8 | /** 9 | * @author Jenly 10 | *

11 | * Follow me 12 | */ 13 | class BindingHolder(convertView: View) : ViewHolder(convertView) { 14 | 15 | val binding by lazy { 16 | DataBindingUtil.bind(convertView) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/adapter/SearchHistoryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.king.mvvmframe.R 7 | import com.king.mvvmframe.constant.Constants 8 | import com.king.mvvmframe.data.model.SearchHistory 9 | import com.king.mvvmframe.util.RandomUtil.randomColor 10 | import com.zhy.view.flowlayout.FlowLayout 11 | import com.zhy.view.flowlayout.TagAdapter 12 | 13 | /** 14 | * @author Jenly 15 | *

16 | * Follow me 17 | */ 18 | class SearchHistoryAdapter(list: MutableList) : TagAdapter(list) { 19 | 20 | override fun getView(parent: FlowLayout, position: Int, data: SearchHistory): View { 21 | val tv = LayoutInflater.from(parent.context) 22 | .inflate(R.layout.search_history_item, parent, false) as TextView 23 | tv.text = data.word 24 | tv.setTextColor(randomColor(Constants.COLOR_RGB_MIN, Constants.COLOR_RGB_MAX)) 25 | return tv 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.base 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.viewModelScope 5 | import com.king.base.util.SystemUtils 6 | import com.king.frame.mvvmframe.base.BaseAndroidViewModel 7 | import com.king.mvvmframe.data.model.Result 8 | import com.king.mvvmframe.R 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import timber.log.Timber 12 | import java.net.ConnectException 13 | import java.net.SocketTimeoutException 14 | import javax.inject.Inject 15 | import kotlin.coroutines.CoroutineContext 16 | import kotlin.coroutines.EmptyCoroutineContext 17 | 18 | /** 19 | * 基类 20 | * 21 | * @author Jenly 22 | *

23 | * Follow me 24 | */ 25 | @HiltViewModel 26 | open class BaseViewModel @Inject constructor(application: Application) : BaseAndroidViewModel(application) { 27 | 28 | /** 29 | * 是否成功 30 | */ 31 | fun isSuccess(result: Result<*>, showError: Boolean = true): Boolean { 32 | if (result.isSuccess()) { 33 | return true 34 | } 35 | if (showError) { 36 | sendMessage(result.message) 37 | } 38 | return false 39 | } 40 | 41 | /** 42 | * 启动一个协程 43 | */ 44 | fun launch( 45 | showLoading: Boolean = true, 46 | context: CoroutineContext = EmptyCoroutineContext, 47 | error: suspend (Throwable) -> Unit = { 48 | Timber.w(it) 49 | if (SystemUtils.isNetWorkActive(getApplication())) { 50 | when (it) { 51 | is SocketTimeoutException -> sendMessage(R.string.result_connect_timeout_error) 52 | is ConnectException -> sendMessage(R.string.result_connect_failed_error) 53 | else -> sendMessage(R.string.result_error) 54 | } 55 | } else { 56 | sendMessage(R.string.result_network_unavailable_error) 57 | } 58 | }, 59 | block: suspend () -> Unit, 60 | ) = viewModelScope.launch(context = context) { 61 | try { 62 | if (showLoading) { 63 | showLoading() 64 | } 65 | block() 66 | } catch (e: Throwable) { 67 | error(e) 68 | } finally { 69 | if (showLoading) { 70 | hideLoading() 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/city/CityActivity.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.city 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.flowWithLifecycle 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import com.king.base.adapter.divider.DividerItemDecoration 8 | import com.king.frame.mvvmframe.base.BaseActivity 9 | import com.king.mvvmframe.R 10 | import com.king.mvvmframe.app.adapter.BindingAdapter 11 | import com.king.mvvmframe.data.model.City 12 | import com.king.mvvmframe.databinding.CityActivityBinding 13 | import dagger.hilt.android.AndroidEntryPoint 14 | import kotlinx.coroutines.launch 15 | 16 | /** 17 | * 示例 18 | * 19 | * @author Jenly 20 | *

21 | * Follow me 22 | */ 23 | @AndroidEntryPoint 24 | class CityActivity : BaseActivity() { 25 | 26 | private val mAdapter by lazy { 27 | BindingAdapter(getContext(), R.layout.rv_city_item) 28 | } 29 | 30 | override fun getLayoutId(): Int { 31 | return R.layout.city_activity 32 | } 33 | 34 | override fun initData(savedInstanceState: Bundle?) { 35 | with(binding.recyclerView) { 36 | layoutManager = LinearLayoutManager(context) 37 | addItemDecoration( 38 | DividerItemDecoration( 39 | context, 40 | DividerItemDecoration.VERTICAL, 41 | R.drawable.list_divider_8 42 | ) 43 | ) 44 | adapter = mAdapter 45 | } 46 | 47 | mAdapter.setOnItemClickListener { _, position -> 48 | showToast(mAdapter.getItem(position)!!.name) 49 | } 50 | 51 | lifecycleScope.launch { 52 | viewModel.cityFlow.flowWithLifecycle(lifecycle).collect { 53 | mAdapter.refreshData(it) 54 | } 55 | } 56 | 57 | binding.srl.setOnRefreshListener { 58 | viewModel.getHotCities() 59 | } 60 | 61 | viewModel.getHotCities() 62 | } 63 | 64 | override fun showLoading() { 65 | if (!binding.srl.isRefreshing) { 66 | super.showLoading() 67 | } 68 | } 69 | 70 | override fun hideLoading() { 71 | super.hideLoading() 72 | binding.srl.isRefreshing = false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/city/CityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.city 2 | 3 | import android.app.Application 4 | import com.king.mvvmframe.app.base.BaseViewModel 5 | import com.king.mvvmframe.data.model.City 6 | import com.king.mvvmframe.data.repository.CityRepository 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asSharedFlow 10 | import javax.inject.Inject 11 | 12 | /** 13 | * 示例 14 | * 15 | * @author Jenly 16 | *

17 | * Follow me 18 | */ 19 | @HiltViewModel 20 | class CityViewModel @Inject constructor( 21 | private val cityRepository: CityRepository, 22 | application: Application 23 | ) : BaseViewModel(application) { 24 | 25 | private val _cityFlow = MutableStateFlow>(emptyList()) 26 | val cityFlow = _cityFlow.asSharedFlow() 27 | 28 | /** 29 | * 获取热门城市 30 | */ 31 | fun getHotCities() { 32 | launch { 33 | cityRepository.getHotCities().also { 34 | _cityFlow.emit(it) 35 | } 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/dictionary/DictionaryActivity.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.dictionary 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.text.InputFilter 6 | import android.text.InputFilter.LengthFilter 7 | import android.view.Menu 8 | import android.view.View 9 | import androidx.appcompat.widget.SearchView 10 | import androidx.appcompat.widget.SearchView.SearchAutoComplete 11 | import androidx.lifecycle.flowWithLifecycle 12 | import androidx.lifecycle.lifecycleScope 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import com.king.base.adapter.divider.DividerItemDecoration 15 | import com.king.base.util.StringUtils 16 | import com.king.frame.mvvmframe.base.BaseActivity 17 | import com.king.mvvmframe.R 18 | import com.king.mvvmframe.app.adapter.BindingAdapter 19 | import com.king.mvvmframe.app.adapter.SearchHistoryAdapter 20 | import com.king.mvvmframe.data.model.SearchHistory 21 | import com.king.mvvmframe.databinding.DictionaryActivityBinding 22 | import dagger.hilt.android.AndroidEntryPoint 23 | import kotlinx.coroutines.launch 24 | 25 | /** 26 | * 示例 27 | * 28 | * @author Jenly 29 | *

30 | * Follow me 31 | */ 32 | @AndroidEntryPoint 33 | class DictionaryActivity : BaseActivity() { 34 | 35 | private var searchView: SearchView? = null 36 | 37 | private val mAdapter by lazy { 38 | BindingAdapter(getContext(), R.layout.rv_dictionary_item) 39 | } 40 | 41 | private val listHistory = mutableListOf() 42 | 43 | private val mSearchHistoryAdapter by lazy { 44 | SearchHistoryAdapter(listHistory) 45 | } 46 | 47 | private var word: String = "" 48 | 49 | override fun getLayoutId(): Int { 50 | return R.layout.dictionary_activity 51 | } 52 | 53 | override fun initData(savedInstanceState: Bundle?) { 54 | binding.recyclerView.layoutManager = LinearLayoutManager(getContext()) 55 | binding.recyclerView.addItemDecoration( 56 | DividerItemDecoration( 57 | getContext(), 58 | DividerItemDecoration.VERTICAL 59 | ) 60 | ) 61 | binding.recyclerView.adapter = mAdapter 62 | 63 | binding.tflHistory.adapter = mSearchHistoryAdapter 64 | 65 | binding.tflHistory.setOnTagClickListener { _, position, _ -> 66 | search(mSearchHistoryAdapter.getItem(position).word) 67 | true 68 | } 69 | 70 | binding.srl.setOnRefreshListener { 71 | search(word) 72 | } 73 | 74 | lifecycleScope.launch { 75 | viewModel.dictionaryFlow.flowWithLifecycle(lifecycle).collect { 76 | mAdapter.refreshData(it?.xiangjie) 77 | } 78 | } 79 | 80 | lifecycleScope.launch { 81 | viewModel.getSearchHistoryFlow().flowWithLifecycle(lifecycle).collect { 82 | listHistory.clear() 83 | listHistory.addAll(it) 84 | mSearchHistoryAdapter.notifyDataChanged() 85 | } 86 | } 87 | } 88 | 89 | private fun search(key: String) { 90 | if (StringUtils.isNotBlank(key)) { 91 | searchView?.setQuery(key, true) 92 | } else { 93 | binding.srl.isRefreshing = false 94 | } 95 | } 96 | 97 | @SuppressLint("RestrictedApi") 98 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 99 | menuInflater.inflate(R.menu.search_view_menu, menu) 100 | val searchItem = menu.findItem(R.id.searchItem) 101 | //通过MenuItem得到SearchView 102 | searchView = searchItem.actionView as SearchView? 103 | val searchSrcTextView = searchView?.findViewById(R.id.search_src_text) 104 | searchSrcTextView?.also { 105 | it.filters = arrayOf(LengthFilter(1)) 106 | } 107 | searchView?.queryHint = getString(R.string.word_search_hint) 108 | searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 109 | override fun onQueryTextSubmit(s: String): Boolean { 110 | if (StringUtils.isNotBlank(s)) { 111 | word = s 112 | viewModel.getDictionaryInfo(word) 113 | searchView?.clearFocus() 114 | } else { 115 | showToast(getString(R.string.tips_search_content)) 116 | } 117 | return true 118 | } 119 | 120 | override fun onQueryTextChange(s: String): Boolean { 121 | return false 122 | } 123 | }) 124 | return super.onCreateOptionsMenu(menu) 125 | } 126 | 127 | override fun showLoading() { 128 | if (!binding.srl.isRefreshing) { 129 | super.showLoading() 130 | } 131 | } 132 | 133 | override fun hideLoading() { 134 | super.hideLoading() 135 | binding.srl.isRefreshing = false 136 | } 137 | 138 | private fun clearSearchHistory() { 139 | viewModel.clearSearchHistory() 140 | mAdapter.refreshData(null) 141 | word = "" 142 | searchView?.setQuery(word, false) 143 | } 144 | 145 | fun onClick(v: View) { 146 | when (v.id) { 147 | R.id.ivDeleteHistory -> clearSearchHistory() 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/dictionary/DictionaryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.dictionary 2 | 3 | import android.app.Application 4 | import com.king.mvvmframe.R 5 | import com.king.mvvmframe.app.base.BaseViewModel 6 | import com.king.mvvmframe.data.model.DictionaryInfo 7 | import com.king.mvvmframe.data.repository.DictionaryRepository 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import javax.inject.Inject 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asSharedFlow 12 | 13 | /** 14 | * 示例 15 | * 16 | * @author Jenly 17 | *

18 | * Follow me 19 | */ 20 | @HiltViewModel 21 | class DictionaryViewModel @Inject constructor( 22 | private val dictionaryRepository: DictionaryRepository, 23 | application: Application 24 | ) : BaseViewModel(application) { 25 | 26 | private val _dictionaryFlow = MutableStateFlow(null) 27 | val dictionaryFlow = _dictionaryFlow.asSharedFlow() 28 | 29 | /** 30 | * 获取字典信息 31 | */ 32 | fun getDictionaryInfo(word: String) { 33 | launch { 34 | val result = dictionaryRepository.getDictionaryInfo(word) 35 | if (isSuccess(result)) { 36 | _dictionaryFlow.emit(result.data) 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * 获取搜索历史 43 | */ 44 | fun getSearchHistoryFlow() = dictionaryRepository.getSearchHistoryFlow() 45 | 46 | /** 47 | * 删除搜索历史 48 | */ 49 | fun clearSearchHistory() { 50 | launch { 51 | dictionaryRepository.clearSearchHistory() 52 | sendMessage(R.string.cleared) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/oil/OilPriceActivity.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.oil 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.flowWithLifecycle 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import com.king.base.adapter.divider.DividerItemDecoration 8 | import com.king.frame.mvvmframe.base.BaseActivity 9 | import com.king.mvvmframe.R 10 | import com.king.mvvmframe.app.adapter.BindingAdapter 11 | import com.king.mvvmframe.data.model.OilPrice 12 | import com.king.mvvmframe.databinding.OilPriceActivityBinding 13 | import dagger.hilt.android.AndroidEntryPoint 14 | import kotlinx.coroutines.launch 15 | 16 | /** 17 | * 示例 18 | * 19 | * @author Jenly 20 | *

21 | * Follow me 22 | */ 23 | @AndroidEntryPoint 24 | class OilPriceActivity : BaseActivity() { 25 | 26 | private val mAdapter by lazy { 27 | BindingAdapter(getContext(), R.layout.rv_oil_price_item) 28 | } 29 | 30 | override fun getLayoutId(): Int { 31 | return R.layout.oil_price_activity 32 | } 33 | 34 | override fun initData(savedInstanceState: Bundle?) { 35 | with(binding.recyclerView) { 36 | layoutManager = LinearLayoutManager(context) 37 | addItemDecoration( 38 | DividerItemDecoration( 39 | context, 40 | DividerItemDecoration.VERTICAL, 41 | R.drawable.list_divider_8 42 | ) 43 | ) 44 | adapter = mAdapter 45 | } 46 | 47 | mAdapter.setOnItemClickListener { _, position -> 48 | showToast(mAdapter.getItem(position)!!.city) 49 | } 50 | 51 | lifecycleScope.launch { 52 | viewModel.oilPriceFlow.flowWithLifecycle(lifecycle).collect { 53 | mAdapter.refreshData(it) 54 | } 55 | } 56 | 57 | binding.srl.setOnRefreshListener { 58 | viewModel.getOilPriceInfo() 59 | } 60 | 61 | viewModel.getOilPriceInfo() 62 | } 63 | 64 | override fun showLoading() { 65 | if (!binding.srl.isRefreshing) { 66 | super.showLoading() 67 | } 68 | } 69 | 70 | override fun hideLoading() { 71 | super.hideLoading() 72 | binding.srl.isRefreshing = false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/app/oil/OilPriceViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.app.oil 2 | 3 | import android.app.Application 4 | import com.king.mvvmframe.app.base.BaseViewModel 5 | import com.king.mvvmframe.data.model.OilPrice 6 | import com.king.mvvmframe.data.repository.OilPriceRepository 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asSharedFlow 10 | import javax.inject.Inject 11 | 12 | /** 13 | * 示例 14 | * 15 | * @author Jenly 16 | *

17 | * Follow me 18 | */ 19 | @HiltViewModel 20 | class OilPriceViewModel @Inject constructor( 21 | private val oilPriceRepository: OilPriceRepository, 22 | application: Application 23 | ) : BaseViewModel(application) { 24 | 25 | 26 | private val _oilPriceFlow = MutableStateFlow>(emptyList()) 27 | val oilPriceFlow = _oilPriceFlow.asSharedFlow() 28 | 29 | /** 30 | * 获取油价信息 31 | */ 32 | fun getOilPriceInfo() { 33 | launch { 34 | val result = oilPriceRepository.getOilPriceInfo() 35 | if (isSuccess(result)) { 36 | result.data?.also { 37 | _oilPriceFlow.emit(it) 38 | } 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/config/AppConfigModule.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.config 2 | 3 | import android.content.Context 4 | import androidx.room.RoomDatabase 5 | import com.google.gson.GsonBuilder 6 | import com.king.frame.mvvmframe.config.AppliesOptions 7 | import com.king.frame.mvvmframe.config.AppliesOptions.GsonOptions 8 | import com.king.frame.mvvmframe.config.AppliesOptions.OkHttpClientOptions 9 | import com.king.frame.mvvmframe.config.AppliesOptions.RetrofitOptions 10 | import com.king.frame.mvvmframe.config.AppliesOptions.RoomDatabaseOptions 11 | import com.king.frame.mvvmframe.config.Config 12 | import com.king.frame.mvvmframe.config.FrameConfigModule 13 | import com.king.frame.mvvmframe.di.module.ConfigModule 14 | import com.king.mvvmframe.BuildConfig 15 | import com.king.mvvmframe.constant.Constants 16 | import com.king.retrofit.retrofithelper.RetrofitHelper 17 | import okhttp3.OkHttpClient 18 | import okhttp3.logging.HttpLoggingInterceptor 19 | import retrofit2.Retrofit 20 | 21 | /** 22 | * 全局配置 23 | * 24 | * @author Jenly 25 | *

26 | * Follow me 27 | */ 28 | class AppConfigModule : FrameConfigModule() { 29 | override fun applyOptions(context: Context, builder: ConfigModule.Builder) { 30 | /** 31 | * > 目前通过设置 BaseUrl 的入口主要有两种: 32 | * >> 1.一种是通过在 Manifest 中配置 meta-data 的来自定义 FrameConfigModule,在里面 通过 [ConfigModule.Builder.baseUrl]来配置 BaseUrl。(一次设置,全局配置) 33 | * > 34 | * >> 2.一种就是通过RetrofitHelper [RetrofitHelper.setBaseUrl] 或 [RetrofitHelper.setBaseUrl] 来配置 BaseUrl。(可多次设置,动态全局配置,有前提条件) 35 | * > 36 | * > 以上两种配置 BaseUrl 的方式都可以达到目的。但是你可以根据不同的场景选择不同的配置方式。 37 | * > 38 | * > 主要场景与选择如下: 39 | * > 40 | * >> 一般场景:对于只使用单个不变的 BaseUrl的 41 | * >>> 场景1:如果本库的默认已满足你的需求,无需额外自定义配置的。 42 | * > 选择:建议你直接使用 [RetrofitHelper.setBaseUrl] 或 [RetrofitHelper.setBaseUrl] 来初始化 BaseUrl,切记在框架配置初始化 BaseUrl之前,建议在你自定义的 [Application.onCreate]中初始化。 43 | * > 44 | * >>> 场景2:如果本库的默认配置不满足你的需求,你需要自定义一些配置的。(比如需要使用 RxJava相关) 45 | * > 选择:建议你在自定义配置中通过 [ConfigModule.Builder.baseUrl] 来初始化 BaseUrl。 46 | * > 47 | * >> 二般场景:对于只使用单个 BaseUrl 但是,BaseUrl中途会变动的。 48 | * >>> 场景3:和一般场景一样,也能分两种,所以选择也和一般场景也可以是一样的。 49 | * > 选择:两种选择都行,但当 BaseUrl需要中途变动时,还需将 [RetrofitHelper.setDynamicDomain] 设置为 `true` 才能支持动态改变 BaseUrl。 50 | * > 51 | * >> 特殊场景:对于支持多个 BaseUrl 且支持动态可变的。 52 | * >>> 选择:这个场景的选择,主要涉及到另外的方法,请查看 [RetrofitHelper.putDomain] 和 [RetrofitHelper.putDomain]相关详情 53 | * > 54 | */ 55 | // 通过第一种方式初始化BaseUrl 56 | builder.baseUrl(Constants.BASE_URL) // TODO 配置Retrofit中的baseUrl 57 | 58 | builder.retrofitOptions(object : RetrofitOptions { 59 | override fun applyOptions(builder: Retrofit.Builder) { 60 | // TODO 配置Retrofit 61 | 62 | } 63 | }) 64 | .okHttpClientOptions(object : OkHttpClientOptions { 65 | override fun applyOptions(builder: OkHttpClient.Builder) { 66 | // TODO 配置OkHttpClient 67 | } 68 | }) 69 | .gsonOptions(object : GsonOptions { 70 | override fun applyOptions(builder: GsonBuilder) { 71 | // TODO 配置Gson 72 | } 73 | }) 74 | .roomDatabaseOptions(object : RoomDatabaseOptions { 75 | override fun applyOptions(builder: RoomDatabase.Builder) { 76 | // TODO 配置RoomDatabase 77 | builder.fallbackToDestructiveMigration() 78 | } 79 | }) 80 | .configOptions(object : AppliesOptions.ConfigOptions { 81 | override fun applyOptions(builder: Config.Builder) { 82 | // TODO 配置Config 83 | builder.httpLoggingLevel( 84 | if (BuildConfig.DEBUG) { 85 | HttpLoggingInterceptor.Level.BODY 86 | } else { 87 | HttpLoggingInterceptor.Level.NONE 88 | } 89 | ) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/constant/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.constant 2 | 3 | /** 4 | * @author Jenly 5 | *

6 | * Follow me 7 | */ 8 | object Constants { 9 | 10 | const val BASE_URL = "http://apis.juhe.cn" 11 | 12 | const val DICTIONARY_BASE_URL = "http://v.juhe.cn" 13 | 14 | const val JENLY_BASE_URL = "https://jenly1314.gitlab.io" 15 | 16 | const val TAG = "Jenly" 17 | 18 | const val OIL_PRICE_KEY = "402e469d939079d5bb758fc5ca65967d" 19 | const val DICTIONARY_KEY = "e38e852fe9307822a50e66763afdc593" 20 | 21 | const val COLOR_RGB_MIN = 20 22 | const val COLOR_RGB_MAX = 230 23 | 24 | const val DOMAIN_DICTIONARY = "dictionary" 25 | const val DOMAIN_JENLY = "jenly1314" 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.king.mvvmframe.data.model.SearchHistory 6 | import com.king.mvvmframe.data.database.dao.SearchHistoryDao 7 | 8 | /** 9 | * 数据库 10 | * 11 | * @author Jenly 12 | *

13 | * Follow me 14 | */ 15 | @Database(entities = [SearchHistory::class], version = 1, exportSchema = false) 16 | abstract class AppDatabase : RoomDatabase() { 17 | /** 18 | * SearchHistoryDao 19 | */ 20 | abstract val searchHistoryDao: SearchHistoryDao 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/database/dao/SearchHistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.king.mvvmframe.data.model.SearchHistory 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | /** 11 | * 搜索历史 12 | * 13 | * @author Jenly 14 | *

15 | * Follow me 16 | */ 17 | @Dao 18 | interface SearchHistoryDao { 19 | /** 20 | * 插入一条历史数据 21 | * @param searchHistory 22 | */ 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | suspend fun insert(searchHistory: SearchHistory) 25 | 26 | /** 27 | * 删除所有历史数据 28 | */ 29 | @Query("DELETE FROM SearchHistory") 30 | suspend fun deleteAll() 31 | 32 | /** 33 | * 获取历史数据对应的[Flow] 34 | * 35 | * @param size 获取历史记录的条数 36 | */ 37 | @Query("SELECT * FROM SearchHistory ORDER BY timestamp DESC LIMIT :size") 38 | fun getHistoryFlow(size: Int): Flow?> 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/model/City.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.model 2 | 3 | /** 4 | * 城市 5 | * 6 | * @author Jenly 7 | *

8 | * Follow me 9 | */ 10 | data class City( 11 | val id: String, 12 | val name: String, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/model/DictionaryInfo.java: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Jenly 7 | */ 8 | public class DictionaryInfo { 9 | 10 | /** 11 | * zi : 聚 12 | * py : ju 13 | * wubi : bcti 14 | * pinyin : jù 15 | * bushou : 耳 16 | * bihua : 14 17 | * jijie : ["聚","jù","会合,集合:聚合。聚会。聚积。聚集。聚谈。聚拢。聚齐。聚餐。欢聚一堂。物以类聚。凝聚。聚沙成塔。","散","","笔画数:14;","部首:耳;","笔顺编号:12211154323334"] 18 | * xiangjie : ["聚","jù","【名】","(形声。小篆,下面是三个人,表示人多;上面的\u201c取\u201d,作声符。本义:村落)","同本义〖village〗","聚,会也。\u2014\u2014《说文》。按,邑落曰聚,今曰邨,曰镇,北方曰集皆是。","一年而所居成聚。\u2014\u2014《史记·五帝纪》","聚曰序。\u2014\u2014《史记·平帝纪》","所止聚落化其德。\u2014\u2014《后汉书·王扶传》。注:\u201c小于乡曰聚。\u201d","乡聚里中人哀而救之。\u2014\u2014刘向《说苑》","禹无十户之聚,以王诸侯。\u2014\u2014枚乘《上书谏吴王》","又如:聚落(村落里邑,人群聚居的地方)","众;集团;一伙〖group〗","我是以有辅氏之聚。\u2014\u2014《左传·成公十三年》","陈人恃其聚。\u2014\u2014《左传·哀公十七年》","又如:聚观(群聚观看);聚口(犹齐声)","地名〖Jucity〗。在今山西省绛县东南","","聚","jù","【动】","会合;聚集〖assemble;flocktogether;gather;gettogether〗","而发于众心之所聚。\u2014\u2014《管子·君臣上》","聚室而谋日。\u2014\u2014《列子·汤问》","以王命聚之。\u2014\u2014唐·柳宗元《捕蛇者说》","鸣锣聚众。\u2014\u2014《广东军务记》","聚至百有余乡。","又如:聚散浮生(指人生聚散无定。浮生:人活着的时候是虚浮无定的);聚麀之诮(比喻和不三不四的女人鬼混在一起,遭人耻笑。麀:母鹿,泛指母兽);聚寇(聚集起来的盗寇)","积蓄,累积〖accumulate〗","聚菽粟。\u2014\u2014《墨子·尚贤中》","我今将畜积并聚之于仓廪。\u2014\u2014《荀子·王制》","又如:聚沙成塔;聚米(堆积米粒做成模型以说明军事形势,运筹决策;米堆。形容矮小);聚货(聚集货物)","征集〖collect〗","太医以王命聚之。\u2014\u2014柳宗元《捕蛇者说》","使民众聚居〖assemble;dwellingroups〗","大叔完聚。(完;修好城墙。)\u2014\u2014《左传》","","聚宝盆","jùbǎopén","〖treasurebowl〗民间传说中装有金银珠宝而且取之不尽的盆儿,比喻资源丰富的地方","聚变","jùbiàn","〖fusion〗某些轻元素结合时形成较重核并导致大量能量释放的原子核的结合","聚餐","jùcān","〖luncheon〗通常在公共食堂为集体而供应的一种比较正规的便餐(如遇到俱乐部的聚会或业务上的会议)","聚餐","jùcān","〖dinetogether〗聚在一起在公共食堂吃比较正规的便餐","聚赌","jùdǔ","〖operate〗多人聚集在一起赌博","聚光","jùguāng","〖condensation〗∶光的聚焦式准直","〖spotlight〗∶对一个物体的小面积进行照明的强光束","聚光灯","jùguāngdēng","〖spotlight〗类似小探照灯的一种设备,有一个可调反光镜,通常装有白炽灯或弧光灯,用于在室内将一窄束强光射在一个选定的小区域内(如舞台或待照相的对象等)","聚合","jùhé","〖gettogether〗∶分散的聚集到一起","〖polymerize〗∶化学结合〖小分子〗成为大分子,尤其非常大的分子;使受到聚合作用","聚会","jùhuì","〖gettogether〗聚集会合","几个老同学聚会","聚伙","jùhuǒ","〖gatheracrowd〗多人合成一伙","聚伙抢劫","聚积","jùjī","〖buildup〗∶聚蓄;积蓄","聚积革命力量","〖accumulate〗∶累积,经年累月,一点一滴地累积","他由于工作努力和投资适当,聚积了大量财富","聚集","jùjí","〖gather;assemble;collect〗集合","机场上聚集着数千人","聚歼","jùjiān","〖roundupandannihilate〗把敌人包围起来全部消灭","聚焦","jùjiāo","〖focus〗使光线或电子束等集中于一点","聚居","jùjū","〖inhabitaregion〗集中居住","少数民族聚居的地方","聚敛","jùliǎn","〖amasswealthbyheavytaxation〗课重税来搜刮(民财)","季氏富于周公,而求也为之聚敛而附益之。\u2014\u2014《论语·先进》","聚拢","jùlǒng","〖gathertogether〗会聚合拢起来","聚齐","jùqí","〖calltogether〗〖在约定地点〗全体集合","参观的人八时在展览馆对面聚齐","聚散","jùsàn","〖meetingandparting〗会聚与分散","人生聚散无常","聚谈","jùtán","〖gettogetherandtalkover〗聚在一起交谈","好久不得如此聚谈快意了!","聚头","jùtóu","〖gettogether;meet〗聚首;会面;人碰在一起","不是冤家不聚头","聚晤","jùwù","〖meet〗会晤","聚晤一堂,谈笑风生","聚议","jùyì","〖gettogetheranddiscuss〗聚集在一起商议","聚议大事","聚义","jùyì","〖gettogetheranduprise〗旧指为正义事业而聚集在一起","聚义英雄","聚饮","jùyǐn","〖gettogetheranddrink〗聚集在一起喝酒取乐","聚众","jùzhòng","〖mob;gatheracrowd〗聚集群众;把许多人聚集在一起","聚众闹事"] 19 | */ 20 | 21 | private String zi; 22 | private String py; 23 | private String wubi; 24 | private String pinyin; 25 | private String bushou; 26 | private String bihua; 27 | private List jijie; 28 | private List xiangjie; 29 | 30 | public String getZi() { 31 | return zi; 32 | } 33 | 34 | public void setZi(String zi) { 35 | this.zi = zi; 36 | } 37 | 38 | public String getPy() { 39 | return py; 40 | } 41 | 42 | public void setPy(String py) { 43 | this.py = py; 44 | } 45 | 46 | public String getWubi() { 47 | return wubi; 48 | } 49 | 50 | public void setWubi(String wubi) { 51 | this.wubi = wubi; 52 | } 53 | 54 | public String getPinyin() { 55 | return pinyin; 56 | } 57 | 58 | public void setPinyin(String pinyin) { 59 | this.pinyin = pinyin; 60 | } 61 | 62 | public String getBushou() { 63 | return bushou; 64 | } 65 | 66 | public void setBushou(String bushou) { 67 | this.bushou = bushou; 68 | } 69 | 70 | public String getBihua() { 71 | return bihua; 72 | } 73 | 74 | public void setBihua(String bihua) { 75 | this.bihua = bihua; 76 | } 77 | 78 | public List getJijie() { 79 | return jijie; 80 | } 81 | 82 | public void setJijie(List jijie) { 83 | this.jijie = jijie; 84 | } 85 | 86 | public List getXiangjie() { 87 | return xiangjie; 88 | } 89 | 90 | public void setXiangjie(List xiangjie) { 91 | this.xiangjie = xiangjie; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "DictionaryInfo{" + 97 | "zi='" + zi + '\'' + 98 | ", py='" + py + '\'' + 99 | ", wubi='" + wubi + '\'' + 100 | ", pinyin='" + pinyin + '\'' + 101 | ", bushou='" + bushou + '\'' + 102 | ", bihua='" + bihua + '\'' + 103 | ", jijie=" + jijie + 104 | ", xiangjie=" + xiangjie + 105 | '}'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/model/OilPrice.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * 石油价格 7 | * 8 | * @author Jenly 9 | *

10 | * Follow me 11 | */ 12 | data class OilPrice( 13 | var city: String, 14 | @SerializedName("92h") 15 | val oil92: String, 16 | @SerializedName("95h") 17 | val oil95: String, 18 | @SerializedName("98h") 19 | val oil98: String, 20 | @SerializedName("0h") 21 | val oil0: String, 22 | ) { 23 | fun getOil92Str() = "¥ $oil92" 24 | 25 | fun getOil95Str() = "¥ $oil95" 26 | 27 | fun getOil98Str() = "¥ $oil98" 28 | 29 | fun getOil0Str() = "¥ $oil0" 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/model/Result.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * 结果 7 | * 8 | * @author Jenly 9 | *

10 | * Follow me 11 | */ 12 | data class Result( 13 | @SerializedName("error_code") 14 | val code: Int, 15 | @SerializedName("reason") 16 | var message: String? = null, 17 | @SerializedName("result") 18 | var data: T? = null 19 | ) { 20 | /** 21 | * 是否成功 22 | */ 23 | fun isSuccess() = code == 0 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/model/SearchHistory.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Index 5 | import androidx.room.PrimaryKey 6 | 7 | /** 8 | * 搜索历史 9 | * 10 | * @author Jenly 11 | *

12 | * Follow me 13 | */ 14 | @Entity(indices = [Index(value = ["word"], unique = true)]) 15 | data class SearchHistory( 16 | @PrimaryKey(autoGenerate = true) 17 | val id: Long = 0, 18 | val word: String, 19 | val timestamp: Long, 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/repository/CityRepository.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.repository 2 | 3 | import com.king.frame.mvvmframe.data.datasource.DataSource 4 | import com.king.mvvmframe.data.model.City 5 | import com.king.mvvmframe.data.service.CityService 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | /** 10 | * 城市数据仓库 11 | * 12 | * @author Jenly 13 | *

14 | * Follow me 15 | */ 16 | @Singleton 17 | class CityRepository @Inject constructor(private val dataSource: DataSource) { 18 | 19 | private val cityService: CityService by lazy { 20 | dataSource.getRetrofitService(CityService::class.java) 21 | } 22 | 23 | /** 24 | * 获取热门城市列表 25 | */ 26 | suspend fun getHotCities(): List { 27 | return cityService.getHotCities() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/repository/DictionaryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.repository 2 | 3 | import com.king.frame.mvvmframe.data.datasource.DataSource 4 | import com.king.mvvmframe.constant.Constants 5 | import com.king.mvvmframe.data.database.AppDatabase 6 | import com.king.mvvmframe.data.model.DictionaryInfo 7 | import com.king.mvvmframe.data.model.Result 8 | import com.king.mvvmframe.data.model.SearchHistory 9 | import com.king.mvvmframe.data.service.DictionaryService 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.filterNotNull 14 | 15 | /** 16 | * 城市数据仓库 17 | * 18 | * @author Jenly 19 | *

20 | * Follow me 21 | */ 22 | @Singleton 23 | class DictionaryRepository @Inject constructor(private val dataSource: DataSource) { 24 | 25 | private val dictionaryService: DictionaryService by lazy { 26 | dataSource.getRetrofitService(DictionaryService::class.java) 27 | } 28 | 29 | private val searchHistoryDao by lazy { 30 | dataSource.getRoomDatabase(AppDatabase::class.java).searchHistoryDao 31 | } 32 | 33 | /** 34 | * 获取热门城市列表 35 | */ 36 | suspend fun getDictionaryInfo( 37 | word: String, 38 | key: String = Constants.DICTIONARY_KEY 39 | ): Result { 40 | searchHistoryDao.insert( 41 | SearchHistory( 42 | word = word, 43 | timestamp = System.currentTimeMillis() 44 | ) 45 | ) 46 | return dictionaryService.getDictionaryInfo(word, key) 47 | } 48 | 49 | /** 50 | * 获取搜索历史 51 | */ 52 | fun getSearchHistoryFlow(size: Int = 10): Flow> { 53 | return searchHistoryDao.getHistoryFlow(size).filterNotNull() 54 | } 55 | 56 | /** 57 | * 清空搜索历史 58 | */ 59 | suspend fun clearSearchHistory() { 60 | searchHistoryDao.deleteAll() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/repository/OilPriceRepository.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.repository 2 | 3 | import com.king.frame.mvvmframe.data.datasource.DataSource 4 | import com.king.mvvmframe.constant.Constants 5 | import com.king.mvvmframe.data.model.OilPrice 6 | import com.king.mvvmframe.data.model.Result 7 | import com.king.mvvmframe.data.service.OilPriceService 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * 油价价格数据仓库 13 | * 14 | * @author Jenly 15 | *

16 | * Follow me 17 | */ 18 | @Singleton 19 | class OilPriceRepository @Inject constructor(private val dataSource: DataSource) { 20 | 21 | private val oilPriceService: OilPriceService by lazy { 22 | dataSource.getRetrofitService(OilPriceService::class.java) 23 | } 24 | 25 | /** 26 | * 获取查询国内油价 27 | */ 28 | suspend fun getOilPriceInfo(key: String = Constants.OIL_PRICE_KEY): Result> { 29 | return oilPriceService.getOilPriceInfo(key) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/service/CityService.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.service 2 | 3 | import com.king.mvvmframe.constant.Constants 4 | import com.king.mvvmframe.data.model.City 5 | import com.king.retrofit.retrofithelper.annotation.DomainName 6 | import com.king.retrofit.retrofithelper.annotation.Timeout 7 | import retrofit2.http.GET 8 | 9 | /** 10 | * 城市API 11 | * 12 | * @author Jenly 13 | *

14 | * Follow me 15 | */ 16 | interface CityService { 17 | 18 | /** 19 | * 动态改变 BaseUrl 示例 20 | * @return 21 | */ 22 | @DomainName(Constants.DOMAIN_JENLY) 23 | @Timeout(connectTimeout = 10, readTimeout = 15, writeTimeout = 10) 24 | @GET("api/city/hotCities.json") 25 | suspend fun getHotCities(): List 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/service/DictionaryService.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.service 2 | 3 | import com.king.mvvmframe.constant.Constants 4 | import com.king.mvvmframe.data.model.DictionaryInfo 5 | import com.king.mvvmframe.data.model.Result 6 | import com.king.retrofit.retrofithelper.annotation.DomainName 7 | import retrofit2.http.GET 8 | import retrofit2.http.Query 9 | 10 | /** 11 | * 字典API 12 | * 13 | * @author Jenly 14 | *

15 | * Follow me 16 | */ 17 | interface DictionaryService { 18 | 19 | /** 20 | * 获取字典信息- 新华字典 21 | * @param key 22 | * @return 23 | */ 24 | @DomainName(Constants.DOMAIN_DICTIONARY) 25 | @GET("xhzd/query") 26 | suspend fun getDictionaryInfo( 27 | @Query("word") word: String, 28 | @Query("key") key: String, 29 | ): Result 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/data/service/OilPriceService.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.data.service 2 | 3 | import com.king.mvvmframe.data.model.OilPrice 4 | import com.king.mvvmframe.data.model.Result 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | /** 9 | * 石油价格API 10 | * 11 | * @author Jenly 12 | *

13 | * Follow me 14 | */ 15 | interface OilPriceService { 16 | 17 | /** 18 | * 查询国内油价 19 | */ 20 | @GET("gnyj/query") 21 | suspend fun getOilPriceInfo(@Query("key") key: String): Result> 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/king/mvvmframe/util/RandomUtil.kt: -------------------------------------------------------------------------------- 1 | package com.king.mvvmframe.util 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.IntRange 6 | import java.util.Random 7 | 8 | /** 9 | * @author Jenly 10 | *

11 | * Follow me 12 | */ 13 | object RandomUtil { 14 | 15 | private val random by lazy { 16 | Random() 17 | } 18 | 19 | @ColorInt 20 | fun randomColor( 21 | @IntRange(from = 0, to = 255) min: Int, 22 | @IntRange(from = 0, to = 255) max: Int 23 | ): Int { 24 | return Color.rgb(random(min, max), random(min, max), random(min, max)) 25 | } 26 | 27 | fun random(min: Int, max: Int): Int { 28 | return random.nextInt(max - min + 1) + min 29 | } 30 | } -------------------------------------------------------------------------------- /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/res/drawable-xxhdpi/btn_delete_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenly1314/MVVMFrame/e3d343fa4adf1fc5b073f9e36c5e5942be8352b5/app/src/main/res/drawable-xxhdpi/btn_delete_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/btn_delete_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenly1314/MVVMFrame/e3d343fa4adf1fc5b073f9e36c5e5942be8352b5/app/src/main/res/drawable-xxhdpi/btn_delete_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_delete_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dialog_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_divider_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tag_bg_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tag_bg_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tag_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/city_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dictionary_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 16 | 28 | 38 | 39 | 46 | 50 | 61 | 65 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 |