In the common case, a View
instance lives within a hierarchy of View
s, up to
a RootView
object that is owned by a Widget
. Calling AddChildView<>()
typically passes ownership of the child to the parent, while
RemoveChildView[T<>]()
gives ownership back to the caller. While other
ownership patterns exist, newly-added code should use this one.
[TOC]
Accordingly, the best practices for ownership and lifetime management in Views code are similar to those in the rest of Chromium, but include some details specific to the Views APIs.
- Avoid bare new.
Use
std::make_unique<>()
when creating objects, and distinguish owning and non-owning uses by using smart and raw pointer types. - Use the unique_ptr<> version of View::AddChildView<>().
This clearly conveys ownership transfer from the caller to the parent
View
. It alsoDCHECK()
s thatset_owned_by_client()
has not been called, preventing double-ownership.
|||---|||
Avoid
Typical code from dice_bubble_sync_promo_view.cc
using bare new
:
Best practice
Rewriting using the unique_ptr<>
version of AddChildView<>()
is shorter and
safer:
|||---|||
|||---|||
...
views::Label* title =
new views::Label(
title_text, views::style::CONTEXT_DIALOG_BODY_TEXT,
text_style);
title->SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_LEFT);
title->SetMultiLine(true);
AddChildView(title);
...
signin_button_view_ =
new DiceSigninButtonView(
account, account_icon, this,
/*use_account_name_as_title=*/true);
AddChildView(signin_button_view_);
...
...
auto* title =
AddChildView(
std::make_unique<views::Label>(
title_text, views::style::CONTEXT_DIALOG_BODY_TEXT,
text_style));
title->SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_LEFT);
title->SetMultiLine(true);
...
signin_button_view_ =
AddChildView(
std::make_unique<DiceSigninButtonView>(
account, account_icon, this,
/*use_account_name_as_title=*/true));
...
|||---|||
The View::set_owned_by_client()
flag means that a View
is owned by something other than its parent View
.
This method is deprecated (and will eventually be removed) since it results
in APIs that are easy to misuse, code where ownership is unclear, and a higher
likelihood of bugs. Needing this flag may be a signal that the View
subclass
in question is too heavyweight, and refactoring using MVC or a similar paradigm
would allow a long-lived "model" or "controller" with more-transient "view"s.
|||---|||
Avoid
Code in time_view.{h,cc}
that uses set_owned_by_client()
to have non-parented View
s, so it can swap
layouts without recreating the children:
Best practice
Rewriting using subclasses to encapsulate layout allows the parent to merely adjust visibility:
|||---|||
|||---|||
class ASH_EXPORT TimeView : public ActionableView,
public ClockObserver {
...
private:
std::unique_ptr<views::Label> horizontal_label_;
...
std::unique_ptr<views::Label> vertical_label_hours_;
std::unique_ptr<views::Label> vertical_label_minutes_;
...
};
void TimeView::SetupLabels() {
horizontal_label_.reset(new views::Label());
SetupLabel(horizontal_label_.get());
vertical_label_hours_.reset(new views::Label());
SetupLabel(vertical_label_hours_.get());
vertical_label_minutes_.reset(new views::Label());
SetupLabel(vertical_label_minutes_.get());
...
}
void TimeView::SetupLabel(views::Label* label) {
label->set_owned_by_client();
...
}
void TimeView::UpdateClockLayout(
ClockLayout clock_layout) {
...
if (clock_layout == ClockLayout::HORIZONTAL_CLOCK) {
RemoveChildView(vertical_label_hours_.get());
RemoveChildView(vertical_label_minutes_.get());
SetLayoutManager(
std::make_unique<views::FillLayout>());
AddChildView(horizontal_label_.get());
} else {
RemoveChildView(horizontal_label_.get());
// Remove the current layout manager since it could
// be the FillLayout which only allows one child.
SetLayoutManager(nullptr);
// Pre-add the children since ownership is being
// retained by this.
AddChildView(vertical_label_hours_.get());
AddChildView(vertical_label_minutes_.get());
views::GridLayout* layout =
SetLayoutManager(
std::make_unique<views::GridLayout>());
...
}
DeprecatedLayoutImmediately();
}
class ASH_EXPORT TimeView : public ActionableView,
public ClockObserver {
...
private:
class HorizontalLabelView;
class VerticalLabelView;
...
HorizontalLabelView* horizontal_label_;
VerticalLabelView* vertical_label_;
...
};
TimeView::HorizontalLabelView::HorizontalLabelView() {
SetLayoutManager(
std::make_unique<views::FillLayout>());
...
}
TimeView::VerticalLabelView::VerticalLabelView() {
views::GridLayout* layout =
SetLayoutManager(
std::make_unique<views::GridLayout>());
...
}
void TimeView::TimeView(ClockLayout clock_layout,
ClockModel* model) {
...
horizontal_label_ =
AddChildView(
std::make_unique<HorizontalLabelView>());
vertical_label_ =
AddChildView(
std::make_unique<VerticalLabelView>());
...
}
void TimeView::UpdateClockLayout(
ClockLayout clock_layout) {
...
const bool is_horizontal =
clock_layout == ClockLayout::HORIZONTAL_CLOCK;
horizontal_label_->SetVisible(is_horizontal);
vertical_label_->SetVisible(!is_horizontal);
DeprecatedLayoutImmediately();
}
|||---|||
Refcounting and WeakPtr
s may also be indicators that a View
is doing more
than merely displaying UI. Views objects should only handle UI. Refcounting and
WeakPtr
needs should generally be handled by helper objects.
|||---|||
Avoid
Old code in cast_dialog_no_sinks_view.{h,cc}
that used weak pointers to
PostDelayedTask()
to itself:
Best practice
Current version
eliminates lifetime concerns by using a OneShotTimer
, which is canceled when
destroyed:
|||---|||
|||---|||
class CastDialogNoSinksView ... {
...
private:
base::WeakPtrFactory<CastDialogNoSinksView>
weak_factory_{this};
...
};
CastDialogNoSinksView::CastDialogNoSinksView(
Profile* profile) : profile_(profile) {
...
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&CastDialogNoSinksView::ShowHelpIconView,
weak_factory_.GetWeakPtr()),
kSearchWaitTime);
}
class CastDialogNoSinksView ... {
...
private:
base::OneShotTimer timer_;
...
};
CastDialogNoSinksView::CastDialogNoSinksView(
Profile* profile) : profile_(profile) {
...
timer_.Start(
FROM_HERE, kSearchWaitTime,
base::BindOnce(
&CastDialogNoSinksView::SetHelpIconView,
base::Unretained(this)));
}
|||---|||
View::RemoveChildViewT<>()
clearly conveys ownership transfer from the parent View
to the caller.
Callers who wish to delete a View
can simply ignore the return argument. This
is preferable to calling RemoveChildView()
and deleting the raw pointer
(cumbersome and error-prone), calling RemoveChildView()
without ever deleting
the pointer (leaks the View
), or simply deleting a pointer to a still-parented
View
(will work today, but is semantically incorrect and may be removed in the
future).
|||---|||
Avoid
Typical code in network_list_view.cc
which manually deletes a child after removing it:
Best practice
Rewriting using RemoveChildViewT<>()
is shorter and safer:
|||---|||
|||---|||
... if (mobile_header_view_) {
scroll_content()->RemoveChildView(
mobile_header_view_);
delete mobile_header_view_;
mobile_header_view_ = nullptr;
needs_relayout_ = true;
}
...
... if (mobile_header_view_) {
scroll_content()->RemoveChildViewT(
mobile_header_view_);
mobile_header_view_ = nullptr;
needs_relayout_ = true;
}
...
|||---|||
Prefer scoping objects to paired Add/Remove-type calls. For example, use
a base::ScopedObservation<>
instead of directly calling View::AddObserver()
and RemoveObserver()
.
Such objects reduce the likelihood of use-after-free.
|||---|||
Avoid
Typical code in avatar_toolbar_button.cc
that uses paired add/remove calls:
Best practice
Rewriting using base::ScopedObservation<>
eliminates the destructor body entirely:
|||---|||
|||---|||
AvatarToolbarButton::AvatarToolbarButton(
Browser* browser, ToolbarIconContainerView* parent)
: browser_(browser), parent_(parent) {
...
if (parent_)
parent_->AddObserver(this);
}
AvatarToolbarButton::~AvatarToolbarButton() {
if (parent_)
parent_->RemoveObserver(this);
}
class AvatarToolbarButton
: public ToolbarButton,
public ToolbarIconContainerView::Observer {
...
private:
base::ScopedObservation<AvatarToolbarButton,
ToolbarIconContainerView::Observer>
observation_{this};
};
AvatarToolbarButton::AvatarToolbarButton(
Browser* browser, ToolbarIconContainerView* parent)
: browser_(browser), parent_(parent) {
...
if (parent_)
observation_.Observe(parent_);
}
AvatarToolbarButton::~AvatarToolbarButton() = default;
|||---|||
For objects you own, destroy in the reverse order you created, so lifetimes
are nested rather than partially-overlapping. This can also reduce the
likelihood of use-after-free, usually by enabling code to make simplifying
assumptions (e.g. that an observed object always outlives this
).
|||---|||
Avoid
Old code in widget_interactive_uitest.cc
that destroys in the same order as
creation:
Best practice
Current version uses scoping objects that are simpler and safer:
|||---|||
|||---|||
TEST_F(WidgetTestInteractive,
ViewFocusOnWidgetActivationChanges) {
Widget* widget1 = CreateTopLevelPlatformWidget();
...
Widget* widget2 = CreateTopLevelPlatformWidget();
...
widget1->CloseNow();
widget2->CloseNow();
}
TEST_F(WidgetTestInteractive,
ViewFocusOnWidgetActivationChanges) {
WidgetAutoclosePtr widget1(
CreateTopLevelPlatformWidget());
...
WidgetAutoclosePtr widget2(
CreateTopLevelPlatformWidget());
...
}
|||---|||
Always use View
s on the main (UI) thread. Like most Chromium code, the
Views toolkit is not thread-safe.
In most cases, add child View
s in a View
's constructor. From an
ownership perspective, doing so is safe even though the View
is not yet in a
Widget
; if the View
is destroyed before ever being placed in a Widget
, it
will still destroy its child View
s. Child View
s may need to be added at
other times (e.g. in AddedToWidget()
or OnThemeChanged()
,
if constructing the View
requires a color; or lazily, if creation is expensive
or a View
is not always needed); however, do not copy any existing code that
adds child View
s in a ViewHierarchyChanged()
override, as such code is usually an artifact of misunderstanding the Views
ownership model.
|||---|||
Avoid
Typical code in native_app_window_views.cc
that sets up a child View
in ViewHierarchyChanged()
:
Best practice
Rewriting to do this at construction/Init()
makes |web_view_
|'s lifetime
easier to reason about:
|||---|||
|||---|||
void NativeAppWindowViews::ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) {
if (details.is_add && details.child == this) {
DCHECK(!web_view_);
web_view_ =
AddChildView(
std::make_unique<views::WebView>(nullptr));
web_view_->SetWebContents(
app_window_->web_contents());
}
}
NativeAppWindowViews::NativeAppWindowViews() {
...
web_view_ =
AddChildView(
std::make_unique<views::WebView>(nullptr));
}
void NativeAppWindowViews::Init(
extensions::AppWindow* app_window,
const extensions::AppWindow::CreateParams&
create_params) {
app_window_ = app_window;
web_view_->SetWebContents(
app_window_->web_contents());
...
}
|||---|||