UML 分析 Android 架构 - KunMinX 的 MVVM 架构框架 (MVVM 架构框架的首选, 7.7k

KunMinX - Jetpack-MVVM-Best-Practice

别忘了按下讚并追蹤我喔~

作者的 Blog

官方架构图

UML MVVM 架构图

既然是 MVVM 我们就依照 ModelViewViewModel 的 Base 来看。

Model

DataResult

DataResult Code 可以知道它负责处理取得资料的部分,而泛型 T 估计就是 JavaBeanEntityJOPO,藉由 interface Result 可以回传 T 的资料。

public class DataResult<T> {    private final T mEntity;    private final ResponseStatus mResponseStatus;    public DataResult(T entity, ResponseStatus responseStatus) {        mEntity = entity;        mResponseStatus = responseStatus;    }    public DataResult(T entity){        mEntity=entity;        mResponseStatus=new ResponseStatus();    }    public T getResult() {        return mEntity;    }    public ResponseStatus getResponseStatus() {        return mResponseStatus;    }    public interface Result<T> {        void onResult(DataResult<T> dataResult);    }}

ResponseStatus

ResponseStatus 为回应的状态、资料与来源。

public class ResponseStatus {    private String responseCode = "";    private boolean success = true;    private Enum<ResultSource> source = ResultSource.NETWORK;    public ResponseStatus() {    }    public ResponseStatus(String responseCode, boolean success) {        this.responseCode = responseCode;        this.success = success;    }    public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) {        this(responseCode, success);        this.source = source;    }    public String getResponseCode() {        return responseCode;    }    public boolean isSuccess() {        return success;    }    public Enum<ResultSource> getSource() {        return source;    }}

ResultSource

ResultSource 为资料的种类。

public enum ResultSource {    NETWORK, DATABASE, LOCAL_FILE}

View

View 的 Base 只有简单的继承。

BaseActivity

public abstract class BaseActivity extends DataBindingActivity {    private final ViewModelScope mViewModelScope = new ViewModelScope();    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        BarUtils.setStatusBarColor(this, Color.TRANSPARENT);        BarUtils.setStatusBarLightMode(this, true);        super.onCreate(savedInstanceState);        getLifecycle().addObserver(NetworkStateManager.getInstance());    }    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {        return mViewModelScope.getActivityScopeViewModel(this, modelClass);    }    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {        return mViewModelScope.getApplicationScopeViewModel(modelClass);    }    @Override    public Resources getResources() {        if (ScreenUtils.isPortrait()) {            return AdaptScreenUtils.adaptWidth(super.getResources(), 360);        } else {            return AdaptScreenUtils.adaptHeight(super.getResources(), 640);        }    }    protected void toggleSoftInput() {        InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);    }    protected void openUrlInBrowser(String url) {        Uri uri = Uri.parse(url);        Intent intent = new Intent(Intent.ACTION_VIEW, uri);        startActivity(intent);    }}

DataBindingActivity

