패키지, 저장소 및 앱 카테고리 검색

패키지 상세v0.2.0

control

병행 지원을 위한 Flutter용 간단한 상태 관리.

AndroidiOSWebmacOSWindowsLinux
30일 다운로드
1.1천
좋아요
45
Pub 점수
70

패키지 설치

flutter pub add control

패키지 개요

공개 정보를 바탕으로 생성한 선택 가이드입니다.

Control은 내장된 동시성 지원을 통해 상태 처리를 단순화하는 플러터용 경량 상태 관리 솔루션입니다. 상태 변경을 효율적으로 관리함으로써 UI 컴포넌트에 반응형 업데이트를 가능하게 하여 실시간 데이터 동기화가 필요한 앱에 적합합니다. 이 패키지는 플러터 프로젝트에 원활하게 통합되며 모바일, 웹, 데스크톱을 포함한 모든 주요 플랫폼을 지원합니다. 최소한의 보일러플레이트로 반응형이고 확장 가능한 애플리케이션 개발을 간편하게 만듭니다.

사용 사례

  • 실시간 데이터 업데이트
  • 폼 상태 관리
  • 비동기 데이터 가져오기
  • 크로스 플랫폼 앱
  • 반응형 UI
  • 위젯 간 상태 공유

주요 기능

  • 내장된 동시성 지원
  • 최소한의 보일러플레이트
  • 반응형 상태 업데이트
  • 크로스 플랫폼 호환성
  • 간단한 API
  • 효율적인 상태 전파

적합한 대상

  • 경량 상태 관리가 필요한 개발자
  • 비동기 상태 처리가 필요한 프로젝트
  • 다중 플랫폼을 대상으로 하는 앱
  • 간단하고 유지보수가 쉬운 상태 로직을 원하는 팀
  • 프로토타이핑 및 MVP 개발

고려 사항

  • 매우 대규모 상태 트리에 적합하지 않음
  • 제한된 내장 디버깅 도구
  • 복잡한 상태 로직을 위해 추가 설정이 필요할 수 있습니다
  • 클린 아키텍처 패턴과 가장 잘 어울림
  • 기능이 풍부한 상태 관리자보다 기능이 적음

원본 README

연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.

Control: State Management for Flutter

Pub Actions Status Coverage License: MIT Linter GitHub stars

A simple, flexible state management library for Flutter with built-in concurrency support.


Features

  • 🎯 Simple API - Easy to learn and use
  • 🔄 Flexible Concurrency - Sequential, concurrent, or droppable operation handling
  • 🛡️ Type Safe - Full type safety with Dart's type system
  • 🔍 Observable - Built-in observer for debugging and logging
  • 🧪 Well Tested - Comprehensive test coverage
  • 📦 Lightweight - Minimal dependencies
  • 🔧 Customizable - Use Mutex for custom concurrency patterns

Installation

Add the following dependency to your pubspec.yaml file:

dependencies:
  control: ^1.0.0

Quick Start

Basic Example
/// Counter state
typedef CounterState = ({int count, bool idle});

/// Counter controller - concurrent by default
class CounterController extends StateController<CounterState> {
  CounterController({CounterState? initialState})
      : super(initialState: initialState ?? (idle: true, count: 0));

  void increment() => handle(() async {
        setState((idle: false, count: state.count));
        await Future<void>.delayed(const Duration(milliseconds: 500));
        setState((idle: true, count: state.count + 1));
      });

  void decrement() => handle(() async {
        setState((idle: false, count: state.count));
        await Future<void>.delayed(const Duration(milliseconds: 500));
        setState((idle: true, count: state.count - 1));
      });
}

Concurrency Strategies

1. Concurrent (Default)

Operations execute in parallel without waiting for each other:

class MyController extends StateController<MyState> {
  MyController() : super(initialState: MyState.initial());

  // These operations run concurrently
  void operation1() => handle(() async { ... });
  void operation2() => handle(() async { ... });
}
2. Sequential (with Mixin)

Operations execute one after another in FIFO order:

class MyController extends StateController<MyState>
    with SequentialControllerHandler {
  MyController() : super(initialState: MyState.initial());

  // These operations run sequentially
  void operation1() => handle(() async { ... });
  void operation2() => handle(() async { ... });
}
3. Droppable (with Mixin)

New operations are dropped if one is already running:

class MyController extends StateController<MyState>
    with DroppableControllerHandler {
  MyController() : super(initialState: MyState.initial());

  // If operation1 is running, operation2 is dropped
  void operation1() => handle(() async { ... });
  void operation2() => handle(() async { ... });
}
4. Custom (with Mutex)

Use Mutex directly for fine-grained control:

class MyController extends StateController<MyState> {
  MyController() : super(initialState: MyState.initial());

  final _criticalMutex = Mutex();
  final _batchMutex = Mutex();

  // Sequential critical operations
  void criticalOperation() => _criticalMutex.synchronize(
    () => handle(() async { ... }),
  );

