BlocMorph Logo

BlocMorph

A powerful and flexible Flutter package for managing UI states with the Bloc pattern, featuring smooth animations and highly customizable widgets. BlocMorph simplifies handling various UI states like loading, error, empty, and network issues, while providing seamless transitions and a polished user experience.

Pub Version License Flutter Version Dart Version

For AndroidStudio IDE:

BlocMorph Plugin on JetBrains Marketplace

🚀 New Feature: MorphSelector and MorphSelectorMultiple

MorphSelector

MorphSelector is a lightweight and powerful Flutter widget designed to simplify UI updates in Bloc-based applications. Unlike BlocMorph, it does not handle separate UI states like loading, error, or empty. Instead, it focuses purely on reacting to relevant state changes and rebuilding the widget tree efficiently.

Key features:

  • Generic <T> type: Only rebuilds when a state of type <T> is emitted.
  • Optional initial value: Provides a default value before the first state arrives.
  • Optional placeholder: Display a fallback UI (like a loader or empty message) until the relevant state is available.
  • Optimized for performance: Ignores irrelevant state changes, preventing unnecessary rebuilds.
  • Easy to integrate: Works seamlessly with Bloc or Cubit.

Example usage:

MorphSelector<MyBloc, MyState, StateData>(
    initial: StateData([]), // Optional initial value
    placeholder: const Center(child: Text('Waiting for data...')), // Optional loading/fallback
    builder: (context, bloc, state, data) {
      return ListView(
         children: data.items.map((e) => ListTile(title: Text(e))).toList(),
      );
    },
)

Perfect for listening to a specific state and updating only the part of the UI that depends on it, ignoring other intermediate states like loading or errors.

MorphSelectorMultiple

MorphSelectorMultiple is an advanced version of MorphSelector, designed for scenarios where your UI depends on multiple states or multiple Blocs/Cubits.

Key features:

  • Track multiple state types or selected values: Rebuild the UI when any relevant state changes.
  • Optional initial value: Set a default before receiving the first relevant state.
  • Optional placeholder: Display fallback UI until the relevant state is emitted.
  • Efficient rebuilds: Ignores irrelevant state changes, keeping your app responsive and fast.
  • Perfect for complex UIs: Use when multiple sources of data need to be displayed in a single widget.

Example usage:

MorphSelectorMultiple<MyBloc, MyState>(
  builderState: (state) => state is StateData, // Only rebuild for StateData
  builder: (context, bloc, state) {
    if (state is StateData) {
      return ListView(
        children: state.items.map((e) => ListTile(title: Text(e))).toList(),
      );
    }
    return const Center(child: CircularProgressIndicator());
  },
)

Demo Video

Watch a short demo of BlocMorph in action:

BlocMorph Demo Video

Demo Comparison

Here is a visual comparison of different screens (square format):

Bad Good
Screen 1 Screen 2
Screen 3 Screen 4

Left column vs Right column comparison.

Example Ease

BlocMorph<BlocCubit,BlocState,BlocSampleState>(
initial: Container(color: Colors.grey,),
empty: Container(color: Colors.orange,),
errorBuilder: (bloc, data) => Container(color: Colors.red,),
netWorkError: (bloc) => Container(color: Colors.blue,),
loading: CircularProgressIndicator(),
builder: (data) {
return _content(data!.data!);
},
)
image_demo image_demo image_demo

Features

  • Smooth Animations: Built-in transitions with scale (0.9 to 1) and fade effects for seamless state changes.
  • Highly Customizable: Customize icons, messages, colors, text styles, and retry buttons for all UI states.
  • Bloc Integration: Works seamlessly with flutter_bloc to manage state changes efficiently.
  • Pagination Support: Handles paginated data with ease, perfect for lists and infinite scrolling.
  • Lightweight and Performant: Optimized for performance with minimal dependencies.
  • Multi-language Support: Configurable messages and text directions for global compatibility.
  • Extensible: Allows custom transition animations and fully replaceable default widgets.

Installation

Add bloc_morph to your pubspec.yaml:

dependencies:
  bloc_morph: ^0.3.7+8

Then, run:

flutter pub get

Alternatively, if you want to use the latest version from the GitHub repository:

dependencies:
  bloc_morph:
    git:
      url: https://github.com/PuzzleTakX/bloc_morph.git
      ref: master

Usage

BlocMorph is designed to work with any Bloc or Cubit from the flutter_bloc package. It handles various UI states (init, loading, empty, error, networkError, next) and provides smooth transitions between them.

Example

Below is an example of using BlocMorph to display a list of items with customizable error, empty, and loading states:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_morph/bloc_morph.dart';
import 'bloc_cubit.dart';
import 'item_image.dart';

class SamplePage extends StatefulWidget {
  const SamplePage({super.key});