public abstract class DataBindingActivity extends AppCompatActivity {    private ViewDataBinding mBinding;    private TextView mTvStrictModeTip;    public DataBindingActivity() {    }    protected abstract void initViewModel();    protected abstract DataBindingConfig getDataBindingConfig();    protected ViewDataBinding getBinding() {        if (this.isDebug() && this.mBinding != null && this.mTvStrictModeTip == null) {            this.mTvStrictModeTip = new TextView(this.getApplicationContext());            this.mTvStrictModeTip.setAlpha(0.4F);            this.mTvStrictModeTip.setPadding(this.mTvStrictModeTip.getPaddingLeft() + 24, this.mTvStrictModeTip.getPaddingTop() + 64, this.mTvStrictModeTip.getPaddingRight() + 24, this.mTvStrictModeTip.getPaddingBottom() + 24);            this.mTvStrictModeTip.setGravity(1);            this.mTvStrictModeTip.setTextSize(10.0F);            this.mTvStrictModeTip.setBackgroundColor(-1);            String tip = this.getString(string.debug_databinding_warning, new Object[]{this.getClass().getSimpleName()});            this.mTvStrictModeTip.setText(tip);            ((ViewGroup)this.mBinding.getRoot()).addView(this.mTvStrictModeTip);        }        return this.mBinding;    }    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        this.initViewModel();        DataBindingConfig dataBindingConfig = this.getDataBindingConfig();        ViewDataBinding binding = DataBindingUtil.setContentView(this, dataBindingConfig.getLayout());        binding.setLifecycleOwner(this);        binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel());        SparseArray<Object> bindingParams = dataBindingConfig.getBindingParams();        int i = 0;        for(int length = bindingParams.size(); i < length; ++i) {            binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i));        }        this.mBinding = binding;    }    public boolean isDebug() {        return this.getApplicationContext().getApplicationInfo() != null && (this.getApplicationContext().getApplicationInfo().flags & 2) != 0;    }    protected void onDestroy() {        super.onDestroy();        this.mBinding.unbind();        this.mBinding = null;    }}

BaseFragment

public abstract class BaseFragment extends DataBindingFragment {    private final ViewModelScope mViewModelScope = new ViewModelScope();    protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) {        return mViewModelScope.getFragmentScopeViewModel(this, modelClass);    }    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {        return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);    }    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {        return mViewModelScope.getApplicationScopeViewModel(modelClass);    }    protected NavController nav() {        return NavHostFragment.findNavController(this);    }    protected void toggleSoftInput() {        InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE);        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);    }    protected void openUrlInBrowser(String url) {        Uri uri = Uri.parse(url);        Intent intent = new Intent(Intent.ACTION_VIEW, uri);        startActivity(intent);    }    protected Context getApplicationContext() {        return mActivity.getApplicationContext();    }}

DataBindingFragment

public abstract class DataBindingFragment extends Fragment {    protected AppCompatActivity mActivity;    private ViewDataBinding mBinding;    private TextView mTvStrictModeTip;    public DataBindingFragment() {    }    public void onAttach(@NonNull Context context) {        super.onAttach(context);        this.mActivity = (AppCompatActivity)context;    }    protected abstract void initViewModel();    public void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        this.initViewModel();    }    protected abstract DataBindingConfig getDataBindingConfig();    protected ViewDataBinding getBinding() {        if (this.isDebug() && this.mBinding != null && this.mTvStrictModeTip == null) {            this.mTvStrictModeTip = new TextView(this.getContext());            this.mTvStrictModeTip.setAlpha(0.5F);            this.mTvStrictModeTip.setPadding(this.mTvStrictModeTip.getPaddingLeft() + 24, this.mTvStrictModeTip.getPaddingTop() + 64, this.mTvStrictModeTip.getPaddingRight() + 24, this.mTvStrictModeTip.getPaddingBottom() + 24);            this.mTvStrictModeTip.setGravity(1);            this.mTvStrictModeTip.setTextSize(10.0F);            this.mTvStrictModeTip.setBackgroundColor(-1);            String tip = this.getString(string.debug_databinding_warning, new Object[]{this.getClass().getSimpleName()});            this.mTvStrictModeTip.setText(tip);            ((ViewGroup)this.mBinding.getRoot()).addView(this.mTvStrictModeTip);        }        return this.mBinding;    }    @Nullable    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        DataBindingConfig dataBindingConfig = this.getDataBindingConfig();        ViewDataBinding binding = DataBindingUtil.inflate(inflater, dataBindingConfig.getLayout(), container, false);        binding.setLifecycleOwner(this.getViewLifecycleOwner());        binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel());        SparseArray<Object> bindingParams = dataBindingConfig.getBindingParams();        int i = 0;        for(int length = bindingParams.size(); i < length; ++i) {            binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i));        }        this.mBinding = binding;        return binding.getRoot();    }    public boolean isDebug() {        return this.mActivity.getApplicationContext().getApplicationInfo() != null && (this.mActivity.getApplicationContext().getApplicationInfo().flags & 2) != 0;    }    public void onDestroyView() {        super.onDestroyView();        this.mBinding.unbind();        this.mBinding = null;    }}

