Field Display initial implementation (#4322)

Field Display initial implementation for the object inspector tool
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics_view.dart
index 308de4b..963ce01 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics_view.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics_view.dart
@@ -78,12 +78,12 @@
         Flexible(
           child: Column(
             children: [
-              Flexible(
+              Expanded(
                 child: GeneralIsolateStatisticsWidget(
                   controller: controller,
                 ),
               ),
-              Flexible(
+              Expanded(
                 child: IsolateMemoryStatisticsWidget(
                   controller: controller,
                 ),
@@ -177,34 +177,28 @@
   @override
   Widget build(BuildContext context) {
     final isolate = controller.isolate;
-    return Column(
-      children: [
-        Flexible(
-          child: VMInfoCard(
-            title: 'Memory',
-            rowKeyValues: [
-              selectableTextBuilderMapEntry(
-                'Dart Heap',
-                _buildMemoryString(
-                  isolate?.dartHeapSize,
-                  isolate?.dartHeapCapacity,
-                ),
-              ),
-              selectableTextBuilderMapEntry(
-                'New Space',
-                _buildMemoryString(
-                  isolate?.newSpaceUsage,
-                  isolate?.newSpaceUsage,
-                ),
-              ),
-              selectableTextBuilderMapEntry(
-                'Old Space',
-                _buildMemoryString(
-                  isolate?.oldSpaceUsage,
-                  isolate?.oldSpaceCapacity,
-                ),
-              ),
-            ],
+    return VMInfoCard(
+      title: 'Memory',
+      rowKeyValues: [
+        selectableTextBuilderMapEntry(
+          'Dart Heap',
+          _buildMemoryString(
+            isolate?.dartHeapSize,
+            isolate?.dartHeapCapacity,
+          ),
+        ),
+        selectableTextBuilderMapEntry(
+          'New Space',
+          _buildMemoryString(
+            isolate?.newSpaceUsage,
+            isolate?.newSpaceUsage,
+          ),
+        ),
+        selectableTextBuilderMapEntry(
+          'Old Space',
+          _buildMemoryString(
+            isolate?.oldSpaceUsage,
+            isolate?.oldSpaceCapacity,
           ),
         ),
       ],
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_viewport.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_viewport.dart
index f8f2894..e14172a 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_viewport.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_viewport.dart
@@ -12,6 +12,7 @@
 import 'vm_class_display.dart';
 import 'vm_code_display.dart';
 import 'vm_developer_common_widgets.dart';
+import 'vm_field_display.dart';
 import 'vm_object_model.dart';
 
 /// Displays the VM information for the currently selected object in the
@@ -81,7 +82,7 @@
     return const VMInfoCard(title: 'TO-DO: Display Function object data');
   }
   if (obj is FieldObject) {
-    return const VMInfoCard(title: 'TO-DO: Display Field object data');
+    return VmFieldDisplay(field: obj);
   }
   if (obj is LibraryObject) {
     return const VMInfoCard(title: 'TO-DO: Display Library object data');
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_class_display.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_class_display.dart
index e41ff30..9b865ba 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_class_display.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_class_display.dart
@@ -5,10 +5,6 @@
 import 'package:flutter/material.dart';
 import 'package:vm_service/vm_service.dart';
 
-import '../../primitives/utils.dart';
-import '../../shared/common_widgets.dart';
-import '../../shared/table.dart';
-import '../../shared/theme.dart';
 import 'vm_developer_common_widgets.dart';
 import 'vm_object_model.dart';
 
@@ -30,27 +26,10 @@
   Widget build(BuildContext context) {
     return Row(
       children: [
-        Expanded(
-          child: Column(
-            children: [
-              ClassInfoWidget(
-                classDataRows: _classDataRows(clazz),
-              ),
-              Flexible(
-                child: ListView(
-                  children: [
-                    RetainingPathWidget(
-                      retainingPath: clazz.retainingPath,
-                      onExpanded: _onExpandRetainingPath,
-                    ),
-                    InboundReferencesWidget(
-                      inboundReferences: clazz.inboundReferences,
-                      onExpanded: _onExpandInboundRefs,
-                    ),
-                  ],
-                ),
-              ),
-            ],
+        Flexible(
+          child: VmObjectDisplayBasicLayout(
+            object: clazz,
+            generalDataRows: _classDataRows(clazz),
           ),
         ),
         if (displayClassInstances)
@@ -62,91 +41,17 @@
       ],
     );
   }
-
-  void _onExpandRetainingPath(bool expanded) {
-    if (clazz.retainingPath.value == null) clazz.requestRetainingPath();
-  }
-
-  void _onExpandInboundRefs(bool expanded) {
-    if (clazz.inboundReferences.value == null) clazz.requestInboundsRefs();
-  }
 }
 
-/// Displays general VM information of the Class Object.
-class ClassInfoWidget extends StatelessWidget implements PreferredSizeWidget {
-  const ClassInfoWidget({
-    required this.classDataRows,
-  });
-
-  final List<MapEntry<String, Widget Function(BuildContext)>> classDataRows;
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox.fromSize(
-      size: preferredSize,
-      child: VMInfoCard(
-        title: 'General Information',
-        rowKeyValues: classDataRows,
-      ),
-    );
-  }
-
-  @override
-  Size get preferredSize => Size.fromHeight(
-        areaPaneHeaderHeight +
-            classDataRows.length * defaultRowHeight +
-            defaultSpacing,
-      );
-}
-
-List<MapEntry<String, Widget Function(BuildContext)>> _classDataRows(
+// TODO(mtaylee): Delete 'Currently allocated instances' row when
+// ClassInstancesWidget implementation is completed.
+/// Generates a list of key-value pairs (map entries) containing the general
+/// information of the class object [clazz].
+List<MapEntry<String, WidgetBuilder>> _classDataRows(
   ClassObject clazz,
 ) {
   return [
-    selectableTextBuilderMapEntry('Object Class', clazz.obj.type),
-    selectableTextBuilderMapEntry(
-      'Shallow Size',
-      prettyPrintBytes(
-        clazz.obj.size ?? 0,
-        includeUnit: true,
-        kbFractionDigits: 1,
-        maxBytes: 512,
-      ),
-    ),
-    MapEntry(
-      'Reachable Size',
-      (context) => ValueListenableBuilder<bool>(
-        valueListenable: clazz.fetchingReachableSize,
-        builder: (context, fetching, _) => fetching
-            ? const CircularProgressIndicator()
-            : RequestableSizeWidget(
-                requestedSize: clazz.reachableSize,
-                requestFunction: clazz.requestReachableSize,
-              ),
-      ),
-    ),
-    MapEntry(
-      'Retained Size',
-      (context) => ValueListenableBuilder<bool>(
-        valueListenable: clazz.fetchingRetainedSize,
-        builder: (context, fetching, _) => fetching
-            ? const CircularProgressIndicator()
-            : RequestableSizeWidget(
-                requestedSize: clazz.retainedSize,
-                requestFunction: clazz.requestRetainedSize,
-              ),
-      ),
-    ),
-    selectableTextBuilderMapEntry(
-      'Library',
-      clazz.obj.library?.name?.isEmpty ?? false
-          ? clazz.script?.uri
-          : clazz.obj.library?.name,
-    ),
-    selectableTextBuilderMapEntry(
-      'Script',
-      '${fileNameFromUri(clazz.script?.uri) ?? ''}:${clazz.pos?.toString() ?? ''}',
-    ),
+    ...vmObjectGeneralDataRows(clazz),
     selectableTextBuilderMapEntry('Superclass', clazz.obj.superClass?.name),
     selectableTextBuilderMapEntry('SuperType', clazz.obj.superType?.name),
     selectableTextBuilderMapEntry(
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
index 5ac49d4..97ebd76 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
@@ -10,6 +10,7 @@
 import '../../shared/common_widgets.dart';
 import '../../shared/table.dart';
 import '../../shared/theme.dart';
+import 'vm_object_model.dart';
 import 'vm_service_private_extensions.dart';
 
 /// A convenience widget used to create non-scrollable information cards.
@@ -29,16 +30,19 @@
   });
 
   final String title;
-  final List<MapEntry<String, Widget Function(BuildContext)>>? rowKeyValues;
+  final List<MapEntry<String, WidgetBuilder>>? rowKeyValues;
   final Widget? table;
 
   @override
   Widget build(BuildContext context) {
-    return Card(
-      child: VMInfoList(
-        title: title,
-        rowKeyValues: rowKeyValues,
-        table: table,
+    return SizedBox.fromSize(
+      size: preferredSize,
+      child: Card(
+        child: VMInfoList(
+          title: title,
+          rowKeyValues: rowKeyValues,
+          table: table,
+        ),
       ),
     );
   }
@@ -56,7 +60,7 @@
   }
 }
 
-MapEntry<String, Widget Function(BuildContext)> selectableTextBuilderMapEntry(
+MapEntry<String, WidgetBuilder> selectableTextBuilderMapEntry(
   String key,
   String? value,
 ) {
@@ -77,7 +81,7 @@
   });
 
   final String title;
-  final List<MapEntry<String, Widget Function(BuildContext)>>? rowKeyValues;
+  final List<MapEntry<String, WidgetBuilder>>? rowKeyValues;
   final Widget? table;
 
   @override
@@ -204,7 +208,7 @@
     objectRefName =
         (objectRef.name?.isEmpty ?? false) ? objectRef.uri : objectRef.name;
   } else if (objectRef is ScriptRef) {
-    objectRefName = fileNameFromUri(objectRef.uri);
+    objectRefName = _fileNameFromUri(objectRef.uri);
   } else if (objectRef is InstanceRef) {
     objectRefName = objectRef.name ??
         'Instance of ${objectRef.classRef?.name ?? '<Class>'}';
@@ -215,6 +219,31 @@
   return objectRefName;
 }
 
+/// Returns the owner name of a Field or Func object, if [object] is a
+/// FuncObject, it returns the complete (qualified) name of the owner.
+String? _ownerName(VmObject object) {
+  assert(object is FieldObject || object is FuncObject);
+  final owner = (object as dynamic).obj.owner as ObjRef?;
+
+  if (owner == null) {
+    return object.script?.uri;
+  }
+
+  if (object is FieldObject) {
+    if (owner is ClassRef || owner is LibraryRef) {
+      return _objectName(owner);
+    }
+  } else if (object is FuncObject) {
+    if (owner is LibraryRef) {
+      return _objectName(owner);
+    } else {
+      return qualifiedName(owner);
+    }
+  }
+
+  throw Exception('Unexpected owner type: ${owner.type}');
+}
+
 /// Returns the name of a function, qualified with the name of
 /// its owner added as a prefix, separated by a period.
 ///
@@ -518,8 +547,139 @@
   }
 }
 
-String? fileNameFromUri(String? uri) {
+String? _fileNameFromUri(String? uri) {
   if (uri == null) return null;
   final splitted = uri.split('/');
   return splitted[splitted.length - 1];
 }
+
+/// A widget for the object inspector historyViewport containing the main
+/// layout of information widgets related to VM object types.
+class VmObjectDisplayBasicLayout extends StatelessWidget {
+  const VmObjectDisplayBasicLayout({
+    required this.object,
+    required this.generalDataRows,
+    this.sideCardDataRows,
+    this.generalInfoTitle = 'General Information',
+    this.sideCardTitle = 'Object Details',
+    this.expandableWidgets,
+  });
+
+  final VmObject object;
+  final List<MapEntry<String, WidgetBuilder>> generalDataRows;
+  final List<MapEntry<String, WidgetBuilder>>? sideCardDataRows;
+  final String generalInfoTitle;
+  final String sideCardTitle;
+  final List<Widget>? expandableWidgets;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        IntrinsicHeight(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: [
+              Flexible(
+                child: VMInfoCard(
+                  title: generalInfoTitle,
+                  rowKeyValues: generalDataRows,
+                ),
+              ),
+              if (sideCardDataRows != null)
+                Flexible(
+                  child: VMInfoCard(
+                    title: sideCardTitle,
+                    rowKeyValues: sideCardDataRows,
+                  ),
+                ),
+            ],
+          ),
+        ),
+        Flexible(
+          child: ListView(
+            children: [
+              RetainingPathWidget(
+                retainingPath: object.retainingPath,
+                onExpanded: _onExpandRetainingPath,
+              ),
+              InboundReferencesWidget(
+                inboundReferences: object.inboundReferences,
+                onExpanded: _onExpandInboundRefs,
+              ),
+              ...?expandableWidgets,
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  void _onExpandRetainingPath(bool expanded) {
+    if (object.retainingPath.value == null) {
+      object.requestRetainingPath();
+    }
+  }
+
+  void _onExpandInboundRefs(bool expanded) {
+    if (object.inboundReferences.value == null) {
+      object.requestInboundsRefs();
+    }
+  }
+}
+
+List<MapEntry<String, WidgetBuilder>> vmObjectGeneralDataRows(
+  VmObject object,
+) {
+  return [
+    selectableTextBuilderMapEntry('Object Class', object.obj.type),
+    selectableTextBuilderMapEntry(
+      'Shallow Size',
+      prettyPrintBytes(
+        object.obj.size ?? 0,
+        includeUnit: true,
+        kbFractionDigits: 1,
+        maxBytes: 512,
+      ),
+    ),
+    MapEntry(
+      'Reachable Size',
+      (context) => ValueListenableBuilder<bool>(
+        valueListenable: object.fetchingReachableSize,
+        builder: (context, fetching, _) => fetching
+            ? const CircularProgressIndicator()
+            : RequestableSizeWidget(
+                requestedSize: object.reachableSize,
+                requestFunction: object.requestReachableSize,
+              ),
+      ),
+    ),
+    MapEntry(
+      'Retained Size',
+      (context) => ValueListenableBuilder<bool>(
+        valueListenable: object.fetchingRetainedSize,
+        builder: (context, fetching, _) => fetching
+            ? const CircularProgressIndicator()
+            : RequestableSizeWidget(
+                requestedSize: object.retainedSize,
+                requestFunction: object.requestRetainedSize,
+              ),
+      ),
+    ),
+    if (object is ClassObject)
+      selectableTextBuilderMapEntry(
+        'Library',
+        _objectName(object.obj.library),
+      ),
+    if (object is FieldObject || object is FuncObject)
+      selectableTextBuilderMapEntry(
+        'Owner',
+        _ownerName(object),
+      ),
+    if (object is! ScriptObject && object is! LibraryObject)
+      selectableTextBuilderMapEntry(
+        'Script',
+        '${_fileNameFromUri(object.script?.uri) ?? ''}:${object.pos?.toString() ?? ''}',
+      ),
+  ];
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_field_display.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_field_display.dart
new file mode 100644
index 0000000..f161fe5
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_field_display.dart
@@ -0,0 +1,77 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'vm_developer_common_widgets.dart';
+import 'vm_object_model.dart';
+import 'vm_service_private_extensions.dart';
+
+/// A widget for the object inspector historyViewport, displaying information
+/// related to field objects in the Dart VM.
+class VmFieldDisplay extends StatelessWidget {
+  const VmFieldDisplay({
+    required this.field,
+  });
+
+  final FieldObject field;
+
+  @override
+  Widget build(BuildContext context) => VmObjectDisplayBasicLayout(
+        object: field,
+        generalDataRows: _fieldDataRows(field),
+      );
+}
+
+/// Generates a list of key-value pairs (map entries) containing the general
+/// information of the field object [field].
+List<MapEntry<String, WidgetBuilder>> _fieldDataRows(
+  FieldObject field,
+) {
+  return [
+    ...vmObjectGeneralDataRows(field),
+    selectableTextBuilderMapEntry(
+      'Observed types',
+      _fieldObservedTypes(field),
+    ),
+    if (field.obj.staticValue is InstanceRef)
+      selectableTextBuilderMapEntry(
+        'Static Value',
+        '${field.obj.staticValue.name ?? field.obj.staticValue.classRef.name}: '
+            '${field.obj.staticValue.valueAsString ?? 'Unknown value'}',
+      ),
+  ];
+}
+
+/// Returns the observed types of a field object, including null.
+///
+/// The observed types can be a single type (guardClassSingle), various types
+/// (guardClassDynamic), or a type that has not been observed yet
+/// (guardClassUnknown).
+String _fieldObservedTypes(FieldObject field) {
+  String type;
+
+  final kind = field.guardClassKind;
+
+  switch (kind) {
+    case GuardClassKind.single:
+      type = field.guardClass!.name ?? '<Observed Type>';
+      break;
+    case GuardClassKind.dynamic:
+      type = GuardClassKind.dynamic.jsonValue();
+      break;
+    case GuardClassKind.unknown:
+      type = 'none';
+      break;
+    default:
+      type = 'Observed types not found';
+  }
+
+  final nullable = field.guardNullable == null
+      ? ''
+      : ' - null ${field.guardNullable! ? '' : 'not '}observed';
+
+  return '$type$nullable';
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_object_model.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_object_model.dart
index 12568e3..9b4beba 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_object_model.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_object_model.dart
@@ -9,6 +9,7 @@
 import '../../shared/globals.dart';
 import '../debugger/debugger_model.dart';
 import '../debugger/program_explorer_model.dart';
+import 'vm_service_private_extensions.dart';
 
 /// Wrapper class for storing Dart VM objects with their relevant VM
 /// information.
@@ -109,7 +110,8 @@
   }
 }
 
-//TODO(mtaylee): finish class implementation.
+/// Stores a 'Class' VM object and provides an interface for obtaining the
+/// Dart VM information related to this object.
 class ClassObject extends VmObject {
   ClassObject({required super.ref, super.scriptRef, super.outlineNode});
 
@@ -146,7 +148,8 @@
   SourceLocation? get _sourceLocation => obj.location;
 }
 
-//TODO(mtaylee): finish class implementation.
+/// Stores a 'Field' VM object and provides an interface for obtaining the
+/// Dart VM information related to this object.
 class FieldObject extends VmObject {
   FieldObject({required super.ref, super.scriptRef, super.outlineNode});
 
@@ -158,6 +161,26 @@
 
   @override
   SourceLocation? get _sourceLocation => obj.location;
+
+  late final bool? guardNullable;
+
+  late final Class? guardClass;
+
+  late final GuardClassKind? guardClassKind;
+
+  @override
+  Future<void> initialize() async {
+    await super.initialize();
+
+    guardNullable = obj.guardNullable;
+    guardClassKind = obj.guardClassKind();
+
+    if (guardClassKind == GuardClassKind.single) {
+      guardClass = await obj.guardClass;
+    } else {
+      guardClass = null;
+    }
+  }
 }
 
 //TODO(mtaylee): finish class implementation.
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
index d893d56..835101b 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
@@ -4,6 +4,8 @@
 
 import 'package:vm_service/vm_service.dart';
 
+import '../../shared/globals.dart';
+
 /// NOTE: this file contains extensions to classes provided by
 /// `package:vm_service` in order to expose VM internal fields in a controlled
 /// fashion. Objects and extensions in this class should not be used in
@@ -172,7 +174,6 @@
 }
 
 /// An extension on [Func] which allows for access to VM internal fields.
-
 extension FunctionPrivateViewExtension on Func {
   static const _unoptimizedCodeKey = '_unoptimizedCode';
 
@@ -182,7 +183,6 @@
 }
 
 /// An extension on [Code] which allows for access to VM internal fields.
-
 extension CodePrivateViewExtension on Code {
   static const _disassemblyKey = '_disassembly';
 
@@ -192,3 +192,67 @@
   set disassembly(Disassembly disassembly) =>
       json![_disassemblyKey] = disassembly.toJson();
 }
+
+/// An extension on [Field] which allows for access to VM internal fields.
+extension FieldPrivateViewExtension on Field {
+  static const guardClassKey = '_guardClass';
+
+  bool? get guardNullable => json!['_guardNullable'];
+
+  Future<Class?> get guardClass async {
+    if (_guardClassIsClass()) {
+      final service = serviceManager.service!;
+      final isolate = serviceManager.isolateManager.selectedIsolate.value;
+
+      return await service.getObject(isolate!.id!, json![guardClassKey]['id'])
+          as Class;
+    }
+
+    return null;
+  }
+
+  GuardClassKind? guardClassKind() {
+    if (_guardClassIsClass()) {
+      return GuardClassKind.single;
+    } else if (json![guardClassKey] == GuardClassKind.dynamic.jsonValue()) {
+      return GuardClassKind.dynamic;
+    } else if (json![guardClassKey] == GuardClassKind.unknown.jsonValue()) {
+      return GuardClassKind.unknown;
+    }
+
+    return null;
+  }
+
+  bool _guardClassIsClass() {
+    String? guardClassType;
+
+    if (json![guardClassKey] is Map) {
+      guardClassType = json![guardClassKey]['type'];
+    }
+
+    if (guardClassType == '@Class' || guardClassType == 'Class') {
+      return true;
+    } else {
+      return false;
+    }
+  }
+}
+
+/// The kinds of Guard Class that determine whether a Field object has
+/// a unique observed type [single], various observed types [dynamic],
+/// or if the field type has not been observed yet [unknown].
+enum GuardClassKind {
+  single,
+  dynamic,
+  unknown;
+
+  String jsonValue() {
+    switch (this) {
+      case GuardClassKind.dynamic:
+        return 'various';
+      case GuardClassKind.single:
+      case GuardClassKind.unknown:
+        return toString().split('.').last;
+    }
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_statistics_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_statistics_view.dart
index 11abc90..5f8655b 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_statistics_view.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_statistics_view.dart
@@ -70,12 +70,12 @@
           flex: 3,
           child: Column(
             children: [
-              Flexible(
+              Expanded(
                 child: GeneralVMStatisticsWidget(
                   controller: controller,
                 ),
               ),
-              Flexible(
+              Expanded(
                 child: ProcessStatisticsWidget(
                   controller: controller,
                 ),
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
index 22a65c4..1422771 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/object_inspector_view_test.dart
@@ -2,10 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart';
 import 'package:devtools_app/src/screens/debugger/program_explorer.dart';
 import 'package:devtools_app/src/screens/vm_developer/object_inspector_view.dart';
 import 'package:devtools_app/src/screens/vm_developer/object_viewport.dart';
+import 'package:devtools_app/src/scripts/script_manager.dart';
+import 'package:devtools_app/src/service/service_manager.dart';
+import 'package:devtools_app/src/shared/globals.dart';
+import 'package:devtools_app/src/shared/split.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/object_viewport_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/object_viewport_test.dart
index d6adace..732f151 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/object_viewport_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/object_viewport_test.dart
@@ -2,10 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart';
 import 'package:devtools_app/src/screens/vm_developer/object_viewport.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_class_display.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_developer_common_widgets.dart';
+import 'package:devtools_app/src/screens/vm_developer/vm_field_display.dart';
+import 'package:devtools_app/src/screens/vm_developer/vm_object_model.dart';
+import 'package:devtools_app/src/scripts/script_manager.dart';
+import 'package:devtools_app/src/service/service_manager.dart';
+import 'package:devtools_app/src/shared/globals.dart';
 import 'package:devtools_app/src/shared/history_viewport.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:flutter/material.dart';
@@ -19,6 +24,8 @@
 
   late MockClassObject mockClassObject;
 
+  late MockFieldObject mockFieldObject;
+
   late FakeServiceManager fakeServiceManager;
 
   late MockScriptManager scriptManager;
@@ -53,12 +60,33 @@
     when(mockClassObject.fetchingRetainedSize)
         .thenReturn(ValueNotifier<bool>(false));
     when(mockClassObject.retainedSize).thenReturn(null);
-    when(mockClassObject.retainingPath).thenReturn(
-      ValueNotifier<RetainingPath?>(null),
-    );
-    when(mockClassObject.inboundReferences).thenReturn(
-      ValueNotifier<InboundReferences?>(null),
-    );
+    when(mockClassObject.retainingPath)
+        .thenReturn(ValueNotifier<RetainingPath?>(null));
+    when(mockClassObject.inboundReferences)
+        .thenReturn(ValueNotifier<InboundReferences?>(null));
+
+    mockFieldObject = MockFieldObject();
+
+    when(mockFieldObject.outlineNode).thenReturn(null);
+    when(mockFieldObject.scriptRef).thenReturn(null);
+    when(mockFieldObject.name).thenReturn(testField.name);
+    when(mockFieldObject.ref).thenReturn(testField);
+    when(mockFieldObject.obj).thenReturn(testField);
+    when(mockFieldObject.script).thenReturn(null);
+    when(mockFieldObject.pos).thenReturn(null);
+    when(mockFieldObject.guardClass).thenReturn(null);
+    when(mockFieldObject.guardNullable).thenReturn(null);
+    when(mockFieldObject.guardClassKind).thenReturn(null);
+    when(mockFieldObject.fetchingReachableSize)
+        .thenReturn(ValueNotifier<bool>(false));
+    when(mockFieldObject.reachableSize).thenReturn(null);
+    when(mockFieldObject.fetchingRetainedSize)
+        .thenReturn(ValueNotifier<bool>(false));
+    when(mockFieldObject.retainedSize).thenReturn(null);
+    when(mockFieldObject.retainingPath)
+        .thenReturn(ValueNotifier<RetainingPath?>(null));
+    when(mockFieldObject.inboundReferences)
+        .thenReturn(ValueNotifier<InboundReferences?>(null));
   });
 
   testWidgets('builds object viewport', (WidgetTester tester) async {
@@ -99,16 +127,16 @@
   });
 
   testWidgets('test for Field Object', (WidgetTester tester) async {
-    final testFieldObject =
-        TestFieldObject(ref: testField, testField: testField);
     testObjectInspectorViewController.fakeObjectHistory
-        .setCurrentObject(testFieldObject);
+        .setCurrentObject(mockFieldObject);
+
     await tester.pumpWidget(
       wrap(ObjectViewport(controller: testObjectInspectorViewController)),
     );
-    expect(viewportTitle(testFieldObject), 'Field FooField');
-    expect(find.text('Field FooField'), findsOneWidget);
-    expect(find.byType(VMInfoCard), findsOneWidget);
+
+    expect(viewportTitle(mockFieldObject), 'Field fooField');
+    expect(find.text('Field fooField'), findsOneWidget);
+    expect(find.byType(VmFieldDisplay), findsOneWidget);
   });
 
   testWidgets('test for Library Object', (WidgetTester tester) async {
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/vm_class_display_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/vm_class_display_test.dart
index e369692..bc65f58 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/vm_class_display_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/vm_class_display_test.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_class_display.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_developer_common_widgets.dart';
+import 'package:devtools_app/src/shared/globals.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -56,7 +57,8 @@
       (WidgetTester tester) async {
     await tester.pumpWidget(wrap(VmClassDisplay(clazz: mockClassObject)));
 
-    expect(find.byType(ClassInfoWidget), findsOneWidget);
+    expect(find.byType(VmObjectDisplayBasicLayout), findsOneWidget);
+    expect(find.byType(VMInfoCard), findsOneWidget);
     expect(find.text('General Information'), findsOneWidget);
     expect(find.text('1 KB'), findsOneWidget);
     expect(find.text('fooLib'), findsOneWidget);
@@ -73,6 +75,5 @@
     expect(find.byType(InboundReferencesWidget), findsOneWidget);
 
     // TODO(mtaylee): test ClassInstancesWidget when implemented
-    // expect(find.byType(ClassInstancesWidget), findsOneWidget);
   });
 }
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
index f283e30..917dfe5 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/vm_code_display_test.dart
@@ -4,9 +4,10 @@
 
 import 'dart:math';
 
-import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_code_display.dart';
 import 'package:devtools_app/src/screens/vm_developer/vm_service_private_extensions.dart';
+import 'package:devtools_app/src/shared/globals.dart';
 import 'package:devtools_app/src/shared/table.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:flutter/material.dart';
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/vm_developer_common_widgets_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/vm_developer_common_widgets_test.dart
index ff6c336..e6ea562 100644
--- a/packages/devtools_app/test/vm_developer/object_inspector/vm_developer_common_widgets_test.dart
+++ b/packages/devtools_app/test/vm_developer/object_inspector/vm_developer_common_widgets_test.dart
@@ -148,7 +148,7 @@
     await tester.pumpAndSettle();
 
     expect(find.byType(SelectableText), findsNWidgets(5));
-    expect(find.text('fooClass'), findsOneWidget);
+    expect(find.text('FooClass'), findsOneWidget);
     expect(
       find.text('Retained by element [1] of <parentListName>'),
       findsOneWidget,
diff --git a/packages/devtools_app/test/vm_developer/object_inspector/vm_field_display_test.dart b/packages/devtools_app/test/vm_developer/object_inspector/vm_field_display_test.dart
new file mode 100644
index 0000000..5143e31
--- /dev/null
+++ b/packages/devtools_app/test/vm_developer/object_inspector/vm_field_display_test.dart
@@ -0,0 +1,138 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart';
+import 'package:devtools_app/src/screens/vm_developer/vm_developer_common_widgets.dart';
+import 'package:devtools_app/src/screens/vm_developer/vm_field_display.dart';
+import 'package:devtools_app/src/screens/vm_developer/vm_service_private_extensions.dart';
+import 'package:devtools_app/src/shared/globals.dart';
+import 'package:devtools_test/devtools_test.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../vm_developer_test_utils.dart';
+
+void main() {
+  late MockFieldObject mockFieldObject;
+
+  const windowSize = Size(4000.0, 4000.0);
+
+  late Field testFieldCopy;
+
+  late InstanceRef fieldStaticValue;
+
+  setUp(() {
+    setGlobal(IdeTheme, IdeTheme());
+
+    mockFieldObject = MockFieldObject();
+
+    final fieldJson = testField.toJson();
+    testFieldCopy = Field.parse(fieldJson)!;
+
+    final instanceJson = testInstance.toJson();
+    fieldStaticValue = Instance.parse(instanceJson)!;
+
+    fieldStaticValue.name = 'FooNumberType';
+    fieldStaticValue.valueAsString = '100';
+
+    testFieldCopy.size = 256;
+    testFieldCopy.staticValue = fieldStaticValue;
+
+    when(mockFieldObject.outlineNode).thenReturn(null);
+    when(mockFieldObject.scriptRef).thenReturn(null);
+    when(mockFieldObject.name).thenReturn(testFieldCopy.name);
+    when(mockFieldObject.ref).thenReturn(testFieldCopy);
+    when(mockFieldObject.obj).thenReturn(testFieldCopy);
+    when(mockFieldObject.script).thenReturn(testScript);
+    when(mockFieldObject.pos).thenReturn(testPos);
+
+    when(mockFieldObject.guardClass).thenReturn(null);
+    when(mockFieldObject.guardNullable).thenReturn(null);
+    when(mockFieldObject.guardClassKind).thenReturn(null);
+
+    when(mockFieldObject.fetchingReachableSize)
+        .thenReturn(ValueNotifier<bool>(false));
+    when(mockFieldObject.reachableSize).thenReturn(testRequestableSize);
+    when(mockFieldObject.fetchingRetainedSize)
+        .thenReturn(ValueNotifier<bool>(false));
+    when(mockFieldObject.retainedSize).thenReturn(testRequestableSize);
+    when(mockFieldObject.retainingPath).thenReturn(
+      ValueNotifier<RetainingPath?>(testRetainingPath),
+    );
+    when(mockFieldObject.inboundReferences).thenReturn(
+      ValueNotifier<InboundReferences?>(testInboundRefs),
+    );
+  });
+
+  group('field data display tests', () {
+    testWidgetsWithWindowSize('basic layout', windowSize,
+        (WidgetTester tester) async {
+      await tester.pumpWidget(wrap(VmFieldDisplay(field: mockFieldObject)));
+
+      expect(find.byType(VmObjectDisplayBasicLayout), findsOneWidget);
+      expect(find.byType(VMInfoCard), findsOneWidget);
+      expect(find.text('General Information'), findsOneWidget);
+      expect(find.text('Field'), findsOneWidget);
+      expect(find.text('256 B'), findsOneWidget);
+      expect(find.text('Owner:'), findsOneWidget);
+      expect(find.text('fooLib'), findsOneWidget);
+      expect(find.text('fooScript.dart:10:4'), findsOneWidget);
+      expect(find.text('Observed types not found'), findsOneWidget);
+      expect(find.text('Static Value:'), findsOneWidget);
+      expect(find.text('FooNumberType: 100'), findsOneWidget);
+
+      expect(find.byType(RequestableSizeWidget), findsNWidgets(2));
+
+      expect(find.byType(RetainingPathWidget), findsOneWidget);
+
+      expect(find.byType(InboundReferencesWidget), findsOneWidget);
+    });
+
+    testWidgetsWithWindowSize('observed type single - nullable', windowSize,
+        (WidgetTester tester) async {
+      when(mockFieldObject.guardClass).thenReturn(testClass);
+      when(mockFieldObject.guardNullable).thenReturn(true);
+      when(mockFieldObject.guardClassKind).thenReturn(GuardClassKind.single);
+
+      await tester.pumpWidget(wrap(VmFieldDisplay(field: mockFieldObject)));
+      expect(find.text('FooClass - null observed'), findsOneWidget);
+    });
+
+    testWidgetsWithWindowSize(
+        'observed type dynamic - non-nullable', windowSize,
+        (WidgetTester tester) async {
+      when(mockFieldObject.guardClass).thenReturn(null);
+      when(mockFieldObject.guardNullable).thenReturn(false);
+      when(mockFieldObject.guardClassKind).thenReturn(GuardClassKind.dynamic);
+
+      await tester.pumpWidget(wrap(VmFieldDisplay(field: mockFieldObject)));
+      expect(find.text('various - null not observed'), findsOneWidget);
+    });
+
+    testWidgetsWithWindowSize(
+        'observed type unknown - null unknown', windowSize,
+        (WidgetTester tester) async {
+      when(mockFieldObject.guardClass).thenReturn(null);
+      when(mockFieldObject.guardNullable).thenReturn(null);
+      when(mockFieldObject.guardClassKind).thenReturn(GuardClassKind.unknown);
+
+      await tester.pumpWidget(wrap(VmFieldDisplay(field: mockFieldObject)));
+      expect(
+        find.text('none'),
+        findsOneWidget,
+      );
+    });
+
+    testWidgetsWithWindowSize('static value is not InstanceRef', windowSize,
+        (WidgetTester tester) async {
+      testFieldCopy.staticValue = testClass;
+
+      await tester.pumpWidget(wrap(VmFieldDisplay(field: mockFieldObject)));
+
+      expect(find.text('Static Value:'), findsNothing);
+    });
+  });
+}
diff --git a/packages/devtools_app/test/vm_developer/vm_developer_test_utils.dart b/packages/devtools_app/test/vm_developer/vm_developer_test_utils.dart
index 1259f25..d3c6805 100644
--- a/packages/devtools_app/test/vm_developer/vm_developer_test_utils.dart
+++ b/packages/devtools_app/test/vm_developer/vm_developer_test_utils.dart
@@ -19,7 +19,7 @@
 );
 
 final testClass = Class(
-  name: 'fooClass',
+  name: 'FooClass',
   library: testLib,
   isAbstract: false,
   isConst: false,
diff --git a/packages/devtools_test/lib/src/mocks/generated.dart b/packages/devtools_test/lib/src/mocks/generated.dart
index f79631b..511e040 100644
--- a/packages/devtools_test/lib/src/mocks/generated.dart
+++ b/packages/devtools_test/lib/src/mocks/generated.dart
@@ -27,6 +27,7 @@
   ObjectGroupBase,
   ClassObject,
   CodeObject,
+  FieldObject,
   ui.Image,
 ])
 void main() {}