Skip to content

Implement Android UI using Material Design components with MAUI and MvvmCross#100

Draft
EvgenBrovchenko wants to merge 98 commits into
developfrom
99-implement-android-ui-using-material-design-components-with-maui-and-mvvmcross
Draft

Implement Android UI using Material Design components with MAUI and MvvmCross#100
EvgenBrovchenko wants to merge 98 commits into
developfrom
99-implement-android-ui-using-material-design-components-with-maui-and-mvvmcross

Conversation

@EvgenBrovchenko

Copy link
Copy Markdown
Contributor

No description provided.

…droid : Buform.Example.Maui; Buform.Maui;

Set  CommunityToolkit.Maui to v.9.0.1;
Set  MvvmCross to v.9.3.1;
Fix Android.App/CreateMauiApp;
Add  Platforms\Android\FormViewHandler.cs;
@EvgenBrovchenko EvgenBrovchenko self-assigned this Mar 10, 2026
@EvgenBrovchenko
EvgenBrovchenko marked this pull request as draft March 10, 2026 13:44
@EvgenBrovchenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Projects were updated to multi-target net9.0-ios;net9.0-android with platform-conditional SupportedOSPlatformVersion values. Android support was added/expanded: a MAUI FormView handler, a generic MauiFormRecyclerAdapter and view-holder base types, many Android form view-holders/components (pickers, sliders, steppers, segments, switches, text inputs, steppers), picker presenters/fragments/adapters, and numerous helper types (reflection keys/helper, context extensions). New Android resources (layouts, drawables, dimens, strings) and Android-specific NuGet package references were added. Miscellaneous changes include sealing some classes, fixing MainApplication.CreateMauiApp return, package version bumps, and a CI Xcode path update.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Please provide a description explaining the changes, objectives, and any important context for this implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main objective of the pull request: implementing Android UI using Material Design components with MAUI and MvvmCross, which directly aligns with the extensive changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Buform.Example.Maui/Platforms/Android/MainApplication.cs (1)

6-7: 🛠️ Refactor suggestion | 🟠 Major

Mark the class as sealed.

MainApplication is not designed for inheritance, contains no virtual/abstract members, and is not inherited elsewhere. As per coding guidelines, "Require marking C# classes as sealed if they are not designed for inheritance."

Proposed fix
 [Application]
-public class MainApplication : MauiApplication
+public sealed class MainApplication : MauiApplication
Buform.Example/MenuViewModel.cs (3)

7-7: 🛠️ Refactor suggestion | 🟠 Major

Mark the class as sealed.

The class MenuViewModel should be marked as sealed as it contains no virtual or abstract members and is not designed for inheritance. As per coding guidelines, C# classes must be marked as sealed if they are not designed for inheritance.

🔒 Proposed fix to mark the class as sealed
-public partial class MenuViewModel : ObservableObject
+public sealed partial class MenuViewModel : ObservableObject

62-62: ⚠️ Potential issue | 🔴 Critical

Add a single newline at the end of the file.

The file does not end with a single newline character, causing a pipeline formatting failure. Ensure the file ends with exactly one newline after the closing brace.


52-54: ⚠️ Potential issue | 🟠 Major

Remove unused cancellationToken parameter.

The CreateEventAsync method declares a cancellationToken parameter but does not use it. The INavigationService.OpenCreateEventAsync() method does not support cancellation tokens, so the parameter cannot be passed through. Remove the unused parameter from the method signature.

🧹 Nitpick comments (3)
Buform.Example/MenuViewModel.cs (1)

18-42: Consider extracting UI string literals to constants.

The constructor contains several hardcoded string literals for form labels ("Gallery", "Show All Components", "Examples", etc.). Whilst these might be considered one-off UI text, extracting them to named constants would improve maintainability and support future localisation.

♻️ Example refactor to extract string constants

Add constants at the class level:

private const string GalleryGroupTitle = "Gallery";
private const string ShowAllComponentsLabel = "Show All Components";
private const string ExamplesGroupTitle = "Examples";
private const string ExamplesGroupFooter = "Contains some real-life examples.";
private const string SetupConnectionLabel = "Setup New Connection";
private const string CreateEventLabel = "Create New Event";

Then update the constructor:

-        new TextFormGroup("Gallery")
+        new TextFormGroup(GalleryGroupTitle)
         {
             new ButtonFormItem(ShowControlsCommand)
             {
-                Label = "Show All Components",
+                Label = ShowAllComponentsLabel,
                 InputType = ButtonInputType.Done
             }
         },
-        new TextFormGroup("Examples", "Contains some real-life examples.")
+        new TextFormGroup(ExamplesGroupTitle, ExamplesGroupFooter)
         {
             new ButtonFormItem(CreateConnectionCommand)
             {
-                Label = "Setup New Connection",
+                Label = SetupConnectionLabel,
                 InputType = ButtonInputType.Done
             },
             new ButtonFormItem(CreateEventCommand)
             {
-                Label = "Create New Event",
+                Label = CreateEventLabel,
                 InputType = ButtonInputType.Done
             }
         }
Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs (1)

117-127: Centralise the header/footer reflection keys.

"HeaderLabel" and "FooterLabel" are domain keys used here and again in the header/footer view holder. Keeping them inline on both sides makes this path easy to break during a rename. Please extract shared constants or a small helper.

As per coding guidelines, "Avoid hardcoded string literals in code logic. Any repeated or domain-relevant string (e.g. keys, identifiers, status values, error codes, comparison values) must be extracted into clearly named constants or static readonly fields."

Buform.Maui/Platforms/Android/FormAdapterItem.cs (1)

12-29: Make the adapter payload non-nullable.

All factory methods take non-null inputs, so exposing Item as nullable just weakens the contract and pushes an unnecessary null path into OnBindViewHolder. Tighten this to object and guard once in the constructor.

Suggested fix
-    public object? Item { get; }
+    public object Item { get; }

-    private FormAdapterItem(FormAdapterItemKind kind, object? item)
+    private FormAdapterItem(FormAdapterItemKind kind, object item)
     {
+        ArgumentNullException.ThrowIfNull(item);
         Kind = kind;
         Item = item;
     }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a83bd230-495b-4309-88e4-1198dad44595

📥 Commits

Reviewing files that changed from the base of the PR and between 92a8d27 and 0d77573.

📒 Files selected for processing (10)
  • Buform.Example.Maui/Buform.Example.Maui.csproj
  • Buform.Example.Maui/Platforms/Android/MainApplication.cs
  • Buform.Example/MenuViewModel.cs
  • Buform.Maui/Buform.Maui.csproj
  • Buform.Maui/Platforms/Android/FormAdapterItem.cs
  • Buform.Maui/Platforms/Android/FormViewHandler.cs
  • Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormItemViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs
  • Buform.MvvmCross/Buform.MvvmCross.csproj

Comment thread Buform.Example.Maui/Platforms/Android/MainApplication.cs Outdated
Comment thread Buform.Example/MenuViewModel.cs Outdated
Comment thread Buform.Maui/Buform.Maui.csproj Outdated
Comment thread Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs Outdated
Comment thread Buform.Maui/Platforms/Android/MauiFormItemViewHolder.cs Outdated
Update  Xamarin.AndroidX.Collection from v1.5.0.4 to v1.5.0.5;
Update  Xamarin.AndroidX.Collection.Ktx from v1.5.0.4 to v1.5.0.5;
Update  Xamarin.AndroidX.Fragment.Ktx from v1.8.6.1 to v 1.8.9.2;
Update Humanizer.Core from v3.0.8 to v3.0.10
Add  Xamarin.AndroidX.Collection v1.5.0.5;
Add  Xamarin.Google.Android.Material v1.12.0.4
@EvgenBrovchenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@codacy-production

codacy-production Bot commented Mar 31, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 medium

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
BestPractice 1 medium
ErrorProne 1 medium

View in Codacy

🟢 Metrics 654 complexity · 19 duplication

Metric Results
Complexity 654
Duplication 19

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@EvgenBrovchenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs (1)

136-176: 🛠️ Refactor suggestion | 🟠 Major

Keep Dispose as the last method in the class.

