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解决了几年的一个问题。。

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

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

  • 使用DeepLink处理Notification返回栈问题

    现象:当应用登录成功后,收到到推送消息然后使用deepLink创建pendingIntent来创建一个Notification,应用切入后台点击通知消息,返回栈会清空,且会出现登陆界面重复进入的问题。

    具体界面:LoginFragment(start)-->HomeFragment(start)->AnyOtherFragment->切后台->点击通知消息->IMFragment->点击返回->回到HomeFragment

    解决:使用条件导航解决登录界面重复进入的问题

    可以使用nested graph部分解决返回栈被清除的问题

    官方文档说明:

    When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination. When nesting graphs, the start destination from each level of nesting—that is, the start destination from each `` element in the hierarchy—is also added to the stack. This means that when a user presses the Back button from a deep link destination, they navigate back up the navigation stack just as though they entered your app from its entry point.

    参考:Proper back stack on Android, every time

    google 相关Issure1

    google相关Issure2

  • DialogFragment

    正确的close方式

    • dismiss
    • 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等对象

    • UIStateHolder(ViewModel)

      UIState:界面元素某一时刻的状态,视界面元素封装成具体类,由ViewModel进行保存,并以LiveData方式提供界面Observe

      https://developer.android.com/jetpack/guide/ui-layer

    • 数据层每一层面可以拥有自己的Model类

      Separating model classes is beneficial in the following ways:

      • It saves app memory by reducing the data to only what's needed.
      • It adapts external data types to data types used by your app—for example, your app might use a different data type to represent dates.
      • It provides better separation of concerns—for example, members of a large team could work individually on the network and UI layers of a feature if the model class is defined beforehand.

      At minimum, it's recommended that you create new models in any case where a data source receives data that doesn't match with what the rest of your app expects.

      https://developer.android.com/jetpack/guide/data-layer

      技术要点

    • 当界面发生配置变更重新创建后会导致View中的LiveData监听器被再次触发,引发不正常的界面行为。

      界面应该只关心界面元素状态UIState,因此当事件触发后,界面应调用ViewModel方法通知界面状态(如已经弹出snackbar,toast提醒等)的改变,以便防止出现重复问题。

      Note: In some apps, you might have seen ViewModel events being exposed to the UI using Kotlin Channels or other reactive streams. These solutions usually require workarounds such as event wrappers in order to guarantee that events are not lost and that they're consumed only once.

      Requiring workarounds is an indication that there's a problem with these approaches. The problem with exposing events from the ViewModel is that it goes against the state-down-events-up principle of Unidirectional Data Flow.

      If you're in that situation, reconsider what that one-off ViewModel event actually means for your UI and convert it to UI state. UI state better represents the UI at a given point in time, it gives you more delivery and processing guarantees, it's usually easier to test, and it integrates consistently with the rest of your app.

      配置更改(如设备旋转)而重新创建了 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(ViewBinding)

      原因:假若Fragment成员中存储了ViewBinding对象,viewbinding会持有View的引用,从而导致Fragment进入返回栈后其rootView被销毁,但Fragment还持有viewBinding对象,从而使view 不能被销毁引发内存泄漏。

      https://halcyonmobile.com/blog/mobile-app-development/android-app-development/patching-fragment-memory-leaks-2019/

      解决:

      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;
          }
      }
      

      有新的问题:

      某些场景下OnCreate 时view lifecycle 还未初始化,导致未建立监听(未验证)

      The bug hiding in plain sight

      The problem is that it is possible for the view lifecycle to be initialized in such a way that fragment.onStart() and therefore viewLifecycleOwner.lifecycle.addObserver never actually happens. The fragment goes directly from onViewCreated to onDestroyView.

      Meaning, fragment.onStart() and fragment.onStop() never happen.

      This means that fragment.viewLifecycleOwner.observe(fragment) will NEVER be ACTIVE, and therefore the observe block will never be called. The viewLifecycle reaches DESTROYED without it being observed.

      解决:

      
      import android.view.View
      import androidx.fragment.app.Fragment
      import androidx.lifecycle.DefaultLifecycleObserver
      import androidx.lifecycle.Lifecycle
      import androidx.lifecycle.LifecycleOwner
      import androidx.lifecycle.Observer
      import androidx.viewbinding.ViewBinding
      import kotlin.properties.ReadOnlyProperty
      import kotlin.reflect.KProperty
      
      class FragmentViewBindingDelegate<T : ViewBinding>(
          val fragment: Fragment,
          val viewBindingFactory: (View) -> T
      ) : ReadOnlyProperty<Fragment, T> {
          private var binding: T? = null
      
          init {
              fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
                  val viewLifecycleOwnerLiveDataObserver =
                      Observer<LifecycleOwner?> {
                          val viewLifecycleOwner = it ?: return@Observer
      
                          viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
                              override fun onDestroy(owner: LifecycleOwner) {
                                  binding = null
                              }
                          })
                      }
      
                  override fun onCreate(owner: LifecycleOwner) {
                      fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerLiveDataObserver)
                  }
      
                  override fun onDestroy(owner: LifecycleOwner) {
                      fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerLiveDataObserver)
                  }
              })
          }
      
          override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
              val binding = binding
              if (binding != null) {
                  return binding
              }
      
              val lifecycle = fragment.viewLifecycleOwner.lifecycle
              if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
                  throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
              }
      
              return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
          }
      }
      
      fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
          FragmentViewBindingDelegate(this, viewBindingFactory)
      

      https://itnext.io/an-update-to-the-fragmentviewbindingdelegate-the-bug-weve-inherited-from-autoclearedvalue-7fc0a89fcae1

      官方文档:

      https://developer.android.com/topic/libraries/view-binding#fragments

    • DI(依赖注入)

      需要单独写文档

posted on 2021-06-01 17:53  kelisi_king  阅读(165)  评论(0编辑  收藏  举报