  // Sequential batch operations (different queue)
  void batchOperation() => _batchMutex.synchronize(
    () => handle(() async { ... }),
  );

  // Concurrent fast operations
  void fastOperation() => handle(() async { ... });
}

Return Values from Operations

The handle() method is generic and can return values:

class UserController extends StateController<UserState> {
  UserController(this.api) : super(initialState: UserState.initial());

  final UserApi api;

  /// Fetch user and return the user object
  Future<User> fetchUser(String id) => handle<User>(() async {
    final user = await api.getUser(id);
    setState(state.copyWith(user: user, loading: false));
    return user; // Type-safe return value
  });

  /// Update user and return success status
  Future<bool> updateUser(User user) => handle<bool>(() async {
    try {
      await api.updateUser(user);
      setState(state.copyWith(user: user));
      return true;
    } catch (e) {
      return false;
    }
  });
}

// Usage
final user = await controller.fetchUser('123');
print('Fetched: ${user.name}');

final success = await controller.updateUser(updatedUser);
if (success) {
  print('User updated successfully');
}

Note: With DroppableControllerHandler, dropped operations return null instead of executing.

Usage in Flutter

Inject Controller

Use ControllerScope to provide controller to widget tree:

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
    home: ControllerScope<CounterController>(
      CounterController.new,
      child: const CounterScreen(),
    ),
  );
}
Consume State

Use StateConsumer to rebuild widgets when state changes:

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
    body: StateConsumer<CounterController, CounterState>(
      builder: (context, state, _) => Text('Count: ${state.count}'),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () => context.controllerOf<CounterController>().increment(),
      child: Icon(Icons.add),
    ),
  );
}
Use ValueListenable

Convert state to ValueListenable for granular updates:

ValueListenableBuilder<bool>(
  valueListenable: controller.select((state) => state.idle),
  builder: (context, isIdle, _) => ElevatedButton(
    onPressed: isIdle ? () => controller.increment() : null,
    child: Text('Increment'),
  ),
)

Advanced Features

Error Handling

The handle() method provides built-in error handling:

void riskyOperation() => handle(
  () async {
    // Your operation
    throw Exception('Something went wrong');
  },
  error: (error, stackTrace) async {
    // Handle error
    print('Error: $error');
  },
  done: () async {
    // Always called, even if error occurs
    print('Operation completed');
  },
  name: 'riskyOperation', // For debugging
);
Observer Pattern

Monitor all controller events for debugging:

class MyObserver implements IControllerObserver {
  @override
  void onCreate(Controller controller) {
    print('Controller created: ${controller.name}');
  }

  @override
  void onHandler(HandlerContext context) {
    print('Handler started: ${context.name}');
  }

  @override
  void onStateChanged<S extends Object>(
    StateController<S> controller,
    S prevState,
    S nextState,
  ) {
    print('State changed: $prevState -> $nextState');
  }

  @override
  void onError(Controller controller, Object error, StackTrace stackTrace) {
    print('Error in ${controller.name}: $error');
  }

  @override
  void onDispose(Controller controller) {
    print('Controller disposed: ${controller.name}');
  }
}

void main() {
  Controller.observer = MyObserver();
  runApp(MyApp());
}
Mutex

Use Mutex for custom synchronization:

final mutex = Mutex();

// Method 1: synchronize (automatic unlock)
await mutex.synchronize(() async {
  // Critical section
});

// Method 2: lock/unlock (manual control)
final unlock = await mutex.lock();
try {
  // Critical section
  if (someCondition) {
    unlock();
    return; // Early exit
  }
  // More code
} finally {
  unlock();
}

// Check if locked
if (mutex.locked) {
  print('Mutex is currently locked');
}

Migration from 0.x to 1.0.0

See MIGRATION.md for detailed migration guide.

Key changes:

  • Remove base from controller classes
  • ConcurrentControllerHandler is deprecated (remove it)
  • Controllers are concurrent by default
  • Use Mutex for custom concurrency patterns

Best Practices

  1. Choose the right concurrency strategy:

    • Default (concurrent) for independent operations
    • Sequential for operations that must complete in order
    • Droppable for operations that should cancel if busy
    • Custom Mutex for complex scenarios
  2. Use handle() for all async operations:

    • Automatic error catching
    • Observer notifications
    • Proper disposal handling
  3. Keep state immutable:

    • Use records or immutable classes for state
    • Always create new state instances
  4. Dispose controllers:

    • Controllers are automatically disposed by ControllerScope
    • Manual disposal only needed for manually created controllers

Advanced Usage

UI Feedback with Callbacks

Use error and done callbacks to provide user feedback through SnackBars, dialogs, or notifications:

class UserController extends StateController<UserState> {
  UserController(this.api) : super(initialState: UserState.initial());

  final UserApi api;

