1. Historical background

Login module is very important for an App, among which stability and smooth user experience are the most important, directly related to the growth and retention of App users. After I took over the object login module, I found some problems in it successively, which would lead to low efficiency of iteration and poor stability. Therefore, I will upgrade the login module for the above problems.

2. How to transform

Through combing the login module code, the first problem is that there are many types and styles of login pages, but the core logic of different login pages is basically similar. However, the existing code practice is to copy and duplicate the way, to generate some different pages, and then do additional differentiation. This implementation method may have only one advantage, that is, it is relatively simple and fast, while the rest should be disadvantages, especially for the object acquisition App, which often has iteration requirements related to login.

How to solve the above problems? Through analysis, it is found that different types of login pages, no matter from the function or UI design or relatively unified, each page can be divided into a number of login components, through the permutation and combination of different components can be a style of login page. Therefore, I decided to divide the login page according to the function, divide it into login components one by one, and then realize different types of login pages through combination, which can greatly reuse components and quickly develop a new page through more combinations in subsequent iterations. This is where modular refactoring comes in.

2.1 Modular Reconstruction

The target

  1. High reuse
  2. Easy extension
  3. Simple maintenance
  4. Clear logic, stable operation

design

In order to achieve the above goals, the concept of login component should be abstracted first. Implementing a Component represents a login widget, which has complete functions. For example, it can be a login button, you can control the appearance of the button, click events, clickable status and so on. A component looks like this,

Key is the component identifier, representing the component identifier, mainly used for communication between components.

LoginScope is a runtime environment for components that allows you to manage pages, get some common configuration for pages, and interact with components. Lifecycle is related to the lifecycle, provided by loginScope. Cache is cache dependent. Track is related to the buried point, and generally refers to the buried point of the click.

LoginScope provides componentStore. Components are registered to componentStore for unified management by way of composition.

ComponentStore can obtain corresponding Component components by key, so as to achieve communication

The container is the host for all the Component components, i.e. the page after page, usually activities and fragments, but can also be customized.

implementation

Define ILoginComponent

interface ILoginComponent : FullLifecycleObserver, ActivityResultCallback {

    val key: Key<*>

    val loginScope: ILoginScope

    interface Key<E : ILoginComponent>

}
Copy the code

Encapsulates an abstract parent component, implements the default lifecycle, requires a key to identify the component, handles the onActivityResult event, and provides a default stabilization view click method

open class AbstractLoginComponent( override val key: ILoginComponent.Key<*> ) : ILoginComponent { companion object { private const val MMKV_LOGIN_KEY = "mmkv_key_****" } private lateinit var delegate:  ILoginScope protected val localCache: MMKV by lazy { MMKV.mmkvWithID(MMKV_LOGIN_KEY, MMKV.MULTI_PROCESS_MODE) } override val loginScope: ILoginScope get() = delegate fun registerComponent(delegate: ILoginScope) { this.delegate = delegate loginScope.loginModelStore.registerLoginComponent(this) } override fun onCreate() { } ... override fun onDestroy() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}}Copy the code

A simple component implementation, which is a header component

class LoginBannerComponent(
    private val titleText: TextView
) : AbstractLoginComponent(LoginBannerComponent) {

    companion object Key : ILoginComponent.Key<LoginBannerComponent>

    override fun onCreate() {
        titleText.isVisible = true
        titleText.text = loginScope.param.title
    }
}
Copy the code

Component components typically don’t care what the view looks like. The core is dealing with the component’s business logic and interactions.

Based on the analysis of login services, the component LoginRuntime environment LoginRuntime can be defined as follows

interface ILoginScope {

    val loginModelStore: ILoginComponentModel

    val loginHost: Any

    val loginContext: Context?

    var isEnable: Boolean

    val param: LoginParam

    val loginLifecycleOwner: LifecycleOwner

    fun toast(message: String?)

    fun showLoading(message: String? = null)

    fun hideLoading()

    fun close()

}
Copy the code

This is the runtime environment for a component hosted by an activity or fragment for a scenario

