別忘了按下讚並追蹤我喔~
作者的 Blog
既然是 MVVM
我們就依照 Model
、View
、ViewModel
的 Base 來看。
從 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
為回應的狀態、資料與來源。
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
為資料的種類。
public enum ResultSource {
NETWORK, DATABASE, LOCAL_FILE
}
View 的 Base 只有簡單的繼承。
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);
}
}
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;
}
}
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();
}
}
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;
}
}
這裡的 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
的參數
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;
}
}
如果說看不清邊界(boundaries)
可以看下面這張
UseCaseScheduler
UseCaseThreadPoolScheduler
ThreadPoolExecutor
執行異步任務來做 UseCase
的 UseCaseCallback
。ThreadPoolExecutor
的好處是可以做到執行緒複用,並且使用盡量少的執行緒去執行更多的任務,效率和效能都相當不錯。UseCaseHandler
execute 方法
負責執行 UseCase。並決定執行結果 onSuccess
and onError
的 UI 畫面。/**
* 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();
}
}
提供 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);
}
}
}
定義排程者要做的事情。
/**
* 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);
}
排程者 : 使用 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);
}
}
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
的 ViewModel
都在各自 View
中建立;都為 View
的內部類別 inner class
。
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 繼承 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
的事件方法,分別為:
ViewModel
負責儲存該 View
的狀態,狀態(state)變數
分別為:
State<Boolean> initTabAndPage
State<String> pageAssetPath
State<List<TestAlbum.TestMusic>> list
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
職責僅限於 "消息分發" 場景承擔 "唯一可信源"。
以上可參考《重學安卓:這是一份 “架構模式” 自駕攻略》。
職責僅限於 "消息分發" 場景承擔 "唯一可信源"的解釋:
以上可參考《吃透 LiveData 本質,享用可靠消息鑑權機制》。
Requester 通常按業務劃分
一個項目中通常存在多個 Requester 類,
每個頁面可根據業務需要持有多個不同 Requester 實例。
requester 職責僅限於 "業務邏輯處理" 和 "消息分發",不建議在此處理 UI 邏輯,
UI 邏輯只適合在 Activity/Fragment 等視圖控制器中完成,是 “數據驅動” 一部分,
將來升級到 Jetpack Compose 更是如此。
以上可參考《如何讓同事愛上架構模式、少寫 bug 多註釋》。
因此得到的結論:
View
的內部類別並且通常與 View 成雙成對只會有1個。題外話我不太喜歡將 MainViewModel 寫在 View 裡面,有時候 主要的ViewModel 要做的事情蠻多的,雖然我知道將 ViewModel 讓 state、event、result 分掉了,就算是這樣畢竟 View 與 ViewModel 的層還是不同。
讓我們來看一下 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:
Cons:
別忘了按下讚並追蹤我喔~