Line 136 introduces Dispose, but helper methods continue after it. Please move cleanup methods to the end and place helpers above.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

♻️ Duplicate comments (7)
.github/workflows/ci.yml (1)

22-23: ⚠️ Potential issue | 🟠 Major

Harden Xcode selection to prevent runner-image breakage.

Using a fixed absolute path on macos-latest can fail when the hosted image changes, causing avoidable CI outages.

Proposed hardening
-    - name: Setup Xcode 26.2 (required for .NET iOS build)
-      run: sudo xcode-select -s /Applications/Xcode_26.2.app
+    - name: Setup Xcode 26.2 (required for .NET iOS build)
+      run: |
+        if [ -d "/Applications/Xcode_26.2.app" ]; then
+          sudo xcode-select -s /Applications/Xcode_26.2.app
+        else
+          echo "Xcode 26.2 not found on this runner image" >&2
+          ls -1 /Applications | grep Xcode || true
+          exit 1
+        fi
Buform/Platforms/Android/Items/DateTime/ContextExtensions.cs (1)

12-26: ⚠️ Potential issue | 🟡 Minor

Add a loop guard when unwrapping ContextWrapper.

The while (true) pattern can loop indefinitely if a wrapper chain is cyclic or if BaseContext returns the same instance. Add a break guard when BaseContext is null or references the same instance.

Suggested fix
         while (true)
         {
             if (current is FragmentActivity fragmentActivity)
             {
                 return fragmentActivity;
             }

             if (current is ContextWrapper wrapper)
             {
-                current = wrapper.BaseContext!;
+                var baseContext = wrapper.BaseContext;
+                if (baseContext is null || ReferenceEquals(baseContext, current))
+                {
+                    break;
+                }
+
+                current = baseContext;
                 continue;
             }

             break;
         }
Buform/Resources/layout/PickerDialogLayout.xml (1)

89-89: ⚠️ Potential issue | 🟡 Minor

Use a string resource for the search hint.

Line 89 still hardcodes "Search"; please move it to @string/... for localisation consistency.

Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs (1)

20-25: ⚠️ Potential issue | 🟠 Major

Replace the touch-event subscription with the slider's proper value-change listener.

The Material Slider API provides AddOnChangeListener with an onValueChange callback that includes a fromUser boolean. Subscribe to this instead of View.Touch (line 24). This captures all user input—dragging, keyboard, and accessibility—and only fires when the slider value is committed. Check fromUser to persist only user-initiated updates, avoiding programmatic feedback loops without needing manual flags.

Note: Due to Xamarin binding limitations with BaseSlider, you may need to use reflection to invoke addOnChangeListener and implement IBaseOnChangeListener.

Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs (1)

14-15: ⚠️ Potential issue | 🟠 Major

Persist the picker item state to survive fragment recreation.

_item is stored only as an instance field without being persisted to Arguments or SaveInstanceState. When DialogFragment is recreated by Android (after rotation or process death), the new instance initialises with _item == null, causing all setup methods to silently exit early and the dialog to render without content or callbacks.

Consider storing a stable identifier in Arguments bundle during Create() and restoring the item reference in OnViewCreated() by re-resolving it from a presenter, parent fragment, or activity.

Also applies to: 25-28

Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs (2)

125-141: ⚠️ Potential issue | 🟠 Major

Implement separate picker flows for DateTimeInputType.Time and DateTimeInputType.DateTime.

The rendering correctly switches on InputType (lines 125–129) to display formatted values, but the interaction always opens a date picker regardless of type. This prevents editing:

  • Time-only items: clicking displays a date picker when only time should be editable
  • Date-time items: time components are preserved from the existing value; only the date can be changed

Implement a time picker for DateTimeInputType.Time and a date-time picker (or sequential date and time pickers) for DateTimeInputType.DateTime in the OnClick method to match the rendering logic.


151-159: ⚠️ Potential issue | 🔴 Critical

MaterialDatePicker converts date values through UTC epoch, not local time.

MaterialDatePicker operates on UTC epoch milliseconds representing 00:00:00 UTC on a given calendar date. The current implementation incorrectly converts full DateTime values (including time components) through local timezone boundaries, causing off-by-one date errors.

The bug manifests in both directions:

  • ToUtcMillis(currentValue) converts the full local timestamp to UTC, then to epoch. For users west of UTC, this shifts the date forward by one day in the picker.
  • FromUtcMillis(selectedMillis.LongValue()).Date converts the UTC epoch back through the local timezone, shifting the date backward.
🐛 Suggested fix
-    private static long ToUtcMillis(DateTime dateTime)
+    private static long ToPickerUtcDateMillis(DateTime dateTime)
     {
-        var utc = dateTime.Kind switch
-        {
-            DateTimeKind.Utc => dateTime,
-            DateTimeKind.Local => dateTime.ToUniversalTime(),
-            _ => DateTime.SpecifyKind(dateTime, DateTimeKind.Local).ToUniversalTime()
-        };
-
-        return new DateTimeOffset(utc).ToUnixTimeMilliseconds();
+        var utcDate = DateTime.SpecifyKind(dateTime.Date, DateTimeKind.Utc);
+        return new DateTimeOffset(utcDate).ToUnixTimeMilliseconds();
     }
 
-    private static DateTime FromUtcMillis(long millis)
+    private static DateTime FromPickerUtcDateMillis(long millis)
     {
-        return DateTimeOffset.FromUnixTimeMilliseconds(millis).LocalDateTime;
+        return DateTimeOffset.FromUnixTimeMilliseconds(millis).UtcDateTime.Date;
     }

Also applies to: 176-189, 243-257

🧹 Nitpick comments (23)
Buform/Resources/drawable/bg_stepper_capsule.xml (1)

4-4: Prefer a colour resource over an inline hex value.

Line 4 hardcodes the background colour, which makes theme/day-night customisation harder. Please move this to @color/... and provide day/night overrides if needed.

Suggested change
-    <solid android:color="#E5E5EA" />
+    <solid android:color="@color/stepper_capsule_background" />
Buform/Resources/drawable/ic_stepper_minus.xml (1)

8-8: Use a theme-aware colour resource for the vector path.

Line 8 hardcodes black; consider @color/... (or view tinting) so the icon adapts better across themes.

Suggested change
-            android:fillColor="#000000"
+            android:fillColor="@color/stepper_icon_color"
Buform/Resources/layout/FormItemDateTimeLayout.xml (1)

22-22: Consider extracting hardcoded dimension to a resource.

The 6dp bottom margin is hardcoded whilst other spacing values use dimension resources (@dimen/form_horizontal_padding, etc.). For consistency and easier maintenance, consider extracting this to a dimension resource.

-                android:layout_marginBottom="6dp"
+                android:layout_marginBottom="@dimen/label_bottom_margin"
Buform/Resources/layout/PickerOptionItemLayout.xml (1)

21-21: Consider using a Material Design icon instead of platform drawable.

@android:drawable/checkbox_on_background is a platform drawable that may appear inconsistent across different Android versions and manufacturer skins. Consider using a Material icon (e.g., from @drawable/ic_check or a vector drawable) for consistent appearance.

Buform/Resources/layout/FormItemTextLayout.xml (1)

23-23: Use a dimension resource for vertical padding.

Line 23 hardcodes 20dp while adjacent spacing is resource-based. Please extract this to @dimen/... to keep spacing rules consistent.

Buform/Resources/layout/FormItemSwitchLayout.xml (1)

7-7: Reference shared height dimen instead of inline 56dp.

Line 7 can use @dimen/form_item_height to align with the new shared sizing tokens and avoid drift.

Buform/Resources/layout/PickerDialogLayout.xml (1)

79-80: Remove redundant namespace declarations in TextInputLayout.

xmlns:android and xmlns:app are already declared on the root node, so repeating them here is unnecessary.

Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs (1)

101-175: Use descriptive names in helper methods.

Please rename abbreviated identifiers like lp, attr, and a to explicit names (layoutParameters, attribute, alphaChannel) for readability.

As per coding guidelines, "Prefer full, descriptive names for variables and class names. Avoid abbreviations like ct, cts, cfg, or svc. Use complete names such as cancellationToken, taskCompletionSource, configuration, service."

