Dynamic Popup

A flexible and customizable dynamic popup system for Flutter with markdown support and interactive components.

๐Ÿ“š Table of Contents

Features

  • Configurable popups from backend APIs
  • Markdown parser with support for dynamic component placeholders
  • Interactive components: RadioButton, Checkbox, TextArea, TextField, Dropdown
  • Automatic validation of required fields
  • Persistent state management (show once, completed/dismissed)
  • Blocking and non-blocking popup behaviors
  • Easy to customize and extend
  • No dependency on GetX - works with any Flutter app
  • Minimal API requirements - only two endpoints required
  • Custom slots support - add custom titles, footers, and action buttons
  • Smart scrolling - automatically shows scroll buttons for long content
  • Advanced conditional logic - show/hide fields or change required status based on other field values with three-tier priority system
  • Dynamic required indicators - real-time asterisk updates based on conditional logic

๐Ÿš€ Installation

Add this to your package's pubspec.yaml file:

dependencies:
  dynamic_popup: ^1.0.7

Then run:

flutter pub get

Note: The http package is only required if you use the example implementations. The core library does not depend on http.

๐Ÿ“ฑ Popup Types

1. Non-blocking Popup

  • User can close without completing
  • Ideal for information, optional surveys, feature discovery

2. Blocking Popup

  • User must complete to continue
  • Ideal for terms of service, privacy policy, required data

๐Ÿ›  Supported Components

RadioButton

<!-- New HTML-like syntax with custom initiator -->
:::dc<radiobutton id="component_id" required label="Label">
  <option id="opt1">Option1</option>
  <option id="opt2">Option2</option>
  <option id="opt3">Option3</option>
</radiobutton>dc:::

Checkbox (single/multiple)

<!-- New HTML-like syntax with custom initiator -->
:::dc<checkbox id="component_id" label="Label">
  <option id="opt1">Option1</option>
  <option id="opt2">Option2</option>
  <option id="opt3">Option3</option>
</checkbox>dc:::

TextArea

<!-- New HTML-like syntax with custom initiator -->
:::dc<textarea id="component_id" required label="Label" placeholder="Placeholder text" />dc:::

TextField

<!-- New HTML-like syntax with custom initiator -->
:::dc<textfield id="component_id" label="Label" placeholder="Placeholder text" />dc:::
<!-- New HTML-like syntax with custom initiator -->
:::dc<dropdown id="component_id" required label="Label">
  <option id="opt1">Option1</option>
  <option id="opt2">Option2</option>
  <option id="opt3">Option3</option>
</dropdown>dc:::

๐Ÿ”ง Placeholder Syntax

New Syntax (HTML-like with Named Attributes and Custom Initiator)

:::dc<component_type id="component_id" required label="Label" placeholder="Placeholder" />dc:::

Custom Initiator: :::dc< and >dc::: - This prevents conflicts with regular HTML tags in markdown content.

Multiline Support: The syntax supports newlines and spaces between the initiator and terminator:

:::dc
<component_type id="component_id" required label="Label">
  <option id="opt1">Option1</option>
</component_type>
dc:::

Attributes:

  • id: Unique component ID (required)
  • required: Presence indicates the field is required (optional)
  • label: Label shown to user (required)
  • placeholder: Placeholder text for text inputs (optional)
  • default: Default value (optional)

Container Components with Option IDs: For components with options (RadioButton, Checkbox, Dropdown), use container syntax with option IDs:

:::dc<component_type id="component_id" required label="Label">
  <option id="option_id_1">Option 1</option>
  <option id="option_id_2">Option 2</option>
</component_type>dc:::

Benefits of Option IDs:

  • Send option IDs instead of text to the backend
  • Language-independent option identification
  • Easier to maintain when option text changes

๐Ÿ’ป Usage

Setup Service

import 'package:dynamic_popup/dynamic_popup.dart';

// Create a repository that implements your API calls
class MyPopupRepository extends BaseDynamicPopupRepository {
  @override
  Future<PopupApiResponse?> checkForPopup({
    required String screenName,
    String? userId,
  }) async {
    // Implement your API call here
    // Only these two methods are required
  }

  @override
  Future<bool> submitPopupResponse({
    required PopupResponse popupResponse,
  }) async {
    // Implement your API call here
    // Only these two methods are required
  }
}