ViewModel

ViewModelScope

这里的 ViewModelScope 跟官方的概念不太一样

Android Architecture Coroutines ViewModelScope

为应用中的每个 ViewModel 定义了ViewModelScope。如果 ViewModel 已清除,则在此範围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作範围限定至ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。

public class ViewModelScope {    private ViewModelProvider mFragmentProvider;    private ViewModelProvider mActivityProvider;    private ViewModelProvider mApplicationProvider;    public ViewModelScope() {    }    public <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Fragment fragment, @NonNull Class<T> modelClass) {        if (this.mFragmentProvider == null) {            this.mFragmentProvider = new ViewModelProvider(fragment);        }        return this.mFragmentProvider.get(modelClass);    }    public <T extends ViewModel> T getActivityScopeViewModel(@NonNull AppCompatActivity activity, @NonNull Class<T> modelClass) {        if (this.mActivityProvider == null) {            this.mActivityProvider = new ViewModelProvider(activity);        }        return this.mActivityProvider.get(modelClass);    }    public <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {        if (this.mApplicationProvider == null) {            this.mApplicationProvider = new ViewModelProvider(ApplicationInstance.getInstance());        }        return this.mApplicationProvider.get(modelClass);    }}

getApplicationScopeViewModel 方法使用 ApplicationInstance.getInstance() 作为宣告ViewModelProvider 的参数

ApplicationInstance

public class ApplicationInstance implements ViewModelStoreOwner {    private static final ApplicationInstance sInstance = new ApplicationInstance();    private ViewModelStore mAppViewModelStore;    private ApplicationInstance() {    }    public static ApplicationInstance getInstance() {        return sInstance;    }    @NonNull    public ViewModelStore getViewModelStore() {        if (this.mAppViewModelStore == null) {            this.mAppViewModelStore = new ViewModelStore();        }        return this.mAppViewModelStore;    }}

UseCase


如果说看不清边界(boundaries)可以看下面这张

UseCaseScheduler
定义排程者要做的事情。execute : 执行 UseCase。notifyResponse : 通知回应。onError : 侦测到错误UseCaseThreadPoolScheduler
排程者 : 使用 ThreadPoolExecutor 执行异步任务来做 UseCaseUseCaseCallback
为什么使用 ThreadPoolExecutor 而不是 Thread 就好 ?
因为使用 Thread 有两个缺点每次都会new一个执行绪,执行完后销燬,不能複用。如果系统的併发量刚好比较大,需要大量执行绪,那么这种每次new的方式会抢资源的。
ThreadPoolExecutor 的好处是可以做到执行绪複用,并且使用尽量少的执行绪去执行更多的任务,效率和效能都相当不错。UseCaseHandler
负责执行 UseCase 的角色。
提供 execute 方法负责执行 UseCase。并决定执行结果 onSuccess and onError 的 UI 画面。UseCase
实作 UseCase 的核心方法。

UseCase

/** * Use cases are the entry points to the domain layer. * * @param <Q> the request type * @param <P> the response type */public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> {    private Q mRequestValues;    private UseCaseCallback<P> mUseCaseCallback;    public Q getRequestValues() {        return mRequestValues;    }    public void setRequestValues(Q requestValues) {        mRequestValues = requestValues;    }    public UseCaseCallback<P> getUseCaseCallback() {        return mUseCaseCallback;    }    public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {        mUseCaseCallback = useCaseCallback;    }    void run() {        executeUseCase(mRequestValues);    }    protected abstract void executeUseCase(Q requestValues);    /**     * Data passed to a request.     */    public interface RequestValues {    }    /**     * Data received from a request.     */    public interface ResponseValue {    }    public interface UseCaseCallback<R> {        void onSuccess(R response);        void onError();    }}