Buform/Platforms/Android/Items/Picker/MultiValuePickerFormViewHolder.cs (1)

20-35: Consider consolidating duplicate switch arms.

The Default, Dialog, and PopUp cases all return identical DialogFragmentPickerPresenter instances. This could be simplified using combined case patterns.

♻️ Proposed refactor
         return inputType switch
         {
-            PickerInputType.Default
-                => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
-                    CreatePickerDialogFragment
-                ),
-            PickerInputType.Dialog
-                => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
-                    CreatePickerDialogFragment
-                ),
-            PickerInputType.PopUp
+            PickerInputType.Default or PickerInputType.Dialog or PickerInputType.PopUp
                 => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
                     CreatePickerDialogFragment
                 ),
             _ => null
         };
Buform/Resources/layout/AsyncPickerDialogLayout.xml (2)

79-93: Remove redundant namespace declarations.

Lines 81-82 redeclare xmlns:android and xmlns:app which are already declared on the root LinearLayout element (lines 3-4). These redundant declarations should be removed.

♻️ Proposed fix
         <com.google.android.material.textfield.TextInputLayout
                 android:id="@+id/SearchLayout"
-                xmlns:android="http://schemas.android.com/apk/res/android"
-                xmlns:app="http://schemas.android.com/apk/res-auto"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 app:boxBackgroundMode="filled"
                 app:hintEnabled="false">

43-43: Hardcoded ripple colour should use a theme attribute or colour resource.

The ripple colour #4A4458 is hardcoded inline. For consistency and theme support, consider extracting this to a colour resource.

Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs (1)

67-83: Consider extracting dimension constants.

The hardcoded padding and text size values (16, 24, 8, 4 for padding; 14, 12 for text size) are scattered through the switch cases. Per coding guidelines, consider extracting these to named constants for maintainability.

♻️ Example constants
private const int PaddingHorizontal = 16;
private const int HeaderPaddingTop = 24;
private const int HeaderPaddingBottom = 8;
private const int FooterPaddingTop = 4;
private const int FooterPaddingBottom = 16;
private const float HeaderTextSizeSp = 14f;
private const float FooterTextSizeSp = 12f;

As per coding guidelines: "Avoid hardcoding format strings directly in code... Move formatting patterns to application-level constants, helpers, or localisation resources."

Buform.Maui/FormReflectionHelper.cs (1)

17-18: Consider caching property lookups for frequently accessed types.

The reflection-based property lookup (GetType().GetProperty(...)) is performed on every call. If this helper is invoked frequently for the same types, consider caching PropertyInfo objects per type to improve performance.

♻️ Example with caching
private static readonly ConcurrentDictionary<(Type, string), PropertyInfo?> PropertyCache = new();

private static string? GetStringProperty(object item, string propertyName)
{
    var key = (item.GetType(), propertyName);
    var property = PropertyCache.GetOrAdd(key, k => k.Item1.GetProperty(k.Item2));
    return property?.GetValue(item) as string;
}
Buform/Resources/layout/FormItemStepperLayout.xml (2)

45-49: Extract hardcoded divider colour to a resource.

The divider background colour #D1D1D6 is hardcoded inline. For consistency and theme support, extract this to a colour resource.

♻️ Proposed fix

In values/colors.xml:

<color name="stepper_divider">#D1D1D6</color>

In the layout:

         <View
                 android:layout_width="1dp"
                 android:layout_height="24dp"
                 android:layout_gravity="center_vertical"
-                android:background="#D1D1D6" />
+                android:background="@color/stepper_divider" />

As per coding guidelines: "Avoid hardcoded string literals in code logic. Any repeated or domain-relevant string... must be extracted into clearly named constants."


34-44: Use app:tint instead of android:tint for AppCompat consistency.

Since the project uses AppCompat (as evidenced by androidx.appcompat.widget.SwitchCompat), prefer app:tint on ImageButton for consistent behaviour across all API levels. While android:tint is supported natively from API 21+, AppCompat provides better compatibility through app:tint.

♻️ Proposed fix
         <ImageButton
                 android:id="@+id/MinusButton"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:background="?attr/selectableItemBackgroundBorderless"
                 android:contentDescription="@string/decrease"
                 android:padding="0dp"
                 android:scaleType="center"
                 android:src="@drawable/ic_stepper_minus"
-                android:tint="@android:color/black" />
+                app:tint="@android:color/black" />
Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs (1)

230-232: Consider a more readable approach for checking option count.

Using Skip(1).Any() to check if there's more than one option works but is less immediately clear than alternatives.

♻️ Optional refactor
-        _searchLayout.Visibility = _item.Options.Skip(1).Any()
+        _searchLayout.Visibility = _item.Options.Count() > 1
             ? ViewStates.Visible
             : ViewStates.Gone;
Buform/Platforms/Android/Items/Text/TextInputFormViewHolder.cs (1)

338-363: Consider moving the extension class to a separate file.

TextInputTypeExtensions is a separate public static class that could be placed in its own file for better organisation and discoverability. However, given it's closely related to this view holder's functionality, keeping it here is acceptable.

Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs (2)

56-64: Move Dispose method to the end of the class.

Per coding guidelines, Dispose() and related cleanup methods should be placed at the end of the class, before any nested types.

As per coding guidelines: "Enforce consistent member ordering in C# types... Always place Dispose() and related cleanup methods at the end of the class."


193-193: Extract hardcoded string literal to a constant.

The "DATE_PICKER" tag should be extracted to a named constant for maintainability.

♻️ Proposed fix
+    private const string DatePickerTag = "DATE_PICKER";
+
     // ... in ShowDatePicker method:
-        picker.Show(activity.SupportFragmentManager, "DATE_PICKER");
+        picker.Show(activity.SupportFragmentManager, DatePickerTag);

As per coding guidelines: "Avoid hardcoded string literals in code logic. Any repeated or domain-relevant string... must be extracted into clearly named constants."

Buform/Platforms/Android/Items/Text/TextFormViewHolder.cs (1)

83-110: Move Dispose() to the end of the class.

There is still a private helper below it, so this type no longer follows the repository’s C# member-ordering rule.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs (1)

235-268: Keep Dispose() as the final member.

ButtonCheckedListener still sits below Dispose(), which breaks the member ordering used for C# types in this repository.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs (1)

128-336: Place Dispose() after the helper methods.

This class still has several private methods below Dispose(), so its layout breaks the repository’s C# member-ordering rule.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform/Platforms/Android/Items/Picker/PresentedPickerFormViewHolderBase.cs (1)

73-83: Consider combining identical switch cases.

PickerInputType.Default and PickerInputType.Dialog produce the same presenter. Combining them reduces duplication and makes the intent clearer.

♻️ Proposed simplification
     protected virtual PickerPresenterBase<TFormItem>? GetPickerPresenter(PickerInputType inputType)
     {
         return inputType switch
         {
-            PickerInputType.Default
-                => new DialogFragmentPickerPresenter<TFormItem>(CreatePickerDialogFragment),
-            PickerInputType.Dialog
+            PickerInputType.Default or PickerInputType.Dialog
                 => new DialogFragmentPickerPresenter<TFormItem>(CreatePickerDialogFragment),
             _ => null
         };
     }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09e97856-48ba-45b1-aed4-ea54074cb111

📥 Commits

Reviewing files that changed from the base of the PR and between 92a8d27 and 3bd26f7.

