Repository: ubuntu/yaru.dart

Issue:
YaruImageTile: replace path with an image widget
A `String path` property is problematic because it doesn't let apps specify the desired type of image widget. For example, the Ubuntu Desktop Installer needs to pass a `FlavorImage` widget for looking up flavor theme thumbnails in canonical/ubuntu-desktop-installer#656. Please consider changing the property to a usual `Widget child` as it is in 95% of cases in Flutter. This would give apps full control over the image.


Discussion: @jpnurmi oh, that is indeed problematic

I tried to make the library web compatible by populating the image with

https://github.com/ubuntu/yaru_widgets.dart/blob/ffd30e150e2758893462181c1392cda08ce7ac45/lib/src/yaru_image_tile.dart#L62-L68

depending on the platform. 
Any idea how we could keep web compatibility? Or do you basically want to remove the whole image part from YaruImageTile and functioally reduce it to a "YaruSelectableTile" ?
> I tried to make the library web compatible by populating the image with

It's better to leave such decisions to apps because there's no way to know what the app needs. For example, even if Image.file() might work for assets on desktop platforms (because assets happen to be files located in a matching directory), it won't work on mobile where assets are not ordinary files on the file system.

Flutter's philosophy is to allow freely composing widgets and to avoid assuming/restricting the type of children unless absolutely necessary. For example, even if IconButton.icon is normally an Icon, it allows any Widget because you might want to e.g. wrap the icon with another widget, or even use something else than Icon.

> reduce it to a "YaruSelectableTile" ?

That's an option if there's nothing left specifically for images.
Alright - will do. Only need to find a solution for the BoxFit.fill, because this is what makes the image work almost everywhere
Could it be something documented in the class description together with an example snippet, as a recommended way to use the widget?

Fix patch:
diff --git a/example/lib/yaru_home.dart b/example/lib/yaru_home.dart
index 7c381de26..905ebca4f 100644
--- a/example/lib/yaru_home.dart
+++ b/example/lib/yaru_home.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:yaru_icons/yaru_icons.dart';
 import 'package:yaru_widgets/yaru_widgets.dart';