UseCaseHandler

提供 execute 方法负责执行 UseCase。并决定执行结果 onSuccess and onError 的 UI 画面。

/** * Runs {@link UseCase}s using a {@link UseCaseScheduler}. */public class UseCaseHandler {    private static UseCaseHandler INSTANCE;    private final UseCaseScheduler mUseCaseScheduler;    public UseCaseHandler(UseCaseScheduler useCaseScheduler) {        mUseCaseScheduler = useCaseScheduler;    }    public static UseCaseHandler getInstance() {        if (INSTANCE == null) {            INSTANCE = new UseCaseHandler(new UseCaseThreadPoolScheduler());        }        return INSTANCE;    }    public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute(            final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) {        useCase.setRequestValues(values);        //noinspection unchecked        useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this));        // The network request might be handled in a different thread so make sure        // Espresso knows        // that the app is busy until the response is handled.        // This callback may be called twice, once for the cache and once for loading        // the data from the server API, so we check before decrementing, otherwise        // it throws "Counter has been corrupted!" exception.        mUseCaseScheduler.execute(useCase::run);    }    private <V extends UseCase.ResponseValue> void notifyResponse(final V response,                                                                  final UseCase.UseCaseCallback<V> useCaseCallback) {        mUseCaseScheduler.notifyResponse(response, useCaseCallback);    }    private <V extends UseCase.ResponseValue> void notifyError(            final UseCase.UseCaseCallback<V> useCaseCallback) {        mUseCaseScheduler.onError(useCaseCallback);    }    private static final class UiCallbackWrapper<V extends UseCase.ResponseValue> implements            UseCase.UseCaseCallback<V> {        private final UseCase.UseCaseCallback<V> mCallback;        private final UseCaseHandler mUseCaseHandler;        public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback,                                 UseCaseHandler useCaseHandler) {            mCallback = callback;            mUseCaseHandler = useCaseHandler;        }        @Override        public void onSuccess(V response) {            mUseCaseHandler.notifyResponse(response, mCallback);        }        @Override        public void onError() {            mUseCaseHandler.notifyError(mCallback);        }    }}

UseCaseScheduler

定义排程者要做的事情。

execute : 执行 UseCase。notifyResponse : 通知回应。onError : 侦测到错误
/** * Interface for schedulers, see {@link UseCaseThreadPoolScheduler}. */public interface UseCaseScheduler {    void execute(Runnable runnable);    <V extends UseCase.ResponseValue> void notifyResponse(final V response,                                                          final UseCase.UseCaseCallback<V> useCaseCallback);    <V extends UseCase.ResponseValue> void onError(            final UseCase.UseCaseCallback<V> useCaseCallback);}

UseCaseThreadPoolScheduler

排程者 : 使用 ThreadPoolExecutor 执行异步任务来做 UseCaseUseCaseCallback

/** * Executes asynchronous tasks using a {@link ThreadPoolExecutor}. * <p> * See also {@link Executors} for a list of factory methods to create common * {@link java.util.concurrent.ExecutorService}s for different scenarios. */public class UseCaseThreadPoolScheduler implements UseCaseScheduler {    public static final int POOL_SIZE = 2;    public static final int MAX_POOL_SIZE = 4 * 2;    public static final int FIXED_POOL_SIZE = 4;    public static final int TIMEOUT = 30;    final ThreadPoolExecutor mThreadPoolExecutor;    private final Handler mHandler = new Handler();    public UseCaseThreadPoolScheduler() {        mThreadPoolExecutor = new ThreadPoolExecutor(FIXED_POOL_SIZE, FIXED_POOL_SIZE, TIMEOUT,                TimeUnit.SECONDS, new LinkedBlockingQueue<>());    }    @Override    public void execute(Runnable runnable) {        mThreadPoolExecutor.execute(runnable);    }    @Override    public <V extends UseCase.ResponseValue> void notifyResponse(final V response,                                                                 final UseCase.UseCaseCallback<V> useCaseCallback) {        mHandler.post(() -> {            if (null != useCaseCallback) {                useCaseCallback.onSuccess(response);            }        });    }    @Override    public <V extends UseCase.ResponseValue> void onError(            final UseCase.UseCaseCallback<V> useCaseCallback) {        mHandler.post(useCaseCallback::onError);    }}

Demo

Model

DataRepository

public class DataRepository {    private static final DataRepository S_REQUEST_MANAGER = new DataRepository();    private DataRepository() {    }    public static DataRepository getInstance() {        return S_REQUEST_MANAGER;    }    private final Retrofit retrofit;    {        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();        logging.setLevel(HttpLoggingInterceptor.Level.BODY);        OkHttpClient client = new OkHttpClient.Builder()            .connectTimeout(8, TimeUnit.SECONDS)            .readTimeout(8, TimeUnit.SECONDS)            .writeTimeout(8, TimeUnit.SECONDS)            .addInterceptor(logging)            .build();        retrofit = new Retrofit.Builder()            .baseUrl(APIs.BASE_URL)            .client(client)            .addConverterFactory(GsonConverterFactory.create())            .build();    }    public void getFreeMusic(DataResult.Result<TestAlbum> result) {        Gson gson = new Gson();        Type type = new TypeToken<TestAlbum>() {        }.getType();        TestAlbum testAlbum = gson.fromJson(Utils.getApp().getString(R.string.free_music_json), type);        result.onResult(new DataResult<>(testAlbum, new ResponseStatus()));    }    public void getLibraryInfo(DataResult.Result<List<LibraryInfo>> result) {        Gson gson = new Gson();        Type type = new TypeToken<List<LibraryInfo>>() {        }.getType();        List<LibraryInfo> list = gson.fromJson(Utils.getApp().getString(R.string.library_json), type);        result.onResult(new DataResult<>(list, new ResponseStatus()));    }        @SuppressLint("CheckResult")    public void downloadFile(DownloadState downloadState, DataResult.Result<DownloadState> result) {        Observable.interval(100, TimeUnit.MILLISECONDS)            .subscribeOn(Schedulers.io())            .observeOn(AndroidSchedulers.mainThread())            .subscribe(aLong -> {                if (downloadState.isForgive || downloadState.progress == 100) {                    return;                }                //模拟下载,假设下载一个文件要 10秒、每 100 毫秒下载 1% 并通知 UI 层                if (downloadState.progress < 100) {                    downloadState.progress = downloadState.progress + 1;                    Log.d("---", "下载进度 " + downloadState.progress + "%");                }                result.onResult(new DataResult<>(downloadState, new ResponseStatus()));                Log.d("---", "回推状态");            });    }        private Call<String> mUserCall;        public void login(User user, DataResult.Result<String> result) {        mUserCall = retrofit.create(AccountService.class).login(user.getName(), user.getPassword());        mUserCall.enqueue(new Callback<String>() {            @Override            public void onResponse(@NotNull Call<String> call, @NotNull Response<String> response) {                ResponseStatus responseStatus = new ResponseStatus(                    String.valueOf(response.code()), response.isSuccessful(), ResultSource.NETWORK);                result.onResult(new DataResult<>(response.body(), responseStatus));                mUserCall = null;            }            @Override            public void onFailure(@NotNull Call<String> call, @NotNull Throwable t) {                result.onResult(new DataResult<>(null,                    new ResponseStatus(t.getMessage(), false, ResultSource.NETWORK)));                mUserCall = null;            }        });    }    public void cancelLogin() {        if (mUserCall != null && !mUserCall.isCanceled()) {            mUserCall.cancel();            mUserCall = null;        }    }}

从这里可以发现 Repository 的方法参数都使用 DataResult.Result

View

MainActivity

MainFragment


ViewModel

各自的 ViewViewModel 都在各自 View 中建立;都为 View内部类别 inner class


UseCase

PlayerService

public class PlayerService extends Service {    public static final String NOTIFY_PREVIOUS = "pure_music.kunminx.previous";    public static final String NOTIFY_CLOSE = "pure_music.kunminx.close";    public static final String NOTIFY_PAUSE = "pure_music.kunminx.pause";    public static final String NOTIFY_PLAY = "pure_music.kunminx.play";    public static final String NOTIFY_NEXT = "pure_music.kunminx.next";    private static final String GROUP_ID = "group_001";    private static final String CHANNEL_ID = "channel_001";    private DownloadUseCase mDownloadUseCase;    @Override    public int onStartCommand(Intent intent, int flags, int startId) {        TestAlbum.TestMusic results = PlayerManager.getInstance().getCurrentPlayingMusic();        if (results == null) {            stopSelf();            return START_NOT_STICKY;        }        createNotification(results);        return START_NOT_STICKY;    }    private void createNotification(TestAlbum.TestMusic testMusic) {        try {            String title = testMusic.getTitle();            TestAlbum album = PlayerManager.getInstance().getAlbum();            String summary = album.getSummary();            RemoteViews simpleContentView = new RemoteViews(                    getApplicationContext().getPackageName(), R.layout.notify_player_small);            RemoteViews expandedView;            expandedView = new RemoteViews(                    getApplicationContext().getPackageName(), R.layout.notify_player_big);            Intent intent = new Intent(getApplicationContext(), MainActivity.class);            intent.setAction("showPlayer");            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {                NotificationManager notificationManager = (NotificationManager)                        getSystemService(Context.NOTIFICATION_SERVICE);                NotificationChannelGroup playGroup = new NotificationChannelGroup(GROUP_ID, getString(R.string.play));                notificationManager.createNotificationChannelGroup(playGroup);                NotificationChannel playChannel = new NotificationChannel(CHANNEL_ID,                        getString(R.string.notify_of_play), NotificationManager.IMPORTANCE_DEFAULT);                playChannel.setGroup(GROUP_ID);                notificationManager.createNotificationChannel(playChannel);            }            Notification notification = new NotificationCompat.Builder(                    getApplicationContext(), CHANNEL_ID)                    .setSmallIcon(R.drawable.ic_player)                    .setContentIntent(contentIntent)                    .setOnlyAlertOnce(true)                    .setContentTitle(title).build();            notification.contentView = simpleContentView;            notification.bigContentView = expandedView;            setListeners(simpleContentView);            setListeners(expandedView);            notification.contentView.setViewVisibility(R.id.player_progress_bar, View.GONE);            notification.contentView.setViewVisibility(R.id.player_next, View.VISIBLE);            notification.contentView.setViewVisibility(R.id.player_previous, View.VISIBLE);            notification.bigContentView.setViewVisibility(R.id.player_next, View.VISIBLE);            notification.bigContentView.setViewVisibility(R.id.player_previous, View.VISIBLE);            notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE);            boolean isPaused = PlayerManager.getInstance().isPaused();            notification.contentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);            notification.contentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);            notification.bigContentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);            notification.bigContentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);            notification.contentView.setTextViewText(R.id.player_song_name, title);            notification.contentView.setTextViewText(R.id.player_author_name, summary);            notification.bigContentView.setTextViewText(R.id.player_song_name, title);            notification.bigContentView.setTextViewText(R.id.player_author_name, summary);            notification.flags |= Notification.FLAG_ONGOING_EVENT;            String coverPath = Configs.COVER_PATH + File.separator + testMusic.getMusicId() + ".jpg";            Bitmap bitmap = ImageUtils.getBitmap(coverPath);            if (bitmap != null) {                notification.contentView.setImageViewBitmap(R.id.player_album_art, bitmap);                notification.bigContentView.setImageViewBitmap(R.id.player_album_art, bitmap);            } else {                requestAlbumCover(testMusic.getCoverImg(), testMusic.getMusicId());                notification.contentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);                notification.bigContentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);            }            startForeground(5, notification);        } catch (Exception e) {            e.printStackTrace();        }    }    @SuppressLint("UnspecifiedImmutableFlag")    public void setListeners(RemoteViews view) {        int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S                ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE                : PendingIntent.FLAG_UPDATE_CURRENT;        try {            PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),                    0, new Intent(NOTIFY_PREVIOUS).setPackage(getPackageName()), flags);            view.setOnClickPendingIntent(R.id.player_previous, pendingIntent);            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),                    0, new Intent(NOTIFY_CLOSE).setPackage(getPackageName()), flags);            view.setOnClickPendingIntent(R.id.player_close, pendingIntent);            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),                    0, new Intent(NOTIFY_PAUSE).setPackage(getPackageName()), flags);            view.setOnClickPendingIntent(R.id.player_pause, pendingIntent);            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),                    0, new Intent(NOTIFY_NEXT).setPackage(getPackageName()), flags);            view.setOnClickPendingIntent(R.id.player_next, pendingIntent);            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),                    0, new Intent(NOTIFY_PLAY).setPackage(getPackageName()), flags);            view.setOnClickPendingIntent(R.id.player_play, pendingIntent);        } catch (Exception e) {            e.printStackTrace();        }    }    private void requestAlbumCover(String coverUrl, String musicId) {        if (mDownloadUseCase == null) {            mDownloadUseCase = new DownloadUseCase();        }        UseCaseHandler.getInstance().execute(mDownloadUseCase,                new DownloadUseCase.RequestValues(coverUrl, musicId + ".jpg"),                response -> startService(new Intent(getApplicationContext(), PlayerService.class)));    }    @Override    public void onDestroy() {        super.onDestroy();    }    @Nullable    @Override    public IBinder onBind(Intent intent) {        return null;    }