📒 Files selected for processing (67)
  • .github/workflows/ci.yml
  • Buform.Example.Droid/Buform.Example.Droid.csproj
  • Buform.Example.Maui/Buform.Example.Maui.csproj
  • Buform.Example.Maui/Platforms/Android/MainApplication.cs
  • Buform.Example.Maui/RandomNumberGeneratorView.xaml
  • Buform.Example/Buform.Example.csproj
  • Buform.Example/MenuViewModel.cs
  • Buform.Maui/Buform.Maui.csproj
  • Buform.Maui/FormReflectionHelper.cs
  • Buform.Maui/FormReflectionKeys.cs
  • Buform.Maui/Platforms/Android/FormAdapterItem.cs
  • Buform.Maui/Platforms/Android/FormViewHandler.cs
  • Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormItemViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs
  • Buform.Maui/Platforms/Android/MauiFormViewHolderBase.cs
  • Buform.MvvmCross/Buform.MvvmCross.csproj
  • Buform/Buform.csproj
  • Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs
  • Buform/Platforms/Android/Items/DateTime/ContextExtensions.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormItemComponent.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/CallbackPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/DialogFragmentPickerPresenter.cs
  • Buform/Platforms/Android/Items/Picker/MultiValuePickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormItemComponent.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionsAdapter.cs
  • Buform/Platforms/Android/Items/Picker/PickerPresenterBase.cs
  • Buform/Platforms/Android/Items/Picker/PopUpPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PresentedPickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormItemComponent.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormItemComponent.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormItemComponent.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormViewHolder.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormItemComponent.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextFormItemComponent.cs
  • Buform/Platforms/Android/Items/Text/TextFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextInputFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextMultilineFormViewHolder.cs
  • Buform/Resources/drawable/bg_stepper_capsule.xml
  • Buform/Resources/drawable/ic_stepper_minus.xml
  • Buform/Resources/drawable/ic_stepper_plus.xml
  • Buform/Resources/layout/AsyncPickerDialogLayout.xml
  • Buform/Resources/layout/FormGroupTextFooterLayout.xml
  • Buform/Resources/layout/FormGroupTextHeaderLayout.xml
  • Buform/Resources/layout/FormItemButtonLayout.xml
  • Buform/Resources/layout/FormItemDateTimeLayout.xml
  • Buform/Resources/layout/FormItemPickerLayout.xml
  • Buform/Resources/layout/FormItemSegmentsLayout.xml
  • Buform/Resources/layout/FormItemSliderLayout.xml
  • Buform/Resources/layout/FormItemStepperLayout.xml
  • Buform/Resources/layout/FormItemSwitchLayout.xml
  • Buform/Resources/layout/FormItemTextInputLayout.xml
  • Buform/Resources/layout/FormItemTextLayout.xml
  • Buform/Resources/layout/FormItemTextMultilineLayout.xml
  • Buform/Resources/layout/PickerDialogLayout.xml
  • Buform/Resources/layout/PickerOptionItemLayout.xml
  • Buform/Resources/values/dimens.xml
  • Buform/Resources/values/strings.xml

Comment thread Buform.Maui/FormReflectionHelper.cs
Comment thread Buform.Maui/Platforms/Android/FormViewHandler.cs
Comment thread Buform.Maui/Platforms/Android/MauiFormViewHolderBase.cs Outdated
Comment thread Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs Outdated
Comment thread Buform/Platforms/Android/Items/Picker/AsyncPickerDialogFragment.cs Outdated
Comment thread Buform/Platforms/Android/Items/Picker/PickerOptionsAdapter.cs
Comment thread Buform/Resources/drawable/ic_stepper_plus.xml
Comment thread Buform/Resources/layout/FormItemButtonLayout.xml Outdated
Comment thread Buform/Resources/layout/FormItemSliderLayout.xml Outdated
Comment thread Buform/Resources/layout/PickerOptionItemLayout.xml
@EvgenBrovchenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (5)
Buform/Resources/layout/FormItemButtonLayout.xml (1)

16-17: ⚠️ Potential issue | 🟠 Major

Verify the compact button height still meets the 48dp touch target minimum.

Using @dimen/form_item_height_compact for both height fields can reintroduce an accessibility blocker if that dimen is under 48dp.

#!/bin/bash
set -euo pipefail

# Find where form_item_height_compact is defined and inspect its value.
fd -i 'dimens*.xml' | xargs -r rg -n --no-heading '<dimen name="form_item_height_compact">[^<]+</dimen>'

# Optional: show related touch-target dimensions for comparison.
fd -i 'dimens*.xml' | xargs -r rg -n --no-heading 'touch|min_height|minHeight|48dp'

Expected result: form_item_height_compact resolves to at least 48dp, or android:minHeight is set to 48dp here.

Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs (1)

14-28: ⚠️ Potential issue | 🟠 Major

Persist the picker state instead of storing it only in _item.

Create() only initialises _item on the fragment instance. After configuration change or process recreation, Android can rebuild PickerDialogFragment with _item == null, and then BindItem(), SetupRecyclerView(), and the button setup all degrade to no-ops. Persist a stable identifier in Arguments and rehydrate the picker during fragment creation instead of relying on the field.

Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs (1)

24-25: ⚠️ Potential issue | 🟠 Major

Prefer the slider change listener over View.Touch.

Touch fires for every motion event and misses keyboard/accessibility adjustments, so this write-back path is both noisy and incomplete. If 1.12.0.4 exposes AddOnChangeListener, bind that instead and use its user-origin flag when updating Data.Value.

In Xamarin.Google.Android.Material 1.12.0.4, what is the recommended C# API for observing Google.Android.Material.Slider.Slider value changes? Does the binding expose AddOnChangeListener / IOnChangeListener, and does it report whether the change came from the user?

Also applies to: 140-152

Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs (2)

133-141: ⚠️ Potential issue | 🟠 Major

Branch the click flow by InputType.

Time-only items cannot be edited correctly here, and DateTime items never let the user change the time component. The interaction should match the rendering switch in GetDisplayValue().


152-159: ⚠️ Potential issue | 🔴 Critical

MaterialDatePicker dates should not round-trip through local-time conversion.

These helpers convert full DateTime values to and from local timestamps, which shifts the calendar date around timezone boundaries. MaterialDatePicker expects a date-only UTC selection value, so both SetSelection(...) and the positive-button readback path need date-only conversion instead.

How does Google MaterialDatePicker represent selected dates for Builder.DatePicker().SetSelection(...) and the positive-button callback? Are the values UTC-midnight milliseconds for the chosen calendar date rather than local-time timestamps?

Also applies to: 176-189, 243-257

🧹 Nitpick comments (12)
Buform/Resources/layout/PickerDialogLayout.xml (1)

43-43: Use theme/resource-driven ripple colours instead of hex literals.

Inline hex ripple colours make theme adaptation brittle. Prefer an attribute or colour resource.

Proposed fix
-                app:rippleColor="#22000000"
+                app:rippleColor="?attr/colorControlHighlight"
@@
-                app:rippleColor="#22000000"
+                app:rippleColor="?attr/colorControlHighlight"

Also applies to: 66-66

Buform.Maui/FormReflectionHelper.cs (2)

5-20: Reorder methods to match the repository’s C# member-order convention.

The private helper should be placed before public methods in this class.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."


17-20: Cache reflection lookups to avoid repeated property discovery overhead.

GetType().GetProperty(...) on every call can become expensive in list binding flows.

Proposed fix
+using System.Collections.Concurrent;
+using System.Reflection;
+
 internal static class FormReflectionHelper
 {
+    private static readonly ConcurrentDictionary<(Type Type, string PropertyName), PropertyInfo?> PropertyCache = new();
+
@@
     private static string? GetStringProperty(object item, string propertyName)
     {
         ArgumentNullException.ThrowIfNull(item);
-        return item.GetType().GetProperty(propertyName)?.GetValue(item) as string;
+        var type = item.GetType();
+        var propertyInfo = PropertyCache.GetOrAdd((type, propertyName), static key => key.Type.GetProperty(key.PropertyName));
+        return propertyInfo?.GetValue(item) as string;
     }
 }
Buform/Resources/drawable/bg_stepper_capsule.xml (1)

4-4: Prefer a colour resource token over a hardcoded hex value.

Using a resource here improves theme consistency and future palette changes.

Proposed fix
-    <solid android:color="#E5E5EA" />
+    <solid android:color="@color/stepper_capsule_background" />
Buform/Resources/layout/FormItemTextLayout.xml (1)

22-23: Consider moving hardcoded text padding to a dimen resource.

On Line 23, android:paddingVertical="20dp" is a literal that may drift from global spacing rules; extracting it to @dimen/... would improve consistency and easier theming.

Proposed refactor
-            android:paddingVertical="20dp"
+            android:paddingVertical="@dimen/form_item_text_vertical_padding"
Buform/Resources/layout/FormItemSwitchLayout.xml (1)

