9.3 KiB
layout | title | permalink |
---|---|---|
page | Decouple HomeActivity and ExternalAppBrowserActivity | /rfc/0011-decouple-home-activity-and-external-app-browser-activity |
- Start date: 2023-12-06
- RFC PR: #4732
Summary
The Custom Tabs feature is partially implemented by ExternalAppBrowserActivity
and AuthCustomTabActivity
. The use of inheritance in these Activity
s contributes to a closely coupled system.
The aim of this RFC is to strive towards a single Activity
architecture as is recommended for Modern Android Development (MAD).
The initial tasks will aim to:
- reduce the dependencies between
ExternalAppBrowserActivity
andHomeActivity
- establish clear responsibilities in
HomeActivity
- provide a foundation on which to continue refactoring to a single
Activity
⚠️ This work is exclusively a refactor of the current implementation, there should be no changes to existing behavior or features.
Motivation
Fenix has numerous open bugs and feature requests relating to the Custom Tabs feature. To facilitate efficient implementation & debugging of these (and future) bugs we should create code that is easy to maintain. Some notable issues:
-
A close coupling of
ExternalAppBrowserActivity
&AuthCustomTabActivity
with theHomeActivity
, which increases the likelihood of:- difficult to detect bugs
- difficultly debugging
- unexpected behavior
- making the area resistant to change
- confusion of responsibility
-
HomeActivity
has a mix of responsibilities, is bloated and monolithic. It contains unused fields, some incorrect visibility modifiers and generally in need of some house-keeping. These issues may be partly due to it's own downstream dependencies. See 0010-add-state-based-navigation.md for reading around the navigation issue which includesHomeActivity
. -
Inheritance has potential to introduce confusion, for example
ExternalAppBrowserFragment
already has duplicate dependencies defined forUserInteractionHandler
. -
ExternalAppBrowserActivity
contains multiple no-op functions.
Activity architecture overview
Note: This is an abridged abstraction of the mentioned classes focusing on only the relevant public
and protected
APIs.
classDiagram
class LocaleAwareAppCompatActivity {
override fun attachBaseContext() calls super
override fun onCreate() calls super
}
AppCompatActivity <|-- LocaleAwareAppCompatActivity
class NavHostActivityInterface {
fun getSupportActionBarAndInflateIfNecessary()
}
class HomeActivity {
override fun getSupportActionBarAndInflateIfNecessary()
override fun onCreate() calls super
override fun onResume() calls super
override fun onStart() calls super
override fun onStop() calls super
override fun onPause() calls super
override fun onProvideAssistContent() calls super
override fun onDestroy() calls super
override fun onConfigurationChanged() calls super
override fun recreate() calls super
override fun onNewIntent() calls super
override fun onCreateView() calls super
override fun onActionModeStarted() calls super
override fun onActionModeFinished() calls super
override fun onBackPressed()
override fun onActivityResult() calls super
override fun onKeyDown() calls super
override fun onKeyUp() calls super
override fun onKeyLongPress() calls super
override fun onUserLeaveHint() calls super
open fun getNavDirections()
open fun getBreadcrumbMessage()
open fun getIntentSource()
open fun getIntentSessionId()
}
NavHostActivityInterface <|-- HomeActivity
LocaleAwareAppCompatActivity <|-- HomeActivity
class ExternalAppBrowserActivity {
override fun onResume() calls super
override fun onDestroy() calls super
override fun onProvideAssistContent() calls super
override fun getNavDirections()
override fun getBreadcrumbMessage()
override fun getIntentSource()
override fun getIntentSessionId()
}
HomeActivity <|-- ExternalAppBrowserActivity
class AuthCustomActivity {
override fun onResume() calls super
}
ExternalAppBrowserActivity <|-- AuthCustomActivity
Observations
-
HomeActivity
is used as a 'base'Activity
forExternalAppBrowserActivity
andAuthCustomTabActivity
. -
ExternalAppBrowserActivity
overrides the followingAppCompatActivity
functions:
onResume()
onDestroy()
onProvideAssistContent()
which all depend on the super
(HomeActivity
) definitions prior to adding the ExternalAppBrowserActivity
behaviour. onResume()
is further propagated to AuthCustomTabActivity
which depends on the ExternalAppBrowserActivity
implementation.
ExternalAppBrowserActivity
is required to override the followingHomeActivity
defined functions:
getNavDirections()
getBreadcrumbMessage()
getIntentSource()
getIntentSessionId()
It's notable that the HomeActivity
getIntentSessionId()
implementation always returns null
and is required to take a redundant intent
parameter.
The HomeActivity
getNavDirections()
implementation is required to take a redundant customTabSessionId
parameter.
- The
ExternalAppBrowserActivity
currently uses theHomeActivity
onCreate()
implementation. This forces the Custom Tabs features to to perform unnecessary checks such as:
maybeShowSplashScreen()
shouldShowOnboarding()
- perform checks for Homepage Contile feature activity
- perform checks for Pocket feature activity
This list is not exhaustive but gives an insight into the redundant work being forced upon the ExternalAppBrowserActivity
. This applies to many of the other HomeActivity
lifecycle related functions.
Proposal
Reduce the coupling of the Activity
s. Establish HomeActivity
as the main Activity
and define clear responsibilities. Below is an list of suggested refactorings to initiate the decoupling.
-
Remove the no-ops from
ExternalAppBrowserActivity
. Example PR. -
Delegate the
Activity
s 'get nav directions' functionality. This removes the definition fromHomeActivity
and the requirement to override it inExternalAppBrowserActivity
. Example PR. -
Delegate the
HomeActivity
'open to browser' functionality. This removes another responsibility fromHomeActivity
. Example PR. -
Extract the
getBreadcrumbMessage
,getIntentSource
andgetIntentSessionId
functions fromHomeActivity
using anActivity
delegate. This removes the definition fromHomeActivity
and the requirement to override it inExternalAppBrowserActivity
. Example PR. -
Update
HomeActivity
&ExternalAppBrowserActivity
with the necessary visibility modifiers to enforce constraints. -
Extract the
handleRequestDesktopMode
function. This removes another responsibility fromHomeActivity
. -
Rename
HomeActivity
to be explicit that this is the mainActivity
. For info, the Focus application uses aMainActivity
, the Reference Browser application uses aBrowserActivity
.
The next steps are less clearly defined and require more investigation on the completion of the above tasks.
- Explore alternatives to the current Inheritance structure. E.g. Kotlin Delegation methods, an alternative 'base'
Activity
model or consolidatingExternalAppBrowserActivity
&HomeActivity
into a singleActivity
. - Prevent unnecessary checks being carried out by
ExternalAppBrowserActivity
inHomeActivity
lifecycle related functions.
Drawbacks
- Current reported Custom Tabs Bugzilla bugs may be affected by the changes mentioned here.
- No immediate tangible user facing improvements.
Rationale and alternatives
Rationale
- It is well established best practice to 'Prefer composition over inheritence'. An implementation of this principal is also exemplified in AC UI classes, see Reference Browser UI classes e.g.
BrowserActivity
&BaseBrowserFragment
. - Separation of concerns.
The most important principle to follow is separation of concerns. It's a common mistake to write all your code in an Activity or a Fragment. These UI-based classes should only contain logic that handles UI and operating system interactions. By keeping these classes as lean as possible, you can avoid many problems related to the component lifecycle, and improve the testability of these classes.
- Reducing the coupling of the
Activity
s and clear delineation of responsibilities should vastly improve their maintainability & testability.
Alternatives
- Leave the inheritance as-is between
HomeActivity
,ExternalAppBrowserActivity
&AuthCustomTabActivity
and only focus on refactoring work.