class LoginScopeImpl : ILoginScope { private var activity: AppCompatActivity? = null private var fragment: Fragment? = null override val loginModelStore: ILoginComponentModel override val loginHost: Any get() = activity ? : requireNotNull(fragment) override val param: LoginParam constructor(owner: ILoginComponentModelOwner, activity: AppCompatActivity, param: LoginParam) { this.loginModelStore = owner.loginModelStore this.param = param this.activity = activity } constructor(owner: ILoginComponentModelOwner, fragment: Fragment, param: LoginParam) { this.loginModelStore = owner.loginModelStore this.param = param this.fragment = fragment } override val loginContext: Context? get() = activity ?: requireNotNull(fragment).context override val loginLifecycleOwner: LifecycleOwner get() = activity ?: SafeViewLifecycleOwner(requireNotNull(fragment)) override var isEnable: Boolean = true override fun toast(message: String?) { // todo toast } override fun showLoading(message: String?) { // todo showLoading } override fun hideLoading() { // todo hideLoading } override fun close() { activity?.finish() ?: requireNotNull(fragment).also { if (it is IBottomAnim) { it.activity?.onBackPressedDispatcher?.onBackPressed() return } if (it is DialogFragment) { it.dismiss() } it.activity?.finish() } } private class SafeViewLifecycleOwner(fragment: Fragment) : LifecycleOwner { private val mLifecycleRegistry = LifecycleRegistry(this) init { fun Fragment.innerSafeViewLifecycleOwner(block: (LifecycleOwner?) -> Unit) { viewLifecycleOwnerLiveData.value?.also { block(it) } ?: run { viewLifecycleOwnerLiveData.observeLifecycleForever(this) { block(it) } } } fragment.innerSafeViewLifecycleOwner { if (it == null) { mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } else { it.lifecycle.addObserver(object : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { mLifecycleRegistry.handleLifecycleEvent(event) } }) } } } override fun getLifecycle(): Lifecycle = mLifecycleRegistry } }Copy the code

This is basically a proxy call wrapper around an activity or fragment. It’s important to note that I’m using the viewLifecyleOwner for the fragment to ensure that there’s no memory leak, and since the viewLifecyleOwner needs to be retrieved during a particular lifecycle, Otherwise an exception will occur, so here we define a safe SafeViewLifecycleOwner in the form of a wrapper class.

The following is the ILoginComponentModel interface, which abstracts the way componentStore manages components

interface ILoginComponentModel {

    fun registerLoginComponent(component: ILoginComponent)

    fun unregisterLoginComponent(loginScope: ILoginScope)

    fun <T : ILoginComponent> tryGet(key: ILoginComponent.Key<T>): T?

    fun <T : ILoginComponent, R> callWithComponent(key: ILoginComponent.Key<T>, block: T.() -> R): R?

    operator fun <T : ILoginComponent> get(key: ILoginComponent.Key<T>): T

