├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── app ├── build.gradle ├── libs │ ├── materialiconlib-1.1.5.aar │ └── videocache-2.7.1.aar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kunminx │ │ └── puremusic │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── bensound-sunny.mp3 │ │ └── summary.html │ ├── java │ │ └── com │ │ │ └── kunminx │ │ │ └── puremusic │ │ │ ├── MainActivity.java │ │ │ ├── data │ │ │ ├── api │ │ │ │ ├── APIs.java │ │ │ │ └── AccountService.java │ │ │ ├── bean │ │ │ │ ├── DownloadState.java │ │ │ │ ├── LibraryInfo.java │ │ │ │ ├── TestAlbum.java │ │ │ │ └── User.java │ │ │ ├── config │ │ │ │ ├── Configs.java │ │ │ │ └── Const.java │ │ │ └── repository │ │ │ │ └── DataRepository.java │ │ │ ├── domain │ │ │ ├── event │ │ │ │ ├── DownloadEvent.java │ │ │ │ └── Messages.java │ │ │ ├── message │ │ │ │ ├── DrawerCoordinateManager.java │ │ │ │ ├── PageMessenger.java │ │ │ │ ├── PlayerReceiver.java │ │ │ │ └── SharedViewModel.java │ │ │ ├── proxy │ │ │ │ └── PlayerManager.java │ │ │ ├── request │ │ │ │ ├── AccountRequester.java │ │ │ │ ├── DownloadRequester.java │ │ │ │ ├── InfoRequester.java │ │ │ │ └── MusicRequester.java │ │ │ └── usecase │ │ │ │ ├── CanBeStoppedUseCase.java │ │ │ │ └── DownloadUseCase.java │ │ │ └── ui │ │ │ ├── bind │ │ │ ├── CommonBindingAdapter.java │ │ │ ├── DrawerBindingAdapter.java │ │ │ ├── IconBindingAdapter.java │ │ │ ├── TabPageBindingAdapter.java │ │ │ └── WebViewBindingAdapter.java │ │ │ ├── page │ │ │ ├── DrawerFragment.java │ │ │ ├── LoginFragment.java │ │ │ ├── MainFragment.java │ │ │ ├── PlayerFragment.java │ │ │ ├── SearchFragment.java │ │ │ ├── adapter │ │ │ │ ├── DiffUtils.java │ │ │ │ ├── DrawerAdapter.java │ │ │ │ └── PlaylistAdapter.java │ │ │ └── helper │ │ │ │ └── DefaultInterface.java │ │ │ ├── view │ │ │ ├── PlayPauseDrawable.java │ │ │ ├── PlayPauseView.java │ │ │ └── PlayerSlideListener.java │ │ │ └── widget │ │ │ └── PlayerService.java │ └── res │ │ ├── anim │ │ ├── h_fragment_enter.xml │ │ ├── h_fragment_exit.xml │ │ ├── h_fragment_pop_enter.xml │ │ └── h_fragment_pop_exit.xml │ │ ├── drawable-xxhdpi │ │ ├── bg_album_default.png │ │ ├── ic_action_pause.png │ │ ├── ic_action_play.png │ │ ├── ic_close_white.png │ │ ├── ic_launcher.png │ │ ├── ic_next_dark.png │ │ ├── ic_player.png │ │ ├── ic_previous_dark.png │ │ └── ic_progress.png │ │ ├── drawable │ │ ├── bar_selector_white.xml │ │ ├── bg_home.png │ │ ├── ic_menu_black_48dp.xml │ │ ├── ic_music_note_black_48dp.xml │ │ ├── ic_search_black_48dp.xml │ │ ├── loading_animation.xml │ │ └── progressbar_color.xml │ │ ├── layout-land │ │ ├── activity_main.xml │ │ ├── fragment_main.xml │ │ └── fragment_player.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── adapter_library.xml │ │ ├── adapter_play_item.xml │ │ ├── fragment_drawer.xml │ │ ├── fragment_login.xml │ │ ├── fragment_main.xml │ │ ├── fragment_player.xml │ │ ├── fragment_search.xml │ │ ├── notify_player_big.xml │ │ └── notify_player_small.xml │ │ ├── navigation │ │ ├── nav_drawer.xml │ │ ├── nav_main.xml │ │ └── nav_slide.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── kunminx │ └── puremusic │ └── ExampleUnitTest.java ├── architecture ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kunminx │ │ └── architecture │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── kunminx │ │ │ └── architecture │ │ │ ├── data │ │ │ └── response │ │ │ │ ├── DataResult.java │ │ │ │ ├── ResponseStatus.java │ │ │ │ ├── ResultSource.java │ │ │ │ └── manager │ │ │ │ ├── NetworkStateManager.java │ │ │ │ └── NetworkStateReceive.java │ │ │ ├── domain │ │ │ ├── request │ │ │ │ ├── AsyncTask.java │ │ │ │ └── Requester.java │ │ │ └── usecase │ │ │ │ ├── UseCase.java │ │ │ │ ├── UseCaseHandler.java │ │ │ │ ├── UseCaseScheduler.java │ │ │ │ └── UseCaseThreadPoolScheduler.java │ │ │ ├── ui │ │ │ ├── adapter │ │ │ │ └── CommonViewPagerAdapter.java │ │ │ ├── bind │ │ │ │ └── DrawablesBindingAdapter.java │ │ │ └── page │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BaseFragment.java │ │ │ │ └── StateHolder.java │ │ │ └── utils │ │ │ ├── AdaptScreenUtils.java │ │ │ ├── BarUtils.java │ │ │ ├── ClickUtils.java │ │ │ ├── DisplayUtils.java │ │ │ ├── ImageUtils.java │ │ │ ├── NetworkUtils.java │ │ │ ├── Res.java │ │ │ ├── ScreenUtils.java │ │ │ ├── ToastUtils.java │ │ │ └── Utils.java │ └── res │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── file_paths.xml │ └── test │ └── java │ └── com │ └── kunminx │ └── architecture │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | ij_java_use_single_class_imports = true 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.yml] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | target-branch: master -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | os: [ubuntu-18.04, macOS-latest, windows-2016] 12 | java: [11, 11.0.3] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: set up JDK 11 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: 'zulu' 19 | java-version: ${{ matrix.java }} 20 | - name: Make gradlew executable 21 | run: chmod +x ./gradlew 22 | - name: Build 23 | run: ./gradlew --parallel app:assembleRelease 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .DS_Store 3 | .externalNativeBuild 4 | .project 5 | .gradle 6 | .mtj.tmp 7 | .vscode 8 | .settings 9 | .cxx 10 | 11 | /.idea 12 | 13 | local.properties 14 | maven-repository 15 | mvn-clone 16 | build 17 | captures 18 | gen 19 | out 20 | target 21 | 22 | *.class 23 | *.txt 24 | *.ear 25 | *.iml 26 | *.jar 27 | *.keystore 28 | *.log 29 | *.nar 30 | *.rar 31 | *.tar.gz 32 | *.war 33 | *.zip 34 | *.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | android { 4 | namespace "com.kunminx.puremusic" 5 | compileSdk appTargetSdk 6 | defaultConfig { 7 | applicationId "com.kunminx.puremusic" 8 | minSdk appMinSdk 9 | targetSdk appTargetSdk 10 | versionCode appVersionCode 11 | versionName appVersionName 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | debug { 16 | applicationIdSuffix ".debug" 17 | manifestPlaceholders = [ 18 | APP_NAME: "@string/app_name_debug", 19 | ] 20 | } 21 | release { 22 | manifestPlaceholders = [ 23 | APP_NAME: "@string/app_name", 24 | ] 25 | minifyEnabled true 26 | shrinkResources true 27 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 28 | } 29 | } 30 | 31 | lintOptions { 32 | checkReleaseBuilds false 33 | abortOnError false 34 | } 35 | 36 | buildFeatures { 37 | dataBinding true 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) 43 | implementation project(":architecture") 44 | 45 | testImplementation "junit:junit:4.13.2" 46 | androidTestImplementation "androidx.test.ext:junit:1.1.5" 47 | androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" 48 | 49 | implementation "org.slf4j:slf4j-android:1.7.36" 50 | implementation "com.sothree.slidinguppanel:library:3.4.0" 51 | implementation 'com.github.KunMinX:Jetpack-MusicPlayer:5.2.0' 52 | implementation 'com.github.KunMinX.KeyValueX:keyvalue:3.7.0-beta' 53 | annotationProcessor 'com.github.KunMinX.KeyValueX:keyvalue-compiler:3.7.0-beta' 54 | } 55 | -------------------------------------------------------------------------------- /app/libs/materialiconlib-1.1.5.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/libs/materialiconlib-1.1.5.aar -------------------------------------------------------------------------------- /app/libs/videocache-2.7.1.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/libs/videocache-2.7.1.aar -------------------------------------------------------------------------------- /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 | 23 | -keep class com.kunminx.puremusic.data.bean.** { *; } 24 | -keep class com.kunminx.puremusic.data.config.* 25 | -keep interface com.kunminx.puremusic.data.config.* 26 | -keep class com.kunminx.player.bean.** { *; } 27 | 28 | -keep class * implements android.os.Parcelable { 29 | public static final android.os.Parcelable$Creator *; 30 | } 31 | 32 | -keepnames class * implements java.io.Serializable 33 | 34 | -keepclassmembers class * implements java.io.Serializable { 35 | static final long serialVersionUID; 36 | private static final java.io.ObjectStreamField[] serialPersistentFields; 37 | !static !transient <fields>; 38 | !private <fields>; 39 | !private <methods>; 40 | private void writeObject(java.io.ObjectOutputStream); 41 | private void readObject(java.io.ObjectInputStream); 42 | java.lang.Object writeReplace(); 43 | java.lang.Object readResolve(); 44 | } 45 | 46 | # webview 47 | -keepclassmembers class fqcn.of.javascript.interface.for.Webview { 48 | public *; 49 | } 50 | -keepclassmembers class * extends android.webkit.WebViewClient { 51 | public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); 52 | public boolean *(android.webkit.WebView, java.lang.String); 53 | } 54 | -keepclassmembers class * extends android.webkit.WebViewClient { 55 | public void *(android.webkit.WebView, jav.lang.String); 56 | } 57 | 58 | 59 | # AndroidX 60 | 61 | -keep class com.google.android.material.** {*;} 62 | -keep class androidx.** {*;} 63 | -keep public class * extends androidx.** 64 | -keep interface androidx.** {*;} 65 | -dontwarn com.google.android.material.** 66 | -dontnote com.google.android.material.** 67 | -dontwarn androidx.** 68 | 69 | # OkHttp 70 | 71 | -dontwarn okhttp3.** 72 | -keep class okhttp3.**{*;} 73 | -dontwarn okio.** 74 | -keep class okio.**{*;} 75 | 76 | # glide 77 | -keep public class * implements com.bumptech.glide.module.GlideModule 78 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 79 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 80 | **[] $VALUES; 81 | public *; 82 | } 83 | 84 | # RxJava 85 | 86 | -keep class rx.schedulers.Schedulers { 87 | public static <methods>; 88 | } 89 | -keep class rx.schedulers.ImmediateScheduler { 90 | public <methods>; 91 | } 92 | -keep class rx.schedulers.TestScheduler { 93 | public <methods>; 94 | } 95 | -keep class rx.schedulers.Schedulers { 96 | public static ** test(); 97 | } 98 | -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { 99 | long producerIndex; 100 | long consumerIndex; 101 | } 102 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { 103 | long producerNode; 104 | long consumerNode; 105 | } 106 | -dontwarn sun.misc.Unsafe 107 | 108 | 109 | # Gson 110 | -keepattributes Signature 111 | -keepattributes *Annotation* 112 | -keep class sun.misc.Unsafe { *; } 113 | -keep class * implements com.google.gson.TypeAdapterFactory 114 | -keep class * implements com.google.gson.JsonSerializer 115 | -keep class * implements com.google.gson.JsonDeserializer 116 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/kunminx/puremusic/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import android.content.Context; 6 | 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | import androidx.test.platform.app.InstrumentationRegistry; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.kunminx.puremusic", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools"> 4 | 5 | <uses-permission android:name="android.permission.INTERNET" /> 6 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 7 | <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> 8 | 9 | <application 10 | android:allowBackup="true" 11 | android:icon="@drawable/ic_launcher" 12 | android:label="${APP_NAME}" 13 | android:networkSecurityConfig="@xml/network_security_config" 14 | android:roundIcon="@drawable/ic_launcher" 15 | android:supportsRtl="true" 16 | android:theme="@style/AppTheme" 17 | android:usesCleartextTraffic="true" 18 | tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> 19 | 20 | <activity 21 | android:name=".MainActivity" 22 | android:exported="true"> 23 | <intent-filter> 24 | <action android:name="android.intent.action.MAIN" /> 25 | 26 | <category android:name="android.intent.category.LAUNCHER" /> 27 | </intent-filter> 28 | </activity> 29 | 30 | <service android:name=".ui.widget.PlayerService" /> 31 | 32 | <receiver 33 | android:name=".domain.message.PlayerReceiver" 34 | android:exported="false" 35 | tools:ignore="ExportedReceiver"> 36 | <intent-filter> 37 | <action android:name="pure_music.kunminx.close" /> 38 | <action android:name="pure_music.kunminx.pause" /> 39 | <action android:name="pure_music.kunminx.next" /> 40 | <action android:name="pure_music.kunminx.play" /> 41 | <action android:name="pure_music.kunminx.previous" /> 42 | <action android:name="android.intent.action.MEDIA_BUTTON" /> 43 | <action android:name="android.media.AUDIO_BECOMING_NOISY" /> 44 | </intent-filter> 45 | </receiver> 46 | 47 | </application> 48 | 49 | </manifest> 50 | -------------------------------------------------------------------------------- /app/src/main/assets/bensound-sunny.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/assets/bensound-sunny.mp3 -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/api/APIs.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.data.api; 2 | 3 | /** 4 | * Create by KunMinX at 2021/6/3 5 | */ 6 | public class APIs { 7 | public final static String BASE_URL = "https://test.com/"; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/api/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.data.api; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.http.Field; 5 | import retrofit2.http.FormUrlEncoded; 6 | import retrofit2.http.POST; 7 | 8 | /** 9 | * Create by KunMinX at 2021/6/3 10 | */ 11 | public interface AccountService { 12 | 13 | @POST("xxx/login") 14 | @FormUrlEncoded 15 | Call<String> login( 16 | @Field("username") String username, 17 | @Field("password") String password 18 | ); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/bean/DownloadState.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.data.bean; 2 | 3 | /** 4 | * Create by KunMinX at 2022/7/15 5 | * <p> 6 | * bean,原始数据,只读, 7 | * Java 我们通过移除 setter 8 | * kotlin 直接将字段设为 val 即可 9 | */ 10 | public class DownloadState { 11 | public final boolean isForgive; 12 | public final int progress; 13 | 14 | public DownloadState() { 15 | this.isForgive = false; 16 | this.progress = 0; 17 | } 18 | 19 | public DownloadState(boolean isForgive, int progress) { 20 | this.isForgive = isForgive; 21 | this.progress = progress; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/bean/LibraryInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.data.bean; 18 | 19 | /** 20 | * Create by KunMinX at 19/11/2 21 | * <p> 22 | * bean,原始数据,只读, 23 | * Java 我们通过移除 setter 24 | * kotlin 直接将字段设为 val 即可 25 | */ 26 | public class LibraryInfo { 27 | private final String title; 28 | private final String summary; 29 | private final String url; 30 | 31 | public LibraryInfo(String title, String summary, String url) { 32 | this.title = title; 33 | this.summary = summary; 34 | this.url = url; 35 | } 36 | 37 | public String getTitle() { 38 | return title; 39 | } 40 | 41 | public String getSummary() { 42 | return summary; 43 | } 44 | 45 | public String getUrl() { 46 | return url; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/bean/TestAlbum.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.data.bean; 18 | 19 | import com.kunminx.player.bean.base.BaseAlbumItem; 20 | import com.kunminx.player.bean.base.BaseArtistItem; 21 | import com.kunminx.player.bean.base.BaseMusicItem; 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * Create by KunMinX at 19/10/31 27 | * <p> 28 | * bean,原始数据,只读 29 | * Java 我们通过移除 setter 30 | * kotlin 直接将字段设为 val 即可 31 | */ 32 | public class TestAlbum extends BaseAlbumItem<TestAlbum.TestMusic, TestAlbum.TestArtist> { 33 | 34 | private String albumMid; 35 | public TestAlbum(String albumId, String title, String summary, TestArtist artist, String coverImg, List<TestMusic> musics) { 36 | super(albumId, title, summary, artist, coverImg, musics); 37 | } 38 | 39 | public String getAlbumMid() { 40 | return albumMid; 41 | } 42 | 43 | public static class TestMusic extends BaseMusicItem<TestArtist> { 44 | 45 | private String songMid; 46 | public TestMusic(String musicId, String coverImg, String url, String title, TestArtist artist) { 47 | super(musicId, coverImg, url, title, artist); 48 | } 49 | 50 | public String getSongMid() { 51 | return songMid; 52 | } 53 | } 54 | 55 | public static class TestArtist extends BaseArtistItem { 56 | 57 | private String birthday; 58 | public TestArtist(String name) { 59 | super(name); 60 | } 61 | 62 | public String getBirthday() { 63 | return birthday; 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/bean/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.data.bean; 18 | 19 | /** 20 | * Create by KunMinX at 20/04/26 21 | * <p> 22 | * bean,原始数据,只读 23 | * Java 我们通过移除 setter 24 | * kotlin 直接将字段设为 val 即可 25 | */ 26 | public class User { 27 | private final String name; 28 | private final String password; 29 | 30 | public User(String name, String password) { 31 | this.name = name; 32 | this.password = password; 33 | } 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public String getPassword() { 40 | return password; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/config/Configs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.data.config; 18 | 19 | import com.kunminx.architecture.data.config.keyvalue.KeyValueBoolean; 20 | import com.kunminx.architecture.data.config.keyvalue.KeyValueInteger; 21 | import com.kunminx.architecture.data.config.keyvalue.KeyValueSerializable; 22 | import com.kunminx.architecture.data.config.keyvalue.KeyValueString; 23 | import com.kunminx.keyvalue.annotation.KeyValueX; 24 | import com.kunminx.puremusic.data.bean.User; 25 | 26 | /** 27 | * TODO tip 1:消除 Android 项目 KeyValue 样板代码,让 key、value、get、put、init 缩减为一,不再 KV 爆炸。 28 | * 如这么说无体会,详见 https://juejin.cn/post/7121955840319291428 29 | * <p> 30 | * Create by KunMinX at 18/9/28 31 | */ 32 | @KeyValueX 33 | public interface Configs { 34 | KeyValueString token(); 35 | KeyValueBoolean isLogin(); 36 | KeyValueInteger alive(); 37 | KeyValueSerializable<User> user(); 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/config/Const.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.data.config; 2 | 3 | import android.os.Environment; 4 | 5 | import com.kunminx.architecture.utils.Utils; 6 | import com.kunminx.puremusic.R; 7 | /** 8 | * Create by KunMinX at 2022/8/18 9 | */ 10 | public class Const { 11 | public static final String COVER_PATH = Utils.getApp().getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); 12 | public static final String COLUMN_LINK = Utils.getApp().getString(R.string.article_navigation); 13 | public static final String PROJECT_LINK = Utils.getApp().getString(R.string.github_project); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/data/repository/DataRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.data.repository; 18 | 19 | import android.annotation.SuppressLint; 20 | 21 | import com.google.gson.Gson; 22 | import com.google.gson.reflect.TypeToken; 23 | import com.kunminx.architecture.data.response.DataResult; 24 | import com.kunminx.architecture.data.response.ResponseStatus; 25 | import com.kunminx.architecture.data.response.ResultSource; 26 | import com.kunminx.architecture.domain.request.AsyncTask; 27 | import com.kunminx.architecture.utils.Utils; 28 | import com.kunminx.puremusic.R; 29 | import com.kunminx.puremusic.data.api.APIs; 30 | import com.kunminx.puremusic.data.api.AccountService; 31 | import com.kunminx.puremusic.data.bean.LibraryInfo; 32 | import com.kunminx.puremusic.data.bean.TestAlbum; 33 | import com.kunminx.puremusic.data.bean.User; 34 | 35 | import java.io.ByteArrayInputStream; 36 | import java.io.ByteArrayOutputStream; 37 | import java.io.IOException; 38 | import java.lang.reflect.Type; 39 | import java.util.List; 40 | import java.util.concurrent.TimeUnit; 41 | 42 | import io.reactivex.Observable; 43 | import okhttp3.OkHttpClient; 44 | import okhttp3.logging.HttpLoggingInterceptor; 45 | import retrofit2.Call; 46 | import retrofit2.Response; 47 | import retrofit2.Retrofit; 48 | import retrofit2.converter.gson.GsonConverterFactory; 49 | 50 | /** 51 | * Create by KunMinX at 19/10/29 52 | */ 53 | public class DataRepository { 54 | 55 | private static final DataRepository S_REQUEST_MANAGER = new DataRepository(); 56 | 57 | private DataRepository() { 58 | } 59 | 60 | public static DataRepository getInstance() { 61 | return S_REQUEST_MANAGER; 62 | } 63 | 64 | private final Retrofit retrofit; 65 | 66 | { 67 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); 68 | logging.setLevel(HttpLoggingInterceptor.Level.BODY); 69 | OkHttpClient client = new OkHttpClient.Builder() 70 | .connectTimeout(8, TimeUnit.SECONDS) 71 | .readTimeout(8, TimeUnit.SECONDS) 72 | .writeTimeout(8, TimeUnit.SECONDS) 73 | .addInterceptor(logging) 74 | .build(); 75 | retrofit = new Retrofit.Builder() 76 | .baseUrl(APIs.BASE_URL) 77 | .client(client) 78 | .addConverterFactory(GsonConverterFactory.create()) 79 | .build(); 80 | } 81 | 82 | //TODO tip: 通过 "响应式框架" 往领域层回推数据, 83 | // 与此相对应,kotlin 下使用 flow{ ... emit(...) }.flowOn(Dispatchers.xx) 84 | 85 | public Observable<DataResult<TestAlbum>> getFreeMusic() { 86 | return AsyncTask.doIO(emitter -> { 87 | Gson gson = new Gson(); 88 | Type type = new TypeToken<TestAlbum>() { 89 | }.getType(); 90 | TestAlbum testAlbum = gson.fromJson(Utils.getApp().getString(R.string.free_music_json), type); 91 | emitter.onNext(new DataResult<>(testAlbum, new ResponseStatus())); 92 | }); 93 | } 94 | 95 | public Observable<DataResult<List<LibraryInfo>>> getLibraryInfo() { 96 | return AsyncTask.doIO(emitter -> { 97 | Gson gson = new Gson(); 98 | Type type = new TypeToken<List<LibraryInfo>>() { 99 | }.getType(); 100 | List<LibraryInfo> list = gson.fromJson(Utils.getApp().getString(R.string.library_json), type); 101 | emitter.onNext(new DataResult<>(list, new ResponseStatus())); 102 | }); 103 | } 104 | 105 | /** 106 | * TODO:模拟下载任务: 107 | */ 108 | @SuppressLint("CheckResult") 109 | public Observable<Integer> downloadFile() { 110 | return AsyncTask.doIO(emitter -> { 111 | //在内存中模拟 "数据读写",假装是在 "文件 IO", 112 | 113 | byte[] bytes = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; 114 | try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); 115 | ByteArrayOutputStream bos = new ByteArrayOutputStream()) { 116 | int b; 117 | while ((b = bis.read()) != -1) { 118 | Thread.sleep(500); 119 | emitter.onNext(b); 120 | } 121 | } catch (IOException | InterruptedException e) { 122 | e.printStackTrace(); 123 | } 124 | }); 125 | } 126 | 127 | /** 128 | * TODO 模拟登录的网络请求 129 | * 130 | * @param user ui 层填写的用户信息 131 | */ 132 | public Observable<DataResult<String>> login(User user) { 133 | 134 | // 使用 retrofit 或任意你喜欢的库实现网络请求。此处以 retrofit 写个简单例子, 135 | // 并且如使用 rxjava,还可额外依赖 RxJavaCallAdapterFactory 来简化编写,具体自行网上查阅,此处不做累述, 136 | 137 | return AsyncTask.doIO(emitter -> { 138 | Call<String> call = retrofit.create(AccountService.class).login(user.getName(), user.getPassword()); 139 | Response<String> response; 140 | try { 141 | response = call.execute(); 142 | ResponseStatus responseStatus = new ResponseStatus( 143 | String.valueOf(response.code()), response.isSuccessful(), ResultSource.NETWORK); 144 | emitter.onNext(new DataResult<>(response.body(), responseStatus)); 145 | } catch (IOException e) { 146 | emitter.onNext(new DataResult<>(null, 147 | new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK))); 148 | } 149 | }); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/event/DownloadEvent.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.domain.event; 2 | 3 | import com.kunminx.puremusic.data.bean.DownloadState; 4 | 5 | /** 6 | * Create by KunMinX at 2022/7/4 7 | */ 8 | public class DownloadEvent { 9 | public final static int EVENT_DOWNLOAD = 1; 10 | public final static int EVENT_DOWNLOAD_GLOBAL = 2; 11 | 12 | public final int eventId; 13 | public final DownloadState downloadState; 14 | 15 | public DownloadEvent(int eventId) { 16 | this.eventId = eventId; 17 | this.downloadState = new DownloadState(); 18 | } 19 | 20 | public DownloadEvent(int eventId, DownloadState downloadState) { 21 | this.eventId = eventId; 22 | this.downloadState = downloadState; 23 | } 24 | 25 | public DownloadEvent copy(DownloadState downloadState) { 26 | return new DownloadEvent(this.eventId, downloadState); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/event/Messages.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.domain.event; 2 | 3 | /** 4 | * Create by KunMinX at 2022/7/4 5 | */ 6 | public class Messages { 7 | public final static int EVENT_CLOSE_SLIDE_PANEL_IF_EXPANDED = 1; 8 | public final static int EVENT_CLOSE_ACTIVITY_IF_ALLOWED = 2; 9 | public final static int EVENT_OPEN_DRAWER = 3; 10 | public final static int EVENT_ADD_SLIDE_LISTENER = 4; 11 | public final static int EVENT_LOGIN_SUCCESS = 5; 12 | 13 | public final int eventId; 14 | 15 | public Messages(int eventId) { 16 | this.eventId = eventId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/message/DrawerCoordinateManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright 2018-present KunMinX 4 | * * 5 | * * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * * you may not use this file except in compliance with the License. 7 | * * You may obtain a copy of the License at 8 | * * 9 | * * http://www.apache.org/licenses/LICENSE-2.0 10 | * * 11 | * * Unless required by applicable law or agreed to in writing, software 12 | * * distributed under the License is distributed on an "AS IS" BASIS, 13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * * See the License for the specific language governing permissions and 15 | * * limitations under the License. 16 | * 17 | */ 18 | 19 | package com.kunminx.puremusic.domain.message; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.lifecycle.DefaultLifecycleObserver; 23 | import androidx.lifecycle.LifecycleOwner; 24 | 25 | import com.kunminx.architecture.domain.message.MutableResult; 26 | import com.kunminx.architecture.domain.message.Result; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | /** 32 | * TODO tip 1:通过 Lifecycle 来实现 "抽屉侧滑禁用与否的判断" 的一致, 33 | * <p> 34 | * 每个 "需要注册和监听生命周期来判断" 的视图控制器,无需在各自内部手动书写解绑等操作。 35 | * 如这么说无体会,详见《为你还原一个真实的 Jetpack Lifecycle》 36 | * https://xiaozhuanlan.com/topic/3684721950 37 | * <p> 38 | * TODO tip 2:与此同时,作为用于 "跨页面通信" 单例,本类也承担 "可信源" 职责, 39 | * 所有对 Drawer 状态协调相关的请求都交由本单例处理,并统一分发给所有订阅者页面。 40 | * <p> 41 | * 如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。 42 | * https://xiaozhuanlan.com/topic/6017825943 43 | * <p> 44 | * <p> 45 | * Create by KunMinX at 19/11/3 46 | */ 47 | public class DrawerCoordinateManager implements DefaultLifecycleObserver { 48 | 49 | private static final DrawerCoordinateManager S_HELPER = new DrawerCoordinateManager(); 50 | 51 | private DrawerCoordinateManager() { 52 | } 53 | 54 | public static DrawerCoordinateManager getInstance() { 55 | return S_HELPER; 56 | } 57 | 58 | private final List<String> tagOfSecondaryPages = new ArrayList<>(); 59 | 60 | private boolean isNoneSecondaryPage() { 61 | return tagOfSecondaryPages.size() == 0; 62 | } 63 | 64 | private final MutableResult<Boolean> enableSwipeDrawer = new MutableResult<>(); 65 | 66 | public Result<Boolean> isEnableSwipeDrawer() { 67 | return enableSwipeDrawer; 68 | } 69 | 70 | public void requestToUpdateDrawerMode(boolean pageOpened, String pageName) { 71 | if (pageOpened) { 72 | tagOfSecondaryPages.add(pageName); 73 | } else { 74 | tagOfSecondaryPages.remove(pageName); 75 | } 76 | enableSwipeDrawer.setValue(isNoneSecondaryPage()); 77 | } 78 | 79 | //TODO tip 3:让 NetworkStateManager 可观察页面生命周期, 80 | // 从而在进入或离开目标页面时,自动在此登记和处理抽屉的禁用和解禁,避免一系列不可预期问题。 81 | 82 | // 关于 Lifecycle 组件的存在意义,可详见《为你还原一个真实的 Jetpack Lifecycle》解析 83 | // https://xiaozhuanlan.com/topic/3684721950 84 | 85 | @Override 86 | public void onCreate(@NonNull LifecycleOwner owner) { 87 | 88 | tagOfSecondaryPages.add(owner.getClass().getSimpleName()); 89 | 90 | enableSwipeDrawer.setValue(isNoneSecondaryPage()); 91 | 92 | } 93 | 94 | @Override 95 | public void onDestroy(@NonNull LifecycleOwner owner) { 96 | 97 | tagOfSecondaryPages.remove(owner.getClass().getSimpleName()); 98 | 99 | enableSwipeDrawer.setValue(isNoneSecondaryPage()); 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/message/PageMessenger.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.domain.message; 2 | 3 | import com.kunminx.architecture.domain.dispatch.MviDispatcher; 4 | import com.kunminx.puremusic.domain.event.Messages; 5 | 6 | /** 7 | * TODO:Note 2022.07.04 8 | * ` 9 | * PageMessenger 是一个领域层组件,可用于 "跨页面通信" 场景, 10 | * 比如跳转到 login 页面完成登录后,login 页面反过来通知其他页面刷新状态, 11 | * <p> 12 | * PageMessenger 基于 MVI-Dispatcher 实现可靠的消息回推, 13 | * 通过消息队列、引用计数等设计,确保 "消息都能被消费,且只消费一次", 14 | * 通过内聚设计,彻底杜绝 mutable 滥用等问题, 15 | * <p> 16 | * 鉴于本项目场景难发挥 MVI-Dispatcher 潜能,故目前仅以改造 DownloadRequester 和 SharedViewModel 为例, 17 | * 通过对比 SharedViewModel 和 PageMessenger 易得,后者可简洁优雅实现可靠一致的消息分发, 18 | * <p> 19 | * <p> 20 | * 具体可参见专为 MVI-Dispatcher 编写的领域层案例: 21 | * <p> 22 | * https://github.com/KunMinX/MVI-Dispatcher 23 | * <p> 24 | * Create by KunMinX at 2022/7/4 25 | */ 26 | public class PageMessenger extends MviDispatcher<Messages> { 27 | @Override 28 | protected void onHandle(Messages event) { 29 | sendResult(event); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/message/PlayerReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.message; 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.view.KeyEvent; 23 | 24 | import com.kunminx.puremusic.domain.proxy.PlayerManager; 25 | import com.kunminx.puremusic.ui.widget.PlayerService; 26 | 27 | import java.util.Objects; 28 | 29 | public class PlayerReceiver extends BroadcastReceiver { 30 | 31 | @Override 32 | public void onReceive(Context context, Intent intent) { 33 | 34 | if (Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)) { 35 | if (intent.getExtras() == null) { 36 | return; 37 | } 38 | KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); 39 | if (keyEvent == null) { 40 | return; 41 | } 42 | if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) { 43 | return; 44 | } 45 | 46 | switch (keyEvent.getKeyCode()) { 47 | case KeyEvent.KEYCODE_HEADSETHOOK: 48 | case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 49 | PlayerManager.getInstance().togglePlay(); 50 | break; 51 | case KeyEvent.KEYCODE_MEDIA_PLAY: 52 | PlayerManager.getInstance().playAudio(); 53 | break; 54 | case KeyEvent.KEYCODE_MEDIA_PAUSE: 55 | PlayerManager.getInstance().pauseAudio(); 56 | break; 57 | case KeyEvent.KEYCODE_MEDIA_STOP: 58 | PlayerManager.getInstance().clear(); 59 | break; 60 | case KeyEvent.KEYCODE_MEDIA_NEXT: 61 | PlayerManager.getInstance().playNext(); 62 | break; 63 | case KeyEvent.KEYCODE_MEDIA_PREVIOUS: 64 | PlayerManager.getInstance().playPrevious(); 65 | break; 66 | default: 67 | } 68 | 69 | } else { 70 | 71 | if (Objects.requireNonNull(intent.getAction()).equals(PlayerService.NOTIFY_PLAY)) { 72 | PlayerManager.getInstance().playAudio(); 73 | } else if (intent.getAction().equals(PlayerService.NOTIFY_PAUSE) 74 | || intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { 75 | PlayerManager.getInstance().pauseAudio(); 76 | } else if (intent.getAction().equals(PlayerService.NOTIFY_NEXT)) { 77 | PlayerManager.getInstance().playNext(); 78 | } else if (intent.getAction().equals(PlayerService.NOTIFY_CLOSE)) { 79 | PlayerManager.getInstance().clear(); 80 | } else if (intent.getAction().equals(PlayerService.NOTIFY_PREVIOUS)) { 81 | PlayerManager.getInstance().playPrevious(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/message/SharedViewModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.message; 18 | 19 | import androidx.lifecycle.ViewModel; 20 | 21 | import com.kunminx.architecture.domain.message.MutableResult; 22 | import com.kunminx.architecture.domain.message.Result; 23 | 24 | /** 25 | * TODO tip:本类专用于跨页面通信, 26 | * 本类已被 PageMessenger 类代替,具体可参见 PageMessenger 类说明 27 | * <p> 28 | * Create by KunMinX at 19/10/16 29 | */ 30 | @Deprecated 31 | public class SharedViewModel extends ViewModel { 32 | 33 | //TODO tip 2:此处演示 UnPeekLiveData 配合 SharedViewModel 实现 "生命周期安全、可靠一致" 消息分发。 34 | 35 | //TODO tip 3:为便于理解,原 UnPeekLiveData 已改名为 MutableResult; 36 | // ProtectedUnPeekLiveData 改名 Result; 37 | 38 | private final MutableResult<Boolean> toCloseSlidePanelIfExpanded = new MutableResult<>(); 39 | 40 | private final MutableResult<Boolean> toCloseActivityIfAllowed = new MutableResult<>(); 41 | 42 | private final MutableResult<Boolean> toOpenOrCloseDrawer = new MutableResult<>(); 43 | 44 | //TODO tip 4:可通过构造器方式配置 MutableResult 45 | 46 | private final MutableResult<Boolean> toAddSlideListener = 47 | new MutableResult.Builder<Boolean>().setAllowNullValue(false).create(); 48 | 49 | public Result<Boolean> isToAddSlideListener() { 50 | return toAddSlideListener; 51 | } 52 | 53 | public Result<Boolean> isToCloseSlidePanelIfExpanded() { 54 | return toCloseSlidePanelIfExpanded; 55 | } 56 | 57 | public Result<Boolean> isToCloseActivityIfAllowed() { 58 | return toCloseActivityIfAllowed; 59 | } 60 | 61 | public Result<Boolean> isToOpenOrCloseDrawer() { 62 | return toOpenOrCloseDrawer; 63 | } 64 | 65 | public void requestToCloseActivityIfAllowed(boolean allow) { 66 | toCloseActivityIfAllowed.setValue(allow); 67 | } 68 | 69 | public void requestToOpenOrCloseDrawer(boolean open) { 70 | toOpenOrCloseDrawer.setValue(open); 71 | } 72 | 73 | public void requestToCloseSlidePanelIfExpanded(boolean close) { 74 | toCloseSlidePanelIfExpanded.setValue(close); 75 | } 76 | 77 | public void requestToAddSlideListener(boolean add) { 78 | toAddSlideListener.setValue(add); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/request/AccountRequester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.request; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.lifecycle.DefaultLifecycleObserver; 21 | import androidx.lifecycle.LifecycleOwner; 22 | 23 | import com.kunminx.architecture.data.response.DataResult; 24 | import com.kunminx.architecture.data.response.ResponseStatus; 25 | import com.kunminx.architecture.data.response.ResultSource; 26 | import com.kunminx.architecture.domain.message.MutableResult; 27 | import com.kunminx.architecture.domain.message.Result; 28 | import com.kunminx.architecture.domain.request.Requester; 29 | import com.kunminx.puremusic.data.bean.User; 30 | import com.kunminx.puremusic.data.repository.DataRepository; 31 | 32 | import org.jetbrains.annotations.NotNull; 33 | 34 | import io.reactivex.Observer; 35 | import io.reactivex.disposables.Disposable; 36 | 37 | /** 38 | * 用户账户 Request 39 | * <p> 40 | * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者 41 | * <p> 42 | * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 43 | * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 44 | * <p> 45 | * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用, 46 | * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化, 47 | * 将来升级到 Jetpack Compose 更是如此, 48 | * <p> 49 | * Activity { 50 | * onCreate(){ 51 | * vm.livedata.observe { result-> 52 | * panel.visible(result.show ? VISIBLE : GONE) 53 | * tvTitle.setText(result.title) 54 | * tvContent.setText(result.content) 55 | * } 56 | * } 57 | * <p> 58 | * 如这么说无体会,详见《Jetpack MVVM 分层设计》解析 59 | * https://xiaozhuanlan.com/topic/6741932805 60 | * <p> 61 | * <p> 62 | * Create by KunMinX at 20/04/26 63 | */ 64 | public class AccountRequester extends Requester implements DefaultLifecycleObserver { 65 | 66 | //TODO tip 3:👆👆👆 让 accountRequest 可观察页面生命周期, 67 | // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时, 68 | // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期问题。 69 | 70 | private final MutableResult<DataResult<String>> tokenResult = new MutableResult<>(); 71 | 72 | //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发, 73 | // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层, 74 | // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动, 75 | 76 | //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。 77 | //https://xiaozhuanlan.com/topic/6017825943 78 | 79 | public Result<DataResult<String>> getTokenResult() { 80 | return tokenResult; 81 | } 82 | 83 | //TODO tip 5:模拟可取消的登录请求: 84 | // 85 | // 配合可观察页面生命周期的 accountRequest, 86 | // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时, 87 | // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期的问题。 88 | 89 | private Disposable mDisposable; 90 | 91 | //TODO tip 6: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 92 | // 93 | // 换言之,此处只关注数据的生成和回推,不关注数据的使用, 94 | // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用, 95 | 96 | public void requestLogin(User user) { 97 | DataRepository.getInstance().login(user).subscribe(new Observer<DataResult<String>>() { 98 | @Override 99 | public void onSubscribe(Disposable d) { 100 | mDisposable = d; 101 | } 102 | @Override 103 | public void onNext(DataResult<String> dataResult) { 104 | tokenResult.postValue(dataResult); 105 | } 106 | @Override 107 | public void onError(Throwable e) { 108 | tokenResult.postValue(new DataResult<>(null, 109 | new ResponseStatus(e.getMessage(), false, ResultSource.NETWORK))); 110 | } 111 | @Override 112 | public void onComplete() { 113 | mDisposable = null; 114 | } 115 | }); 116 | } 117 | 118 | public void cancelLogin() { 119 | if (mDisposable != null && !mDisposable.isDisposed()) { 120 | mDisposable.dispose(); 121 | mDisposable = null; 122 | } 123 | } 124 | 125 | //TODO tip 7:让 accountRequest 可观察页面生命周期, 126 | // 从而在页面即将退出、且登录请求由于网络延迟尚未完成时, 127 | // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期问题。 128 | 129 | // 关于 Lifecycle 组件的存在意义,详见《为你还原一个真实的 Jetpack Lifecycle》解析 130 | // https://xiaozhuanlan.com/topic/3684721950 131 | 132 | @Override 133 | public void onStop(@NonNull @NotNull LifecycleOwner owner) { 134 | cancelLogin(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/request/DownloadRequester.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.domain.request; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.lifecycle.LifecycleOwner; 5 | 6 | import com.kunminx.architecture.domain.dispatch.MviDispatcher; 7 | import com.kunminx.architecture.domain.request.AsyncTask; 8 | import com.kunminx.puremusic.data.bean.DownloadState; 9 | import com.kunminx.puremusic.data.repository.DataRepository; 10 | import com.kunminx.puremusic.domain.event.DownloadEvent; 11 | 12 | import io.reactivex.disposables.Disposable; 13 | 14 | /** 15 | * 数据下载 Request 16 | * <p> 17 | * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者 18 | * <p> 19 | * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 20 | * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 21 | * <p> 22 | * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用, 23 | * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化, 24 | * 将来升级到 Jetpack Compose 更是如此, 25 | * <p> 26 | * Activity { 27 | * onCreate(){ 28 | * vm.livedata.observe { result-> 29 | * panel.visible(result.show ? VISIBLE : GONE) 30 | * tvTitle.setText(result.title) 31 | * tvContent.setText(result.content) 32 | * } 33 | * } 34 | * <p> 35 | * 如这么说无体会,详见《Jetpack MVVM 分层设计》解析 36 | * https://xiaozhuanlan.com/topic/6741932805 37 | * <p> 38 | * <p> 39 | * Create by KunMinX at 20/03/18 40 | */ 41 | public class DownloadRequester extends MviDispatcher<DownloadEvent> { 42 | 43 | private Disposable mDisposable; 44 | 45 | //TODO Tip 2:基于 "单一职责原则",宜将 Jetpack ViewModel 框架划分为 state-ViewModel 和 result-ViewModel, 46 | // result-ViewModel 作为领域层组件,仅提取和继承 Jetpack ViewModel 框架中 "作用域管理" 的能力, 47 | // 使业务实例能根据需要,被单个页面独享,或多个页面共享,例如: 48 | // 49 | // mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class); 50 | // mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class); 51 | // 52 | // 在本案例中,fragment 级作用域的 mDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD 业务, 53 | // Activity 级作用域的 mGlobalDownloadRequester 只走 DownloadEvent.EVENT_DOWNLOAD_GLOBAL 业务, 54 | // 二者都为 SearchFragment 所持有,用于对比不同作用域的效果, 55 | 56 | @Override 57 | protected void onHandle(DownloadEvent event) { 58 | DataRepository repo = DataRepository.getInstance(); 59 | switch (event.eventId) { 60 | case DownloadEvent.EVENT_DOWNLOAD: 61 | repo.downloadFile().subscribe(new AsyncTask.Observer<Integer>() { 62 | @Override 63 | public void onSubscribe(Disposable d) { 64 | mDisposable = d; 65 | } 66 | @Override 67 | public void onNext(Integer integer) { 68 | sendResult(event.copy(new DownloadState(true, integer))); 69 | } 70 | }); 71 | break; 72 | case DownloadEvent.EVENT_DOWNLOAD_GLOBAL: 73 | repo.downloadFile().subscribe((AsyncTask.Observer<Integer>) integer -> { 74 | sendResult(event.copy(new DownloadState(true, integer))); 75 | }); 76 | break; 77 | } 78 | } 79 | 80 | @Override 81 | public void onStop(@NonNull LifecycleOwner owner) { 82 | super.onStop(owner); 83 | if (mDisposable != null && !mDisposable.isDisposed()) { 84 | mDisposable.dispose(); 85 | mDisposable = null; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/request/InfoRequester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.request; 18 | 19 | import android.annotation.SuppressLint; 20 | 21 | import com.kunminx.architecture.data.response.DataResult; 22 | import com.kunminx.architecture.domain.message.MutableResult; 23 | import com.kunminx.architecture.domain.message.Result; 24 | import com.kunminx.architecture.domain.request.Requester; 25 | import com.kunminx.puremusic.data.bean.LibraryInfo; 26 | import com.kunminx.puremusic.data.repository.DataRepository; 27 | 28 | import java.util.List; 29 | 30 | /** 31 | * 信息列表 Request 32 | * <p> 33 | * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者 34 | * <p> 35 | * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 36 | * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 37 | * <p> 38 | * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用, 39 | * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化, 40 | * 将来升级到 Jetpack Compose 更是如此, 41 | * <p> 42 | * Activity { 43 | * onCreate(){ 44 | * vm.livedata.observe { result-> 45 | * panel.visible(result.show ? VISIBLE : GONE) 46 | * tvTitle.setText(result.title) 47 | * tvContent.setText(result.content) 48 | * } 49 | * } 50 | * <p> 51 | * TODO tip 2:Requester 通常按业务划分 52 | * 一个项目中通常可存在多个 Requester 类, 53 | * 每个页面可根据业务需要,持有多个不同 Requester 实例, 54 | * 通过 PublishSubject 回推一次性消息,并在表现层 Observer 中分流, 55 | * 对于 Event,直接执行,对于 State,使用 BehaviorSubject 通知 View 渲染和兜着状态, 56 | * <p> 57 | * Activity { 58 | * onCreate(){ 59 | * request.observe {result -> 60 | * is Event ? -> execute one time 61 | * is State ? -> BehaviorSubject setValue and notify 62 | * } 63 | * } 64 | * <p> 65 | * 如这么说无体会,详见《Jetpack MVVM 分层设计解析》解析 66 | * https://xiaozhuanlan.com/topic/6741932805 67 | * <p> 68 | * <p> 69 | * Create by KunMinX at 19/11/2 70 | */ 71 | public class InfoRequester extends Requester { 72 | 73 | private final MutableResult<DataResult<List<LibraryInfo>>> mLibraryResult = new MutableResult<>(); 74 | 75 | //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发, 76 | // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层, 77 | // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动, 78 | 79 | //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。 80 | //https://xiaozhuanlan.com/topic/6017825943 81 | 82 | public Result<DataResult<List<LibraryInfo>>> getLibraryResult() { 83 | return mLibraryResult; 84 | } 85 | 86 | //TODO tip 5: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 87 | // 88 | // 换言之,此处只关注数据的生成和回推,不关注数据的使用, 89 | // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用, 90 | 91 | @SuppressLint("CheckResult") 92 | public void requestLibraryInfo() { 93 | if (mLibraryResult.getValue() == null) 94 | DataRepository.getInstance().getLibraryInfo().subscribe(mLibraryResult::setValue); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/request/MusicRequester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.request; 18 | 19 | import android.annotation.SuppressLint; 20 | 21 | import com.kunminx.architecture.data.response.DataResult; 22 | import com.kunminx.architecture.domain.message.MutableResult; 23 | import com.kunminx.architecture.domain.message.Result; 24 | import com.kunminx.architecture.domain.request.Requester; 25 | import com.kunminx.puremusic.data.bean.TestAlbum; 26 | import com.kunminx.puremusic.data.repository.DataRepository; 27 | 28 | /** 29 | * 音乐资源 Request 30 | * <p> 31 | * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者 32 | * <p> 33 | * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 34 | * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 35 | * <p> 36 | * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用, 37 | * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化, 38 | * 将来升级到 Jetpack Compose 更是如此, 39 | * <p> 40 | * Activity { 41 | * onCreate(){ 42 | * vm.livedata.observe { result-> 43 | * panel.visible(result.show ? VISIBLE : GONE) 44 | * tvTitle.setText(result.title) 45 | * tvContent.setText(result.content) 46 | * } 47 | * } 48 | * <p> 49 | * TODO tip 2:Requester 通常按业务划分 50 | * 一个项目中通常可存在多个 Requester 类, 51 | * 每个页面可根据业务需要,持有多个不同 Requester 实例, 52 | * 通过 PublishSubject 回推一次性消息,并在表现层 Observer 中分流, 53 | * 对于 Event,直接执行,对于 State,使用 BehaviorSubject 通知 View 渲染和兜着状态, 54 | * <p> 55 | * Activity { 56 | * onCreate(){ 57 | * request.observe {result -> 58 | * is Event ? -> execute one time 59 | * is State ? -> BehaviorSubject setValue and notify 60 | * } 61 | * } 62 | * <p> 63 | * 如这么说无体会,详见《Jetpack MVVM 分层设计解析》解析 64 | * https://xiaozhuanlan.com/topic/6741932805 65 | * <p> 66 | * <p> 67 | * Create by KunMinX at 19/10/29 68 | */ 69 | public class MusicRequester extends Requester { 70 | 71 | private final MutableResult<DataResult<TestAlbum>> mFreeMusicsResult = new MutableResult<>(); 72 | 73 | //TODO tip 4:应顺应 "响应式编程",做好 "单向数据流" 开发, 74 | // MutableResult 应仅限 "鉴权中心" 内部使用,且只暴露 immutable Result 给 UI 层, 75 | // 通过 "读写分离" 实现数据从 "领域层" 到 "表现层" 的单向流动, 76 | 77 | //如这么说无体会,详见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析。 78 | //https://xiaozhuanlan.com/topic/6017825943 79 | 80 | public Result<DataResult<TestAlbum>> getFreeMusicsResult() { 81 | return mFreeMusicsResult; 82 | } 83 | 84 | //TODO tip 5: requester 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 85 | // 86 | // 换言之,此处只关注数据的生成和回推,不关注数据的使用, 87 | // 改变 UI 状态的逻辑代码,只应在表现层页面中编写,例如 Jetpack Compose 的使用, 88 | 89 | @SuppressLint("CheckResult") 90 | public void requestFreeMusics() { 91 | DataRepository.getInstance().getFreeMusic().subscribe(mFreeMusicsResult::setValue); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/usecase/CanBeStoppedUseCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.domain.usecase; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.lifecycle.DefaultLifecycleObserver; 21 | import androidx.lifecycle.LifecycleOwner; 22 | 23 | import com.kunminx.architecture.data.response.DataResult; 24 | import com.kunminx.architecture.domain.usecase.UseCase; 25 | import com.kunminx.puremusic.data.bean.DownloadState; 26 | 27 | /** 28 | * UseCase 示例,实现 LifeCycle 接口,单独服务于 有 “叫停” 需求 的业务 29 | * <p> 30 | * TODO tip: 31 | * 同样是“下载”,我不是在数据层分别写两个方法, 32 | * 而是遵循开闭原则,在 ViewModel 和 数据层之间,插入一个 UseCase,来专门负责可叫停的情况, 33 | * 除了开闭原则,使用 UseCase 还有个考虑就是避免内存泄漏, 34 | * 具体缘由可详见 https://xiaozhuanlan.com/topic/6257931840 评论区 15 楼 35 | * 以及《这是一份 “架构模式” 自驾攻略》的解析 36 | * https://xiaozhuanlan.com/topic/8204519736 37 | * <p> 38 | * <p> 39 | * 现已更换为在 MVI-Dispatcher 中处理,具体可参见 DownloadRequest 实现 40 | * <p> 41 | * <p> 42 | * Create by KunMinX at 19/11/25 43 | */ 44 | @Deprecated 45 | public class CanBeStoppedUseCase extends UseCase<CanBeStoppedUseCase.RequestValues, 46 | CanBeStoppedUseCase.ResponseValue> implements DefaultLifecycleObserver { 47 | 48 | // private final DownloadState downloadState = new DownloadState(); 49 | 50 | //TODO tip:让 CanBeStoppedUseCase 可观察页面生命周期, 51 | // 从而在页面即将退出、且下载请求尚未完成时, 52 | // 及时通知数据层取消本次请求,以避免资源浪费和一系列不可预期的问题。 53 | 54 | // 关于 Lifecycle 组件的存在意义,可详见《为你还原一个真实的 Jetpack Lifecycle》篇的解析 55 | // https://xiaozhuanlan.com/topic/3684721950 56 | 57 | @Override 58 | public void onStop(@NonNull LifecycleOwner owner) { 59 | if (getRequestValues() != null) { 60 | // downloadState.isForgive = true; 61 | // downloadState.file = null; 62 | // downloadState.progress = 0; 63 | // getUseCaseCallback().onError(); 64 | } 65 | } 66 | 67 | @Override 68 | protected void executeUseCase(RequestValues requestValues) { 69 | 70 | //访问数据层资源,在 UseCase 中处理带叫停性质的业务 71 | 72 | // DataRepository.getInstance().downloadFile(downloadState, dataResult -> { 73 | // getUseCaseCallback().onSuccess(new ResponseValue(dataResult)); 74 | // }); 75 | } 76 | 77 | public static final class RequestValues implements UseCase.RequestValues { 78 | 79 | } 80 | 81 | public static final class ResponseValue implements UseCase.ResponseValue { 82 | 83 | private final DataResult<DownloadState> mDataResult; 84 | 85 | public ResponseValue(DataResult<DownloadState> dataResult) { 86 | mDataResult = dataResult; 87 | } 88 | 89 | public DataResult<DownloadState> getDataResult() { 90 | return mDataResult; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/domain/usecase/DownloadUseCase.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.domain.usecase; 2 | 3 | import com.kunminx.architecture.domain.usecase.UseCase; 4 | import com.kunminx.puremusic.data.config.Const; 5 | 6 | import java.io.File; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.net.URL; 12 | 13 | /** 14 | * Create by KunMinX at 20/03/16 15 | */ 16 | public class DownloadUseCase extends UseCase<DownloadUseCase.RequestValues, DownloadUseCase.ResponseValue> { 17 | 18 | @Override 19 | protected void executeUseCase(RequestValues requestValues) { 20 | try { 21 | URL url = new URL(requestValues.url); 22 | InputStream is = url.openStream(); 23 | File file = new File(Const.COVER_PATH, requestValues.path); 24 | OutputStream os = new FileOutputStream(file); 25 | byte[] buffer = new byte[1024]; 26 | int len = 0; 27 | while ((len = is.read(buffer)) > 0) { 28 | os.write(buffer, 0, len); 29 | } 30 | is.close(); 31 | os.close(); 32 | 33 | getUseCaseCallback().onSuccess(new ResponseValue(file)); 34 | 35 | } catch (IOException e) { 36 | e.printStackTrace(); 37 | } 38 | } 39 | 40 | public static final class RequestValues implements UseCase.RequestValues { 41 | private String url; 42 | private String path; 43 | 44 | public RequestValues(String url, String path) { 45 | this.url = url; 46 | this.path = path; 47 | } 48 | 49 | public String getUrl() { 50 | return url; 51 | } 52 | 53 | public void setUrl(String url) { 54 | this.url = url; 55 | } 56 | 57 | public String getPath() { 58 | return path; 59 | } 60 | 61 | public void setPath(String path) { 62 | this.path = path; 63 | } 64 | } 65 | 66 | public static final class ResponseValue implements UseCase.ResponseValue { 67 | private File mFile; 68 | 69 | public ResponseValue(File file) { 70 | mFile = file; 71 | } 72 | 73 | public File getFile() { 74 | return mFile; 75 | } 76 | 77 | public void setFile(File file) { 78 | mFile = file; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/bind/CommonBindingAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.bind; 18 | 19 | import android.graphics.drawable.Drawable; 20 | import android.util.Pair; 21 | import android.view.View; 22 | import android.widget.ImageView; 23 | import android.widget.TextView; 24 | 25 | import androidx.coordinatorlayout.widget.CoordinatorLayout; 26 | import androidx.databinding.BindingAdapter; 27 | 28 | import com.bumptech.glide.Glide; 29 | import com.kunminx.architecture.utils.ClickUtils; 30 | 31 | /** 32 | * Create by KunMinX at 19/9/18 33 | */ 34 | public class CommonBindingAdapter { 35 | 36 | @BindingAdapter(value = {"imageUrl", "placeHolder"}, requireAll = false) 37 | public static void imageUrl(ImageView view, String url, Drawable placeHolder) { 38 | Glide.with(view.getContext()).load(url).placeholder(placeHolder).into(view); 39 | } 40 | 41 | @BindingAdapter(value = {"visible"}, requireAll = false) 42 | public static void visible(View view, boolean visible) { 43 | if (visible && view.getVisibility() == View.GONE) { 44 | view.setVisibility(View.VISIBLE); 45 | } else if (!visible && view.getVisibility() == View.VISIBLE) { 46 | view.setVisibility(View.GONE); 47 | } 48 | } 49 | 50 | @BindingAdapter(value = {"invisible"}, requireAll = false) 51 | public static void invisible(View view, boolean visible) { 52 | if (visible && view.getVisibility() == View.INVISIBLE) { 53 | view.setVisibility(View.VISIBLE); 54 | } else if (!visible && view.getVisibility() == View.VISIBLE) { 55 | view.setVisibility(View.INVISIBLE); 56 | } 57 | } 58 | 59 | @BindingAdapter(value = {"size"}, requireAll = false) 60 | public static void size(View view, Pair<Integer, Integer> size) { 61 | CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) view.getLayoutParams(); 62 | params.width = size.first; 63 | params.height = size.second; 64 | view.setLayoutParams(params); 65 | } 66 | 67 | @BindingAdapter(value = {"transX"}, requireAll = false) 68 | public static void translationX(View view, float translationX) { 69 | view.setTranslationX(translationX); 70 | } 71 | 72 | @BindingAdapter(value = {"transY"}, requireAll = false) 73 | public static void translationY(View view, float translationY) { 74 | view.setTranslationY(translationY); 75 | } 76 | 77 | @BindingAdapter(value = {"x"}, requireAll = false) 78 | public static void x(View view, float x) { 79 | view.setX(x); 80 | } 81 | 82 | @BindingAdapter(value = {"y"}, requireAll = false) 83 | public static void y(View view, float y) { 84 | view.setY(y); 85 | } 86 | 87 | @BindingAdapter(value = {"alpha"}, requireAll = false) 88 | public static void alpha(View view, float alpha) { 89 | view.setAlpha(alpha); 90 | } 91 | 92 | @BindingAdapter(value = {"textColor"}, requireAll = false) 93 | public static void setTextColor(TextView textView, int textColorRes) { 94 | textView.setTextColor(textView.getContext().getColor(textColorRes)); 95 | } 96 | 97 | @BindingAdapter(value = {"selected"}, requireAll = false) 98 | public static void selected(View view, boolean select) { 99 | view.setSelected(select); 100 | } 101 | 102 | @BindingAdapter(value = {"onClickWithDebouncing"}, requireAll = false) 103 | public static void onClickWithDebouncing(View view, View.OnClickListener clickListener) { 104 | ClickUtils.applySingleDebouncing(view, clickListener); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/bind/DrawerBindingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.bind; 2 | 3 | import androidx.core.view.GravityCompat; 4 | import androidx.databinding.BindingAdapter; 5 | import androidx.drawerlayout.widget.DrawerLayout; 6 | 7 | /** 8 | * Create by KunMinX at 2020/3/13 9 | */ 10 | public class DrawerBindingAdapter { 11 | 12 | @BindingAdapter(value = {"isOpenDrawer"}, requireAll = false) 13 | public static void openDrawer(DrawerLayout drawerLayout, boolean isOpenDrawer) { 14 | if (isOpenDrawer && !drawerLayout.isDrawerOpen(GravityCompat.START)) { 15 | drawerLayout.openDrawer(GravityCompat.START); 16 | } else { 17 | drawerLayout.closeDrawer(GravityCompat.START); 18 | } 19 | } 20 | 21 | @BindingAdapter(value = {"allowDrawerOpen"}, requireAll = false) 22 | public static void allowDrawerOpen(DrawerLayout drawerLayout, boolean allowDrawerOpen) { 23 | drawerLayout.setDrawerLockMode(allowDrawerOpen 24 | ? DrawerLayout.LOCK_MODE_UNLOCKED 25 | : DrawerLayout.LOCK_MODE_LOCKED_CLOSED); 26 | } 27 | 28 | @BindingAdapter(value = {"bindDrawerListener"}, requireAll = false) 29 | public static void listenDrawerState(DrawerLayout drawerLayout, DrawerLayout.SimpleDrawerListener listener) { 30 | drawerLayout.addDrawerListener(listener); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/bind/IconBindingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.bind; 2 | 3 | import androidx.databinding.BindingAdapter; 4 | 5 | import com.kunminx.puremusic.ui.view.PlayPauseView; 6 | 7 | import net.steamcrafted.materialiconlib.MaterialDrawableBuilder; 8 | import net.steamcrafted.materialiconlib.MaterialIconView; 9 | 10 | /** 11 | * Create by KunMinX at 2020/3/13 12 | */ 13 | public class IconBindingAdapter { 14 | 15 | @BindingAdapter(value = {"isPlaying"}, requireAll = false) 16 | public static void isPlaying(PlayPauseView pauseView, boolean isPlaying) { 17 | if (isPlaying) { 18 | pauseView.play(); 19 | } else { 20 | pauseView.pause(); 21 | } 22 | } 23 | 24 | @BindingAdapter(value = {"mdIcon"}, requireAll = false) 25 | public static void setIcon(MaterialIconView view, MaterialDrawableBuilder.IconValue iconValue) { 26 | view.setIcon(iconValue); 27 | } 28 | 29 | @BindingAdapter(value = {"circleAlpha"}, requireAll = false) 30 | public static void circleAlpha(PlayPauseView pauseView, int circleAlpha) { 31 | pauseView.setCircleAlpha(circleAlpha); 32 | } 33 | 34 | @BindingAdapter(value = {"drawableColor"}, requireAll = false) 35 | public static void drawableColor(PlayPauseView pauseView, int drawableColor) { 36 | pauseView.setDrawableColor(drawableColor); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/bind/TabPageBindingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.bind; 2 | 3 | import androidx.databinding.BindingAdapter; 4 | import androidx.viewpager.widget.ViewPager; 5 | 6 | import com.google.android.material.tabs.TabLayout; 7 | import com.kunminx.architecture.ui.adapter.CommonViewPagerAdapter; 8 | import com.kunminx.puremusic.R; 9 | 10 | /** 11 | * Create by KunMinX at 2020/3/13 12 | */ 13 | public class TabPageBindingAdapter { 14 | 15 | @BindingAdapter(value = {"initTabAndPage"}, requireAll = false) 16 | public static void initTabAndPage(ViewPager viewPager, boolean initTabAndPage) { 17 | TabLayout tabLayout = (viewPager.getRootView()).findViewById(R.id.tab_layout); 18 | int count = tabLayout.getTabCount(); 19 | String[] title = new String[count]; 20 | for (int i = 0; i < count; i++) { 21 | TabLayout.Tab tab = tabLayout.getTabAt(i); 22 | if (tab != null && tab.getText() != null) { 23 | title[i] = tab.getText().toString(); 24 | } 25 | } 26 | viewPager.setAdapter(new CommonViewPagerAdapter(false, title)); 27 | tabLayout.setupWithViewPager(viewPager); 28 | } 29 | 30 | @BindingAdapter(value = {"tabSelectedListener"}, requireAll = false) 31 | public static void tabSelectedListener(TabLayout tabLayout, TabLayout.OnTabSelectedListener listener) { 32 | tabLayout.addOnTabSelectedListener(listener); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/bind/WebViewBindingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.bind; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.view.View; 7 | import android.webkit.WebResourceRequest; 8 | import android.webkit.WebSettings; 9 | import android.webkit.WebView; 10 | import android.webkit.WebViewClient; 11 | 12 | import androidx.databinding.BindingAdapter; 13 | 14 | import com.kunminx.architecture.utils.Utils; 15 | 16 | /** 17 | * Create by KunMinX at 2020/3/13 18 | */ 19 | public class WebViewBindingAdapter { 20 | 21 | @SuppressLint("SetJavaScriptEnabled") 22 | @BindingAdapter(value = {"pageAssetPath"}, requireAll = false) 23 | public static void loadAssetsPage(WebView webView, String assetPath) { 24 | webView.setWebViewClient(new WebViewClient() { 25 | @Override 26 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 27 | Uri uri = request.getUrl(); 28 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 29 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 30 | Utils.getApp().startActivity(intent); 31 | return true; 32 | } 33 | }); 34 | webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); 35 | WebSettings webSettings = webView.getSettings(); 36 | webSettings.setJavaScriptEnabled(true); 37 | webSettings.setDefaultTextEncodingName("UTF-8"); 38 | webSettings.setSupportZoom(true); 39 | webSettings.setBuiltInZoomControls(true); 40 | webSettings.setDisplayZoomControls(false); 41 | webSettings.setUseWideViewPort(true); 42 | webSettings.setLoadWithOverviewMode(true); 43 | String url = "file:///android_asset/" + assetPath; 44 | webView.loadUrl(url); 45 | } 46 | 47 | @SuppressLint("SetJavaScriptEnabled") 48 | @BindingAdapter(value = {"loadPage"}, requireAll = false) 49 | public static void loadPage(WebView webView, String loadPage) { 50 | webView.setWebViewClient(new WebViewClient()); 51 | webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); 52 | WebSettings webSettings = webView.getSettings(); 53 | webSettings.setJavaScriptEnabled(true); 54 | webSettings.setDefaultTextEncodingName("UTF-8"); 55 | webSettings.setSupportZoom(true); 56 | webSettings.setBuiltInZoomControls(true); 57 | webSettings.setDisplayZoomControls(false); 58 | webSettings.setUseWideViewPort(true); 59 | webSettings.setLoadWithOverviewMode(true); 60 | webView.loadUrl(loadPage); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/DrawerFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.page; 18 | 19 | import android.os.Bundle; 20 | import android.view.View; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.annotation.Nullable; 24 | 25 | import com.kunminx.architecture.ui.page.BaseFragment; 26 | import com.kunminx.architecture.ui.page.DataBindingConfig; 27 | import com.kunminx.architecture.ui.page.StateHolder; 28 | import com.kunminx.architecture.ui.state.State; 29 | import com.kunminx.puremusic.BR; 30 | import com.kunminx.puremusic.R; 31 | import com.kunminx.puremusic.data.bean.LibraryInfo; 32 | import com.kunminx.puremusic.data.config.Const; 33 | import com.kunminx.puremusic.domain.request.InfoRequester; 34 | import com.kunminx.puremusic.ui.page.adapter.DrawerAdapter; 35 | 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | 39 | /** 40 | * Create by KunMinX at 19/10/29 41 | */ 42 | public class DrawerFragment extends BaseFragment { 43 | 44 | //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel, 45 | // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面, 46 | // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定 47 | 48 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736 49 | 50 | private DrawerStates mStates; 51 | private InfoRequester mInfoRequester; 52 | 53 | @Override 54 | protected void initViewModel() { 55 | mStates = getFragmentScopeViewModel(DrawerStates.class); 56 | mInfoRequester = getFragmentScopeViewModel(InfoRequester.class); 57 | } 58 | 59 | @Override 60 | protected DataBindingConfig getDataBindingConfig() { 61 | 62 | //TODO tip 2: DataBinding 严格模式: 63 | // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, 64 | // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, 65 | // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 66 | // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。 67 | 68 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 69 | 70 | return new DataBindingConfig(R.layout.fragment_drawer, BR.vm, mStates) 71 | .addBindingParam(BR.click, new ClickProxy()) 72 | .addBindingParam(BR.adapter, new DrawerAdapter(getContext())); 73 | } 74 | 75 | @Override 76 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 77 | super.onViewCreated(view, savedInstanceState); 78 | 79 | //TODO tip 3: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化, 80 | // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态, 81 | 82 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 83 | 84 | mInfoRequester.getLibraryResult().observe(getViewLifecycleOwner(), dataResult -> { 85 | if (!dataResult.getResponseStatus().isSuccess()) return; 86 | if (dataResult.getResult() != null) mStates.list.set(dataResult.getResult()); 87 | }); 88 | 89 | mInfoRequester.requestLibraryInfo(); 90 | } 91 | 92 | public class ClickProxy { 93 | public void logoClick() { 94 | openUrlInBrowser(Const.PROJECT_LINK); 95 | } 96 | } 97 | 98 | //TODO tip 5:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder, 99 | // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源", 100 | // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态, 101 | 102 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 103 | 104 | public static class DrawerStates extends StateHolder { 105 | public final State<List<LibraryInfo>> list = new State<>(new ArrayList<>()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/SearchFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.page; 18 | 19 | import android.os.Bundle; 20 | import android.view.View; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.annotation.Nullable; 24 | 25 | import com.kunminx.architecture.ui.page.BaseFragment; 26 | import com.kunminx.architecture.ui.page.DataBindingConfig; 27 | import com.kunminx.architecture.ui.page.StateHolder; 28 | import com.kunminx.architecture.ui.state.State; 29 | import com.kunminx.puremusic.BR; 30 | import com.kunminx.puremusic.R; 31 | import com.kunminx.puremusic.data.bean.DownloadState; 32 | import com.kunminx.puremusic.data.config.Const; 33 | import com.kunminx.puremusic.domain.event.DownloadEvent; 34 | import com.kunminx.puremusic.domain.message.DrawerCoordinateManager; 35 | import com.kunminx.puremusic.domain.request.DownloadRequester; 36 | 37 | /** 38 | * Create by KunMinX at 19/10/29 39 | */ 40 | public class SearchFragment extends BaseFragment { 41 | 42 | //TODO tip 1:基于 "单一职责原则",应将 ViewModel 划分为 state-ViewModel 和 result-ViewModel, 43 | // state-ViewModel 职责仅限于托管、保存和恢复本页面 state,作用域仅限于本页面, 44 | // result-ViewModel 职责仅限于 "消息分发" 场景承担 "可信源",作用域依 "数据请求" 或 "跨页通信" 消息分发范围而定 45 | 46 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/8204519736 47 | 48 | private SearchStates mStates; 49 | private DownloadRequester mDownloadRequester; 50 | private DownloadRequester mGlobalDownloadRequester; 51 | 52 | @Override 53 | protected void initViewModel() { 54 | mStates = getFragmentScopeViewModel(SearchStates.class); 55 | mDownloadRequester = getFragmentScopeViewModel(DownloadRequester.class); 56 | mGlobalDownloadRequester = getActivityScopeViewModel(DownloadRequester.class); 57 | } 58 | 59 | @Override 60 | protected DataBindingConfig getDataBindingConfig() { 61 | 62 | //TODO tip 2: DataBinding 严格模式: 63 | // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, 64 | // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, 65 | // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 66 | // 而 DataBindingConfig 就是在这样背景下,用于为 base 页面 DataBinding 提供绑定项。 67 | 68 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 69 | 70 | return new DataBindingConfig(R.layout.fragment_search, BR.vm, mStates) 71 | .addBindingParam(BR.click, new ClickProxy()); 72 | } 73 | 74 | @Override 75 | public void onCreate(@Nullable Bundle savedInstanceState) { 76 | super.onCreate(savedInstanceState); 77 | 78 | getLifecycle().addObserver(DrawerCoordinateManager.getInstance()); 79 | 80 | //TODO tip 3:绑定跟随视图控制器生命周期、可叫停、单独放在 UseCase 中处理的业务 81 | getLifecycle().addObserver(mDownloadRequester); 82 | } 83 | 84 | @Override 85 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 86 | super.onViewCreated(view, savedInstanceState); 87 | 88 | //TODO tip 8: 此处演示使用 MVI-Dispatcher input-output 接口完成数据请求响应 89 | 90 | //如这么说无体会,详见《领域层设计》篇拆解 https://juejin.cn/post/7117498113983512589 91 | 92 | mDownloadRequester.output(this, downloadEvent -> { 93 | if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD) { 94 | DownloadState state = downloadEvent.downloadState; 95 | mStates.progress_cancelable.set(state.progress); 96 | mStates.enableDownload.set(state.progress == 100 || state.progress == 0); 97 | } 98 | }); 99 | 100 | //TODO tip 9: 此处演示 "同一 Result-ViewModel 类,在不同作用域下实例化,造成的不同结果" 101 | 102 | mGlobalDownloadRequester.output(this, downloadEvent -> { 103 | if (downloadEvent.eventId == DownloadEvent.EVENT_DOWNLOAD_GLOBAL) { 104 | DownloadState state = downloadEvent.downloadState; 105 | mStates.progress.set(state.progress); 106 | mStates.enableGlobalDownload.set(state.progress == 100 || state.progress == 0); 107 | } 108 | }); 109 | } 110 | 111 | // TODO tip 4:此处通过 DataBinding 规避 setOnClickListener 时存在的 View 实例 Null 安全一致性问题, 112 | 113 | // 也即,有视图就绑定,无就无绑定,总之 不会因不一致性造成 View 实例 Null 安全问题。 114 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 115 | 116 | public class ClickProxy { 117 | 118 | public void back() { 119 | nav().navigateUp(); 120 | } 121 | 122 | public void testNav() { 123 | openUrlInBrowser(Const.COLUMN_LINK); 124 | } 125 | 126 | public void subscribe() { 127 | openUrlInBrowser(Const.COLUMN_LINK); 128 | } 129 | 130 | //TODO tip: 同 tip 8 131 | 132 | public void testDownload() { 133 | mGlobalDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD_GLOBAL)); 134 | } 135 | 136 | //TODO tip 5: 在 UseCase 中 执行可跟随生命周期中止的下载任务 137 | 138 | public void testLifecycleDownload() { 139 | mDownloadRequester.input(new DownloadEvent(DownloadEvent.EVENT_DOWNLOAD)); 140 | } 141 | } 142 | 143 | //TODO tip 6:基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder, 144 | // 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源", 145 | // 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态, 146 | 147 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 148 | 149 | public static class SearchStates extends StateHolder { 150 | 151 | public final State<Integer> progress = new State<>(1); 152 | 153 | public final State<Integer> progress_cancelable = new State<>(1); 154 | 155 | public final State<Boolean> enableDownload = new State<>(true); 156 | 157 | public final State<Boolean> enableGlobalDownload = new State<>(true); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DiffUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.page.adapter; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.recyclerview.widget.DiffUtil; 21 | 22 | import com.kunminx.puremusic.data.bean.LibraryInfo; 23 | import com.kunminx.puremusic.data.bean.TestAlbum; 24 | 25 | /** 26 | * Create by KunMinX at 2020/7/19 27 | */ 28 | public class DiffUtils { 29 | 30 | private DiffUtil.ItemCallback<LibraryInfo> mLibraryInfoItemCallback; 31 | 32 | private DiffUtil.ItemCallback<TestAlbum.TestMusic> mTestMusicItemCallback; 33 | 34 | private DiffUtils() { 35 | } 36 | 37 | private static final DiffUtils S_DIFF_UTILS = new DiffUtils(); 38 | 39 | public static DiffUtils getInstance() { 40 | return S_DIFF_UTILS; 41 | } 42 | 43 | public DiffUtil.ItemCallback<LibraryInfo> getLibraryInfoItemCallback() { 44 | if (mLibraryInfoItemCallback == null) { 45 | mLibraryInfoItemCallback = new DiffUtil.ItemCallback<LibraryInfo>() { 46 | @Override 47 | public boolean areItemsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) { 48 | return oldItem.equals(newItem); 49 | } 50 | 51 | @Override 52 | public boolean areContentsTheSame(@NonNull LibraryInfo oldItem, @NonNull LibraryInfo newItem) { 53 | return oldItem.getTitle().equals(newItem.getTitle()); 54 | } 55 | }; 56 | } 57 | return mLibraryInfoItemCallback; 58 | } 59 | 60 | public DiffUtil.ItemCallback<TestAlbum.TestMusic> getTestMusicItemCallback() { 61 | if (mTestMusicItemCallback == null) { 62 | mTestMusicItemCallback = new DiffUtil.ItemCallback<TestAlbum.TestMusic>() { 63 | @Override 64 | public boolean areItemsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) { 65 | return oldItem.equals(newItem); 66 | } 67 | 68 | @Override 69 | public boolean areContentsTheSame(@NonNull TestAlbum.TestMusic oldItem, @NonNull TestAlbum.TestMusic newItem) { 70 | return oldItem.musicId.equals(newItem.musicId); 71 | } 72 | }; 73 | } 74 | return mTestMusicItemCallback; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/adapter/DrawerAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.page.adapter; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.net.Uri; 22 | 23 | import androidx.recyclerview.widget.RecyclerView; 24 | 25 | import com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter; 26 | import com.kunminx.puremusic.R; 27 | import com.kunminx.puremusic.data.bean.LibraryInfo; 28 | import com.kunminx.puremusic.databinding.AdapterLibraryBinding; 29 | 30 | /** 31 | * Create by KunMinX at 20/4/19 32 | */ 33 | public class DrawerAdapter extends SimpleDataBindingAdapter<LibraryInfo, AdapterLibraryBinding> { 34 | 35 | public DrawerAdapter(Context context) { 36 | super(context, R.layout.adapter_library, DiffUtils.getInstance().getLibraryInfoItemCallback()); 37 | 38 | //TODO item click 回调可以在 adapter 中实现,也可以在外部实现 39 | setOnItemClickListener((viewId, item, position) -> { 40 | Uri uri = Uri.parse(item.getUrl()); 41 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 42 | mContext.startActivity(intent); 43 | }); 44 | } 45 | 46 | @Override 47 | protected void onBindItem(AdapterLibraryBinding binding, LibraryInfo item, RecyclerView.ViewHolder holder) { 48 | binding.setInfo(item); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/adapter/PlaylistAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.puremusic.ui.page.adapter; 18 | 19 | import android.content.Context; 20 | import android.graphics.Color; 21 | 22 | import androidx.recyclerview.widget.RecyclerView; 23 | 24 | import com.kunminx.binding_recyclerview.adapter.SimpleDataBindingAdapter; 25 | import com.kunminx.puremusic.R; 26 | import com.kunminx.puremusic.data.bean.TestAlbum; 27 | import com.kunminx.puremusic.databinding.AdapterPlayItemBinding; 28 | import com.kunminx.puremusic.domain.proxy.PlayerManager; 29 | 30 | /** 31 | * Create by KunMinX at 20/4/19 32 | */ 33 | public class PlaylistAdapter extends SimpleDataBindingAdapter<TestAlbum.TestMusic, AdapterPlayItemBinding> { 34 | 35 | public PlaylistAdapter(Context context) { 36 | super(context, R.layout.adapter_play_item, DiffUtils.getInstance().getTestMusicItemCallback()); 37 | } 38 | 39 | @Override 40 | protected void onBindItem(AdapterPlayItemBinding binding, TestAlbum.TestMusic item, RecyclerView.ViewHolder holder) { 41 | binding.setAlbum(item); 42 | int currentIndex = PlayerManager.getInstance().getAlbumIndex(); 43 | binding.ivPlayStatus.setColor(currentIndex == holder.getAbsoluteAdapterPosition() 44 | ? binding.getRoot().getContext().getColor(R.color.gray) : Color.TRANSPARENT); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/page/helper/DefaultInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * * Copyright 2018-present KunMinX 4 | * * 5 | * * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * * you may not use this file except in compliance with the License. 7 | * * You may obtain a copy of the License at 8 | * * 9 | * * http://www.apache.org/licenses/LICENSE-2.0 10 | * * 11 | * * Unless required by applicable law or agreed to in writing, software 12 | * * distributed under the License is distributed on an "AS IS" BASIS, 13 | * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * * See the License for the specific language governing permissions and 15 | * * limitations under the License. 16 | * 17 | */ 18 | 19 | package com.kunminx.puremusic.ui.page.helper; 20 | 21 | import android.view.View; 22 | import android.widget.SeekBar; 23 | 24 | import com.sothree.slidinguppanel.SlidingUpPanelLayout; 25 | 26 | /** 27 | * Create by KunMinX at 2020/12/3 28 | */ 29 | public class DefaultInterface { 30 | 31 | public interface OnSeekBarChangeListener extends SeekBar.OnSeekBarChangeListener { 32 | @Override 33 | default void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 34 | } 35 | 36 | @Override 37 | default void onStartTrackingTouch(SeekBar seekBar) { 38 | } 39 | 40 | @Override 41 | default void onStopTrackingTouch(SeekBar seekBar) { 42 | } 43 | } 44 | 45 | public interface PanelSlideListener extends SlidingUpPanelLayout.PanelSlideListener { 46 | @Override 47 | default void onPanelSlide(View panel, float slideOffset) { 48 | } 49 | 50 | @Override 51 | default void onPanelStateChanged(View panel, 52 | SlidingUpPanelLayout.PanelState previousState, 53 | SlidingUpPanelLayout.PanelState newState) { 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseDrawable.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.ColorFilter; 9 | import android.graphics.Paint; 10 | import android.graphics.Path; 11 | import android.graphics.PixelFormat; 12 | import android.graphics.Rect; 13 | import android.graphics.RectF; 14 | import android.graphics.drawable.Drawable; 15 | import android.util.Property; 16 | 17 | import androidx.annotation.ColorInt; 18 | 19 | public class PlayPauseDrawable extends Drawable { 20 | 21 | private static final Property<PlayPauseDrawable, Float> PROGRESS = new Property<PlayPauseDrawable, Float>(Float.class, "progress") { 22 | @Override 23 | public Float get(PlayPauseDrawable d) { 24 | return d.getProgress(); 25 | } 26 | 27 | @Override 28 | public void set(PlayPauseDrawable d, Float value) { 29 | d.setProgress(value); 30 | } 31 | }; 32 | 33 | private final Path mLeftPauseBar = new Path(); 34 | private final Path mRightPauseBar = new Path(); 35 | private final Paint mPaint = new Paint(); 36 | private final RectF mBounds = new RectF(); 37 | private float mPauseBarWidth; 38 | private float mPauseBarHeight; 39 | private float mPauseBarDistance; 40 | 41 | private float mWidth; 42 | private float mHeight; 43 | 44 | private float mProgress; 45 | private boolean mIsPlay; 46 | 47 | public PlayPauseDrawable() { 48 | mPaint.setAntiAlias(true); 49 | mPaint.setStyle(Paint.Style.FILL); 50 | mPaint.setColor(Color.BLACK); 51 | } 52 | 53 | public PlayPauseDrawable(@ColorInt int color) { 54 | mPaint.setAntiAlias(true); 55 | mPaint.setStyle(Paint.Style.FILL); 56 | mPaint.setColor(color); 57 | } 58 | 59 | private static float interpolate(float a, float b, float t) { 60 | return a + (b - a) * t; 61 | } 62 | 63 | public void setIsPlay(boolean isPlay) { 64 | this.mIsPlay = isPlay; 65 | } 66 | 67 | @Override 68 | protected void onBoundsChange(Rect bounds) { 69 | super.onBoundsChange(bounds); 70 | mBounds.set(bounds); 71 | mWidth = mBounds.width(); 72 | mHeight = mBounds.height(); 73 | 74 | mPauseBarWidth = mWidth / 8; 75 | mPauseBarHeight = mHeight * 0.40f; 76 | mPauseBarDistance = mPauseBarWidth; 77 | 78 | } 79 | 80 | @Override 81 | public void draw(Canvas canvas) { 82 | mLeftPauseBar.rewind(); 83 | mRightPauseBar.rewind(); 84 | 85 | final float barDist = interpolate(mPauseBarDistance, 0, mProgress); 86 | final float barWidth = interpolate(mPauseBarWidth, mPauseBarHeight / 2f, mProgress); 87 | final float firstBarTopLeft = interpolate(0, barWidth, mProgress); 88 | final float secondBarTopRight = interpolate(2 * barWidth + barDist, barWidth + barDist, mProgress); 89 | 90 | mLeftPauseBar.moveTo(0, 0); 91 | mLeftPauseBar.lineTo(firstBarTopLeft, -mPauseBarHeight); 92 | mLeftPauseBar.lineTo(barWidth, -mPauseBarHeight); 93 | mLeftPauseBar.lineTo(barWidth, 0); 94 | mLeftPauseBar.close(); 95 | 96 | mRightPauseBar.moveTo(barWidth + barDist, 0); 97 | mRightPauseBar.lineTo(barWidth + barDist, -mPauseBarHeight); 98 | mRightPauseBar.lineTo(secondBarTopRight, -mPauseBarHeight); 99 | mRightPauseBar.lineTo(2 * barWidth + barDist, 0); 100 | mRightPauseBar.close(); 101 | 102 | canvas.save(); 103 | 104 | canvas.translate(interpolate(0, mPauseBarHeight / 8f, mProgress), 0); 105 | 106 | final float rotationProgress = mIsPlay ? 1 - mProgress : mProgress; 107 | final float startingRotation = mIsPlay ? 90 : 0; 108 | canvas.rotate(interpolate(startingRotation, startingRotation + 90, rotationProgress), mWidth / 2f, mHeight / 2f); 109 | 110 | canvas.translate(mWidth / 2f - ((2 * barWidth + barDist) / 2f), mHeight / 2f + (mPauseBarHeight / 2f)); 111 | 112 | canvas.drawPath(mLeftPauseBar, mPaint); 113 | canvas.drawPath(mRightPauseBar, mPaint); 114 | 115 | canvas.restore(); 116 | } 117 | 118 | public Animator getPausePlayAnimator() { 119 | final Animator anim = ObjectAnimator.ofFloat(this, PROGRESS, mIsPlay ? 1 : 0, mIsPlay ? 0 : 1); 120 | anim.addListener(new AnimatorListenerAdapter() { 121 | @Override 122 | public void onAnimationEnd(Animator animation) { 123 | mIsPlay = !mIsPlay; 124 | } 125 | }); 126 | return anim; 127 | } 128 | 129 | public boolean isPlay() { 130 | return mIsPlay; 131 | } 132 | 133 | private float getProgress() { 134 | return mProgress; 135 | } 136 | 137 | private void setProgress(float progress) { 138 | mProgress = progress; 139 | invalidateSelf(); 140 | } 141 | 142 | @Override 143 | public void setAlpha(int alpha) { 144 | mPaint.setAlpha(alpha); 145 | invalidateSelf(); 146 | } 147 | 148 | public void setDrawableColor(@ColorInt int color) { 149 | mPaint.setColor(color); 150 | invalidateSelf(); 151 | } 152 | 153 | @Override 154 | public void setColorFilter(ColorFilter cf) { 155 | mPaint.setColorFilter(cf); 156 | invalidateSelf(); 157 | } 158 | 159 | @Override 160 | public int getOpacity() { 161 | return PixelFormat.TRANSLUCENT; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/kunminx/puremusic/ui/view/PlayPauseView.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic.ui.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.content.Context; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Outline; 10 | import android.graphics.Paint; 11 | import android.graphics.drawable.Drawable; 12 | import android.util.AttributeSet; 13 | import android.view.View; 14 | import android.view.ViewOutlineProvider; 15 | import android.view.animation.DecelerateInterpolator; 16 | import android.widget.FrameLayout; 17 | 18 | import androidx.annotation.ColorInt; 19 | import androidx.annotation.NonNull; 20 | 21 | import com.kunminx.puremusic.R; 22 | 23 | public class PlayPauseView extends FrameLayout { 24 | 25 | private static final long PLAY_PAUSE_ANIMATION_DURATION = 200; 26 | public final boolean isDrawCircle; 27 | private final PlayPauseDrawable mDrawable; 28 | private final Paint mPaint = new Paint(); 29 | public int circleAlpha; 30 | private int mDrawableColor; 31 | private AnimatorSet mAnimatorSet; 32 | private int mBackgroundColor; 33 | private int mWidth; 34 | private int mHeight; 35 | private boolean mIsPlay; 36 | 37 | public PlayPauseView(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | setWillNotDraw(false); 40 | 41 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PlayPauseView); 42 | isDrawCircle = typedArray.getBoolean(R.styleable.PlayPauseView_isCircleDraw, true); 43 | circleAlpha = typedArray.getInt(R.styleable.PlayPauseView_circleAlpha, 255); 44 | mDrawableColor = typedArray.getInt(R.styleable.PlayPauseView_drawableColor, Color.WHITE); 45 | typedArray.recycle(); 46 | 47 | mPaint.setAntiAlias(true); 48 | mPaint.setStyle(Paint.Style.FILL); 49 | mPaint.setAlpha(circleAlpha); 50 | mPaint.setColor(mBackgroundColor); 51 | mDrawable = new PlayPauseDrawable(mDrawableColor); 52 | mDrawable.setCallback(this); 53 | 54 | } 55 | 56 | @Override 57 | protected void onSizeChanged(final int w, final int h, int oldw, int oldh) { 58 | super.onSizeChanged(w, h, oldw, oldh); 59 | mDrawable.setBounds(0, 0, w, h); 60 | mWidth = w; 61 | mHeight = h; 62 | 63 | setOutlineProvider(new ViewOutlineProvider() { 64 | @Override 65 | public void getOutline(View view, Outline outline) { 66 | outline.setOval(0, 0, view.getWidth(), view.getHeight()); 67 | } 68 | }); 69 | setClipToOutline(true); 70 | } 71 | 72 | public void setCircleAlpha(int alpah) { 73 | circleAlpha = alpah; 74 | invalidate(); 75 | } 76 | 77 | private int getCircleColor() { 78 | return mBackgroundColor; 79 | } 80 | 81 | public void setCircleColor(@ColorInt int color) { 82 | mBackgroundColor = color; 83 | invalidate(); 84 | } 85 | 86 | public int getDrawableColor() { 87 | return mDrawableColor; 88 | } 89 | 90 | public void setDrawableColor(@ColorInt int color) { 91 | mDrawableColor = color; 92 | mDrawable.setDrawableColor(color); 93 | invalidate(); 94 | } 95 | 96 | @Override 97 | protected boolean verifyDrawable(@NonNull Drawable who) { 98 | return who == mDrawable || super.verifyDrawable(who); 99 | } 100 | 101 | @Override 102 | public boolean hasOverlappingRendering() { 103 | return false; 104 | } 105 | 106 | @Override 107 | protected void onDraw(Canvas canvas) { 108 | super.onDraw(canvas); 109 | mPaint.setColor(mBackgroundColor); 110 | final float radius = Math.min(mWidth, mHeight) / 2f; 111 | if (isDrawCircle) { 112 | mPaint.setColor(mBackgroundColor); 113 | mPaint.setAlpha(circleAlpha); 114 | canvas.drawCircle(mWidth / 2f, mHeight / 2f, radius, mPaint); 115 | } 116 | mDrawable.draw(canvas); 117 | } 118 | 119 | public boolean isPlay() { 120 | return mIsPlay; 121 | } 122 | 123 | public void play() { 124 | if (mAnimatorSet != null) { 125 | mAnimatorSet.cancel(); 126 | } 127 | mAnimatorSet = new AnimatorSet(); 128 | mDrawable.setIsPlay(mIsPlay = true); 129 | final Animator pausePlayAnim = mDrawable.getPausePlayAnimator(); 130 | mAnimatorSet.setInterpolator(new DecelerateInterpolator()); 131 | mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION); 132 | pausePlayAnim.start(); 133 | } 134 | 135 | public void pause() { 136 | if (mAnimatorSet != null) { 137 | mAnimatorSet.cancel(); 138 | } 139 | mAnimatorSet = new AnimatorSet(); 140 | mDrawable.setIsPlay(mIsPlay = false); 141 | final Animator pausePlayAnim = mDrawable.getPausePlayAnimator(); 142 | mAnimatorSet.setInterpolator(new DecelerateInterpolator()); 143 | mAnimatorSet.setDuration(PLAY_PAUSE_ANIMATION_DURATION); 144 | pausePlayAnim.start(); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_enter.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:duration="150"> 4 | <translate 5 | android:fromXDelta="10%p" 6 | android:interpolator="@android:anim/decelerate_interpolator" 7 | android:toXDelta="0" /> 8 | 9 | <alpha 10 | android:fromAlpha="0" 11 | android:toAlpha="1.0" /> 12 | </set> -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_exit.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:duration="200"> 4 | <translate 5 | android:fromXDelta="0" 6 | android:interpolator="@android:anim/accelerate_interpolator" 7 | android:toXDelta="-10%p" /> 8 | </set> -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_pop_enter.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:duration="200"> 4 | <translate 5 | android:fromXDelta="-10%p" 6 | android:toXDelta="0" /> 7 | 8 | </set> -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_pop_exit.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:duration="200"> 4 | <translate 5 | android:fromXDelta="0" 6 | android:toXDelta="10%p" /> 7 | 8 | <alpha 9 | android:fromAlpha="1.0" 10 | android:toAlpha="0" /> 11 | </set> -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/bg_album_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/bg_album_default.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_action_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_action_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_close_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_next_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_next_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_player.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_previous_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_previous_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable-xxhdpi/ic_progress.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bar_selector_white.xml: -------------------------------------------------------------------------------- 1 | <!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 18 | <item android:state_pressed="true"> 19 | <shape android:shape="rectangle"> 20 | <solid android:color="#40ffffff" /> 21 | </shape> 22 | </item> 23 | <item android:state_focused="true"> 24 | <shape android:shape="rectangle"> 25 | <solid android:color="#40ffffff" /> 26 | </shape> 27 | </item> 28 | <item android:state_selected="true"> 29 | <shape android:shape="rectangle"> 30 | <solid android:color="#40ffffff" /> 31 | </shape> 32 | </item> 33 | <item android:drawable="@color/transparent" /> 34 | </selector> -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/app/src/main/res/drawable/bg_home.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_black_48dp.xml: -------------------------------------------------------------------------------- 1 | <vector android:height="48dp" 2 | android:tint="#666666" 3 | android:viewportHeight="24.0" 4 | android:viewportWidth="24.0" 5 | android:width="48dp" 6 | xmlns:android="http://schemas.android.com/apk/res/android"> 7 | <path 8 | android:fillColor="#FF000000" 9 | android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" /> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music_note_black_48dp.xml: -------------------------------------------------------------------------------- 1 | <vector android:height="48dp" 2 | android:tint="#666666" 3 | android:viewportHeight="24.0" 4 | android:viewportWidth="24.0" 5 | android:width="48dp" 6 | xmlns:android="http://schemas.android.com/apk/res/android"> 7 | <path 8 | android:fillColor="#FF000000" 9 | android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z" /> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_black_48dp.xml: -------------------------------------------------------------------------------- 1 | <vector android:height="48dp" 2 | android:tint="#666666" 3 | android:viewportHeight="24.0" 4 | android:viewportWidth="24.0" 5 | android:width="48dp" 6 | xmlns:android="http://schemas.android.com/apk/res/android"> 7 | <path 8 | android:fillColor="#FF000000" 9 | android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" /> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/loading_animation.xml: -------------------------------------------------------------------------------- 1 | <rotate xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:drawable="@drawable/ic_progress" 3 | android:fromDegrees="0" 4 | android:pivotX="50%" 5 | android:pivotY="50%" 6 | android:toDegrees="360" /> -------------------------------------------------------------------------------- /app/src/main/res/drawable/progressbar_color.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:id="@+id/progress"> 4 | <scale 5 | android:scaleWidth="100%" 6 | android:scaleGravity="left"> 7 | <shape> 8 | <corners android:radius="0.0dip" /> 9 | <gradient 10 | android:angle="270.0" 11 | android:centerColor="#ffaaaaaa" 12 | android:centerY="0.75" 13 | android:endColor="#ffaaaaaa" 14 | android:startColor="#ffaaaaaa" /> 15 | </shape> 16 | </scale> 17 | </item> 18 | <item> 19 | <scale 20 | android:scaleWidth="100%" 21 | android:scaleGravity="fill_horizontal"> 22 | <shape> 23 | <corners android:radius="0.0dip" /> 24 | <gradient 25 | android:angle="270.0" 26 | android:centerColor="#10000000" 27 | android:centerY="0.75" 28 | android:endColor="#10000000" 29 | android:startColor="#10000000" /> 30 | </shape> 31 | </scale> 32 | </item> 33 | 34 | </layer-list> -------------------------------------------------------------------------------- /app/src/main/res/layout-land/activity_main.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto"> 19 | 20 | <data> 21 | 22 | <variable 23 | name="vm" 24 | type="com.kunminx.puremusic.MainActivity.MainActivityStates" /> 25 | 26 | </data> 27 | 28 | 29 | <LinearLayout 30 | android:layout_width="match_parent" 31 | android:layout_height="match_parent" 32 | android:baselineAligned="false" 33 | android:orientation="horizontal"> 34 | 35 | <fragment 36 | android:id="@+id/main_fragment_host" 37 | android:name="androidx.navigation.fragment.NavHostFragment" 38 | android:layout_width="0dp" 39 | android:layout_height="match_parent" 40 | android:layout_marginTop="-25dp" 41 | android:layout_weight="1" 42 | android:fitsSystemWindows="true" 43 | app:defaultNavHost="true" 44 | app:navGraph="@navigation/nav_main" /> 45 | 46 | <fragment 47 | android:id="@+id/slide_fragment_host" 48 | android:name="androidx.navigation.fragment.NavHostFragment" 49 | android:layout_width="0dp" 50 | android:layout_height="match_parent" 51 | android:layout_weight="1" 52 | android:fitsSystemWindows="true" 53 | app:defaultNavHost="true" 54 | app:navGraph="@navigation/nav_slide" /> 55 | 56 | </LinearLayout> 57 | </layout> 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fragment_main.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto"> 19 | 20 | <data> 21 | 22 | <variable 23 | name="click" 24 | type="com.kunminx.puremusic.ui.page.MainFragment.ClickProxy" /> 25 | 26 | <variable 27 | name="vm" 28 | type="com.kunminx.puremusic.ui.page.MainFragment.MainStates" /> 29 | 30 | <variable 31 | name="adapter" 32 | type="androidx.recyclerview.widget.RecyclerView.Adapter" /> 33 | 34 | </data> 35 | 36 | <androidx.coordinatorlayout.widget.CoordinatorLayout 37 | android:layout_width="match_parent" 38 | android:layout_height="match_parent" 39 | android:fitsSystemWindows="true" 40 | android:overScrollMode="never"> 41 | 42 | <com.google.android.material.appbar.AppBarLayout 43 | android:id="@+id/appbar_layout" 44 | android:layout_width="match_parent" 45 | android:layout_height="wrap_content" 46 | android:fitsSystemWindows="true" 47 | android:theme="@style/AppTheme"> 48 | 49 | <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖 50 | 此处只是为了便于展示 BindingAdapter 的业务能力,而使用的语法糖, 51 | 在实际开发中,BindingAdapter 的通知时机和 TabLayout 的某些 52 | 机制并不完美配合,导致可能出现 TabLayout 和 ViewPager 联动失效、 53 | ViewPager child 不显示内容等问题 --> 54 | 55 | <com.google.android.material.tabs.TabLayout 56 | android:id="@+id/tab_layout" 57 | android:layout_width="match_parent" 58 | android:layout_height="48dp" 59 | android:background="@color/white" 60 | app:tabBackground="@color/white" 61 | app:tabIndicatorColor="@color/gray" 62 | app:tabIndicatorFullWidth="true" 63 | app:tabIndicatorHeight="4dp" 64 | app:tabSelectedTextColor="@color/gray" 65 | app:tabTextColor="@color/light_gray"> 66 | 67 | <com.google.android.material.tabs.TabItem 68 | android:layout_width="wrap_content" 69 | android:layout_height="wrap_content" 70 | android:text="@string/recently" /> 71 | 72 | <com.google.android.material.tabs.TabItem 73 | android:layout_width="wrap_content" 74 | android:layout_height="wrap_content" 75 | android:text="@string/best_practice" /> 76 | 77 | </com.google.android.material.tabs.TabLayout> 78 | 79 | </com.google.android.material.appbar.AppBarLayout> 80 | 81 | <!-- TODO 建议不要使用如下 TabLayout 和 ViewPager 的嵌套式语法糖 82 | 此处只是为了便于展示 BindingAdapter 的业务能力,而使用的语法糖, 83 | 在实际开发中,BindingAdapter 的通知时机和 TabLayout 的某些 84 | 机制并不完美配合,导致可能出现 TabLayout 和 ViewPager 联动失效、 85 | ViewPager child 不显示内容等问题 --> 86 | 87 | <androidx.viewpager.widget.ViewPager 88 | android:id="@+id/view_pager" 89 | initTabAndPage="@{true}" 90 | android:layout_width="match_parent" 91 | android:layout_height="match_parent" 92 | app:layout_behavior="@string/appbar_scrolling_view_behavior"> 93 | 94 | <!-- TODO 以下 adapter 和 sumbitList 属性皆乃 BindingAdapter 中定义的属性, 95 | 旨在将 ViewModel 中的数据绑定到 BindingAdapter, 96 | 以便可以间接通知布局中存在的视图实例,避免空指针安全问题, 97 | 如果这样说还不理解的话,可参考 DataBinding 篇的解析 98 | https://xiaozhuanlan.com/topic/9816742350 --> 99 | 100 | <!-- TODO 该 BindingAdapter 现已抽到 Strict-DataBinding 开源库中独立维护 101 | 可在本项目中搜索 RecyclerViewBindingAdapter 找到--> 102 | 103 | <androidx.recyclerview.widget.RecyclerView 104 | android:id="@+id/rv" 105 | adapter="@{adapter}" 106 | submitList="@{vm.list}" 107 | android:layout_width="match_parent" 108 | android:layout_height="match_parent" 109 | android:clipToPadding="false" 110 | app:layoutManager="com.kunminx.binding_recyclerview.layout_manager.WrapContentLinearLayoutManager" /> 111 | 112 | <WebView 113 | android:id="@+id/web_view" 114 | pageAssetPath="@{vm.pageAssetPath}" 115 | android:layout_width="match_parent" 116 | android:layout_height="match_parent" 117 | android:clipToPadding="false" /> 118 | 119 | </androidx.viewpager.widget.ViewPager> 120 | 121 | </androidx.coordinatorlayout.widget.CoordinatorLayout> 122 | </layout> 123 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/fragment_player.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto" 19 | xmlns:playpauseview="http://schemas.android.com/apk/res-auto"> 20 | 21 | <data> 22 | 23 | <variable 24 | name="click" 25 | type="com.kunminx.puremusic.ui.page.PlayerFragment.ClickProxy" /> 26 | 27 | <variable 28 | name="listener" 29 | type="com.kunminx.puremusic.ui.page.PlayerFragment.ListenerHandler" /> 30 | 31 | <variable 32 | name="vm" 33 | type="com.kunminx.puremusic.ui.page.PlayerFragment.PlayerStates" /> 34 | 35 | </data> 36 | 37 | <androidx.coordinatorlayout.widget.CoordinatorLayout 38 | android:id="@+id/topContainer" 39 | android:layout_width="match_parent" 40 | android:layout_height="match_parent" 41 | android:layout_gravity="top"> 42 | 43 | <androidx.appcompat.widget.AppCompatImageView 44 | android:id="@+id/album_art" 45 | imageUrl="@{vm.coverImg}" 46 | placeHolder="@{vm.placeHolder}" 47 | android:layout_width="@dimen/sliding_up_header_land" 48 | android:layout_height="@dimen/sliding_up_header_land" 49 | android:layout_gravity="center_horizontal" 50 | android:layout_marginTop="48dp" 51 | android:scaleType="centerCrop" 52 | android:src="@drawable/bg_album_default" /> 53 | 54 | <androidx.constraintlayout.widget.ConstraintLayout 55 | android:id="@+id/icon_container" 56 | android:layout_width="match_parent" 57 | android:layout_height="wrap_content" 58 | android:layout_gravity="bottom" 59 | android:layout_marginTop="10dp" 60 | android:layout_marginBottom="48dp" 61 | android:orientation="horizontal"> 62 | 63 | <net.steamcrafted.materialiconlib.MaterialIconView 64 | android:id="@+id/previous" 65 | android:layout_width="36dp" 66 | android:layout_height="36dp" 67 | android:layout_marginEnd="24dp" 68 | android:background="?attr/selectableItemBackgroundBorderless" 69 | android:onClick="@{()->click.previous()}" 70 | android:scaleType="center" 71 | app:layout_constraintBottom_toBottomOf="parent" 72 | app:layout_constraintRight_toLeftOf="@+id/play_pause" 73 | app:layout_constraintTop_toTopOf="parent" 74 | app:materialIcon="skip_previous" 75 | app:materialIconColor="@android:color/black" 76 | app:materialIconSize="28dp" /> 77 | 78 | <com.kunminx.puremusic.ui.view.PlayPauseView 79 | android:id="@+id/play_pause" 80 | isPlaying="@{vm.isPlaying}" 81 | onClickWithDebouncing="@{()->click.togglePlay()}" 82 | android:layout_width="36dp" 83 | android:layout_height="36dp" 84 | android:clickable="true" 85 | android:focusable="true" 86 | android:foreground="?attr/selectableItemBackground" 87 | app:layout_constraintBottom_toBottomOf="parent" 88 | app:layout_constraintLeft_toLeftOf="parent" 89 | app:layout_constraintRight_toRightOf="parent" 90 | app:layout_constraintTop_toTopOf="parent" 91 | playpauseview:drawableColor="@color/white" 92 | playpauseview:isCircleDraw="true" /> 93 | 94 | <net.steamcrafted.materialiconlib.MaterialIconView 95 | android:id="@+id/next" 96 | android:layout_width="36dp" 97 | android:layout_height="36dp" 98 | android:layout_marginStart="24dp" 99 | android:background="?attr/selectableItemBackgroundBorderless" 100 | android:onClick="@{()->click.next()}" 101 | android:scaleType="center" 102 | app:layout_constraintBottom_toBottomOf="parent" 103 | app:layout_constraintLeft_toRightOf="@+id/play_pause" 104 | app:layout_constraintTop_toTopOf="parent" 105 | app:materialIcon="skip_next" 106 | app:materialIconColor="@android:color/black" 107 | app:materialIconSize="28dp" /> 108 | 109 | </androidx.constraintlayout.widget.ConstraintLayout> 110 | 111 | <SeekBar 112 | android:id="@+id/seek_bottom" 113 | android:layout_width="match_parent" 114 | android:layout_height="wrap_content" 115 | android:layout_gravity="bottom" 116 | android:background="@color/transparent" 117 | android:clickable="true" 118 | android:focusable="true" 119 | android:max="@{vm.maxSeekDuration}" 120 | android:minHeight="6dp" 121 | android:paddingTop="24dp" 122 | android:progress="@{vm.currentSeekPosition}" 123 | android:progressDrawable="@drawable/progressbar_color" 124 | android:thumb="@null" 125 | android:visibility="visible" 126 | app:onSeekBarChangeListener="@{listener}" /> 127 | 128 | </androidx.coordinatorlayout.widget.CoordinatorLayout> 129 | </layout> 130 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | xmlns:sothree="http://schemas.android.com/apk/res-auto"> 5 | 6 | <data> 7 | 8 | <variable 9 | name="vm" 10 | type="com.kunminx.puremusic.MainActivity.MainActivityStates" /> 11 | 12 | <variable 13 | name="listener" 14 | type="com.kunminx.puremusic.MainActivity.ListenerHandler" /> 15 | 16 | </data> 17 | 18 | <androidx.drawerlayout.widget.DrawerLayout 19 | android:id="@+id/dl" 20 | allowDrawerOpen="@{vm.allowDrawerOpen}" 21 | bindDrawerListener="@{listener}" 22 | isOpenDrawer="@{vm.openDrawer}" 23 | android:layout_width="match_parent" 24 | android:layout_height="match_parent"> 25 | 26 | <com.sothree.slidinguppanel.SlidingUpPanelLayout 27 | android:id="@+id/sliding_layout" 28 | android:layout_width="match_parent" 29 | android:layout_height="match_parent" 30 | android:gravity="bottom" 31 | sothree:umanoDragView="@+id/slide_fragment_host" 32 | sothree:umanoOverlay="false" 33 | sothree:umanoPanelHeight="@dimen/sliding_up_header" 34 | sothree:umanoShadowHeight="5dp"> 35 | 36 | <fragment 37 | android:id="@+id/main_fragment_host" 38 | android:name="androidx.navigation.fragment.NavHostFragment" 39 | android:layout_width="match_parent" 40 | android:layout_height="match_parent" 41 | android:fitsSystemWindows="true" 42 | app:defaultNavHost="true" 43 | app:navGraph="@navigation/nav_main" /> 44 | 45 | <fragment 46 | android:id="@+id/slide_fragment_host" 47 | android:name="androidx.navigation.fragment.NavHostFragment" 48 | android:layout_width="match_parent" 49 | android:layout_height="match_parent" 50 | android:fitsSystemWindows="true" 51 | app:defaultNavHost="true" 52 | app:navGraph="@navigation/nav_slide" /> 53 | 54 | </com.sothree.slidinguppanel.SlidingUpPanelLayout> 55 | 56 | <fragment 57 | android:id="@+id/drawer_fragment_host" 58 | android:name="androidx.navigation.fragment.NavHostFragment" 59 | android:layout_width="330dp" 60 | android:layout_height="match_parent" 61 | android:layout_gravity="start" 62 | android:fitsSystemWindows="true" 63 | app:defaultNavHost="true" 64 | app:navGraph="@navigation/nav_drawer" /> 65 | 66 | </androidx.drawerlayout.widget.DrawerLayout> 67 | 68 | </layout> 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_library.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:tools="http://schemas.android.com/tools"> 19 | 20 | <data> 21 | 22 | <variable 23 | name="info" 24 | type="com.kunminx.puremusic.data.bean.LibraryInfo" /> 25 | 26 | </data> 27 | 28 | <LinearLayout 29 | android:layout_width="match_parent" 30 | android:layout_height="wrap_content" 31 | android:background="?attr/selectableItemBackground" 32 | android:orientation="vertical"> 33 | 34 | <TextView 35 | android:id="@+id/tv_title" 36 | android:layout_width="match_parent" 37 | android:layout_height="wrap_content" 38 | android:layout_marginStart="16dp" 39 | android:layout_marginTop="12dp" 40 | android:text="@{info.title}" 41 | android:textColor="@color/black" 42 | android:textSize="18sp" 43 | android:textStyle="bold" 44 | tools:text="@string/project_title" /> 45 | 46 | <TextView 47 | android:id="@+id/tv_summary" 48 | android:layout_width="match_parent" 49 | android:layout_height="wrap_content" 50 | android:layout_marginStart="16dp" 51 | android:layout_marginTop="4dp" 52 | android:layout_marginEnd="16dp" 53 | android:layout_marginBottom="12dp" 54 | android:text="@{info.summary}" 55 | android:textColor="@color/light_gray" 56 | android:textSize="13sp" 57 | tools:text="@string/app_summary" /> 58 | 59 | </LinearLayout> 60 | </layout> -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_play_item.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto" 19 | xmlns:tools="http://schemas.android.com/tools"> 20 | 21 | <data> 22 | 23 | <variable 24 | name="album" 25 | type="com.kunminx.puremusic.data.bean.TestAlbum.TestMusic" /> 26 | 27 | </data> 28 | 29 | <androidx.constraintlayout.widget.ConstraintLayout 30 | android:id="@+id/root_view" 31 | android:layout_width="match_parent" 32 | android:layout_height="72dp" 33 | android:orientation="vertical" 34 | tools:background="@color/light_gray"> 35 | 36 | <androidx.appcompat.widget.AppCompatImageView 37 | android:id="@+id/iv_cover" 38 | imageUrl="@{album.coverImg}" 39 | android:layout_width="56dp" 40 | android:layout_height="56dp" 41 | android:layout_marginStart="12dp" 42 | android:scaleType="centerCrop" 43 | app:layout_constraintBottom_toBottomOf="parent" 44 | app:layout_constraintLeft_toLeftOf="parent" 45 | app:layout_constraintTop_toTopOf="parent" 46 | tools:src="@drawable/bg_home" /> 47 | 48 | <LinearLayout 49 | android:layout_width="wrap_content" 50 | android:layout_height="wrap_content" 51 | android:orientation="vertical" 52 | app:layout_constraintBottom_toBottomOf="parent" 53 | app:layout_constraintLeft_toRightOf="@+id/iv_cover" 54 | app:layout_constraintTop_toTopOf="parent"> 55 | 56 | <TextView 57 | android:id="@+id/tv_title" 58 | android:layout_width="wrap_content" 59 | android:layout_height="wrap_content" 60 | android:layout_marginStart="12dp" 61 | android:text="@{album.title}" 62 | android:textSize="18sp" 63 | tools:text="title" /> 64 | 65 | <TextView 66 | android:id="@+id/tv_artist" 67 | android:layout_width="wrap_content" 68 | android:layout_height="wrap_content" 69 | android:layout_marginStart="12dp" 70 | android:layout_marginTop="4dp" 71 | android:text="@{album.artist.name}" 72 | android:textSize="14sp" 73 | tools:text="summary" /> 74 | 75 | </LinearLayout> 76 | 77 | <net.steamcrafted.materialiconlib.MaterialIconView 78 | android:id="@+id/iv_play_status" 79 | android:layout_width="36dp" 80 | android:layout_height="36dp" 81 | android:layout_marginEnd="12dp" 82 | android:background="?attr/selectableItemBackgroundBorderless" 83 | android:scaleType="center" 84 | app:layout_constraintBottom_toBottomOf="parent" 85 | app:layout_constraintRight_toRightOf="parent" 86 | app:layout_constraintTop_toTopOf="parent" 87 | app:materialIcon="music_note" 88 | app:materialIconColor="@color/gray" 89 | app:materialIconSize="28dp" /> 90 | 91 | </androidx.constraintlayout.widget.ConstraintLayout> 92 | </layout> -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_drawer.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto"> 19 | 20 | <data> 21 | 22 | <variable 23 | name="vm" 24 | type="com.kunminx.puremusic.ui.page.DrawerFragment.DrawerStates" /> 25 | 26 | <variable 27 | name="click" 28 | type="com.kunminx.puremusic.ui.page.DrawerFragment.ClickProxy" /> 29 | 30 | <variable 31 | name="adapter" 32 | type="androidx.recyclerview.widget.RecyclerView.Adapter" /> 33 | </data> 34 | 35 | <androidx.constraintlayout.widget.ConstraintLayout 36 | android:layout_width="match_parent" 37 | android:layout_height="match_parent" 38 | android:background="@color/white"> 39 | 40 | <androidx.appcompat.widget.AppCompatImageView 41 | android:id="@+id/iv_logo" 42 | android:layout_width="100dp" 43 | android:layout_height="100dp" 44 | android:layout_marginTop="40dp" 45 | android:onClick="@{()->click.logoClick()}" 46 | android:src="@drawable/ic_launcher" 47 | app:layout_constraintLeft_toLeftOf="parent" 48 | app:layout_constraintRight_toRightOf="parent" 49 | app:layout_constraintTop_toTopOf="parent" /> 50 | 51 | <TextView 52 | android:id="@+id/tv_app" 53 | android:layout_width="wrap_content" 54 | android:layout_height="wrap_content" 55 | android:layout_marginTop="16dp" 56 | android:background="?attr/selectableItemBackground" 57 | android:onClick="@{()->click.logoClick()}" 58 | android:text="@string/app_name" 59 | android:textColor="@color/black" 60 | android:textSize="20sp" 61 | android:textStyle="bold" 62 | app:layout_constraintLeft_toLeftOf="parent" 63 | app:layout_constraintRight_toRightOf="parent" 64 | app:layout_constraintTop_toBottomOf="@+id/iv_logo" /> 65 | 66 | <TextView 67 | android:id="@+id/tv_summary" 68 | android:layout_width="wrap_content" 69 | android:layout_height="wrap_content" 70 | android:layout_marginTop="16dp" 71 | android:background="?attr/selectableItemBackground" 72 | android:onClick="@{()->click.logoClick()}" 73 | android:text="@string/app_summary" 74 | android:textColor="@color/light_gray" 75 | android:textSize="12sp" 76 | app:layout_constraintLeft_toLeftOf="parent" 77 | app:layout_constraintRight_toRightOf="parent" 78 | app:layout_constraintTop_toBottomOf="@+id/tv_app" /> 79 | 80 | <!-- TODO 以下 adapter 和 sumbitList 属性皆乃 BindingAdapter 中定义的属性, 81 | 旨在将 ViewModel 中的数据绑定到 BindingAdapter, 82 | 以便可以间接通知布局中存在的视图实例,避免空指针安全问题, 83 | 如果这样说还不理解的话,可参考 DataBinding 篇的解析 84 | https://xiaozhuanlan.com/topic/9816742350 --> 85 | 86 | <!-- TODO 该 BindingAdapter 现已抽到 Strict-DataBinding 开源库中独立维护 87 | 可在本项目中搜索 RecyclerViewBindingAdapter 找到--> 88 | 89 | <androidx.recyclerview.widget.RecyclerView 90 | android:id="@+id/rv" 91 | adapter="@{adapter}" 92 | submitList="@{vm.list}" 93 | android:layout_width="0dp" 94 | android:layout_height="0dp" 95 | android:layout_marginTop="24dp" 96 | app:layoutManager="com.kunminx.binding_recyclerview.layout_manager.WrapContentLinearLayoutManager" 97 | app:layout_constraintBottom_toTopOf="@+id/tv_copyright" 98 | app:layout_constraintLeft_toLeftOf="parent" 99 | app:layout_constraintRight_toRightOf="parent" 100 | app:layout_constraintTop_toBottomOf="@+id/tv_summary" /> 101 | 102 | <TextView 103 | android:id="@+id/tv_copyright" 104 | android:layout_width="0dp" 105 | android:layout_height="48dp" 106 | android:background="?attr/selectableItemBackground" 107 | android:gravity="center" 108 | android:onClick="@{()->click.logoClick()}" 109 | android:text="@string/Copyright" 110 | android:textColor="@color/light_gray" 111 | android:textSize="12sp" 112 | app:layout_constraintBottom_toBottomOf="parent" 113 | app:layout_constraintLeft_toLeftOf="parent" 114 | app:layout_constraintRight_toRightOf="parent" 115 | app:layout_constraintTop_toBottomOf="@+id/rv" /> 116 | 117 | </androidx.constraintlayout.widget.ConstraintLayout> 118 | </layout> 119 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <layout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | xmlns:tools="http://schemas.android.com/tools" 5 | tools:ignore="RtlSymmetry"> 6 | 7 | <data> 8 | 9 | <variable 10 | name="vm" 11 | type="com.kunminx.puremusic.ui.page.LoginFragment.LoginStates" /> 12 | 13 | <variable 14 | name="click" 15 | type="com.kunminx.puremusic.ui.page.LoginFragment.ClickProxy" /> 16 | 17 | </data> 18 | 19 | <androidx.constraintlayout.widget.ConstraintLayout 20 | android:layout_width="match_parent" 21 | android:layout_height="match_parent" 22 | android:background="@color/white"> 23 | 24 | <net.steamcrafted.materialiconlib.MaterialIconView 25 | android:id="@+id/btn_back" 26 | android:layout_width="24dp" 27 | android:layout_height="24dp" 28 | android:layout_marginStart="16dp" 29 | android:layout_marginTop="48dp" 30 | android:background="?attr/selectableItemBackgroundBorderless" 31 | android:onClick="@{()->click.back()}" 32 | android:scaleType="center" 33 | app:layout_constraintLeft_toLeftOf="parent" 34 | app:layout_constraintTop_toTopOf="parent" 35 | app:materialIcon="arrow_left" 36 | app:materialIconColor="@color/gray" 37 | app:materialIconSize="28dp" /> 38 | 39 | <TextView 40 | android:id="@+id/tv_title" 41 | android:layout_width="0dp" 42 | android:layout_height="wrap_content" 43 | android:layout_marginStart="12dp" 44 | android:layout_marginTop="120dp" 45 | android:gravity="center" 46 | android:text="@string/login_title" 47 | android:textColor="@color/black" 48 | android:textSize="20sp" 49 | app:layout_constraintLeft_toLeftOf="parent" 50 | app:layout_constraintRight_toRightOf="parent" 51 | app:layout_constraintTop_toTopOf="parent" /> 52 | 53 | <TextView 54 | android:id="@+id/tv_content" 55 | android:layout_width="0dp" 56 | android:layout_height="wrap_content" 57 | android:layout_marginStart="12dp" 58 | android:layout_marginTop="8dp" 59 | android:gravity="center" 60 | android:text="@string/login_content" 61 | android:textColor="@color/black" 62 | android:textSize="12sp" 63 | app:layout_constraintLeft_toLeftOf="parent" 64 | app:layout_constraintRight_toRightOf="parent" 65 | app:layout_constraintTop_toBottomOf="@+id/tv_title" /> 66 | 67 | <androidx.appcompat.widget.AppCompatEditText 68 | android:id="@+id/et_name" 69 | drawable_radius="@{12}" 70 | drawable_strokeColor="@{0xffeeeeee}" 71 | drawable_strokeWidth="@{1}" 72 | android:layout_width="0dp" 73 | android:layout_height="wrap_content" 74 | android:layout_marginStart="24dp" 75 | android:layout_marginTop="56dp" 76 | android:layout_marginEnd="24dp" 77 | android:hint="@string/user_name" 78 | android:inputType="text" 79 | android:paddingStart="12dp" 80 | android:singleLine="true" 81 | android:text="@={vm.name}" 82 | app:layout_constraintLeft_toLeftOf="parent" 83 | app:layout_constraintRight_toRightOf="parent" 84 | app:layout_constraintTop_toBottomOf="@+id/tv_content" /> 85 | 86 | <androidx.appcompat.widget.AppCompatEditText 87 | android:id="@+id/et_pwd" 88 | drawable_radius="@{12}" 89 | drawable_strokeColor="@{0xffeeeeee}" 90 | drawable_strokeWidth="@{1}" 91 | android:layout_width="0dp" 92 | android:layout_height="wrap_content" 93 | android:layout_marginStart="24dp" 94 | android:layout_marginTop="24dp" 95 | android:layout_marginEnd="24dp" 96 | android:hint="@string/user_password" 97 | android:inputType="textPassword" 98 | android:paddingStart="12dp" 99 | android:singleLine="true" 100 | android:text="@={vm.password}" 101 | app:layout_constraintLeft_toLeftOf="parent" 102 | app:layout_constraintRight_toRightOf="parent" 103 | app:layout_constraintTop_toBottomOf="@+id/et_name" /> 104 | 105 | <ProgressBar 106 | android:id="@+id/progress" 107 | style="@style/Widget.AppCompat.ProgressBar.Horizontal" 108 | visible="@{vm.loadingVisible}" 109 | android:layout_width="160dp" 110 | android:layout_height="wrap_content" 111 | android:layout_margin="24dp" 112 | android:indeterminate="true" 113 | app:layout_constraintLeft_toLeftOf="parent" 114 | app:layout_constraintRight_toRightOf="parent" 115 | app:layout_constraintTop_toBottomOf="@+id/et_pwd" /> 116 | 117 | <Button 118 | android:id="@+id/btn_login" 119 | drawable_radius="@{25}" 120 | drawable_solidColor="@{0xffFF7055}" 121 | android:layout_width="200dp" 122 | android:layout_height="wrap_content" 123 | android:layout_marginTop="24dp" 124 | android:onClick="@{()->click.login()}" 125 | android:text="@string/login" 126 | android:textColor="@color/white" 127 | android:textSize="18sp" 128 | android:textStyle="bold" 129 | app:layout_constraintLeft_toLeftOf="parent" 130 | app:layout_constraintRight_toRightOf="parent" 131 | app:layout_constraintTop_toBottomOf="@+id/progress" 132 | app:layout_goneMarginTop="72dp" /> 133 | 134 | </androidx.constraintlayout.widget.ConstraintLayout> 135 | </layout> 136 | -------------------------------------------------------------------------------- /app/src/main/res/layout/notify_player_small.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:tools="http://schemas.android.com/tools" 19 | android:layout_width="match_parent" 20 | android:layout_height="64dp" 21 | android:background="@color/light_gray"> 22 | 23 | <ImageView 24 | android:layout_width="match_parent" 25 | android:layout_height="wrap_content" 26 | android:src="@color/transparent" 27 | tools:ignore="ContentDescription" /> 28 | 29 | <ImageView 30 | android:id="@+id/player_album_art" 31 | android:layout_width="40dp" 32 | android:layout_height="40dp" 33 | android:layout_centerVertical="true" 34 | android:layout_marginStart="12dp" 35 | android:scaleType="fitXY" 36 | tools:ignore="ContentDescription" /> 37 | 38 | <LinearLayout 39 | android:layout_width="match_parent" 40 | android:layout_height="wrap_content" 41 | android:layout_centerVertical="true" 42 | android:layout_marginEnd="180dp" 43 | android:orientation="vertical"> 44 | 45 | <TextView 46 | android:id="@+id/player_song_name" 47 | android:layout_width="wrap_content" 48 | android:layout_height="wrap_content" 49 | android:layout_marginStart="64dp" 50 | android:ellipsize="end" 51 | android:gravity="top|start" 52 | android:singleLine="true" 53 | android:textColor="#ffffffff" 54 | android:textSize="16sp" /> 55 | 56 | <TextView 57 | android:id="@+id/player_author_name" 58 | android:layout_width="match_parent" 59 | android:layout_height="wrap_content" 60 | android:layout_marginStart="64dp" 61 | android:layout_marginTop="2dp" 62 | android:ellipsize="end" 63 | android:singleLine="true" 64 | android:textColor="@android:color/white" 65 | android:textSize="13sp" /> 66 | 67 | </LinearLayout> 68 | 69 | <ImageView 70 | android:id="@+id/player_close" 71 | android:layout_width="wrap_content" 72 | android:layout_height="wrap_content" 73 | android:layout_alignParentEnd="true" 74 | android:background="@drawable/bar_selector_white" 75 | android:padding="8dp" 76 | android:scaleType="center" 77 | android:src="@drawable/ic_close_white" 78 | tools:ignore="ContentDescription" /> 79 | 80 | <LinearLayout 81 | android:layout_width="wrap_content" 82 | android:layout_height="48dp" 83 | android:layout_alignParentEnd="true" 84 | android:layout_centerVertical="true" 85 | android:layout_marginEnd="40dp" 86 | android:gravity="center" 87 | android:orientation="horizontal"> 88 | 89 | <ProgressBar 90 | android:id="@+id/player_progress_bar" 91 | android:layout_width="30dp" 92 | android:layout_height="30dp" 93 | android:layout_gravity="center" 94 | android:layout_marginEnd="56dp" 95 | android:indeterminateDrawable="@drawable/loading_animation" 96 | android:indeterminateDuration="1500" /> 97 | 98 | <ImageView 99 | android:id="@+id/player_previous" 100 | android:layout_width="32dp" 101 | android:layout_height="32dp" 102 | android:layout_marginLeft="8dp" 103 | android:layout_marginRight="8dp" 104 | android:background="@drawable/bar_selector_white" 105 | android:scaleType="center" 106 | android:src="@drawable/ic_next_dark" 107 | tools:ignore="ContentDescription" /> 108 | 109 | <ImageView 110 | android:id="@+id/player_pause" 111 | android:layout_width="36dp" 112 | android:layout_height="36dp" 113 | android:layout_marginLeft="8dp" 114 | android:layout_marginRight="8dp" 115 | android:background="@drawable/bar_selector_white" 116 | android:scaleType="center" 117 | android:src="@drawable/ic_action_pause" 118 | tools:ignore="ContentDescription" /> 119 | 120 | <ImageView 121 | android:id="@+id/player_play" 122 | android:layout_width="36dp" 123 | android:layout_height="36dp" 124 | android:layout_marginLeft="8dp" 125 | android:layout_marginRight="8dp" 126 | android:background="@drawable/bar_selector_white" 127 | android:scaleType="center" 128 | android:src="@drawable/ic_action_play" 129 | android:visibility="gone" 130 | tools:ignore="ContentDescription" /> 131 | 132 | <ImageView 133 | android:id="@+id/player_next" 134 | android:layout_width="32dp" 135 | android:layout_height="32dp" 136 | android:layout_marginLeft="8dp" 137 | android:layout_marginRight="8dp" 138 | android:background="@drawable/bar_selector_white" 139 | android:scaleType="center" 140 | android:src="@drawable/ic_previous_dark" 141 | tools:ignore="ContentDescription" /> 142 | </LinearLayout> 143 | 144 | </RelativeLayout> -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_drawer.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <navigation xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto" 19 | xmlns:tools="http://schemas.android.com/tools" 20 | android:id="@+id/nav_drawer" 21 | app:startDestination="@id/drawerFragment"> 22 | 23 | <fragment 24 | android:id="@+id/drawerFragment" 25 | android:name="com.kunminx.puremusic.ui.page.DrawerFragment" 26 | android:label="fragment_drawer" 27 | tools:layout="@layout/fragment_drawer" /> 28 | </navigation> -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_main.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <navigation xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto" 19 | xmlns:tools="http://schemas.android.com/tools" 20 | android:id="@+id/nav_main" 21 | app:startDestination="@id/mainFragment"> 22 | 23 | <fragment 24 | android:id="@+id/mainFragment" 25 | android:name="com.kunminx.puremusic.ui.page.MainFragment" 26 | android:label="fragment_main" 27 | tools:layout="@layout/fragment_main"> 28 | 29 | <action 30 | android:id="@+id/action_mainFragment_to_searchFragment" 31 | app:destination="@id/searchFragment" 32 | app:enterAnim="@anim/h_fragment_enter" 33 | app:exitAnim="@anim/h_fragment_exit" 34 | app:popEnterAnim="@anim/h_fragment_pop_enter" 35 | app:popExitAnim="@anim/h_fragment_pop_exit" /> 36 | 37 | <action 38 | android:id="@+id/action_mainFragment_to_loginFragment" 39 | app:destination="@id/loginFragment" 40 | app:enterAnim="@anim/h_fragment_enter" 41 | app:exitAnim="@anim/h_fragment_exit" 42 | app:popEnterAnim="@anim/h_fragment_pop_enter" 43 | app:popExitAnim="@anim/h_fragment_pop_exit" /> 44 | 45 | </fragment> 46 | 47 | <fragment 48 | android:id="@+id/searchFragment" 49 | android:name="com.kunminx.puremusic.ui.page.SearchFragment" 50 | android:label="fragment_search" 51 | tools:layout="@layout/fragment_search"> 52 | 53 | </fragment> 54 | 55 | <fragment 56 | android:id="@+id/loginFragment" 57 | android:name="com.kunminx.puremusic.ui.page.LoginFragment" 58 | android:label="LoginFragment" 59 | tools:layout="@layout/fragment_login" /> 60 | 61 | </navigation> 62 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_slide.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <navigation xmlns:android="http://schemas.android.com/apk/res/android" 18 | xmlns:app="http://schemas.android.com/apk/res-auto" 19 | xmlns:tools="http://schemas.android.com/tools" 20 | android:id="@+id/nav_slide" 21 | app:startDestination="@id/playerFragment"> 22 | 23 | <fragment 24 | android:id="@+id/playerFragment" 25 | android:name="com.kunminx.puremusic.ui.page.PlayerFragment" 26 | android:label="fragment_player" 27 | tools:layout="@layout/fragment_player" /> 28 | </navigation> -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <resources> 18 | 19 | <declare-styleable name="PlayPauseView"> 20 | <attr name="isCircleDraw" format="boolean" /> 21 | <attr name="circleAlpha" format="integer" /> 22 | <attr name="backgroundColor" format="reference|color" /> 23 | <attr name="drawableColor" format="reference|color" /> 24 | </declare-styleable> 25 | 26 | </resources> -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <color name="colorPrimary">#008577</color> 4 | <color name="colorPrimaryDark">#00574B</color> 5 | <color name="colorAccent">#D81B60</color> 6 | 7 | <color name="white">#fff</color> 8 | <color name="black">#000</color> 9 | <color name="gray">#666</color> 10 | <color name="light_gray">#999</color> 11 | <color name="transparent">#00000000</color> 12 | 13 | </resources> 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <resources> 18 | <dimen name="sliding_up_header">55dp</dimen> 19 | <dimen name="sliding_up_header_land">200dp</dimen> 20 | 21 | 22 | </resources> -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | 3 | <!-- Base application theme. --> 4 | <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> 5 | <!-- Customize your theme here. --> 6 | <item name="colorPrimary">@color/colorPrimary</item> 7 | <item name="colorPrimaryDark">@color/white</item> 8 | <item name="colorAccent">@color/colorAccent</item> 9 | </style> 10 | 11 | </resources> 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <network-security-config xmlns:tools="http://schemas.android.com/tools"> 3 | <base-config 4 | cleartextTrafficPermitted="true" 5 | tools:ignore="InsecureBaseConfiguration" /> 6 | </network-security-config> 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/kunminx/puremusic/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.puremusic; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /architecture/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: "com.android.library" 18 | 19 | android { 20 | compileSdk appTargetSdk 21 | defaultConfig { 22 | minSdk appMinSdk 23 | targetSdk appTargetSdk 24 | 25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildFeatures { 29 | dataBinding true 30 | } 31 | } 32 | 33 | 34 | dependencies { 35 | api fileTree(dir: "libs", include: ["*.jar", "*.aar"]) 36 | 37 | testImplementation "junit:junit:4.13.2" 38 | androidTestImplementation "androidx.test.ext:junit:1.1.5" 39 | androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" 40 | 41 | //常用基础组件 42 | 43 | api "androidx.appcompat:appcompat:1.6.1" 44 | api "org.jetbrains:annotations:24.0.1" 45 | api "androidx.navigation:navigation-runtime:2.5.3" 46 | 47 | api "com.google.android.material:material:1.9.0" 48 | api "androidx.constraintlayout:constraintlayout:2.1.4" 49 | api "androidx.recyclerview:recyclerview:1.3.1" 50 | 51 | //常用架构组件,已按功能提取分割为多个独立库,可按需选配 52 | 53 | api 'com.github.KunMinX:MVI-Dispatcher:7.6.0' 54 | api 'com.github.KunMinX:UnPeek-LiveData:7.8.0' 55 | api 'com.github.KunMinX:Smooth-Navigation:v4.0.0' 56 | api 'com.github.KunMinX.Strict-DataBinding:binding_state:6.2.0' 57 | api 'com.github.KunMinX.Strict-DataBinding:strict_databinding:6.2.0' 58 | api 'com.github.KunMinX.Strict-DataBinding:binding_recyclerview:6.2.0' 59 | 60 | //常用数据、媒体组件 61 | 62 | api "com.github.bumptech.glide:glide:4.16.0" 63 | 64 | api "com.google.code.gson:gson:2.10.1" 65 | api "com.squareup.retrofit2:retrofit:2.9.0" 66 | api "com.squareup.retrofit2:converter-gson:2.9.0" 67 | api "com.squareup.okhttp3:logging-interceptor:4.11.0" 68 | api "com.squareup.okhttp3:okhttp:4.11.0" 69 | 70 | api 'io.reactivex.rxjava2:rxandroid:2.1.1' 71 | api 'io.reactivex.rxjava2:rxjava:2.2.21' 72 | } 73 | -------------------------------------------------------------------------------- /architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import android.content.Context; 22 | 23 | import androidx.test.ext.junit.runners.AndroidJUnit4; 24 | import androidx.test.platform.app.InstrumentationRegistry; 25 | 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | 29 | /** 30 | * Instrumented test, which will execute on an Android device. 31 | * 32 | * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> 33 | */ 34 | @RunWith(AndroidJUnit4.class) 35 | public class ExampleInstrumentedTest { 36 | @Test 37 | public void useAppContext() { 38 | // Context of the app under test. 39 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 40 | 41 | assertEquals("com.kunminx.architecture.test", appContext.getPackageName()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /architecture/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" 18 | package="com.kunminx.architecture"> 19 | 20 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 21 | <uses-permission android:name="android.permission.INTERNET" /> 22 | 23 | <application> 24 | 25 | <provider 26 | android:name="androidx.core.content.FileProvider" 27 | android:authorities="${applicationId}.fileprovider" 28 | android:exported="false" 29 | android:grantUriPermissions="true"> 30 | <meta-data 31 | android:name="android.support.FILE_PROVIDER_PATHS" 32 | android:resource="@xml/file_paths" /> 33 | </provider> 34 | 35 | </application> 36 | 37 | </manifest> 38 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2018-present KunMinX 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package com.kunminx.architecture.data.response; 20 | 21 | /** 22 | * TODO: 专用于数据层返回结果至 domain 层或 ViewModel,原因如下: 23 | * <p> 24 | * liveData 专用于页面开发、解决生命周期安全问题, 25 | * 有时数据并非通过 liveData 分发给页面,也可是通过其他方式通知非页面组件, 26 | * 这时 repo 方法中内定通过 liveData 分发便不合适,不如一开始就规定不在数据层通过 liveData 返回结果。 27 | * <p> 28 | * 如这么说无体会,详见《这是一份 “架构模式” 自驾攻略》解析 29 | * https://xiaozhuanlan.com/topic/8204519736 30 | * <p> 31 | * Create by KunMinX at 2020/7/20 32 | */ 33 | public class DataResult<T> { 34 | 35 | private final T mEntity; 36 | private final ResponseStatus mResponseStatus; 37 | 38 | public DataResult(T entity, ResponseStatus responseStatus) { 39 | mEntity = entity; 40 | mResponseStatus = responseStatus; 41 | } 42 | 43 | public DataResult(T entity) { 44 | mEntity = entity; 45 | mResponseStatus = new ResponseStatus(); 46 | } 47 | 48 | public T getResult() { 49 | return mEntity; 50 | } 51 | 52 | public ResponseStatus getResponseStatus() { 53 | return mResponseStatus; 54 | } 55 | 56 | public interface Result<T> { 57 | void onResult(DataResult<T> dataResult); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.data.response; 18 | 19 | /** 20 | * TODO:本类仅用作示例参考,请根据 "实际项目需求" 配置自定义的 "响应状态元信息" 21 | * <p> 22 | * Create by KunMinX at 19/10/11 23 | */ 24 | public class ResponseStatus { 25 | 26 | private String responseCode = ""; 27 | private boolean success = true; 28 | private Enum<ResultSource> source = ResultSource.NETWORK; 29 | 30 | public ResponseStatus() { 31 | } 32 | 33 | public ResponseStatus(String responseCode, boolean success) { 34 | this.responseCode = responseCode; 35 | this.success = success; 36 | } 37 | 38 | public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) { 39 | this(responseCode, success); 40 | this.source = source; 41 | } 42 | 43 | public String getResponseCode() { 44 | return responseCode; 45 | } 46 | 47 | public boolean isSuccess() { 48 | return success; 49 | } 50 | 51 | public Enum<ResultSource> getSource() { 52 | return source; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.data.response; 2 | 3 | /** 4 | * Create by KunMinX at 2020/11/30 5 | */ 6 | public enum ResultSource { 7 | NETWORK, DATABASE, LOCAL_FILE 8 | } 9 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.data.response.manager; 18 | 19 | import android.content.IntentFilter; 20 | import android.net.ConnectivityManager; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.lifecycle.DefaultLifecycleObserver; 24 | import androidx.lifecycle.LifecycleOwner; 25 | 26 | import com.kunminx.architecture.utils.Utils; 27 | 28 | /** 29 | * Create by KunMinX at 19/10/11 30 | */ 31 | public class NetworkStateManager implements DefaultLifecycleObserver { 32 | 33 | private static final NetworkStateManager S_MANAGER = new NetworkStateManager(); 34 | private final NetworkStateReceive mNetworkStateReceive = new NetworkStateReceive(); 35 | 36 | private NetworkStateManager() { 37 | } 38 | 39 | public static NetworkStateManager getInstance() { 40 | return S_MANAGER; 41 | } 42 | 43 | //TODO tip:让 NetworkStateManager 可观察页面生命周期, 44 | // 从而在页面失去焦点时, 45 | // 及时断开本页面对网络状态的监测,以避免重复回调和一系列不可预期的问题。 46 | 47 | // 关于 Lifecycle 组件的存在意义,可详见《为你还原一个真实的 Jetpack Lifecycle》篇的解析 48 | // https://xiaozhuanlan.com/topic/3684721950 49 | 50 | @Override 51 | public void onResume(@NonNull LifecycleOwner owner) { 52 | IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); 53 | Utils.getApp().getApplicationContext().registerReceiver(mNetworkStateReceive, filter); 54 | } 55 | 56 | @Override 57 | public void onPause(@NonNull LifecycleOwner owner) { 58 | Utils.getApp().getApplicationContext().unregisterReceiver(mNetworkStateReceive); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/data/response/manager/NetworkStateReceive.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.data.response.manager; 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.net.ConnectivityManager; 23 | import android.widget.Toast; 24 | 25 | import com.kunminx.architecture.R; 26 | import com.kunminx.architecture.utils.NetworkUtils; 27 | 28 | import java.util.Objects; 29 | 30 | /** 31 | * Create by KunMinX at 19/8/5 32 | */ 33 | public class NetworkStateReceive extends BroadcastReceiver { 34 | 35 | @Override 36 | public void onReceive(Context context, Intent intent) { 37 | if (Objects.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { 38 | if (!NetworkUtils.isConnected()) { 39 | Toast.makeText(context, context.getString(R.string.network_not_good), Toast.LENGTH_SHORT).show(); 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/request/AsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.request; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import io.reactivex.Observable; 6 | import io.reactivex.ObservableEmitter; 7 | import io.reactivex.ObservableOnSubscribe; 8 | import io.reactivex.android.schedulers.AndroidSchedulers; 9 | import io.reactivex.annotations.NonNull; 10 | import io.reactivex.disposables.Disposable; 11 | import io.reactivex.schedulers.Schedulers; 12 | 13 | /** 14 | * Create by KunMinX at 2022/6/14 15 | */ 16 | public class AsyncTask { 17 | 18 | @SuppressLint("CheckResult") 19 | public static <T> Observable<T> doIO(Action<T> start) { 20 | return Observable.create(start::onEmit) 21 | .subscribeOn(Schedulers.io()) 22 | .observeOn(AndroidSchedulers.mainThread()); 23 | } 24 | 25 | @SuppressLint("CheckResult") 26 | public static <T> Observable<T> doCalculate(Action<T> start) { 27 | return Observable.create(start::onEmit) 28 | .subscribeOn(Schedulers.computation()) 29 | .observeOn(AndroidSchedulers.mainThread()); 30 | } 31 | 32 | public interface Action<T> { 33 | void onEmit(ObservableEmitter<T> emitter); 34 | } 35 | 36 | public interface Observer<T> extends io.reactivex.Observer<T> { 37 | default void onSubscribe(@NonNull Disposable d) { 38 | } 39 | 40 | void onNext(@NonNull T t); 41 | 42 | default void onError(@NonNull Throwable e) { 43 | } 44 | 45 | default void onComplete() { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/request/Requester.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.request; 2 | 3 | import androidx.lifecycle.ViewModel; 4 | /** 5 | * TODO tip 1: 6 | * 基于单一职责原则,抽取 Jetpack ViewModel "作用域管理" 的能力作为 "领域层组件", 7 | * 8 | * TODO tip 2:让 UI 和业务分离,让数据总是从生产者流向消费者 9 | * 10 | * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 11 | * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发", 12 | * 13 | * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用, 14 | * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化, 15 | * 将来升级到 Jetpack Compose 更是如此, 16 | * 17 | * Activity { 18 | * onCreate(){ 19 | * vm.livedata.observe { result-> 20 | * if(result.show) 21 | * panel.visible(VISIBLE) 22 | * else 23 | * panel.visible(GONE) 24 | * tvTitle.setText(result.title) 25 | * tvContent.setText(result.content) 26 | * } 27 | * } 28 | * 29 | * TODO tip 3:Requester 通常按业务划分 30 | * 一个项目中通常可存在多个 Requester 类, 31 | * 每个页面可根据业务需要,持有多个不同 Requester 实例, 32 | * 通过 PublishSubject 回推一次性消息,并在表现层 Observer 中分流, 33 | * 对于 Event,直接执行,对于 State,使用 BehaviorSubject 通知 View 渲染和兜着状态, 34 | * 35 | * Activity { 36 | * onCreate(){ 37 | * request.observe {result -> 38 | * is Event ? -> execute one time 39 | * is State ? -> BehaviorSubject setValue and notify 40 | * } 41 | * } 42 | * 43 | * 如这么说无体会,详见《Jetpack MVVM 分层设计解析》解析 44 | * https://xiaozhuanlan.com/topic/6741932805 45 | * 46 | * Create by KunMinX at 2023/6/5 47 | */ 48 | public class Requester extends ViewModel { 49 | 50 | } 51 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCase.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.usecase; 2 | 3 | /** 4 | * Use cases are the entry points to the domain layer. 5 | * 6 | * @param <Q> the request type 7 | * @param <P> the response type 8 | */ 9 | public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> { 10 | 11 | private Q mRequestValues; 12 | 13 | private UseCaseCallback<P> mUseCaseCallback; 14 | 15 | public Q getRequestValues() { 16 | return mRequestValues; 17 | } 18 | 19 | public void setRequestValues(Q requestValues) { 20 | mRequestValues = requestValues; 21 | } 22 | 23 | public UseCaseCallback<P> getUseCaseCallback() { 24 | return mUseCaseCallback; 25 | } 26 | 27 | public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) { 28 | mUseCaseCallback = useCaseCallback; 29 | } 30 | 31 | void run() { 32 | executeUseCase(mRequestValues); 33 | } 34 | 35 | protected abstract void executeUseCase(Q requestValues); 36 | 37 | /** 38 | * Data passed to a request. 39 | */ 40 | public interface RequestValues { 41 | } 42 | 43 | /** 44 | * Data received from a request. 45 | */ 46 | public interface ResponseValue { 47 | } 48 | 49 | public interface UseCaseCallback<R> { 50 | void onSuccess(R response); 51 | 52 | default void onError() { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseHandler.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.usecase; 2 | 3 | /** 4 | * Runs {@link UseCase}s using a {@link UseCaseScheduler}. 5 | */ 6 | public class UseCaseHandler { 7 | 8 | private static UseCaseHandler INSTANCE; 9 | 10 | private final UseCaseScheduler mUseCaseScheduler; 11 | 12 | public UseCaseHandler(UseCaseScheduler useCaseScheduler) { 13 | mUseCaseScheduler = useCaseScheduler; 14 | } 15 | 16 | public static UseCaseHandler getInstance() { 17 | if (INSTANCE == null) { 18 | INSTANCE = new UseCaseHandler(new UseCaseThreadPoolScheduler()); 19 | } 20 | return INSTANCE; 21 | } 22 | 23 | public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute( 24 | final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) { 25 | useCase.setRequestValues(values); 26 | //noinspection unchecked 27 | useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this)); 28 | 29 | // The network request might be handled in a different thread so make sure 30 | // Espresso knows 31 | // that the app is busy until the response is handled. 32 | 33 | // This callback may be called twice, once for the cache and once for loading 34 | // the data from the server API, so we check before decrementing, otherwise 35 | // it throws "Counter has been corrupted!" exception. 36 | mUseCaseScheduler.execute(useCase::run); 37 | } 38 | 39 | private <V extends UseCase.ResponseValue> void notifyResponse(final V response, 40 | final UseCase.UseCaseCallback<V> useCaseCallback) { 41 | mUseCaseScheduler.notifyResponse(response, useCaseCallback); 42 | } 43 | 44 | private <V extends UseCase.ResponseValue> void notifyError( 45 | final UseCase.UseCaseCallback<V> useCaseCallback) { 46 | mUseCaseScheduler.onError(useCaseCallback); 47 | } 48 | 49 | private static final class UiCallbackWrapper<V extends UseCase.ResponseValue> implements 50 | UseCase.UseCaseCallback<V> { 51 | private final UseCase.UseCaseCallback<V> mCallback; 52 | private final UseCaseHandler mUseCaseHandler; 53 | 54 | public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback, 55 | UseCaseHandler useCaseHandler) { 56 | mCallback = callback; 57 | mUseCaseHandler = useCaseHandler; 58 | } 59 | 60 | @Override 61 | public void onSuccess(V response) { 62 | mUseCaseHandler.notifyResponse(response, mCallback); 63 | } 64 | 65 | @Override 66 | public void onError() { 67 | mUseCaseHandler.notifyError(mCallback); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseScheduler.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.usecase; 2 | 3 | /** 4 | * Interface for schedulers, see {@link UseCaseThreadPoolScheduler}. 5 | */ 6 | public interface UseCaseScheduler { 7 | 8 | void execute(Runnable runnable); 9 | 10 | <V extends UseCase.ResponseValue> void notifyResponse(final V response, 11 | final UseCase.UseCaseCallback<V> useCaseCallback); 12 | 13 | <V extends UseCase.ResponseValue> void onError( 14 | final UseCase.UseCaseCallback<V> useCaseCallback); 15 | } 16 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/domain/usecase/UseCaseThreadPoolScheduler.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.domain.usecase; 2 | 3 | import android.os.Handler; 4 | 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | import java.util.concurrent.ThreadPoolExecutor; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Executes asynchronous tasks using a {@link ThreadPoolExecutor}. 12 | * <p> 13 | * See also {@link Executors} for a list of factory methods to create common 14 | * {@link java.util.concurrent.ExecutorService}s for different scenarios. 15 | */ 16 | public class UseCaseThreadPoolScheduler implements UseCaseScheduler { 17 | 18 | public static final int POOL_SIZE = 2; 19 | public static final int MAX_POOL_SIZE = 4 * 2; 20 | public static final int FIXED_POOL_SIZE = 4; 21 | public static final int TIMEOUT = 30; 22 | final ThreadPoolExecutor mThreadPoolExecutor; 23 | private final Handler mHandler = new Handler(); 24 | 25 | /** 26 | * 固定线程数的无界线程池 27 | */ 28 | public UseCaseThreadPoolScheduler() { 29 | mThreadPoolExecutor = new ThreadPoolExecutor(FIXED_POOL_SIZE, FIXED_POOL_SIZE, TIMEOUT, 30 | TimeUnit.SECONDS, new LinkedBlockingQueue<>()); 31 | } 32 | 33 | @Override 34 | public void execute(Runnable runnable) { 35 | mThreadPoolExecutor.execute(runnable); 36 | } 37 | 38 | @Override 39 | public <V extends UseCase.ResponseValue> void notifyResponse(final V response, 40 | final UseCase.UseCaseCallback<V> useCaseCallback) { 41 | mHandler.post(() -> { 42 | if (null != useCaseCallback) { 43 | useCaseCallback.onSuccess(response); 44 | } 45 | }); 46 | } 47 | 48 | @Override 49 | public <V extends UseCase.ResponseValue> void onError( 50 | final UseCase.UseCaseCallback<V> useCaseCallback) { 51 | mHandler.post(useCaseCallback::onError); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/ui/adapter/CommonViewPagerAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.ui.adapter; 18 | 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.annotation.Nullable; 24 | import androidx.viewpager.widget.PagerAdapter; 25 | 26 | /** 27 | * Create by KunMinX at 19/6/15 28 | */ 29 | public class CommonViewPagerAdapter extends PagerAdapter { 30 | 31 | private final int count; 32 | private final boolean enableDestroyItem; 33 | private final String[] title; 34 | 35 | public CommonViewPagerAdapter(boolean enableDestroyItem, String[] title) { 36 | this.count = title.length; 37 | this.enableDestroyItem = enableDestroyItem; 38 | this.title = title; 39 | } 40 | 41 | @Override 42 | public int getCount() { 43 | return count; 44 | } 45 | 46 | @Override 47 | public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { 48 | return view == object; 49 | } 50 | 51 | @NonNull 52 | @Override 53 | public Object instantiateItem(@NonNull ViewGroup container, int position) { 54 | return container.getChildAt(position); 55 | } 56 | 57 | @Override 58 | public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 59 | if (enableDestroyItem) { 60 | container.removeView((View) object); 61 | } 62 | } 63 | 64 | @Nullable 65 | @Override 66 | public CharSequence getPageTitle(int position) { 67 | return title[position]; 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.ui.page; 18 | 19 | import android.app.Activity; 20 | import android.content.Intent; 21 | import android.content.res.Resources; 22 | import android.graphics.Color; 23 | import android.net.Uri; 24 | import android.os.Bundle; 25 | import android.view.inputmethod.InputMethodManager; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import androidx.lifecycle.ViewModel; 30 | 31 | import com.kunminx.architecture.data.response.manager.NetworkStateManager; 32 | import com.kunminx.architecture.ui.scope.ViewModelScope; 33 | import com.kunminx.architecture.utils.AdaptScreenUtils; 34 | import com.kunminx.architecture.utils.BarUtils; 35 | import com.kunminx.architecture.utils.ScreenUtils; 36 | 37 | /** 38 | * Create by KunMinX at 19/8/1 39 | */ 40 | public abstract class BaseActivity extends DataBindingActivity { 41 | 42 | private final ViewModelScope mViewModelScope = new ViewModelScope(); 43 | 44 | @Override 45 | protected void onCreate(@Nullable Bundle savedInstanceState) { 46 | 47 | BarUtils.setStatusBarColor(this, Color.TRANSPARENT); 48 | BarUtils.setStatusBarLightMode(this, true); 49 | 50 | super.onCreate(savedInstanceState); 51 | 52 | getLifecycle().addObserver(NetworkStateManager.getInstance()); 53 | 54 | //TODO tip 1: DataBinding 严格模式(详见 DataBindingActivity - - - - - ): 55 | // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, 56 | // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, 57 | // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 58 | 59 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 60 | } 61 | 62 | //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控, 63 | //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域, 64 | //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个, 65 | //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。 66 | 67 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840 68 | 69 | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) { 70 | return mViewModelScope.getActivityScopeViewModel(this, modelClass); 71 | } 72 | 73 | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) { 74 | return mViewModelScope.getApplicationScopeViewModel(modelClass); 75 | } 76 | 77 | @Override 78 | public Resources getResources() { 79 | if (ScreenUtils.isPortrait()) { 80 | return AdaptScreenUtils.adaptWidth(super.getResources(), 360); 81 | } else { 82 | return AdaptScreenUtils.adaptHeight(super.getResources(), 640); 83 | } 84 | } 85 | 86 | protected void toggleSoftInput() { 87 | InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); 88 | imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); 89 | } 90 | 91 | protected void openUrlInBrowser(String url) { 92 | Uri uri = Uri.parse(url); 93 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 94 | startActivity(intent); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.ui.page; 18 | 19 | import android.app.Activity; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.net.Uri; 23 | import android.view.inputmethod.InputMethodManager; 24 | 25 | import androidx.annotation.NonNull; 26 | import androidx.lifecycle.ViewModel; 27 | import androidx.navigation.NavController; 28 | import androidx.navigation.fragment.NavHostFragment; 29 | 30 | import com.kunminx.architecture.ui.scope.ViewModelScope; 31 | 32 | /** 33 | * Create by KunMinX at 19/7/11 34 | */ 35 | public abstract class BaseFragment extends DataBindingFragment { 36 | 37 | private final ViewModelScope mViewModelScope = new ViewModelScope(); 38 | 39 | //TODO tip 1: DataBinding 严格模式(详见 DataBindingFragment - - - - - ): 40 | // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, 41 | // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, 42 | // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 43 | 44 | // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 45 | 46 | //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控, 47 | //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域, 48 | //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个, 49 | //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。 50 | 51 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840 52 | 53 | protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) { 54 | return mViewModelScope.getFragmentScopeViewModel(this, modelClass); 55 | } 56 | 57 | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) { 58 | return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass); 59 | } 60 | 61 | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) { 62 | return mViewModelScope.getApplicationScopeViewModel(modelClass); 63 | } 64 | 65 | protected NavController nav() { 66 | return NavHostFragment.findNavController(this); 67 | } 68 | 69 | protected void toggleSoftInput() { 70 | InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE); 71 | imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); 72 | } 73 | 74 | protected void openUrlInBrowser(String url) { 75 | Uri uri = Uri.parse(url); 76 | Intent intent = new Intent(Intent.ACTION_VIEW, uri); 77 | startActivity(intent); 78 | } 79 | 80 | protected Context getApplicationContext() { 81 | return mActivity.getApplicationContext(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.ui.page; 2 | 3 | import androidx.lifecycle.ViewModel; 4 | 5 | /** 6 | * Create by KunMinX at 2022/8/11 7 | */ 8 | public class StateHolder extends ViewModel { 9 | 10 | //TODO tip 6:每个页面都需单独准备一个 state-ViewModel,托管与 "控件属性" 发生绑定的 State, 11 | // 此外,state-ViewModel 职责仅限于状态托管和保存恢复,不建议在此处理 UI 逻辑, 12 | 13 | // UI 逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者, 14 | // 数据总是来自领域层业务逻辑的处理,并单向回推至 UI 层,在 UI 层中响应数据的变化(也即处理 UI 逻辑), 15 | // 换言之,UI 逻辑只适合在 Activity/Fragment 等视图控制器中编写,将来升级到 Jetpack Compose 更是如此。 16 | 17 | //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 18 | 19 | } 20 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.util.DisplayMetrics; 5 | import android.util.Log; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | /** 10 | * <pre> 11 | * author: Blankj 12 | * blog : http://blankj.com 13 | * time : 2016/09/23 14 | * desc : AdaptScreenUtils 15 | * </pre> 16 | */ 17 | public final class AdaptScreenUtils { 18 | 19 | private static boolean isInitMiui = false; 20 | private static Field mTmpMetricsField; 21 | 22 | /** 23 | * Adapt for the horizontal screen, and call it in [android.app.Activity.getResources]. 24 | */ 25 | public static Resources adaptWidth(Resources resources, int designWidth) { 26 | DisplayMetrics dm = getDisplayMetrics(resources); 27 | float newXdpi = dm.xdpi = (dm.widthPixels * 72f) / designWidth; 28 | setAppDmXdpi(newXdpi); 29 | return resources; 30 | } 31 | 32 | /** 33 | * Adapt for the vertical screen, and call it in [android.app.Activity.getResources]. 34 | */ 35 | public static Resources adaptHeight(Resources resources, int designHeight) { 36 | DisplayMetrics dm = getDisplayMetrics(resources); 37 | float newXdpi = dm.xdpi = (dm.heightPixels * 72f) / designHeight; 38 | setAppDmXdpi(newXdpi); 39 | return resources; 40 | } 41 | 42 | /** 43 | * @param resources The resources. 44 | * @return the resource 45 | */ 46 | public static Resources closeAdapt(Resources resources) { 47 | DisplayMetrics dm = getDisplayMetrics(resources); 48 | float newXdpi = dm.xdpi = dm.density * 72; 49 | setAppDmXdpi(newXdpi); 50 | return resources; 51 | } 52 | 53 | /** 54 | * Value of pt to value of px. 55 | * 56 | * @param ptValue The value of pt. 57 | * @return value of px 58 | */ 59 | public static int pt2Px(float ptValue) { 60 | DisplayMetrics metrics = Utils.getApp().getResources().getDisplayMetrics(); 61 | return (int) (ptValue * metrics.xdpi / 72f + 0.5); 62 | } 63 | 64 | /** 65 | * Value of px to value of pt. 66 | * 67 | * @param pxValue The value of px. 68 | * @return value of pt 69 | */ 70 | public static int px2Pt(float pxValue) { 71 | DisplayMetrics metrics = Utils.getApp().getResources().getDisplayMetrics(); 72 | return (int) (pxValue * 72 / metrics.xdpi + 0.5); 73 | } 74 | 75 | private static void setAppDmXdpi(final float xdpi) { 76 | Utils.getApp().getResources().getDisplayMetrics().xdpi = xdpi; 77 | } 78 | 79 | private static DisplayMetrics getDisplayMetrics(Resources resources) { 80 | DisplayMetrics miuiDisplayMetrics = getMiuiTmpMetrics(resources); 81 | if (miuiDisplayMetrics == null) { 82 | return resources.getDisplayMetrics(); 83 | } 84 | return miuiDisplayMetrics; 85 | } 86 | 87 | private static DisplayMetrics getMiuiTmpMetrics(Resources resources) { 88 | if (!isInitMiui) { 89 | DisplayMetrics ret = null; 90 | String simpleName = resources.getClass().getSimpleName(); 91 | if ("MiuiResources".equals(simpleName) || "XResources".equals(simpleName)) { 92 | try { 93 | //noinspection JavaReflectionMemberAccess 94 | mTmpMetricsField = Resources.class.getDeclaredField("mTmpMetrics"); 95 | mTmpMetricsField.setAccessible(true); 96 | ret = (DisplayMetrics) mTmpMetricsField.get(resources); 97 | } catch (Exception e) { 98 | Log.e("AdaptScreenUtils", "no field of mTmpMetrics in resources."); 99 | } 100 | } 101 | isInitMiui = true; 102 | return ret; 103 | } 104 | if (mTmpMetricsField == null) { 105 | return null; 106 | } 107 | try { 108 | return (DisplayMetrics) mTmpMetricsField.get(resources); 109 | } catch (Exception e) { 110 | return null; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/ClickUtils.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.IntRange; 6 | import androidx.annotation.NonNull; 7 | 8 | /** 9 | * <pre> 10 | * author: Blankj 11 | * blog : http://blankj.com 12 | * time : 2019/06/12 13 | * desc : utils about click 14 | * </pre> 15 | */ 16 | public class ClickUtils { 17 | 18 | private static final int DEBOUNCING_TAG = -7; 19 | private static final long DEBOUNCING_DEFAULT_VALUE = 700; 20 | 21 | private ClickUtils() { 22 | throw new UnsupportedOperationException("u can't instantiate me..."); 23 | } 24 | 25 | /** 26 | * Apply single debouncing for the view's click. 27 | * 28 | * @param view The view. 29 | * @param listener The listener. 30 | */ 31 | public static void applySingleDebouncing(final View view, final View.OnClickListener listener) { 32 | applySingleDebouncing(new View[]{view}, listener); 33 | } 34 | 35 | /** 36 | * Apply single debouncing for the views' click. 37 | * 38 | * @param views The views. 39 | * @param listener The listener. 40 | */ 41 | public static void applySingleDebouncing(final View[] views, final View.OnClickListener listener) { 42 | applySingleDebouncing(views, DEBOUNCING_DEFAULT_VALUE, listener); 43 | } 44 | 45 | /** 46 | * Apply single debouncing for the views' click. 47 | * 48 | * @param views The views. 49 | * @param duration The duration of debouncing. 50 | * @param listener The listener. 51 | */ 52 | public static void applySingleDebouncing(final View[] views, 53 | @IntRange(from = 0) long duration, 54 | final View.OnClickListener listener) { 55 | applyDebouncing(views, false, duration, listener); 56 | } 57 | 58 | private static void applyDebouncing(final View[] views, 59 | final boolean isGlobal, 60 | @IntRange(from = 0) long duration, 61 | final View.OnClickListener listener) { 62 | if (views == null || views.length == 0 || listener == null) { 63 | return; 64 | } 65 | for (View view : views) { 66 | if (view == null) { 67 | continue; 68 | } 69 | view.setOnClickListener(new OnDebouncingClickListener(isGlobal, duration) { 70 | @Override 71 | public void onDebouncingClick(View v) { 72 | listener.onClick(v); 73 | } 74 | }); 75 | } 76 | } 77 | 78 | public static abstract class OnDebouncingClickListener implements View.OnClickListener { 79 | 80 | private static boolean mEnabled = true; 81 | 82 | private static final Runnable ENABLE_AGAIN = () -> mEnabled = true; 83 | private final long mDuration; 84 | private final boolean mIsGlobal; 85 | 86 | public OnDebouncingClickListener() { 87 | this(true, DEBOUNCING_DEFAULT_VALUE); 88 | } 89 | 90 | public OnDebouncingClickListener(final boolean isGlobal) { 91 | this(isGlobal, DEBOUNCING_DEFAULT_VALUE); 92 | } 93 | 94 | public OnDebouncingClickListener(final long duration) { 95 | this(true, duration); 96 | } 97 | 98 | public OnDebouncingClickListener(final boolean isGlobal, final long duration) { 99 | mIsGlobal = isGlobal; 100 | mDuration = duration; 101 | } 102 | 103 | private static boolean isValid(@NonNull final View view, final long duration) { 104 | long curTime = System.currentTimeMillis(); 105 | Object tag = view.getTag(DEBOUNCING_TAG); 106 | if (!(tag instanceof Long)) { 107 | view.setTag(DEBOUNCING_TAG, curTime); 108 | return true; 109 | } 110 | long preTime = (Long) tag; 111 | if (curTime - preTime <= duration) { 112 | return false; 113 | } 114 | view.setTag(DEBOUNCING_TAG, curTime); 115 | return true; 116 | } 117 | 118 | public abstract void onDebouncingClick(View v); 119 | 120 | @Override 121 | public final void onClick(View v) { 122 | if (mIsGlobal) { 123 | if (mEnabled) { 124 | mEnabled = false; 125 | v.postDelayed(ENABLE_AGAIN, mDuration); 126 | onDebouncingClick(v); 127 | } 128 | } else { 129 | if (isValid(v, mDuration)) { 130 | onDebouncingClick(v); 131 | } 132 | } 133 | } 134 | } 135 | 136 | 137 | } 138 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/DisplayUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture.utils; 18 | 19 | /** 20 | * Create by KunMinX at 19/7/20 21 | */ 22 | 23 | public class DisplayUtils { 24 | 25 | /** 26 | * convert px to its equivalent dp 27 | * <p> 28 | * 将px转换为与之相等的dp 29 | */ 30 | public static int px2dp(float pxValue) { 31 | final float scale = Utils.getApp().getResources().getDisplayMetrics().density; 32 | return (int) (pxValue / scale + 0.5f); 33 | } 34 | 35 | /** 36 | * convert dp to its equivalent px 37 | * <p> 38 | * 将dp转换为与之相等的px 39 | */ 40 | public static int dp2px(float dipValue) { 41 | final float scale = Utils.getApp().getResources().getDisplayMetrics().density; 42 | return (int) (dipValue * scale + 0.5f); 43 | } 44 | 45 | /** 46 | * convert px to its equivalent sp 47 | * <p> 48 | * 将px转换为sp 49 | */ 50 | public static int px2sp(float pxValue) { 51 | final float fontScale = Utils.getApp().getResources().getDisplayMetrics().scaledDensity; 52 | return (int) (pxValue / fontScale + 0.5f); 53 | } 54 | 55 | /** 56 | * convert sp to its equivalent px 57 | * <p> 58 | * 将sp转换为px 59 | */ 60 | public static int sp2px(float spValue) { 61 | final float fontScale = Utils.getApp().getResources().getDisplayMetrics().scaledDensity; 62 | return (int) (spValue * fontScale + 0.5f); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | 6 | /** 7 | * <pre> 8 | * author: Blankj 9 | * blog : http://blankj.com 10 | * time : 2016/08/12 11 | * desc : utils about image 12 | * </pre> 13 | */ 14 | public final class ImageUtils { 15 | 16 | /** 17 | * Return bitmap. 18 | * 19 | * @param filePath The path of file. 20 | * @return bitmap 21 | */ 22 | public static Bitmap getBitmap(final String filePath) { 23 | if (isSpace(filePath)) { 24 | return null; 25 | } 26 | return BitmapFactory.decodeFile(filePath); 27 | } 28 | 29 | private static boolean isSpace(final String s) { 30 | if (s == null) { 31 | return true; 32 | } 33 | for (int i = 0, len = s.length(); i < len; ++i) { 34 | if (!Character.isWhitespace(s.charAt(i))) { 35 | return false; 36 | } 37 | } 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/NetworkUtils.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import static android.Manifest.permission.ACCESS_NETWORK_STATE; 4 | 5 | import android.content.Context; 6 | import android.net.ConnectivityManager; 7 | import android.net.NetworkInfo; 8 | 9 | import androidx.annotation.RequiresPermission; 10 | 11 | /** 12 | * <pre> 13 | * author: Blankj 14 | * blog : http://blankj.com 15 | * time : 2016/08/02 16 | * desc : utils about network 17 | * </pre> 18 | */ 19 | public final class NetworkUtils { 20 | 21 | /** 22 | * Return whether network is connected. 23 | * <p>Must hold {@code <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />}</p> 24 | * 25 | * @return {@code true}: connected<br>{@code false}: disconnected 26 | */ 27 | @RequiresPermission(ACCESS_NETWORK_STATE) 28 | public static boolean isConnected() { 29 | NetworkInfo info = getActiveNetworkInfo(); 30 | return info != null && info.isConnected(); 31 | } 32 | 33 | @RequiresPermission(ACCESS_NETWORK_STATE) 34 | private static NetworkInfo getActiveNetworkInfo() { 35 | ConnectivityManager cm = 36 | (ConnectivityManager) Utils.getApp().getSystemService(Context.CONNECTIVITY_SERVICE); 37 | if (cm == null) { 38 | return null; 39 | } 40 | return cm.getActiveNetworkInfo(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/Res.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import androidx.core.content.ContextCompat; 6 | 7 | import java.util.Objects; 8 | /** 9 | * Create by KunMinX at 2023/6/5 10 | */ 11 | public class Res { 12 | public static Drawable getDrawable(int resId) { 13 | return Objects.requireNonNull(ContextCompat.getDrawable(Utils.getApp(), resId)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package com.kunminx.architecture.utils; 2 | 3 | import android.widget.Toast; 4 | 5 | /** 6 | * Create by KunMinX at 2021/8/19 7 | */ 8 | public class ToastUtils { 9 | 10 | public static void showLongToast(String text) { 11 | Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_LONG).show(); 12 | } 13 | 14 | public static void showShortToast(String text) { 15 | Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_SHORT).show(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /architecture/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | <!-- 2 | ~ Copyright 2018-present KunMinX 3 | ~ 4 | ~ Licensed under the Apache License, Version 2.0 (the "License"); 5 | ~ you may not use this file except in compliance with the License. 6 | ~ You may obtain a copy of the License at 7 | ~ 8 | ~ http://www.apache.org/licenses/LICENSE-2.0 9 | ~ 10 | ~ Unless required by applicable law or agreed to in writing, software 11 | ~ distributed under the License is distributed on an "AS IS" BASIS, 12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ~ See the License for the specific language governing permissions and 14 | ~ limitations under the License. 15 | --> 16 | 17 | <resources> 18 | <string name="app_name">architecture</string> 19 | 20 | <string name="network_not_good">网络不给力</string> 21 | 22 | </resources> 23 | -------------------------------------------------------------------------------- /architecture/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <paths> 4 | <external-path 5 | name="camera_photos" 6 | path="" /> 7 | </paths> 8 | </resources> -------------------------------------------------------------------------------- /architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present KunMinX 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kunminx.architecture; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import org.junit.Test; 22 | 23 | /** 24 | * Example local unit test, which will execute on the development machine (host). 25 | * 26 | * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> 27 | */ 28 | public class ExampleUnitTest { 29 | @Test 30 | public void addition_isCorrect() { 31 | assertEquals(4, 2 + 2); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | appTargetSdk = 33 4 | appMinSdk = 23 5 | appVersionCode = 50500 6 | appVersionName = "5.5.0" 7 | } 8 | 9 | repositories { 10 | google() 11 | gradlePluginPortal() 12 | maven { url 'https://jitpack.io' } 13 | 14 | //默认使用 gradlePluginPortal,以便在依赖库有紧急更新时能第一时间获取 15 | //如对日常的拉取速度有追求,可考虑使用以下远程仓库(是对 central 的国内同步仓库,存在 1 天左右的时差) 16 | //maven { url "https://maven.aliyun.com/repository/public" } 17 | } 18 | dependencies { 19 | classpath 'com.android.tools.build:gradle:7.3.1' 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | google() 26 | mavenCentral() 27 | maven { url 'https://jitpack.io' } 28 | //maven { url "https://maven.aliyun.com/repository/public" } 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | rootProject.allprojects { 34 | delete(it.buildDir) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.injected.testOnly=false 3 | android.useAndroidX=true 4 | org.gradle.caching=true 5 | org.gradle.configureondemand=true 6 | org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8 7 | org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KunMinX/Jetpack-MVVM-Best-Practice/543eb8659089d74ccad403763cb16596febc89b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 10 12:54:34 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':architecture' 2 | rootProject.name = 'PureMusic' 3 | --------------------------------------------------------------------------------