7-7: Inconsistent hardcoded minHeight value.

This layout uses a hardcoded 56dp for minHeight, while FormItemPickerLayout.xml uses @dimen/form_item_height for the same purpose. Consider using the dimension resource for consistency and easier maintenance.

Suggested fix
-        android:minHeight="56dp"
+        android:minHeight="@dimen/form_item_height"
Buform/Platforms/Android/Items/Picker/MultiValuePickerFormViewHolder.cs (1)

20-35: Consolidate duplicate switch arms.

The Default, Dialog, and PopUp cases all create identical DialogFragmentPickerPresenter instances. Consider combining them to reduce duplication.

Suggested refactor
         return inputType switch
         {
-            PickerInputType.Default
-                => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
-                    CreatePickerDialogFragment
-                ),
-            PickerInputType.Dialog
-                => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
-                    CreatePickerDialogFragment
-                ),
-            PickerInputType.PopUp
+            PickerInputType.Default or PickerInputType.Dialog or PickerInputType.PopUp
                 => new DialogFragmentPickerPresenter<IMultiValuePickerFormItem>(
                     CreatePickerDialogFragment
                 ),
             _ => null
         };
Buform/Platforms/Android/Items/Picker/PickerOptionsAdapter.cs (1)

12-27: Move ItemCount above the constructor.

ItemCount is this type’s only property, so it should appear before the constructor to stay aligned with the repo’s C# member ordering rule.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs (1)

125-129: Extract these date/time format specifiers.

"d", "t" and "g" are user-facing formatting rules; per repo guidance they should live in shared constants/helpers/resources rather than inline in the view holder.

As per coding guidelines: "Avoid hardcoding format strings directly in code, especially for dates, text templates, or numeric formats. Move formatting patterns to application-level constants, helpers, or localisation resources."

Buform/Platforms/Android/Items/Text/TextInputFormViewHolder.cs (1)

195-208: Redundant type conversion used only for validation gating.

The TryConvertText method parses the input and produces a typed value, but only the original text string is passed to SetValue(). The conversion result is used solely to gate whether SetValue is called. If ITextInputFormItem.SetValue already performs its own validation/conversion internally (which the past review comment indicates), this pre-validation may be redundant and could prevent valid inputs from being accepted if parsing differs between the two implementations.

Consider whether TryConvertText validation is truly necessary, or if SetValue should handle all validation.

Buform/Platforms/Android/Items/Picker/PickerFormViewHolderBase.cs (2)

59-73: Consider using theme error colour instead of hardcoded Color.Red.

For Material Design consistency, consider resolving the error colour from the theme (e.g., colorError) rather than using a hardcoded Color.Red. This would respect the app's theme and dark mode configurations.

Suggested improvement
     protected virtual void UpdateValidationErrorMessage(string? validationErrorMessage)
     {
         if (LabelView == null)
         {
             return;
         }

         var hasValidationError = !string.IsNullOrWhiteSpace(validationErrorMessage);

         LabelView.SetTextColor(
             !hasValidationError
                 ? ResolveThemeColor(Android.Resource.Attribute.TextColorPrimary)
-                : Color.Red
+                : ResolveThemeColor(Resource.Attribute.colorError)
         );
     }

75-90: Fallback colour Color.Black may not suit dark themes.

If theme attribute resolution fails, Color.Black is returned as a fallback. This would be invisible on dark backgrounds. Consider using a neutral fallback or logging when resolution fails.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf231f14-eea3-4999-adb7-7654abc2e02e

📥 Commits

Reviewing files that changed from the base of the PR and between 92a8d27 and 7de08ea.

📒 Files selected for processing (67)
  • .github/workflows/ci.yml
  • Buform.Example.Droid/Buform.Example.Droid.csproj
  • Buform.Example.Maui/Buform.Example.Maui.csproj
  • Buform.Example.Maui/Platforms/Android/MainApplication.cs
  • Buform.Example.Maui/RandomNumberGeneratorView.xaml
  • Buform.Example/Buform.Example.csproj
  • Buform.Example/MenuViewModel.cs
  • Buform.Maui/Buform.Maui.csproj
  • Buform.Maui/FormReflectionHelper.cs
  • Buform.Maui/FormReflectionKeys.cs
  • Buform.Maui/Platforms/Android/FormAdapterItem.cs
  • Buform.Maui/Platforms/Android/FormViewHandler.cs
  • Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormItemViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs
  • Buform.Maui/Platforms/Android/MauiFormViewHolderBase.cs
  • Buform.MvvmCross/Buform.MvvmCross.csproj
  • Buform/Buform.csproj
  • Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs
  • Buform/Platforms/Android/Items/DateTime/ContextExtensions.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormItemComponent.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/CallbackPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/DialogFragmentPickerPresenter.cs
  • Buform/Platforms/Android/Items/Picker/MultiValuePickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormItemComponent.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionsAdapter.cs
  • Buform/Platforms/Android/Items/Picker/PickerPresenterBase.cs
  • Buform/Platforms/Android/Items/Picker/PopUpPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PresentedPickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormItemComponent.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormItemComponent.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormItemComponent.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormViewHolder.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormItemComponent.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextFormItemComponent.cs
  • Buform/Platforms/Android/Items/Text/TextFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextInputFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextMultilineFormViewHolder.cs
  • Buform/Resources/drawable/bg_stepper_capsule.xml
  • Buform/Resources/drawable/ic_stepper_minus.xml
  • Buform/Resources/drawable/ic_stepper_plus.xml
  • Buform/Resources/layout/AsyncPickerDialogLayout.xml
  • Buform/Resources/layout/FormGroupTextFooterLayout.xml
  • Buform/Resources/layout/FormGroupTextHeaderLayout.xml
  • Buform/Resources/layout/FormItemButtonLayout.xml
  • Buform/Resources/layout/FormItemDateTimeLayout.xml
  • Buform/Resources/layout/FormItemPickerLayout.xml
  • Buform/Resources/layout/FormItemSegmentsLayout.xml
  • Buform/Resources/layout/FormItemSliderLayout.xml
  • Buform/Resources/layout/FormItemStepperLayout.xml
  • Buform/Resources/layout/FormItemSwitchLayout.xml
  • Buform/Resources/layout/FormItemTextInputLayout.xml
  • Buform/Resources/layout/FormItemTextLayout.xml
  • Buform/Resources/layout/FormItemTextMultilineLayout.xml
  • Buform/Resources/layout/PickerDialogLayout.xml
  • Buform/Resources/layout/PickerOptionItemLayout.xml
  • Buform/Resources/values/dimens.xml
  • Buform/Resources/values/strings.xml

Comment thread Buform.Maui/Buform.Maui.csproj
Comment thread Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs
Comment thread Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs
Comment thread Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs
Comment thread Buform/Resources/layout/AsyncPickerDialogLayout.xml Outdated
Comment thread Buform/Resources/layout/FormItemStepperLayout.xml
Comment thread Buform/Resources/layout/PickerDialogLayout.xml Outdated
@EvgenBrovchenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs (1)

57-58: ⚠️ Potential issue | 🟠 Major

Disabled buttons lack visual indication of their disabled state.

UpdateReadOnlyState() correctly sets Enabled = false, but ApplyTextStyle() overrides Material's built-in disabled state styling. It creates single-state ColorStateList instances and sets StateListAnimator = null, removing the default greyed/dimmed appearance for disabled buttons. The button becomes non-interactive but visually unchanged, potentially confusing users about whether they can interact with it.

To fix this, use ColorStateList definitions that include disabled state colours, or re-apply appropriate visual styling (e.g. reduced opacity, greyed text) when the button becomes disabled.

♻️ Duplicate comments (12)
Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Still unresolved: replace obsolete [Preserve].

CI is already warning on this attribute. Please verify the trimming strategy against current .NET for Android guidance and move this to DynamicDependency or a linker descriptor for the actual reflected members.

Buform/Resources/layout/AsyncPickerDialogLayout.xml (1)

29-30: ⚠️ Potential issue | 🟠 Major