我们可以看一下 requestAlbumCover 方法

执行 usecase

UseCaseHandler.getInstance().execute(mDownloadUseCase,

request 请求参数

new DownloadUseCase.RequestValues(coverUrl, musicId + ".jpg"),

response 回传参数

response -> startService(new Intent(getApplicationContext(), PlayerService.class)));

非常好懂


DownloadUseCase

DownloadUseCase 继承 UseCase
DownloadUseCase 是一个关于下载的 UseCase。
只要将下载的实作写在 executeUseCase 方法 里,在任何地方都能使用该功能。

public class DownloadUseCase extends UseCase<DownloadUseCase.RequestValues, DownloadUseCase.ResponseValue> {    @Override    protected void executeUseCase(RequestValues requestValues) {        try {            URL url = new URL(requestValues.url);            InputStream is = url.openStream();            File file = new File(Configs.COVER_PATH, requestValues.path);            OutputStream os = new FileOutputStream(file);            byte[] buffer = new byte[1024];            int len = 0;            while ((len = is.read(buffer)) > 0) {                os.write(buffer, 0, len);            }            is.close();            os.close();            getUseCaseCallback().onSuccess(new ResponseValue(file));        } catch (IOException e) {            e.printStackTrace();        }    }    public static final class RequestValues implements UseCase.RequestValues {        private String url;        private String path;        public RequestValues(String url, String path) {            this.url = url;            this.path = path;        }        public String getUrl() {            return url;        }        public void setUrl(String url) {            this.url = url;        }        public String getPath() {            return path;        }        public void setPath(String path) {            this.path = path;        }    }    public static final class ResponseValue implements UseCase.ResponseValue {        private File mFile;        public ResponseValue(File file) {            mFile = file;        }        public File getFile() {            return mFile;        }        public void setFile(File file) {            mFile = file;        }    }}