    fun <T : ILoginComponent, R> requireCallWithComponent(key: ILoginComponent.Key<T>, block: T.() -> R): R
}
Copy the code

This is a concrete implementation class, here mainly solve the viewModelStore store and manage viewModel idea, and Kotlin coroutine through the key to get CoroutineContext idea to implement this componentStore,

class LoginComponentModelStore : ILoginComponentModel { private var componentArrays: Array<ILoginComponent> = emptyArray() private val lifecycleObserverMap by lazy { SparseArrayCompat<LoginScopeLifecycleObserver>() } fun initLoginComponent(loginScope: ILoginScope, vararg componentArrays: ILoginComponent) { lifecycleObserverMap[System.identityHashCode(loginScope)]? .apply { componentArrays.forEach { initLoginComponentLifecycle(it) } } } override fun registerLoginComponent(component: ILoginComponent) { component.loginScope.apply { if (loginLifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { return } lifecycleObserverMap.putIfAbsentV2(System.identityHashCode(this)) { LoginScopeLifecycleObserver(this).also { loginLifecycleOwner.lifecycle.addObserver(it) } }.also { componentArrays = componentArrays.plus(component) it.initLoginComponentLifecycle(component) } } } override fun unregisterLoginComponent(loginScope: ILoginScope) { lifecycleObserverMap.remove(System.identityHashCode(loginScope)) componentArrays = componentArrays.mapNotNull { if (it.loginScope === loginScope) { null } else { it } }.toTypedArray() } override fun <T :  ILoginComponent> tryGet(key: ILoginComponent.Key<T>): T? { return componentArrays.find { it.key === key && it.loginScope.isEnable }? .let { @Suppress("UNCHECKED_CAST") it as? T? } } override fun <T : ILoginComponent, R> callWithComponent(key: ILoginComponent.Key<T>, block: T.() -> R): R? { return tryGet(key)? .run(block) } override fun <T : ILoginComponent> get(key: ILoginComponent.Key<T>): T { return tryGet(key) ?: Throw IllegalStateException(" didn't find the specified ILoginComponent: $key")} Override fun <T: ILoginComponent, R> requireCallWithComponent(key: ILoginComponent.Key<T>, block: T.() -> R): R { return callWithComponent(key, block) ?: Throw IllegalStateException(" ILoginComponent: $key")} private Fun Dispatch (loginScope: ILoginScope, block: ILoginComponent.() -> Unit) { componentArrays.forEach { if (it.loginScope === loginScope) { it.block() } } } /** * ILoginComponent lifecycle distribution * * / private inner class LoginScopeLifecycleObserver (private val loginScope: ILoginScope) : LifecycleEventObserver { private var event = Lifecycle.Event.ON_ANY override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { this.event = event when (event) { Lifecycle.Event.ON_CREATE -> { dispatch(loginScope) { onCreate() } } Lifecycle.Event.ON_START -> { dispatch(loginScope) { onStart() } } Lifecycle.Event.ON_RESUME -> { dispatch(loginScope)  { onResume() } } Lifecycle.Event.ON_PAUSE -> { dispatch(loginScope) { onPause() } } Lifecycle.Event.ON_STOP -> { dispatch(loginScope) { onStop() } } Lifecycle.Event.ON_DESTROY -> { dispatch(loginScope) { onDestroy() } loginScope.loginLifecycleOwner.lifecycle.removeObserver(this) unregisterLoginComponent(loginScope) } else -> throw IllegalArgumentException("ON_ANY must not been send by anybody") } } } }Copy the code

Finally, a modular refactoring is presented to quickly implement a login page using a composite approach

internal class FullOneKeyLoginFragment : OneKeyLoginFragment() { override val eventPage: String = LoginSensorUtil.PAGE_ONE_KEY_LOGIN_FULL override fun layoutId() = R.layout.fragment_module_phone_onekey_login override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val btnClose = view.findViewById<ImageView>(R.id.btn_close) val tvTitle = view.findViewById<TextView>(R.id.tv_title) val thirdLayout = view.findViewById<ThirdLoginLayout>(R.id.third_layout) val btnLogin = view.findViewById<View>(R.id.btn_login) val btnOtherLogin = view.findViewById<TextView>(R.id.btn_other_login)  val cbPrivacy = view.findViewById<CheckBox>(R.id.cb_privacy) val tvAgreement = view.findViewById<TextView>(R.id.tv_agreement) loadLoginComponent( loginScope, LoginCloseComponent(btnClose), LoginBannerComponent(tvTitle), OneKeyLoginComponent(null, btnLogin, loginType), LoginOtherStyleComponent(thirdLayout), LoginOtherButtonComponent(btnOtherLogin), loginPrivacyLinkComponent(btnLogin, cbPrivacy, tvAgreement) ) } }Copy the code

In general, you only need to implement a layout XML file, or you can implement it by adding or inheriting a copy component if you have special requirements.

2.2 Separate Login Componentization

After the login logic is refactored, the next goal is to separate the login service from du_Account into a component, du_login. For the independent login service, a new login interface will be redesigned based on the existing service to make it clearer and easier to maintain.

The target

  1. Interface design responsibilities are clear
  2. Login information is dynamically configured
  3. Ability to degrade the login routing page
  4. You can know the whole login process
  5. Multi-process support
  6. Log in to engine AB to switch

design

The ILoginModuleService interface is designed to expose only the methods required by the business.

Interface ILoginModuleService: IProvider {/** * Whether to log in */ fun isLogged(): Boolean /** * Open the login page, kotlin uses * @return to return the unique identifier of the login */ @mainThread fun showLoginPage(context: context? = null, builder: (LoginBuilder.() -> Unit)? */ @mainThread fun showLoginPage(context: context?) = null): String /** * Open the login page. = null, builder: LoginBuilder): String /** * OAuthModel, cancelIfUnLogin: Boolean) /** * loginStatusLiveData(): LiveData<LoginStatus> /** * Log event LiveData, support cross-process */ fun loginEventLiveData(): LiveData<LoginEvent> /** * logout */ fun logout()}Copy the code

Setting Login Parameters

class NewLoginConfig private constructor(
    val styles: IntArray,
    val title: String,
    val from: String,
    val tag: String,
    val enterAnimId: Int,
    val exitAnimId: Int,
    val flag: Int,
    val extra: Bundle?
) 
Copy the code

Multiple login pages can be configured based on their priorities. If a route fails, it will be degraded automatically

Support to trace the source of the login, conducive to burying points

Support configuration page to open and close animation

Supports configuring the Bundle parameter

Supports cross-process observation of login status changes

internal sealed class LoginStatus {

