The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | 


--------------------------------------------------------------------------------