KunMinX - Jetpack-MVVM-Best-Practice
别忘了按下讚并追蹤我喔~
作者的 Blog
官方架构图
UML MVVM 架构图
既然是 MVVM
我们就依照 Model
、View
、ViewModel
的 Base 来看。
Model
DataResult
从 DataResult
Code 可以知道它负责处理取得资料的部分,而泛型 T 估计就是 JavaBean
、Entity
、JOPO
,藉由 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
执行异步任务来做 UseCase
的 UseCaseCallback
。为什么使用 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
执行异步任务来做 UseCase
的 UseCaseCallback
。
/** * 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
各自的 View
的 ViewModel
都在各自 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
为例,这里我们不讨论 PageMessenger
与 PlaylistAdapter
,先来看一下 MainFragment
的 UML 图。
负责
MainFragment
的事件方法,分别为:openMenuloginsearchMainViewModelMainViewModel 为
ViewModel
负责储存该 View
的状态,状态(state)变数
分别为:State<Boolean> initTabAndPage
State<String> pageAssetPath
State<List<TestAlbum.TestMusic>> list
MusicRequesterMusicRequester 也继承
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 架构而是独立的一个逻辑层,因此具有独立性。
Cons:
UseCase class 会越来越多。没玩过的 Libariy
umano - AndroidSlidingUpPanel
参考文献
KunMinX - Jetpack-MVVM-Best-Practicekunminx 小专栏The Clean Architecture无瑕的程式码-整洁的软体设计与架构篇
别忘了按下讚并追蹤我喔~