总结

架构

从上述来看架构的 UML 图可以得知,该架构框架并没有很複杂,BaseActivity 或是 BaseFragment 做的事情也不多,base 的继承与实作体系也不複杂,所以就不讨论 base 之间的关係。

不过还是有些值得一提的,以 MainFragment 为例,这里我们不讨论 PageMessengerPlaylistAdapter,先来看一下 MainFragment 的 UML 图。

ClickProxy
负责 MainFragment 的事件方法,分别为:openMenuloginsearchMainViewModel
MainViewModel 为 ViewModel 负责储存该 View 的状态,状态(state)变数分别为:State<Boolean> initTabAndPageState<String> pageAssetPathState<List<TestAlbum.TestMusic>> listMusicRequester
MusicRequester 也继承 ViewModel ,负责处理获取音乐资源的 Request

到这里有些人会有些疑问,有些 MVP 的 BaseFragment 会这样编写 BaseFragment<P extends BasePresenter> 限制继承 base 的子类别填入 Presenter,但在这个架构框架里不但没有看到类似BaseFragment<VM extends BaseViewModel> 这样的 base,还有两个继承 ViewModel 的子类别。

根据作者的 Comment 解释:

基于 "单一职责原则",应将 ViewModel 划分为
State-ViewModel 职责仅限于託管、保存和恢复本页面 state。
Event-ViewModel 或称 Result-ViewModel 职责仅限于 "消息分发" 场景承担 "唯一可信源"。

