[발췌] The 5 Design Patterns For Every Flutter Engineer
Level Up Your Engineering Skills.
Flutter, with its hot reload and tons of widgets, is a dream for crafting beautiful and engaging UIs. But as our apps grows, so does the complexity of managing data flow and keeping your code organized.
당신의 공학기술의 수준을 높여라.
Flutter, 핫 리로드와 수많은 위젯과함께, 플러터는 아름답고 사용자 친화적인(매력적인) UI를 만드는데 이상적이다. 하지만 앱이 커질수록, 데이터 흐름을 관리하고 코드를 체계적으로 유지하는 일의 복잡성도 함께 늘어납니다.
Remember, it’s always advisable to avoid technical debt which for most of the part, happens when your app codebase starts getting to the thousands of lines.
기억하세요, 기술적 부채를 피하는 것이 항상 현명한데, 대부분의 경우 이는 앱의 코드베이스가 수천 줄에 이르기 시작할 때 발생합니다.”
This is where design patterns come in — they’re battle-tested solutions to common software development challenges that engineers have faced since system engineering and design began.
이것이 바로 디자인 패턴이 등장하는 이유입니다. 디자인 패턴은 시스템 엔지니어링과 설계가 시작된 이래로 엔지니어들이 직면해온 일반적인 소프트웨어 개발 문제에 대한 검증된 해결책입니다.
Think of design patterns as architectural blueprints for your code. They provide a structured approach to object creation, communication, and relationships, leading to more:
디자인 패턴을 코드의 건축 청사진으로 생각해보세요. 디자인 패턴은 객체 생성, 소통, 관계에 대한 구조화된 접근 방식을 제공하여 더 많은 장점을 이끌어냅니다.
- Reusability: Don’t reinvent the wheel! Design patterns offer pre-defined solutions that can be adapted across different parts of your app. Easy huh 😊.
- Maintainability: Clean and well-structured code is easier to understand, modify, and debug, saving you time and frustration in the long run 🚀.
- Testability: Design patterns often promote separation of concerns, making it easier to isolate and test individual components of your app ✅.
재사용성: 이미 있는 것을 다시 발명하지 마세요! 디자인 패턴은 앱의 여러 부분에서 적용할 수 있는 사전 정의된 솔루션을 제공합니다. 간단하죠? 😊
유지보수성: 깔끔하고 잘 구조화된 코드는 이해하고, 수정하고, 디버깅하기가 더 쉬워서 장기적으로 시간과 스트레스를 줄여줍니다 🚀.
테스트 용이성: 디자인 패턴은 종종 관심사의 분리를 촉진하여 앱의 개별 컴포넌트를 분리하고 테스트하기 쉽게 만들어줍니다 ✅.
Let’s check out some of the most common design patterns for Flutter engineers, along with practical examples to illustrate their power:
Flutter 엔지니어들을 위한 가장 일반적인 디자인 패턴 몇 가지를 살펴보면서, 그 강력함을 보여줄 실용적인 예제들도 함께 확인해보겠습니다.”
Singleton Pattern
Ever needed a central place to store app-wide data like user preferences or a theme manager?
앱 전역의 데이터(예: 사용자 선호 설정이나 테마 관리자)를 저장할 중앙 장소가 필요했던 적이 있나요?
The Singleton pattern ensures only one instance of a class exists throughout the application. Imagine a single “SettingsManager” class that controls the app’s theme.
싱글톤 패턴은 애플리케이션 전체에서 클래스의 인스턴스가 오직 하나만 존재하도록 보장합니다. 예를 들어, 앱의 테마를 관리하는 단 하나의 ‘SettingsManager’ 클래스를 생각해보세요
Any part of your UI can access this manager to retrieve the current theme settings and style its widgets accordingly.
UI의 어느 부분이든 이 관리자를 통해 현재 테마 설정에 접근하고, 해당 설정에 맞게 위젯들을 스타일링할 수 있다
For apps using GraphQL, imagine a single GraphQL client that you can use to execute queries and mutations.
GraphQL을 사용하는 앱을 위해, 쿼리와 변이를 실행할 수 있는 단일 GraphQL 클라이언트를 상상해보세요.
An even more common example is a websocket connection, your app will almost always need only one.
심지어 더 일반적인 예로는 웹소켓 연결이있다, 당신앱에 거의 항상 한번만 필요할것이다.
This is where this pattern can be utilized
이 패턴이 활용될 수 있는 부분이 바로 여기입니다.
class SettingsManager {
static final SettingsManager _instance = SettingsManager._internal();
factory SettingsManager() => _instance;
SettingsManager._internal();
ThemeData _themeData = ThemeData.light();
void switchTheme() {
_themeData = _themeData.brightness == Brightness.light ? ThemeData.dark() : ThemeData.light();
// Notify listeners of theme change (implementation omitted for brevity)
}
ThemeData getTheme() => _themeData;
}
Factory Method Pattern
When you need to dynamically generate UI elements based on data, The Factory Method pattern provides an interface for creating objects without specifying the exact class upfront.
데이터에 따라 동적으로 UI 요소를 생성해야 할 때, 팩토리 메서드 패턴은 정확한 클래스를 미리 지정하지 않고 객체를 생성할 수 있는 인터페이스를 제공합니다.
This allows for greater flexibility — for instance, a factory class could create different types of buttons (primary, secondary) based on the data it receives.
로 인해 더 큰 유연성을 제공합니다. 예를 들어, 팩토리 클래스는 입력받은 데이터에 따라 다양한 종류의 버튼(예: 기본 버튼, 보조 버튼)을 생성할 수 있습니다.
For sports lovers think of a dashboard showing the bets a user has placed but depending on the game result, the user can see a won or lost widget.
스포츠 애호가들을 위해, 사용자가 베팅한 내역을 보여주는 대시보드를 생각해보세요. 하지만 게임 결과에 따라, 사용자는 승리 또는 패배 위젯을 볼 수 있습니다.
abstract class ButtonFactory {
Widget createButton(String text);
}
class PrimaryButtonFactory implements ButtonFactory {
@override
Widget createButton(String text) => ElevatedButton(onPressed: null, child: Text(text));
}
class SecondaryButtonFactory implements ButtonFactory {
@override
Widget createButton(String text) => TextButton(onPressed: null, child: Text(text));
}
// Usage
ButtonFactory buttonFactory = (isPrimary) => isPrimary ? PrimaryButtonFactory() : SecondaryButtonFactory();
Widget myButton = buttonFactory(true).createButton("Click Me"); // Creates a primary button
Provider Pattern
The provider pattern is a lightweight state management solution ideal for efficient data management.
프로바이더 패턴은 효율적인 데이터 관리를 위한 경량 상태 관리 솔루션입니다
(“프로바이더 패턴”은 Flutter와 같은 프레임워크에서 흔히 사용되는 디자인 패턴으로, 데이터나 상태를 관리하는 방식 중 하나입니다.)
It leverages InheritedWidget to propagate data changes down the widget tree.
이 패턴은 InheritedWidget을 활용하여 데이터 변경 사항을 위젯 트리 아래로 전파합니다
Think of a global list of user expenses for an expense app.
지출 관리 앱에서 전역으로 관리되는 사용자 지출 목록을 생각해보세요.
The Provider pattern allows you to create a single ExpenseProvider that holds the counter state and “listens” for changes.
프로바이더 패턴을 사용하면 ExpenseProvider라는 하나의 프로바이더를 생성할 수 있으며, 이 프로바이더는 카운터 상태를 유지하고 변경 사항을 ‘감지’합니다
Any widget down the tree that needs to display the expenses can access it through the Provider.
트리 아래에 있는 어느 위젯이든, 지출 내역을 표시해야 한다면 프로바이더를 통해 접근할 수 있습니다
class MyExpensesProvider with ChangeNotifier {
DateTime startDate = DateTime.now().subtract(const Duration(days: 30));
DateTime endDate = DateTime.now();
bool isLoading = false;
String? error;
List<Expense> expsenses = [];
MyExpensesProvider() {
getExpenses();
}
getExpenses() async {
}
}
Composition Pattern
Flutter’s main strength lies in its composable widget system.
Flutter의 주요 강점은 조합 가능한 위젯 시스템에 있습니다
This aligns perfectly with the Composition Pattern, which promotes building complex UIs by assembling smaller, reusable widgets.
이는 작은 재사용 가능한 위젯들을 조합하여 복잡한 UI를 구성하는 것을 장려하는 컴포지션 패턴과 완벽하게 일치합니다
This pattern promotes reusability, flexibility and testability.
이 패턴은 재사용성, 유연성, 그리고 테스트 용이성을 촉진합니다.
class RoundedButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const RoundedButton({required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Container(
padding: EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10.0),
),
child: Text(text, style: TextStyle(color: Colors.white)),
),
);
}
}
// Usage
Widget myScreen = Scaffold(
body: Center(
child: RoundedButton(text: 'Click Me', onPressed: () => print('Button Pressed')),
),
);
Bloc Pattern
The Bloc pattern offers a structured approach for state management, separating the UI (presentation layer) from the business logic (data fetching, calculations, etc.).
Bloc 패턴은 상태 관리를 위한 구조화된 접근 방식을 제공하며, UI(프레젠테이션 레이어)와 비즈니스 로직(데이터 가져오기, 계산 등)을 분리합니다.
This leads to cleaner, more maintainable, and highly testable code.
이로 인해 코드가 더 깔끔해지고, 유지보수가 용이해지며, 테스트하기도 매우 쉬워집니다
It includes Blocks that handle events, update and emit state, Events that represent app actions, State which is data contained in an app and the BlocProvider to manage Block lifecycle and provide it to child widgets.
이 패턴에는 이벤트를 처리하고, 상태를 업데이트하며, 상태를 내보내는 Bloc, 앱의 동작을 나타내는 Events, 앱에 포함된 데이터를 나타내는 State, 그리고 Bloc의 생명주기를 관리하고 자식 위젯에 이를 제공하는 BlocProvider가 포함됩니다.
For those who are familiar with TypeScript and React, this is very similar to React Redux in a number of ways.
@immutable
abstract class CounterEvent {}
// Events
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// State
class CounterState {
final int counter;
CounterState(this.counter);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0));
@override
Stream<CounterState> mapEventToState(CounterEvent event) async* {
if (event is IncrementEvent) {
yield CounterState(state.counter + 1);
} else if (event is DecrementEvent) {
yield CounterState(state.counter - 1);
}
}
}
// Usage (in UI)
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Column(
mainAxisAlignment: MainCenter,
children: [
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => Text('Count: ${state.counter}'),
),
Row(
mainAxisAlignment: MainSpaceEvenly,
children: [
ElevatedButton(
onPressed: () => BlocProvider.of<CounterBloc>(context).add(IncrementEvent()),
child: Icon(Icons.add),
),
ElevatedButton(
onPressed: () => BlocProvider.of<CounterBloc>(context).add(DecrementEvent()),
child: Icon(Icons.remove),
),
],
),
],
),
),
);
}
Conclusion
Most mobile engineers who work on mobile apps are not as familiar with design patterns and the goal of this guide was to offer a starting point for them as well as to give senior engineers a refresher on the most common patterns.
대부분의 모바일 엔지니어들은 디자인 패턴에 익숙하지 않은 경우가 많으며, 이 가이드의 목표는 그들에게 출발점을 제공하고, 동시에 시니어 엔지니어들에게는 가장 흔한 패턴들에 대한 상기 기회를 제공하는 것이었습니다
I created it for Flutter because the flutter docs don’t exactly offer these patterns in an explicit manner like Swift does for example.
이 가이드를 Flutter용으로 작성한 이유는, 예를 들어 Swift처럼 Flutter 문서에서는 이러한 패턴들을 명확하게 제공하지 않기 때문입니다.


