Android Arch
-
工程模块
-
界面导航
简要说明
使用Jetpack Nav库采用单Activity架构模式
-
UI复用(Fragment)
-
Activity之间跳转动画的问题。界面跳转会出现状态栏闪现
-
Activity之间共享数据问题
要使用单例(Application Scope)来保存数据
而单Activity可通过共享的ViewModel来传递数据。
-
向Fragment传递数据有时候会特别痛苦
-
Navigation UI库便于处理BotNavView,NavDrawer等
启动页只处理权限申请相关,权限申请可选方案:
https://codix.io/repo/27043/similar
- Nammu
- Dexter
- RxPermissions
- PermissionsDispatcher
最终选用了RxPermission,放弃使用PermissionDispatcher,与Dagger有兼容问题(使用了@Deprecated method)
DialogFragment作为全局的弹框界面单独拎了出来,优劣有待考证。
-
技术细节
-
使用Navigation库如何保留Fragment界面状态
被吐槽多年的
Navigation
lib问题:使用botNavView每次切换导航页后Fragment页面状态会被重置
-
DialogFragment
- dismiss
- [x] findNavController().navigateUp()
- findNavController().navigate(destination)
Navigation to DialogFragment Attention
当导航到DialogFragment时,因为前一Fragment也处于可见状态,因此其生命周期状态为
STARTED
,会导致调用getCurrentBackStackEntry()
返回DialogFragment的情况,引发一系列异常,因此需要通过添加NavBackStackEntry的生命周期OnResume
事件监听做相应的UI操作。同时,监听DialogFragment按钮点击使用监听
navBackStackEntry.getSavedStateHandle().getLiveData(dialogLiveDataKey);
方式DialogFragment
public class PttAlertDialogFragment extends BaseDialog { private static final String TAG = "[fragment][dialog][alert]"; /** * 按钮点击回调. */ public static final String KEY_DIALOG_CLICKED_BUTTON = "KEY_DIALOG_BUTTON"; @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { logger.d(TAG, "onCreateDialog"); final NavBackStackEntry navBackStackEntry = findNavController().getPreviousBackStackEntry(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()) .setPositiveButton(R.string.dialog_btn_ok, ((dialog, which) -> { logger.d(TAG, "on click ok"); findNavController().navigateUp(); Objects.requireNonNull(navBackStackEntry).getSavedStateHandle().set(KEY_DIALOG_CLICKED_BUTTON, Button.POSITIVE); })); builder.setNegativeButton(R.string.dialog_btn_cancel, (dialog, which) -> { logger.d(TAG, "on click cancel"); findNavController().navigateUp(); Objects.requireNonNull(navBackStackEntry).getSavedStateHandle().set(KEY_DIALOG_CLICKED_BUTTON, Button.NEGATIVE); //checkpoint: dissmiss() vs navigateUp():https://stackoverflow.com/questions/61035058/correct-way-to-close-dialogfragment-when-using-navigation-component // dismiss(); }); } return builder.show(); } }
Fragment
@Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final NavBackStackEntry navBackStackEntry = getCurrentNavBackStackEntry(); final LifecycleEventObserver lifecycleEventObserver = (source, event) -> { //导航到DialogFragment后,旋转屏幕触发重建逻辑,LoginResult LiveData在被Observe时会发送上一次设置的数据, // 因此如果不放在NavBackStackEntry OnResume中,会导致在显示DialogFragment情况下根据上次LoginResult绘制LoginFragment的UI,引发异常(如再次导航到DialogFragment会crash,因为当前就是DialogFragment)。 //旋转一次,observe 上一次的livedata会触发一次, //不dismiss DialogFragment再旋转一次,系统创新创建了Dialog Fragment, // dissmiss后observe 了liveData会再重新创建一次,因此会弹出两次DialogFragment if (event.equals(Lifecycle.Event.ON_RESUME)) { setupUI(); } }; navBackStackEntry.getLifecycle().addObserver(lifecycleEventObserver); getViewLifecycleOwner().getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> { if (event.equals(Lifecycle.Event.ON_DESTROY)) { navBackStackEntry.getLifecycle().removeObserver(lifecycleEventObserver); } }); } @NonNull protected final <T> Observable<T> navToDialogFragment(@NonNull NavDirections dialogNavDirection, final String dialogLiveDataKey) { findNavController().navigate(dialogNavDirection); //setup dialog button click listener final NavBackStackEntry navBackStackEntry = getCurrentNavBackStackEntry(); MutableLiveData<T> liveData = navBackStackEntry .getSavedStateHandle() .getLiveData(dialogLiveDataKey); return Observable.create(emitter -> { liveData.observe(navBackStackEntry, result -> { //must remove to handle result only once navBackStackEntry.getSavedStateHandle().remove(dialogLiveDataKey); emitter.onNext(result); emitter.onComplete(); }); }); } /** * Implement this Method to set up ui. */ protected abstract void setupUI(); /** * Gets current nav back stack entry. * <p> * However, when navigating to a dialog destination, * the previous destination is also visible on the screen and is therefore also STARTED despite not being the current destination. * This means that calls to getCurrentBackStackEntry() from within lifecycle methods * such as onViewCreated() will return the NavBackStackEntry of the dialog destination * after a configuration change or process death and recreation (since the dialog is restored above the other destination). * Therefore you should use getBackStackEntry() with the ID of your destination to ensure that you always use the correct NavBackStackEntry. * </p> * * @return the current nav back stack entry */ protected abstract NavBackStackEntry getCurrentNavBackStackEntry(); }
-
Fragment之间的数据传递
-
Fragment与Activity之间的数据传递
-
Activity之间的数据传递
详情可参阅Android官方文档:https://developer.android.com/guide/fragments/communicate
-
数据流
简要说明
-
技术栈
LiveData
+RxJava
+Hilt
+Dagger
+Retrofit
+OkHttp
使用LiveData的目的是因为它与界面生命周期绑定,可以防止后台刷新界面等异常操作。
-
非特定情况下限定使用以上三种Layout,参阅图片中说明1.2.3条
-
控件优先选择Material Design Component中的Widget
Material Design Component 样例代码(重点查阅内部文档)
-
建议只需使用ViewBinding
-
ViewBinding 是 DataBinding的子集(ViewBinding能做的事DataBinding都能做,反过来不行)
-
ViewBinding更高效,编译速度更快(Main Advantage)编译包体积更小
-
使用了DataBinding没必要再使用viewbinding
-
ViewBinding不需要在布局文件嵌套一层TAG
<layout>
-
-
Theme
设计人员可使用以下工具参阅:
https://material.io/resources/color/#!/?view.left=0&view.right=0
应用主题相关
是一个自定义Resource的集合(theme attribute),可被layout、style等引用。theme的attribute不限定于一个控件的属性,这些值实在整个应用中贯穿使用,
是应用视图的一个抽象集合,便于更换整个应用的主题,类似于一个interface,然后在不同主题下实现不同的属性配置。
-
Style
对同一类别控件封装管理
view attribute的集合:key只能为控件定义好的属性名称,好处是可对同一类别控件的属性封装后可复用,便于统一管理,只对当前控件有效
-
ViewModel中不应引用任何Android framework Class
不可引用Activity/Fragment/View等对象
技术要点
-
当界面发生配置变更重新创建后会导致View中的LiveData监听器被再次触发,引发不正常的界面行为。
配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
使用SingleLiveData
@ThreadSafe public class Event<T> { private final AtomicBoolean consumed = new AtomicBoolean(false); private final T value; public Event(T value) { this.value = value; } public T getContentIfNotHandled() { if (consumed.compareAndSet(false, true)) { return value; } else { return null; } } public T peekContent() { return value; } } public class SingleLiveEventObserver<T> implements Observer<Event<T>> { private final Listener<T> listener; public SingleLiveEventObserver(Listener<T> listener) { this.listener = listener; } @Override public void onChanged(Event<T> event) { if (event != null) { T content = event.getContentIfNotHandled(); if (content != null) { listener.onEventUnhandledContent(content); } } } public interface Listener<T> { void onEventUnhandledContent(T t); } }
-
可以使用扩展LiveData方式实现应用切后台释放事件监听,回到前台恢复监听
public class PttBtnObservableLiveData extends LiveData<MediaBtnEvent.Action> { private final Observable<MediaBtnEvent.PttBtnEvent> pttBtnEventObservable; private Disposable pttBtnEventDisposable; /** * Instantiates a new Ptt btn observable live data. * * @param pttBtnEventObservable the ptt btn event observable */ public PttBtnObservableLiveData(Observable<MediaBtnEvent.PttBtnEvent> pttBtnEventObservable) { this.pttBtnEventObservable = pttBtnEventObservable; } @Override protected void onActive() { super.onActive(); subscribePttBtnEvent(); } @Override protected void onInactive() { super.onInactive(); disposePttBtnEventObserve(); } /** * 监听Ptt按键事件处理. */ private void subscribePttBtnEvent() { pttBtnEventDisposable = pttBtnEventObservable.subscribe(pttBtnEvent -> { setValue(pttBtnEvent.action); }); } private void disposePttBtnEventObserve() { if (pttBtnEventDisposable != null && !pttBtnEventDisposable.isDisposed()) { pttBtnEventDisposable.dispose(); pttBtnEventDisposable = null; } } }
-
警惕ViewModel被Repository引用引发的内存泄漏
onClearer()
中释放引用 -
第三方lib callback回调如何转RxJava Observable
使用
ObservableEmitter
public Observable<LoginResponse> handleLogin(String phone, String password) { logger.d(TAG, "do Phone " + phone + " Pwd Login"); if (!sdkConfigured) { //should never happen throw new UnsupportedOperationException("CTChat SDK Not Configured Yet"); } return Observable.create(emitter -> { AccountManager.login(application, phone, password, new ObservableLoginCallback(emitter)); }); private final class ObservableLoginCallback implements LoginCallback { @NonNull private final ObservableEmitter<LoginResponse> emitter; public ObservableLoginCallback(@NonNull ObservableEmitter<LoginResponse> emitter) { this.emitter = emitter; } @Override public void onLoginSuccess(boolean needChangePassword) { logger.d(TAG, "on login success -> needChangePwd:" + needChangePassword); emitter.onNext(new LoginResponse(SUCCESS, "")); emitter.onComplete(); } @Override public void onLoginError(int errCode) { logger.w(TAG, "on login err -> code:" + errCode); LoginResponse loginResponse = new LoginResponse(getLoginResponseCode(errCode), ""); emitter.onNext(loginResponse); emitter.onComplete(); } } }
-
使用RxJava merge操作符进行本地数据与远端数据依次告知UI
-
跟随Fragment声明周期的变量AutoClearedValue
public class AutoClearedValue<T> { private T value; public AutoClearedValue(@NotNull Fragment fragment, T value) { this.value = value; fragment.getLifecycle().addObserver(new DefaultLifecycleObserver() { @Override public void onCreate(@NonNull LifecycleOwner owner) { fragment.getViewLifecycleOwnerLiveData().observe(fragment, viewLifecycleOwner -> { viewLifecycleOwner.getLifecycle().addObserver(new DefaultLifecycleObserver() { @Override public void onDestroy(@NonNull LifecycleOwner owner) { AutoClearedValue.this.value = null; } }); }); } }); } public T get() { return value; } }
-
DI(依赖注入)
需要单独写文档
- Hild Fragment 不允许被Retain,否则会抛异常,即配置发生变更等行为导致Activity 重建后,Fragment必须重建。
-