  @override
  State<SamplePage> createState() => _SamplePageState();
}

class _SamplePageState extends State<SamplePage> {
  @override
  void initState() {
    super.initState();
    context.read<BlocCubit>().sample1();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BlocMorph Sample'),
        actions: [
          IconButton(
            onPressed: () => context.read<BlocCubit>().sample1(),
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: BlocMorph<BlocCubit, BlocState, BlocSampleState>(
        builder: (data) => _content(data!.data!),
        errorMessage: 'Failed to load data!',
        errorIcon: Icons.error_outline,
        errorColor: Colors.redAccent,
        emptyMessage: 'No items found!',
        emptySubMessage: 'Try loading again.',
        emptyIcon: Icons.inbox,
        initMessage: 'Initializing...',
        networkErrorMessage: 'Check your internet connection!',
        networkErrorIcon: Icons.wifi_off,
        tryAgainButton: (onTry) => ElevatedButton(
          onPressed: onTry,
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue,
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
          ),
          child: const Text(
            'Retry',
            style: TextStyle(color: Colors.white),
          ),
        ),
        onTry: (bloc) => bloc.sample1(),
        animationDuration: const Duration(milliseconds: 600),
        switchInCurve: Curves.easeInOut,
      ),
    );
  }

  Widget _content(List<ImageItem> data) {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: data.length,
      itemBuilder: (context, index) {
        final item = data[index];
        return Card(
          elevation: 5,
          margin: const EdgeInsets.only(bottom: 20),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ClipRRect(
                borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
                child: Image.network(
                  item.imageUrl,
                  height: 200,
                  width: double.infinity,
                  fit: BoxFit.cover,
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      item.title,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 6),
                    Text(
                      item.description,
                      style: TextStyle(
                        fontSize: 14,
                        color: Colors.grey[700],
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

Available States

BlocMorph supports the following UI states, each with customizable widgets:

  • init: Initial state before loading starts.
  • loading: Displays while data is being fetched.
  • empty: Shown when no data is available.
  • error: Displayed for general errors.
  • networkError: Shown for network-related issues.
  • next: Renders the main content when data is available.

Customization Options

You can customize the appearance and behavior of each state:

  • Messages: Set custom messages for error, empty, init, and networkError states.
  • Icons: Provide custom icons for each state (e.g., errorIcon, emptyIcon).
  • Colors: Define colors for icons and text (e.g., errorColor, networkErrorColor).
  • Text Styles: Customize text styles for messages (e.g., errorTextStyle, emptyTextStyle).
  • Retry Button: Provide a custom retry button using tryAgainButton.
  • Animations: Adjust animation duration, curves, or provide a custom transitionBuilder.

Example with customizations:

BlocMorph<MyBloc, MyState, MyData>(
  builder: (data) => Text(data?.toString() ?? 'No Data'),
  errorMessage: 'Custom error message',
  errorIcon: Icons.error_outline,
  errorColor: Colors.redAccent,
  tryAgainButton: (onTry) => ElevatedButton(
    onPressed: onTry,
    style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
    child: const Text('Retry', style: TextStyle(color: Colors.white)),
  ),
  animationDuration: const Duration(milliseconds: 600),
  transitionBuilder: (child, animation) => SlideTransition(
    position: Tween<Offset>(
      begin: const Offset(0, 0.1),
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)),
    child: child,
  ),
)

Project Structure

The BlocMorph package has the following structure:

bloc_morph/
├── lib/
│   └── bloc_morph.dart          # Main widget and logic
├── example/
│   ├── lib/
│   │   ├── main.dart            # Sample app
│   │   ├── bloc_cubit.dart      # Sample Bloc/Cubit
│   │   ├── bloc_state.dart      # Sample Bloc/State
│   │   └── item_image.dart      # Sample data model
│   └── pubspec.yaml             # Example dependencies
├── test/
│   └── bloc_morph_test.dart     # Unit tests
├── CHANGELOG.md
├── LICENSE
└── README.md

Running the Example

To run the example project, navigate to the example directory and run:

cd example
flutter pub get
flutter run

The example demonstrates how to use BlocMorph with a simple Cubit to handle different UI states with smooth animations.

Dependencies

  • flutter_bloc: ^8.1.3
  • cupertino_icons: ^1.0.6

Supported Versions

  • Dart: >=2.17.0 <4.0.0
  • Flutter: >=3.0.0

Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository.
  2. Create a new branch (git checkout -b feature/your-feature).
  3. Make your changes and commit (git commit -m 'Add your feature').
  4. Push to the branch (git push origin feature/your-feature).
  5. Create a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contact

For questions or feedback, feel free to open an issue on the GitHub repository or contact the maintainer at puzzletakx@gmail.com.


Built with ❤️ for the PuzzleTakX

Libraries

bloc_morph