// In your app initialization (e.g., in initState of your main widget)
final popupService = DynamicPopupService(
  repository: MyPopupRepository(),
);
popupService.init(); // Initialize the service

Show Manual Popup

// Show specific popup by ID
await popupService.showPopupById('privacy_update_2024', context: context);

// Check for and show popup for a screen
await popupService.checkAndShowPopup(
  screenName: 'home_screen',
  context: context,
);

// Reset state for testing
await popupService.resetPopupState('privacy_update_2024');

// Reset all states
popupService.resetAllPopupStates();

Using Custom Slots

// Add custom title, footer, and action buttons
final config = PopupConfig(
  id: 'custom_popup',
  title: 'Custom Popup',
  markdownContent: '''
## Welcome!

:::dc<textfield id="name" label="Your Name" placeholder="Enter your name" />dc:::
  ''',
  isBlocking: false,
);

showDialog(
  context: context,
  builder: (BuildContext context) {
    return DynamicPopupWidget(
      config: config,
      customTitle: const Text('Custom Title', style: TextStyle(color: Colors.white, fontSize: 24)),
      customFooter: const Padding(
        padding: EdgeInsets.all(16),
        child: Text('This is a custom footer', style: TextStyle(fontStyle: FontStyle.italic)),
      ),
      customActions: [
        TextButton(
          onPressed: () {
            // Custom action
            print('Custom button pressed');
          },
          child: const Text('Custom Action'),
        ),
      ],
      onCompleted: (response) => print('Response: ${response.responses}'),
    );
  },
);

Manual Popup Display

``dart // Test specific component final config = PopupConfig( id: 'test_popup', title: 'Test', markdownContent: ''' :::dc