Still unresolved: restore the clear action's minimum tap target.

minHeight="0dp" lets this button drop below the 48dp accessibility floor once the small vertical padding is applied.

Suggested fix
-                android:minHeight="0dp"
+                android:minHeight="48dp"
Buform/Resources/layout/PickerDialogLayout.xml (1)

29-30: ⚠️ Potential issue | 🟠 Major

Still unresolved: restore a 48dp minimum height on the dialog actions.

Both text buttons opt out of the minimum tap target here, so they can drop below accessible size on short strings or tighter font metrics.

Suggested fix
-                android:minHeight="0dp"
+                android:minHeight="48dp"
@@
-                android:minHeight="0dp"
+                android:minHeight="48dp"

Also applies to: 51-52

Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs (2)

10-10: 🛠️ Refactor suggestion | 🟠 Major

Mark class as sealed.

PickerDialogFragment does not declare any virtual or abstract members and is not designed for inheritance. Per coding guidelines, it should be marked sealed.

♻️ Proposed fix
-public class PickerDialogFragment : AndroidX.Fragment.App.DialogFragment
+public sealed class PickerDialogFragment : AndroidX.Fragment.App.DialogFragment

As per coding guidelines: "Require marking C# classes as sealed if they are not designed for inheritance."


14-15: ⚠️ Potential issue | 🟠 Major

Persist the picker item state to survive fragment recreation.

_item is stored only as an instance field without being persisted to Arguments or SaveInstanceState. When DialogFragment is recreated by Android (after rotation or process death), the new instance initialises with _item == null, causing all setup methods to silently exit early and the dialog to render without content or callbacks.

Consider storing a stable identifier in the Arguments bundle during Create() and resolving the item from a presenter or parent owner in OnViewCreated().

Also applies to: 25-28

Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs (3)

133-141: ⚠️ Potential issue | 🟠 Major

Implement separate picker flows for DateTimeInputType.Time and DateTimeInputType.DateTime.

The display formatting correctly handles different InputType values (lines 125–129), but OnClick always calls ShowDatePicker() regardless of type. This prevents proper editing:

  • Time-only items: clicking displays a date picker when only time should be editable.
  • Date-time items: only the date can be changed; a sequential date + time picker flow is needed.

Consider implementing ShowTimePicker() and a combined flow for DateTimeInputType.DateTime.


243-258: ⚠️ Potential issue | 🔴 Critical

MaterialDatePicker converts date values through UTC epoch, not local time.

MaterialDatePicker operates on UTC epoch milliseconds representing 00:00:00 UTC on a given calendar date. The current implementation incorrectly converts full DateTime values (including time components) through local timezone boundaries, causing off-by-one date errors for users in timezones offset from UTC.

🐛 Suggested fix
-    private static long ToUtcMillis(DateTime dateTime)
+    private static long ToPickerUtcDateMillis(DateTime dateTime)
     {
-        var utc = dateTime.Kind switch
-        {
-            DateTimeKind.Utc => dateTime,
-            DateTimeKind.Local => dateTime.ToUniversalTime(),
-            _ => DateTime.SpecifyKind(dateTime, DateTimeKind.Local).ToUniversalTime()
-        };
-
-        return new DateTimeOffset(utc).ToUnixTimeMilliseconds();
+        var utcDate = DateTime.SpecifyKind(dateTime.Date, DateTimeKind.Utc);
+        return new DateTimeOffset(utcDate).ToUnixTimeMilliseconds();
     }
 
-    private static DateTime FromUtcMillis(long millis)
+    private static DateTime FromPickerUtcDateMillis(long millis)
     {
-        return DateTimeOffset.FromUnixTimeMilliseconds(millis).LocalDateTime;
+        return DateTimeOffset.FromUnixTimeMilliseconds(millis).UtcDateTime.Date;
     }

Update call sites at lines 152, 158, 159, and 176 to use the renamed methods.


38-54: ⚠️ Potential issue | 🟡 Minor

Handle InputType changes and full-refresh notifications.

GetDisplayValue() depends on Data.InputType, but OnDataPropertyChanged does not handle nameof(DateTimeFormItem.InputType). Additionally, a null or empty propertyName (indicating a full refresh) falls through to the empty default case without updating the UI.

♻️ Proposed fix
     protected override void OnDataPropertyChanged(string? propertyName)
     {
         switch (propertyName)
         {
             case nameof(DateTimeFormItem.Label):
                 UpdateLabel();
                 break;
             case nameof(DateTimeFormItem.Value):
+            case nameof(DateTimeFormItem.InputType):
                 UpdateValue();
                 break;
             case nameof(DateTimeFormItem.IsReadOnly):
                 UpdateReadOnlyState();
                 break;
             default:
+                UpdateLabel();
+                UpdateValue();
+                UpdateReadOnlyState();
                 break;
         }
     }
Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Missing using Android.Widget; import — TextView and LinearLayout are unresolved.

Line 10 uses TextView? and line 168 uses LinearLayout.LayoutParams, but the required Android.Widget namespace is not imported. This will cause compilation errors.

Minimal fix
 using Android.Runtime;
 using Android.Views;
+using Android.Widget;
 using Google.Android.Material.Button;
Buform/Platforms/Android/Items/Picker/CallbackPickerFormViewHolder.cs (1)

57-76: ⚠️ Potential issue | 🟠 Major

Add a fallback refresh for unhandled property changes.

The default branch is a no-op, so any update that arrives under another property name can leave the label, value, or validation state stale until the row is rebound. This was previously flagged.

Proposed fix
             case nameof(ICallbackPickerFormItem.ValidationErrorMessage):
                 UpdateValidationErrorMessage(Data?.ValidationErrorMessage);
                 break;
             default:
-                break;
+                UpdateReadOnlyState();
+                UpdateLabel(Data?.Label);
+                UpdateValue(Data?.FormattedValue);
+                UpdateValidationErrorMessage(Data?.ValidationErrorMessage);
+                break;
         }
Buform/Platforms/Android/Items/Picker/AsyncPickerDialogFragment.cs (2)

288-328: 🛠️ Refactor suggestion | 🟠 Major

Extract hardcoded UI strings to constants or localisation resources.

The state text values "Loading...", "No items", and "Failed to load items" are hardcoded inline. As per coding guidelines, user-facing strings should be extracted to constants or localisation resources.