以上可参考《重学安卓:这是一份 “架构模式” 自驾攻略》。


职责仅限于 "消息分发" 场景承担 "唯一可信源"的解释:

常见消息分发场景包括:数据请求,页面间通信等,数据请求 Requester 负责,页面通信 Messenger 负责所有事件都可交由 "唯一可信源" 在内部决策和处理,并统一分发结果给所有订阅者页面。

以上可参考《吃透 LiveData 本质,享用可靠消息鉴权机制》。


Requester 通常按业务划分
一个项目中通常存在多个 Requester 类,
每个页面可根据业务需要持有多个不同 Requester 实例。
requester 职责仅限于 "业务逻辑处理" 和 "消息分发",不建议在此处理 UI 逻辑,
UI 逻辑只适合在 Activity/Fragment 等视图控制器中完成,是 “数据驱动” 一部分,
将来升级到 Jetpack Compose 更是如此。
以上可参考《如何让同事爱上架构模式、少写 bug 多注释》。


因此得到的结论:

State-ViewModel 为 MainViewModel 同时也是 View 的内部类别并且通常与 View 成双成对只会有1个。Event-ViewModel or Result-ViewModel 为 MusicRequester(请求音乐资源的)或是未介绍的PageMessenger(SharedViewModel,可能是 Fragment 之间的通讯)。
而 Requester 可以在 View 里存在很多个。