dc::: ''', isBlocking: false, );

showDialog( context: context, builder: (BuildContext context) { return DynamicPopupWidget( config: config, onCompleted: (response) => print('Response: ${response.responses}'), ); }, );


## ๐Ÿ”Œ Custom API Integration

The package is designed to work with any backend API with minimal requirements. Here's how to integrate with your custom API:

### 1. Minimal Implementation (Only 2 Required Methods)

```dart
import 'package:dynamic_popup/dynamic_popup.dart';

class MySimplePopupRepository extends BaseDynamicPopupRepository {
  @override
  Future<PopupApiResponse?> checkForPopup({
    required String screenName,
    String? userId,
  }) async {
    // Only implement these two required methods
    // All other methods are optional
  }

  @override
  Future<bool> submitPopupResponse({
    required PopupResponse popupResponse,
  }) async {
    // Only implement these two required methods
    // All other methods are optional
  }
}

2. Direct Usage Without Service

// Fetch popup config from your API directly
final popupConfig = await MyApiService.getPopupForScreen('home_screen');

if (popupConfig != null) {
  await showDialog(
    context: context,
    builder: (BuildContext context) {
      return DynamicPopupWidget(
        config: popupConfig,
        onCompleted: (response) async {
          // Submit response to your API directly
          await MyApiService.submitResponse(response);
          Navigator.of(context).pop();
        },
      );
    },
  );
}

๐Ÿงช Testing

Test Page

The example app now includes a comprehensive test page with all popup types and a documentation screen.

๐Ÿ“‹ Complete Examples

Privacy Policy Popup

# Privacy Policy Update

Our privacy policy has been updated. Please review and confirm your preferences.

## Required Consent
:::dc<radiobutton id="privacy_accept" required label="Do you accept the new privacy policy?">
  <option id="accept">Accept</option>
  <option id="decline">Decline</option>
</radiobutton>dc:::

## Data Usage (Optional)
:::dc<checkbox id="data_usage" label="What data can we use?">
  <option id="analytics">Analytics</option>
  <option id="marketing">Marketing</option>
  <option id="performance">Performance</option>
  <option id="crash">Crash Reports</option>
</checkbox>dc:::

## Feedback
:::dc<textarea id="feedback" label="Comments or questions?" placeholder="Share your thoughts..." />dc:::

**Thank you for your attention.**

Complete Survey

# Satisfaction Survey

Help us improve the app by completing this brief survey.

:::dc<textfield id="name" required label="Full Name" placeholder="Enter your name" />dc:::

:::dc<dropdown id="frequency" required label="Usage Frequency">
  <option id="daily">Daily</option>
  <option id="weekly">Weekly</option>
  <option id="monthly">Monthly</option>
  <option id="occasional">Occasional</option>
</dropdown>dc:::

:::dc<checkbox id="features" required label="Which features do you use?">
  <option id="restaurant">Restaurant</option>
  <option id="sales">Special Sales</option>
  <option id="pcm">PCM</option>
  <option id="snack">Snack Bar</option>
</checkbox>dc:::

:::dc<textarea id="suggestions" required label="Improvement suggestions" placeholder="Describe your ideas..." />dc:::

:::dc<radiobutton id="recommend" required label="Would you recommend the app?">
  <option id="definitely">Definitely</option>
  <option id="probably">Probably</option>
  <option id="not_sure">Not Sure</option>
  <option id="probably_not">Probably Not</option>
  <option id="definitely_not">Definitely Not</option>
</radiobutton>dc:::

๐Ÿ— Architecture

File Structure

lib/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ data/
โ”‚   โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ popup_config.dart
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_component.dart
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ popup_models.dart
โ”‚   โ”‚   โ””โ”€โ”€ repository/
โ”‚   โ”‚       โ”œโ”€โ”€ dynamic_popup_repository.dart
โ”‚   โ”‚       โ””โ”€โ”€ base_dynamic_popup_repository.dart
โ”‚   โ”œโ”€โ”€ parser/
โ”‚   โ”‚   โ””โ”€โ”€ markdown_dynamic_parser.dart
โ”‚   โ”œโ”€โ”€ service/
โ”‚   โ”‚   โ””โ”€โ”€ dynamic_popup_service.dart
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_radio_button.dart
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_checkbox.dart
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_text_area.dart
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_text_field.dart
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dynamic_dropdown.dart
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ dynamic_component_factory.dart
โ”‚   โ”‚   โ””โ”€โ”€ dynamic_popup_widget.dart
โ””โ”€โ”€ dynamic_popup.dart

๐Ÿ”’ Security and Privacy

  • All data is validated client-side
  • Responses are sent in secure JSON format
  • Local storage is encrypted for popup states
  • No sensitive data stored permanently

๐ŸŽจ Customization

Themes and Styles

The system uses the current app theme. To customize:

// In dynamic_popup_widget.dart, modify colors:
primaryColor: Theme.of(context).primaryColor,
errorColor: Colors.red.shade600,
backgroundColor: Colors.grey.shade50,

Custom Components

Add new components by extending DynamicComponentFactory:

case DynamicComponentType.newType:
  return NewCustomWidget(/* ... */);

๐Ÿšจ Troubleshooting

  • Verify service is initialized
  • Check API logs for errors
  • Verify target screen is correct

Parsing errors

  • Check placeholder markdown syntax
  • Verify all components have unique IDs
  • Ensure options don't contain special characters

Validation issues

  • Check that required fields have values
  • Verify data types are correct
  • Test with simple popups first

๐Ÿ“ Changelog

1.0.4 - 2025-10-09

Added

  • Support for conditional required status in addition to conditional visibility
  • New required-when-visible attribute for components to control required status based on visibility
  • Case-insensitive conditional logic evaluation for better user experience
  • Comprehensive documentation for conditional logic features
  • New examples demonstrating both conditional visibility and conditional required status

1.0.3 - 2025-09-29

Added

  • Support for option IDs in radio buttons, checkboxes, and dropdowns
  • Smart scrolling for long content in popups
  • Support for multiline syntax with newlines and spaces in component definitions
  • Custom slots support for titles, footers, and action buttons
  • Dedicated examples/documentation screen in the example app
  • View Markdown button in example popups

Changed

  • Improved markdown syntax from positional parameters to HTML-like syntax
  • Updated component initiator from ::: to :::dc<component>dc::: for better identification
  • Removed support for old positional parameter syntax
  • Updated all examples to use new HTML-like syntax with option IDs
  • Enhanced UI components to send option IDs instead of text values in responses
  • Consolidated test interface into main example screen
  • Improved dialog dismissal handling to prevent overlay issues

Fixed

  • Ensured option IDs are properly used in popup responses instead of text values
  • Fixed overlay persistence issue when dismissing non-required popups

1.0.2

  • Removed http dependency from core library (moved to example project only)
  • Removed unused DefaultDynamicPopupRepository from core library
  • Added .pubignore file to prevent publishing unnecessary files
  • Added LICENSE file with MIT license
  • Added CONTRIBUTING.md with contribution guidelines
  • Added CODE_OF_CONDUCT.md with community standards
  • Fixed flutter analyze issues:
    • Removed unused local variable in dynamic_popup_service.dart
    • Removed unnecessary imports in dynamic_popup_widget.dart
    • Updated deprecated onPopInvoked to onPopInvokedWithResult
    • Updated deprecated value parameter to initialValue in dropdown
  • Updated documentation to clarify dependency requirements
  • Improved package structure for pub.dev publishing

1.0.1

  • Simplified repository pattern by reducing required methods from 7 to 3
  • Removed complex optional methods (markPopupAsShown, markPopupAsDismissed) for easier implementation
  • Streamlined example code by removing redundant buttons and simplifying demonstrations
  • Improved documentation with clearer, more concise instructions
  • Added JSON logging in example apps for better debugging of popup responses
  • Enhanced snackbar colors in examples for better readability and user experience
  • Fixed undefined variable error in API integration example
  • Removed unnecessary http dependency from core library (now only in example)
  • Removed unused DefaultDynamicPopupRepository from core library
  • Maintained all core functionality while reducing complexity

1.0.0

  • โœ… Base dynamic popup system
  • โœ… Markdown parser with placeholders
  • โœ… Interactive UI components
  • โœ… API repository and service
  • โœ… Controller integration
  • โœ… Testing system
  • โœ… Removed GetX dependency for better compatibility
  • โœ… Simplified API requirements (only 2 endpoints required)

๐Ÿ“ž Support

For questions or issues with the dynamic popup system, please open an issue on GitHub.

๐Ÿ”„ Conditional Logic

Dynamic Popup supports conditional logic to show/hide fields or change their required status based on other field values.

Conditional Visibility

Fields can be shown or hidden based on the value of another field:

:::dc<radiobutton id="consent" required label="Do you consent?">
  <option id="yes">Yes</option>
  <option id="no">No</option>
</radiobutton>dc:::

:::dc<textfield id="reason" label="Reason for consent" depends-on="consent" when-value="yes" />dc:::

In this example, the "reason" text field will only be visible when the "consent" radio button has the value "yes".

Conditional Required Status

Fields can change their required status based on the value of another field:

Required When Visible

:::dc<radiobutton id="employment" required label="Are you employed?">
  <option id="yes">Yes</option>
  <option id="no">No</option>
</radiobutton>dc:::

:::dc<textfield id="employer" label="Employer Name" required-when-visible="true" depends-on="employment" when-value="yes" />dc:::

In this example, the "employer" text field is always visible but becomes required only when the "employment" radio button has the value "yes".

Required When Value

:::dc<radiobutton id="info_needed" required label="Do you have additional information?">
  <option id="yes">Yes</option>
  <option id="no">No</option>
</radiobutton>dc:::

:::dc<textfield id="additional_info" label="Additional Information" depends-on="info_needed" required-when-value="yes" />dc:::

In this example, the "additional_info" text field is always visible but becomes required only when the "info_needed" radio button has the value "yes", regardless of its visibility.

Available Conditional Attributes

  • depends-on: The ID of the component this field depends on
  • when-value: The value that triggers the visibility condition
  • required-when-visible: Whether the field should be required when visible (true/false)
  • required-when-value: The value of the depends-on field that makes this field required (instead of a boolean flag)
  • condition: The type of condition to check (default: equals)
  • disable-when-hidden: Whether to disable validation when hidden (default: true)

Supported Conditions

  • equals: Field value equals the specified value (default)
  • notEquals: Field value does not equal the specified value
  • contains: Field value contains the specified value (for list values like checkboxes)
  • notContains: Field value does not contain the specified value (for list values like checkboxes)

Priority Order

When multiple conditional required attributes are specified, they are evaluated in this order:

  1. required-when-value (highest priority)
  2. required-when-visible (medium priority)
  3. Component's default required attribute (lowest priority)

Libraries

dynamic_popup