♻️ Proposed fix
 public sealed class AsyncPickerDialogFragment : PickerDialogFragment
 {
+    private const string LoadingStateText = "Loading...";
+    private const string EmptyStateText = "No items";
+    private const string FailedStateText = "Failed to load items";
+
     private readonly List<IPickerOptionFormItem> _filteredOptions = [];

Then use them in UpdateState():

-                _stateTextView.Text = "Loading...";
+                _stateTextView.Text = LoadingStateText;
-                    _stateTextView.Text = "No items";
+                    _stateTextView.Text = EmptyStateText;
-                _stateTextView.Text = "Failed to load items";
+                _stateTextView.Text = FailedStateText;

298-312: ⚠️ Potential issue | 🟡 Minor

Check _filteredOptions instead of _item.Options for empty state.

When the user filters with no matches, _item.Options may still have items while _filteredOptions is empty. The empty state check should use the filtered list to correctly show "No items" for zero search results.

♻️ Proposed fix
             case AsyncPickerLoadingState.Loaded:
                 _progressIndicator.Visibility = ViewStates.Gone;

-                if (!_item.Options.Any())
+                if (!_filteredOptions.Any())
                 {
                     _stateTextView.Visibility = ViewStates.Visible;
                     _stateTextView.Text = "No items";
                     _contentContainer.Visibility = ViewStates.Gone;
                 }
🧹 Nitpick comments (12)
Buform/Resources/drawable/bg_stepper_capsule.xml (1)

4-4: Avoid hardcoded background colour in a Material-themed control.

Using a fixed hex colour here can look off in dark mode and across themes. Prefer a theme attribute or colour resource.

Proposed tweak
-    <solid android:color="#E5E5EA" />
+    <solid android:color="?attr/colorSurfaceVariant" />
Buform/Resources/layout/FormItemSegmentsLayout.xml (1)

12-25: Link the label to the toggle group for TalkBack.

If Label is shown, it will be announced separately rather than as the control's name. Adding android:labelFor makes that relationship explicit.

♿ Suggested change
     <com.google.android.material.textview.MaterialTextView
           android:id="@+id/Label"
+          android:labelFor="@id/ToggleGroup"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginBottom="@dimen/item_spacing_small"
Buform/Resources/layout/FormItemStepperLayout.xml (1)

7-13: Move the remaining sizing values into @dimen resources.

This layout still hardcodes the row height, paddings, control width, divider height, and text size even though the same PR introduces Resources/values/dimens.xml. Pulling those values through resources will make the Android Material sizing much easier to tune consistently across item layouts.

Also applies to: 22-22, 29-30, 46-47

Buform/Platforms/Android/Items/Switch/SwitchFormViewHolder.cs (1)

21-152: Apply the repository member order in this holder.

The private listener/update helpers currently sit below the protected overrides. Please move the private methods above Initialize/OnDataSet/OnDataPropertyChanged, while keeping Dispose at the end.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform.Maui/FormReflectionHelper.cs (1)

5-20: Cache the reflected lookup in the private helper.

Even though this is fallback code, GetType().GetProperty(...) will still run on every bind that lands here. A small cache in GetStringProperty would keep reflection out of that path, and moving the private helper above the public accessors would also bring the file back in line with the repository member-order rule.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs (1)

20-155: Apply the repository member order in this holder.

The private event handler currently sits below the protected update members. Please move private helpers above the protected methods, while keeping Dispose at the end.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."

Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs (1)

67-83: Consider extracting hardcoded dimensions to resource constants.

The padding values (16, 24, 8, 4 dp) and text sizes (14, 12 sp) are hardcoded inline. For consistency with Material Design resource management and easier maintenance, consider moving these to dimens.xml or at least to named constants within the class.

Buform/Platforms/Android/Items/Text/TextFormViewHolder.cs (1)

22-22: Extract hardcoded text size to a constant.

The text size 18 is hardcoded inline. Per coding guidelines, hardcoded values should be extracted to named constants for maintainability and consistency.

♻️ Proposed fix
+private const int DefaultTextSizeSp = 18;
+
 public TextFormViewHolder(IntPtr javaReference, JniHandleOwnership transfer)
     : base(javaReference, transfer) { }
-_textView.SetTextSize(ComplexUnitType.Sp, 18);
+_textView.SetTextSize(ComplexUnitType.Sp, DefaultTextSizeSp);
Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs (1)

162-163: Store button index using Java.Lang.Integer explicitly.

The implicit boxing of index to Tag works, but the retrieval at line 96 expects Java.Lang.Integer. For clarity and type safety, consider explicitly wrapping the value.

♻️ Suggested improvement
-            Tag = index
+            Tag = new Java.Lang.Integer(index)
Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs (1)

220-250: Consider caching the chrome count calculation.

GetItemsCount() iterates all sections on every ItemCount access. For large forms, this could be called frequently (e.g., during scrolling). Consider caching the computed count and invalidating it when the form changes.

Buform/Platforms/Android/Items/Picker/PickerFormViewHolderBase.cs (1)

68-72: Use theme-aware colour for validation errors.

Color.Red is hardcoded, which bypasses Material theme attributes and may not render correctly in dark mode. Other view holders in this PR (e.g., SegmentsFormViewHolder) use Resource.Attribute.colorError for consistency.

♻️ Proposed fix
     protected virtual void UpdateValidationErrorMessage(string? validationErrorMessage)
     {
         if (LabelView == null)
         {
             return;
         }

         var hasValidationError = !string.IsNullOrWhiteSpace(validationErrorMessage);

         LabelView.SetTextColor(
             !hasValidationError
                 ? ResolveThemeColor(Android.Resource.Attribute.TextColorPrimary)
-                : Color.Red
+                : ResolveThemeColor(Resource.Attribute.colorError)
         );
     }
Buform/Platforms/Android/Items/Picker/PresentedPickerFormViewHolderBase.cs (1)

27-42: Regroup the private click handler with the private methods.

OnItemViewClick sits in the middle of the protected method block, so the methods section no longer follows the repo’s private → protected → public ordering.

As per coding guidelines, "Enforce consistent member ordering in C# types: order members as follows – static, const, properties, constructors, methods. Within each group, order by access level: private, protected, then public. Always place Dispose() and related cleanup methods at the end of the class."


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aca40aee-bfae-425f-ae37-49a7b3be1b89

📥 Commits

Reviewing files that changed from the base of the PR and between 92a8d27 and 7de08ea.

📒 Files selected for processing (67)
  • .github/workflows/ci.yml
  • Buform.Example.Droid/Buform.Example.Droid.csproj
  • Buform.Example.Maui/Buform.Example.Maui.csproj
  • Buform.Example.Maui/Platforms/Android/MainApplication.cs
  • Buform.Example.Maui/RandomNumberGeneratorView.xaml
  • Buform.Example/Buform.Example.csproj
  • Buform.Example/MenuViewModel.cs
  • Buform.Maui/Buform.Maui.csproj
  • Buform.Maui/FormReflectionHelper.cs
  • Buform.Maui/FormReflectionKeys.cs
  • Buform.Maui/Platforms/Android/FormAdapterItem.cs
  • Buform.Maui/Platforms/Android/FormViewHandler.cs
  • Buform.Maui/Platforms/Android/MauiFormHeaderFooterViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormItemViewHolder.cs
  • Buform.Maui/Platforms/Android/MauiFormRecyclerAdapter.cs
  • Buform.Maui/Platforms/Android/MauiFormViewHolderBase.cs
  • Buform.MvvmCross/Buform.MvvmCross.csproj
  • Buform/Buform.csproj
  • Buform/Platforms/Android/Items/Button/ButtonFormViewHolder.cs
  • Buform/Platforms/Android/Items/DateTime/ContextExtensions.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormItemComponent.cs
  • Buform/Platforms/Android/Items/DateTime/DateTimeFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/AsyncPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/CallbackPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/DialogFragmentPickerPresenter.cs
  • Buform/Platforms/Android/Items/Picker/MultiValuePickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerDialogFragment.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormItemComponent.cs
  • Buform/Platforms/Android/Items/Picker/PickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PickerOptionsAdapter.cs
  • Buform/Platforms/Android/Items/Picker/PickerPresenterBase.cs
  • Buform/Platforms/Android/Items/Picker/PopUpPickerFormViewHolder.cs
  • Buform/Platforms/Android/Items/Picker/PresentedPickerFormViewHolderBase.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormItemComponent.cs
  • Buform/Platforms/Android/Items/Segments/SegmentsFormViewHolder.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormItemComponent.cs
  • Buform/Platforms/Android/Items/Slider/SliderFormViewHolder.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormItemComponent.cs
  • Buform/Platforms/Android/Items/Stepper/StepperFormViewHolder.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormItemComponent.cs
  • Buform/Platforms/Android/Items/Switch/SwitchFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextFormItemComponent.cs
  • Buform/Platforms/Android/Items/Text/TextFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextInputFormViewHolder.cs
  • Buform/Platforms/Android/Items/Text/TextMultilineFormViewHolder.cs
  • Buform/Resources/drawable/bg_stepper_capsule.xml
  • Buform/Resources/drawable/ic_stepper_minus.xml
  • Buform/Resources/drawable/ic_stepper_plus.xml
  • Buform/Resources/layout/AsyncPickerDialogLayout.xml
  • Buform/Resources/layout/FormGroupTextFooterLayout.xml
  • Buform/Resources/layout/FormGroupTextHeaderLayout.xml
  • Buform/Resources/layout/FormItemButtonLayout.xml
  • Buform/Resources/layout/FormItemDateTimeLayout.xml
  • Buform/Resources/layout/FormItemPickerLayout.xml
  • Buform/Resources/layout/FormItemSegmentsLayout.xml
  • Buform/Resources/layout/FormItemSliderLayout.xml
  • Buform/Resources/layout/FormItemStepperLayout.xml
  • Buform/Resources/layout/FormItemSwitchLayout.xml
  • Buform/Resources/layout/FormItemTextInputLayout.xml
  • Buform/Resources/layout/FormItemTextLayout.xml
  • Buform/Resources/layout/FormItemTextMultilineLayout.xml
  • Buform/Resources/layout/PickerDialogLayout.xml
  • Buform/Resources/layout/PickerOptionItemLayout.xml
  • Buform/Resources/values/dimens.xml
  • Buform/Resources/values/strings.xml

Comment on lines +81 to +95
_container.RemoveAllViews();

_legacyViewHolder = viewHolder;

_container.AddView(view);

if (item is not IDisposable disposable)
{
return false;
}

viewHolder.Initialize(disposable);

return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reorder the IDisposable check to avoid partial binding state.

The current logic adds the view to the container (lines 81, 85) before checking if item is IDisposable (line 87). If the check fails, the method returns false but the view remains attached, leaving the holder in an inconsistent state where subsequent binding paths may be attempted whilst the legacy view is already displayed.

🐛 Proposed fix
         if (Activator.CreateInstance(viewHolderType, view) is not FormViewHolder viewHolder)
         {
             return false;
         }
 
+        if (item is not IDisposable disposable)
+        {
+            return false;
+        }
+
         _container.RemoveAllViews();
 
         _legacyViewHolder = viewHolder;
 
         _container.AddView(view);
 
-        if (item is not IDisposable disposable)
-        {
-            return false;
-        }
-
         viewHolder.Initialize(disposable);
 
         return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_container.RemoveAllViews();
_legacyViewHolder = viewHolder;
_container.AddView(view);
if (item is not IDisposable disposable)
{
return false;
}
viewHolder.Initialize(disposable);
return true;
}
if (item is not IDisposable disposable)
{
return false;
}
_container.RemoveAllViews();
_legacyViewHolder = viewHolder;
_container.AddView(view);
viewHolder.Initialize(disposable);
return true;