题外话我不太喜欢将 MainViewModel 写在 View 里面,有时候 主要的ViewModel 要做的事情蛮多的,虽然我知道将 ViewModel 让 state、event、result 分掉了,就算是这样毕竟 View 与 ViewModel 的层还是不同。


UseCase

让我们来看一下 Clean Architecture

根据 Uncle Bob 的 Clean Architecture 文章表示

Use Cases
The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
We do not expect changes in this layer to affect the entities. We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. This layer is isolated from such concerns.
We do, however, expect that changes to the operation of the application will affect the use-cases and therefore the software in this layer. If the details of a use-case change, then some code in this layer will certainly be affected.

Pros:

业务逻辑分的很清楚。重複的Code大幅减少。UseCase 彼此能互相使用,功能重用性提高。UseCase 属于领域层(Domain Layer)并非过往 Android App 架构而是独立的一个逻辑层,因此具有独立性。
各个 UseCase 易于测试。ViewModel 的 LiveData 变数大幅减少

Cons:

UseCase class 会越来越多。

没玩过的 Libariy

umano - AndroidSlidingUpPanel

参考文献

KunMinX - Jetpack-MVVM-Best-Practicekunminx 小专栏The Clean Architecture无瑕的程式码-整洁的软体设计与架构篇
Android 官方 Jetpack 指南 Domain Layerjava ThreadPoolExecutor使用方法简单介绍

别忘了按下讚并追蹤我喔~


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章