    object UnLogged : LoginStatus()

    object Logging : LoginStatus()

    object Logged : LoginStatus()
}
Copy the code

Supports cross-process awareness of the login process

/** * [type] * -1 Failed to open the login page, Conditions are not met. * 0 Cancel * 1 Logging * 2 LOGGED * 3 Logout * 4 Open the first login page * 5 Authorize the login page to open */ class LoginEvent constructor(val type: Int, val key: String, val user: UsersModel? )Copy the code

implementation

At the heart of the entire component is LoginServiceImpl, which implements the ILoginModuleService interface to manage the entire login process. To ensure user experience, the login page is not opened repeatedly, so it is important to maintain the login status correctly. How to ensure that the login status is correct? In addition to ensuring proper business logic, thread safety and process safety are critical.

Process safety and thread safety

How to ensure process safety and thread safety?

This uses one of the four components of the Activity to achieve process safety and thread safety. LoginHelperActivity is a transparent, invisible activity.

<activity
    android:name=".LoginHelperActivity"
    android:label=""
    android:launchMode="singleInstance"
    android:screenOrientation="portrait"
    android:theme="@style/TranslucentStyle" />
Copy the code

The main purpose of LoginHelperActivity is to take advantage of its thread-safe process-safe features to maintain the login process, prevent the login page from being opened repeatedly, and close immediately after the execution logic is opened. Its startup mode is singleInstance, which has a separate task stack, i.e. on and off, and can be started at any time without affecting the login process, but also can solve the problem of cross-process and thread safety. Logging out is also implemented using LoginHelperActivity, which also takes advantage of thread-safe cross-process features to ensure state error.

internal companion object { internal const val KEY_TYPE = "key_type" internal fun login(context: Context, newConfig: NewLoginConfig) { context.startActivity(Intent(context, LoginHelperActivity::class.java).also { if (context ! is Activity) { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } it.putExtra(KEY_TYPE, 0) it.putExtra(NewLoginConfig.KEY, newConfig) }) } internal fun logout(context: Context) { context.startActivity(Intent(context, LoginHelperActivity::class.java).also { if (context ! is Activity) { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } it.putExtra(KEY_TYPE, 1) }) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (isFinishing) { return } try { if (intent? .getIntExtra(KEY_TYPE, 0) == 0) { tryOpenLoginPage() } else { loginImpl.logout() } } catch (e: Exception) { } finally { finish() } }Copy the code

The login logic opens a secondary LoginEntryActivity, which is also transparent and invisible. Its launch mode is singleTask, and it acts as the root Activity for all login processes, with the exception of special cases (such as not keeping active mode, Process killed, out of memory), and the destruction of LoginEntryActivity represents the end of the login process (except in special cases). The LoginEntryActivity is routed to the real login page only during its onResume life cycle. To prevent accidents, a timeout check is enabled during routing to prevent the real login page from being opened. The LoginEntryActivity interface does not respond.

<activity android:name=".LoginEntryActivity" android:label="" android:launchMode="singleTask" android:screenOrientation="portrait" android:theme="@style/TranslucentStyle" /> internal companion object { private const val SAVE_STATE_KEY = "save_state_key" internal fun login(activity: Activity, extra: Bundle?) { activity.startActivity(Intent(activity, LoginEntryActivity::class.java).also { if (extra ! = null) {it. PutExtras (extra)}})} /** * end the login process */ Internal fun finishLoginFlow(activity: cash inflow) LoginEntryActivity) { activity.startActivity(Intent(activity, LoginEntryActivity::class.java).also { it.putExtra(KEY_TYPE, 2) }) } }Copy the code

Through changing registerActivityLifecycleCallbacks perception activity lifecycle, the login process to start and end, and the abnormal exit of the login process. Like other business through registerActivityLifecycleCallbacks access to take the initiative to finish after LoginEntryActivity behavior, will be perceived, then log out process.

At the end of the login process, the singleTask feature is used to destroy all the login pages. There is a small detail here in case the LoginEntryActivity is destroyed prematurely and the other pages may not be destroyed using the singleTask feature. So there is still a backpocket operation that actively caches activities.

Distribute events across processes

The state and events of the cross-process distribution logon process are implemented via ArbitraryIPCEvent, which may be considered open later. The main schematic diagram is as follows:

Ab solution

Therefore, it is necessary to design a reliable AB scheme because of the large changes in the reconstruction and independent componentization. In order to make ab solution more simple and controllable, the modularization code only exists in the new login component, and the original du_account code remains unchanged. A in AB runs the original du_account code, b runs the original du_login code, and also make sure that the value of AB doesn’t change during the entire app life, because if it does, the code becomes uncontrollable. Since ab values need to be delivered by the server, some initialization work of login is in the process of application initialization. In order to make the online device run the code according to the delivered AB experimental configuration as much as possible, the initialization operation is postponed. The main strategy is to initialize the application immediately when it starts. A 3s timeout timer will be executed first. If the value delivered by AB is obtained before the timeout, the initialization will be performed immediately. If the ab configuration is not obtained after the timeout, the system initializes the ab configuration immediately. By default, the AB configuration is A. If any login code is invoked during the timeout wait, it is initialized immediately.

use

ServiceManager.getLoginModuleService().showLoginPage(activity) { withStyle(*LoginBuilder.transformArrayByStyle(config)) withTitle(config.title) withFrom(config.callFrom) config.tag? .also { withTag(it) } config.extra? .also { if (it is Bundle) { withExtra(it) } } }Copy the code
if (LoginABTestHelper.INSTANCE.getAbApplyLoginModule()) { LoginBuilder builder = new LoginBuilder(); builder.withTitle(LoginHelper.LoginTipsType.TYPE_NEW_USER_RED_PACKET.getType()); if (LoginHelper.abWechatOneKey) { builder.withStyle(LoginStyle.HALF_RED_TECH, LoginStyle.HALF_WECHAT); } else { builder.withStyle(LoginStyle.HALF_RED_TECH); } builder.addFlag(LoginBuilder.FLAG_FORCE_PRE_VERIFY_IF_NULL); Bundle bundle = new Bundle(); bundle.putString("url", imageUrl); bundle.putInt("popType", data.popType); builder.withExtra(bundle); builder.withHook(() -> fragmentManager.isResumed() && ! fragmentManager.isHidden()); final String tag = ServiceManager.getLoginModuleService().showLoginPage(context, builder); LiveData<LoginEvent> liveData = ServiceManager.getLoginModuleService().loginEventLiveData(); liveData.removeObservers(fragmentManager); liveData.observe(fragmentManager, loginEvent -> { if (! TextUtils.equals(tag, loginEvent.getKey())) { return; } if (loginevent.getType () == -1) {if (loginevent.getType () == -1) { Pop-up afterLoginFailedPop(fragmentManager, Data, dialog Listener); } else if (loginEvent.getType() == 2) { if (TextUtils.isEmpty(finalRouterUrl)) return; Navigator.getInstance().build(finalRouterUrl).navigation(context); } if (loginEvent.isEndEvent()) { liveData.removeObservers(fragmentManager); }}); }Copy the code

Potholes encountered in development

1. The time-consuming task is to rebuild the view ID of the Fragment page.

When testing a case that does not retain activity, the page will go blank, but the fragmentManger queries are normal (isAdded = true, isHided = false, isAttached = true). After searching for a long time, I suddenly thought of the ID problem. The ID of containerView, the host of fragment, was dynamically generated by me. Instead of using XML to write the layout, I used code to generate the view.

2, there is also a view onRestoreInstanceState timing

This problem is also encountered in the test does not retain the activity case, according to common sense as long as the view id is set, Android native controls will retain the previous state, such as checkBox will retain the check state. In the onViewCreated method of the fragment page reconstruction, I found findViewById in the checkBox, but the value obtained by isChecked was always false. I was puzzled and did not debug the source code. Later, when the custom control ThirdLoginLayout was implemented to save the state, it was found that the onRestoreInstanceState callback time was relatively late, and the View had not recovered the state when onViewCreated.

The text/Dylan

Pay attention to the technology, do the most fashionable technology!