Comment on lines +67 to +88
protected void BindMauiView(object bindingContext, Type viewType)
{
ArgumentNullException.ThrowIfNull(bindingContext);
ArgumentNullException.ThrowIfNull(viewType);

EnsureMauiView(viewType);
EnsureMauiContentAttached();

if (_mauiView == null)
{
return;
}

OnBeforeBind(_mauiView, bindingContext);

_mauiView.BindingContext = bindingContext;

OnAfterBind(_mauiView, bindingContext);

_contentMode = ContentMode.Maui;
MeasureAndArrange();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the layout retry budget for each new bind.

_layoutRetryCount is only cleared after a successful measure. Once a holder has exhausted MaxLayoutRetries, later binds on the same recycled instance inherit that exhausted state and stop posting retries, even if the next item would lay out correctly.

♻️ Possible fix
     protected void BindMauiView(object bindingContext, Type viewType)
     {
         ArgumentNullException.ThrowIfNull(bindingContext);
         ArgumentNullException.ThrowIfNull(viewType);
+        _layoutRetryCount = 0;
 
         EnsureMauiView(viewType);
         EnsureMauiContentAttached();
@@
     protected void ClearAllCachedContent()
     {
         if (_platformView?.Parent == _container)
         {
             _container.RemoveView(_platformView);
         }
@@
         _fallbackView = null;
         _contentMode = ContentMode.None;
+        _layoutRetryCount = 0;
     }

Also applies to: 154-176, 245-263

Comment on lines +90 to +111
protected void ShowFallback(AView fallbackView)
{
ArgumentNullException.ThrowIfNull(fallbackView);

if (ReferenceEquals(_fallbackView, fallbackView) && _contentMode == ContentMode.Fallback)
{
return;
}

DetachCurrentContent();

_fallbackView = fallbackView;
_container.AddView(
_fallbackView,
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.WrapContent
)
);

_contentMode = ContentMode.Fallback;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unbind cached views before you replace them.

ShowFallback(), EnsureMauiView() and ClearAllCachedContent() can all drop the current view without going through the unbind path. On a recycled holder that switches MAUI view type, the abandoned _mauiView keeps its previous BindingContext; fallback swaps also skip OnFallbackUnbind().

♻️ Possible fix
     protected void ShowFallback(AView fallbackView)
     {
         ArgumentNullException.ThrowIfNull(fallbackView);
@@
+        Unbind();
         DetachCurrentContent();
@@
     protected void ClearAllCachedContent()
     {
+        Unbind();
         if (_platformView?.Parent == _container)
         {
             _container.RemoveView(_platformView);
         }
@@
     private void EnsureMauiView(Type viewType)
     {
         if (_mauiView != null && _platformView != null && _boundViewType == viewType)
         {
             return;
         }
 
+        Unbind();
         if (_platformView?.Parent == _container)
         {
             _container.RemoveView(_platformView);
         }

Also applies to: 154-176, 178-205

Comment on lines +81 to +111
var min = Data.MinValue;
var max = Data.MaxValue;

if (max < min)
{
max = min;
}

if (Math.Abs(_slider.ValueFrom - min) > Tolerance)
{
_slider.ValueFrom = min;
}

if (Math.Abs(_slider.ValueTo - max) > Tolerance)
{
_slider.ValueTo = max;
}
}

protected virtual void UpdateStepSize()
{
if (_slider == null || Data == null)
{
return;
}

const int stepSize = 1;
if (Math.Abs(_slider.StepSize - stepSize) > Tolerance)
{
_slider.StepSize = stepSize;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the slider model and related references without modifying the repo.
fd -a "SliderFormItem.cs" -x sed -n '1,220p' {}
printf '\n--- slider model references ---\n'
rg -n --type=cs -C2 '\b(SliderFormItem|MinValue|MaxValue|Value|Step(Size)?|Increment)\b'

Repository: fedandburk/buform

Length of output: 50373


Do not hardcode step size for float-based sliders, and validate range configuration.

SliderFormItem is a FormItem<float> supporting fractional values across a floating-point range. Hardcoding StepSize = 1 (line 107) breaks this capability—you cannot represent fractional increments or sliders with ranges smaller than 1. Additionally, when MaxValue < MinValue, the code collapses both to the same value (lines 84–87), leaving zero selectable interval.

Derive the step size from the form item or calculate it from the range. Validate that MinValue ≤ MaxValue before assignment, or reject invalid configurations early.

Comment on lines +111 to +132
private void UpdateValue()
{
if (_switch == null || Data == null)
{
return;
}

if (_switch.Checked == Data.Value)
{
return;
}

_isUpdatingFromModel = true;

try
{
_switch.Checked = Data.Value;
}
finally
{
_isUpdatingFromModel = false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset the switch when no item is bound.

UpdateTitle() and UpdateReadOnlyState() already cope with Data == null, but UpdateValue() returns before touching _switch.Checked. On a recycled holder that can leave the previous row's value visible until another item is bound.

💡 Suggested fix
     private void UpdateValue()
     {
-        if (_switch == null || Data == null)
+        if (_switch == null)
         {
             return;
         }
+
+        var isChecked = Data?.Value ?? false;
 
-        if (_switch.Checked == Data.Value)
+        if (_switch.Checked == isChecked)
         {
             return;
         }
 
         _isUpdatingFromModel = true;
         try
         {
-            _switch.Checked = Data.Value;
+            _switch.Checked = isChecked;
         }
         finally
         {
             _isUpdatingFromModel = false;
         }

Comment on lines +21 to +23
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent long picker values from hiding the field label.

Value is the only non-weighted child here, so a long formatted value can consume the row and leave Label at 0 width. Give the value view an explicit width cap or constraint so both texts remain visible.

Also applies to: 28-35

Comment on lines +17 to +18
<com.google.android.material.textview.MaterialTextView
android:id="@+id/Title"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Give the switch an explicit accessibility label.

The visible title is a separate TextView, so accessibility services can land on an unnamed switch. Link the label to @id/Switch (or move the text onto the control itself).

Suggested fix
     <com.google.android.material.textview.MaterialTextView
             android:id="@+id/Title"
+            android:labelFor="@id/Switch"
             android:layout_width="0dp"

Also applies to: 26-30

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Android UI using Material Design components with MAUI and MvvmCross

1 participant