@@ -17,6 +20,8 @@ class YaruHome extends StatefulWidget {
 class _YaruHomeState extends State<YaruHome> {
   bool _extraOptionValue = false;
   bool _isImageSelected = false;
+  bool _isOvalSelected = false;
+  bool _isTextSelected = false;
   final _textEditingController = TextEditingController();
   double _sliderValue = 0;
 
@@ -138,22 +143,44 @@ class _YaruHomeState extends State<YaruHome> {
         ),
       ),
       YaruPageItem(
-        title: 'ImageTile',
+        title: 'YaruSelectableContainer',
         iconData: YaruIcons.emote_devilish,
         builder: (_) => YaruPage(
           children: [
-            YaruImageTile(
-              currentlySelected: _isImageSelected,
-              onTap: () {
-                setState(() {
-                  if (_isImageSelected) {
-                    _isImageSelected = false;
-                  } else {
-                    _isImageSelected = true;
-                  }
-                });
-              },
-              path: "assets/ubuntuhero.jpg",
+            YaruSelectableContainer(
+              selected: _isImageSelected,
+              onTap: () => setState(() => _isImageSelected = !_isImageSelected),
+              child: kIsWeb
+                  ? Image.network('assets/ubuntuhero.jpg',
+                      filterQuality: FilterQuality.low, fit: BoxFit.fill)
+                  : Image.file(File('assets/ubuntuhero.jpg'),
+                      filterQuality: FilterQuality.low, fit: BoxFit.fill),
+            ),
+            SizedBox(
+              height: 20,
+            ),
+            YaruSelectableContainer(
+              selected: _isTextSelected,
+              onTap: () => setState(() => _isTextSelected = !_isTextSelected),
+              child: Padding(
+                padding: const EdgeInsets.all(18.0),
+                child: Text('This is just text but can be selected!'),
+              ),
+            ),
+            SizedBox(
+              height: 20,
+            ),
+            YaruSelectableContainer(
+              borderRadius: BorderRadius.circular(100.0),
+              selected: _isOvalSelected,
+              onTap: () => setState(() => _isOvalSelected = !_isOvalSelected),
+              child: ClipOval(
+                child: Material(
+                  color: Colors.amber, // Button color
+                  child: SizedBox(
+                      width: 56, height: 56, child: Icon(YaruIcons.heart)),
+                ),
+              ),
             )
           ],
         ),
diff --git a/lib/src/yaru_image_tile.dart b/lib/src/yaru_image_tile.dart
deleted file mode 100644
index 9c47febb8..000000000
--- a/lib/src/yaru_image_tile.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-import 'dart:io';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-
-class YaruImageTile extends StatelessWidget {
-  /// Creates a Image Tile from the image path given in the path property.
-  const YaruImageTile(
-      {Key? key,
-      required this.path,
-      this.onTap,
-      required this.currentlySelected,
-      this.filterQuality})
-      : super(key: key);
-
-  // Specifies the path of the asset
-  final String path;
-
-  /// Current Value of the imageTile.
-  /// Based on the this value selection of the image can be managed.
-  /// If this value is `true` [Container] border will have color from [Theme.of(context).primaryColor]
-  /// else if the value is `false` the border color will be [Colors.transparent].
-  final bool currentlySelected;
-
-  /// Callback triggered when the [YaruImageTile] is clicked.
-  final VoidCallback? onTap;
-
-  /// The rendering quality of the image.
-  ///
-  /// If the image is of a high quality and its pixels are perfectly aligned
-  /// with the physical screen pixels, extra quality enhancement may not be
-  /// necessary. If so, then [FilterQuality.none] would be the most efficient.
-  ///
-  /// If the pixels are not perfectly aligned with the screen pixels, or if the
-  /// image itself is of a low quality, [FilterQuality.none] may produce
-  /// undesirable artifacts. Consider using other [FilterQuality] values to
-  /// improve the rendered image quality in this case. Pixels may be misaligned
-  /// with the screen pixels as a result of transforms or scaling.
-  ///
-  /// See also:
-  ///
-  ///  * [FilterQuality], the enum containing all possible filter quality
-  ///    options.
-  ///  * By default [FilterQuality] will be [FilterQuality.low].
-  final FilterQuality? filterQuality;
-
-  @override
-  Widget build(BuildContext context) {
-    return InkWell(
-      borderRadius: BorderRadius.circular(6.0),
-      onTap: onTap,
-      child: Container(
-        decoration: BoxDecoration(
-            borderRadius: BorderRadius.circular(8.0),
-            color: currentlySelected
-                ? Theme.of(context).primaryColor.withOpacity(0.8)
-                : Colors.transparent),
-        child: Padding(
-          padding: const EdgeInsets.all(6.0),
-          child: ClipRRect(
-            borderRadius: BorderRadius.circular(4),
-            child: kIsWeb
-                ? Image.network(path,
-                    filterQuality: filterQuality ?? FilterQuality.low,
-                    fit: BoxFit.fill)
-                : Image.file(File(path),
-                    filterQuality: filterQuality ?? FilterQuality.low,
-                    fit: BoxFit.fill),
-          ),
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/src/yaru_selectable_container.dart b/lib/src/yaru_selectable_container.dart
new file mode 100644
index 000000000..3b3df3f23
--- /dev/null
+++ b/lib/src/yaru_selectable_container.dart
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+
+class YaruSelectableContainer extends StatelessWidget {
+  /// Creates a Image Tile from the image path given in the path property.
+  const YaruSelectableContainer({
+    Key? key,
+    required this.child,
+    this.onTap,
+    required this.selected,
+    this.borderRadius,
+    this.radius = 6.0,
+    this.padding,
+    this.selectionColor,
+  }) : super(key: key);
+
+  // The child which will be selected with [onTap]
+  final Widget child;
+
+  /// Current Value of the imageTile.
+  /// Based on the this value selection of the image can be managed.
+  /// If this value is `true` [Container] border will have color from [Theme.of(context).primaryColor]
+  /// else if the value is `false` the border color will be [Colors.transparent].
+  final bool selected;
+
+  /// Callback triggered when the [YaruSelectableContainer] is clicked.
+  final VoidCallback? onTap;
+
+  /// Optional custom radius for the corners which defaults to a circular 6.0 [BorderRadius]
+  final BorderRadius? borderRadius;
+
+  /// Optional double value used for a symmetric circular [BorderRadius] if [borderRadius]
+  /// is not specified.
+  final double radius;
+
+  /// Optional custom padding for the child which defaults to 6.0 on all sides.
+  final EdgeInsets? padding;
+
+  /// Optional custom [Color] which is used for the selection border.
+  final Color? selectionColor;
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      borderRadius: borderRadius ?? BorderRadius.circular(radius),
+      onTap: onTap,
+      child: Container(
+        decoration: BoxDecoration(
+            borderRadius: borderRadius ?? BorderRadius.circular(radius),
+            color: selected
+                ? selectionColor ??
+                    Theme.of(context).primaryColor.withOpacity(0.8)
+                : Colors.transparent),
+        child: Padding(
+          padding: padding ?? const EdgeInsets.all(6.0),
+          child: ClipRRect(
+            borderRadius:
+                borderRadius ?? BorderRadius.circular(radius - radius / 3),
+            child: child,
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/yaru_widgets.dart b/lib/yaru_widgets.dart
index c5db80002..abdc01ae2 100644
--- a/lib/yaru_widgets.dart
+++ b/lib/yaru_widgets.dart
@@ -6,7 +6,6 @@ export 'src/yaru_color_picker_button.dart';
 export 'src/yaru_dialog_title.dart';
 export 'src/yaru_expansion_panel_list.dart';
 export 'src/yaru_extra_option_row.dart';
-export 'src/yaru_image_tile.dart';
 export 'src/yaru_linear_progress_indicator.dart';
 export 'src/yaru_master_detail_page.dart';
 export 'src/yaru_narrow_layout.dart';
@@ -17,6 +16,7 @@ export 'src/yaru_page_item.dart';
 export 'src/yaru_row.dart';
 export 'src/yaru_search_app_bar.dart';
 export 'src/yaru_section.dart';
+export 'src/yaru_selectable_container.dart';
 export 'src/yaru_simple_dialog.dart';
 export 'src/yaru_single_info_row.dart';
 export 'src/yaru_slider_row.dart';


Test patch:
diff --git a/test/yaru_image_tile_test.dart b/test/yaru_image_tile_test.dart
deleted file mode 100644
index 102a766f1..000000000
--- a/test/yaru_image_tile_test.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:yaru_widgets/src/yaru_image_tile.dart';
-
-void main() {
-  testWidgets('- YaruImageTile Test', (WidgetTester tester) async {
-    await tester.pumpWidget(MaterialApp(
-      theme: ThemeData(primarySwatch: Colors.red),
-      home: Scaffold(
-        body: YaruImageTile(
-          currentlySelected: true,
-          path: "images/filename",
-          onTap: () {},
-        ),
-      ),
-    ));
-
-    expect(find.byType(InkWell), findsOneWidget);
-    expect(find.byType(Image), findsOneWidget);
-  });
-}
diff --git a/test/yaru_selectable_container_test.dart b/test/yaru_selectable_container_test.dart
new file mode 100644
index 000000000..ea5bc0ebb
--- /dev/null
+++ b/test/yaru_selectable_container_test.dart
@@ -0,0 +1,29 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:yaru_widgets/src/yaru_selectable_container.dart';
+
+void main() {
+  testWidgets('- YaruImageTile Test', (WidgetTester tester) async {
+    bool selected = false;
+
+    await tester.pumpWidget(MaterialApp(
+      theme: ThemeData(primarySwatch: Colors.red),
+      home: Scaffold(
+        body: YaruSelectableContainer(
+          selected: true,
+          child: kIsWeb
+              ? Image.network('assets/ubuntuhero.jpg',
+                  filterQuality: FilterQuality.low, fit: BoxFit.fill)
+              : Image.file(File('assets/ubuntuhero.jpg'),
+                  filterQuality: FilterQuality.low, fit: BoxFit.fill),
+          onTap: () => selected = !selected,
+        ),
+      ),
+    ));
+
+    expect(find.byType(YaruSelectableContainer), findsOneWidget);
+  });
+}