  Future<User?> updateProfile(
    User user, {
    void Function(User user)? onSuccess,
    void Function(Object error)? onError,
  }) => handle<User>(
    () async {
      final updatedUser = await api.updateUser(user);
      setState(state.copyWith(user: updatedUser));
      onSuccess?.call(updatedUser);
      return updatedUser;
    },
    error: (error, stackTrace) async {
      onError?.call(error);
    },
    name: 'updateProfile',
    meta: {'userId': user.id},
  );
}

// Usage in UI
ElevatedButton(
  onPressed: () => controller.updateProfile(
    updatedUser,
    onSuccess: (user) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Profile updated: ${user.name}')),
      );
    },
    onError: (error) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Error: $error'),
          backgroundColor: Colors.red,
        ),
      );
    },
  ),
  child: const Text('Update Profile'),
)
Interactive Dialogs During Processing

Add interactive dialogs in the middle of processing for user input:

class AuthController extends StateController<AuthState> {
  AuthController(this.api) : super(initialState: AuthState.initial());

  final AuthApi api;

  Future<bool?> login(
    String email,
    String password, {
    required Future<String> Function() requestSmsCode,
  }) => handle<bool>(
    () async {
      // Step 1: Initial login
      final session = await api.login(email, password);

      // Step 2: Check if 2FA is required
      if (session.requires2FA) {
        // Request SMS code from user via dialog
        final smsCode = await requestSmsCode();

        // Step 3: Verify SMS code
        await api.verify2FA(session.id, smsCode);
      }

      setState(state.copyWith(isAuthenticated: true));
      return true;
    },
    error: (error, stackTrace) async {
      setState(state.copyWith(error: error.toString()));
    },
    name: 'login',
    meta: {'email': email, 'requires2FA': true},
  );
}

// Usage in UI
ElevatedButton(
  onPressed: () => controller.login(
    email,
    password,
    requestSmsCode: () async {
      // Show dialog and wait for user input
      final code = await showDialog<String>(
        context: context,
        builder: (context) => SmsCodeDialog(),
      );
      return code ?? '';
    },
  ),
  child: const Text('Login'),
)
Debugging and Observability

Use name and meta parameters for debugging, logging, and integration with error tracking services like Sentry or Crashlytics:

class ControllerObserver implements IControllerObserver {
  const ControllerObserver();

  @override
  void onHandler(HandlerContext context) {
    // Log operation start with metadata
    print('START | ${context.controller.name}.${context.name}');
    print('META  | ${context.meta}');

    final stopwatch = Stopwatch()..start();

    context.done.whenComplete(() {
      // Log operation completion with duration
      stopwatch.stop();
      print('DONE  | ${context.controller.name}.${context.name} | '
            'duration: ${stopwatch.elapsed}');
    });
  }

  @override
  void onError(Controller controller, Object error, StackTrace stackTrace) {
    final context = Controller.context;

    if (context != null) {
      // Send breadcrumbs to Sentry/Crashlytics
      Sentry.addBreadcrumb(Breadcrumb(
        message: '${controller.name}.${context.name}',
        data: context.meta,
        level: SentryLevel.error,
      ));

      // Report error with full context
      Sentry.captureException(
        error,
        stackTrace: stackTrace,
        hint: Hint.withMap({
          'controller': controller.name,
          'operation': context.name,
          'metadata': context.meta,
        }),
      );
    }
  }

  @override
  void onStateChanged<S extends Object>(
    StateController<S> controller,
    S prevState,
    S nextState,
  ) {
    final context = Controller.context;

    // Log state changes with operation context
    if (context != null) {
      print('STATE | ${controller.name}.${context.name} | '
            '$prevState -> $nextState');
      print('META  | ${context.meta}');
    }
  }

  @override
  void onCreate(Controller controller) {
    print('CREATE | ${controller.name}');
  }

  @override
  void onDispose(Controller controller) {
    print('DISPOSE | ${controller.name}');
  }
}

// Setup in main
void main() {
  Controller.observer = const ControllerObserver();
  runApp(const App());
}

Benefits of using name and meta:

  • Debugging: Easily track which operation is executing
  • Logging: Add context to logs for better traceability
  • Profiling: Measure operation duration and performance
  • Error tracking: Send rich context to Sentry/Crashlytics
  • Analytics: Track user actions with metadata
  • Breadcrumbs: Build execution trail for debugging crashes

Examples

See example/ directory for complete examples:

  • Basic counter
  • Advanced concurrency patterns
  • Error handling
  • Custom observers

Coverage

Changelog

Refer to the Changelog to get all release notes.

Maintainers

Funding

If you want to support the development of our library, there are several ways you can do it:

We appreciate any form of support, whether it's a financial donation or just a star on GitHub. It helps us to continue developing and improving our library. Thank you for your support!

License

MIT

카테고리

의존성

의존성 목록 1 개

소스 저장소

이 패키지를 사용하는 프로젝트

아직 데이터가 없습니다.