zoukankan      html  css  js  c++  java
  • Android App Arch

    Android Arch

    1. 工程模块

      Module

    2. 界面导航

      UI Flow

      简要说明

      使用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界面状态

      被吐槽多年的Navigationlib

      问题:使用botNavView每次切换导航页后Fragment页面状态会被重置

      Google Issure

      最终在2.4.0-alpha解决了几年的一个问题。。

      然后引入了一个新的顶层导航页显示导航箭头的问题。。。

      又一个坑:底部导航快速切换会引发崩溃

    • DialogFragment

      正确的close方式

      • 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

    1. 数据流

      DataFlow

      简要说明

      • 技术栈

        LiveData + RxJava + Hilt + Dagger + Retrofit + OkHttp

        使用LiveData的目的是因为它与界面生命周期绑定,可以防止后台刷新界面等异常操作。

      • 非特定情况下限定使用以上三种Layout,参阅图片中说明1.2.3条

        Layout 渲染效率对比结果

      • 控件优先选择Material Design Component中的Widget

        控件实现方案与要点

        Material Design Component 样例代码(重点查阅内部文档)

        Android官网UI实现指南

      • 建议只需使用ViewBinding

        PreferenceActivity 还不支持databinding

        • 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如何重建被销毁的界面

      • 警惕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(依赖注入)

        需要单独写文档

  • 相关阅读:
    react 学习
    redux saga学习
    Power BI连接至Amazon Redshift
    php时间日期
    layui select 禁止点击
    微信小程序二维码是无法识别二维码跳转到小程序
    JSON字符串与JSON对象的区别
    前端切图要选择png和jpg呢?
    @media媒体查询
    TortoiseGit revert failed
  • 原文地址:https://www.cnblogs.com/kelisi-king/p/14837466.html
Copyright © 2011-2022 走看看