반응형

Flutter는 다양한 디자인 패턴을 사용하여 애플리케이션 개발을 더욱 구조적이고 유지보수가 쉽게 만들 수 있습니다. 주요 패턴들을 정리해보겠습니다.

 

1. Provider 패턴

 

설명: Flutter 애플리케이션에서 상태 관리를 간단하고 효율적으로 수행하기 위해 사용됩니다. InheritedWidget을 기반으로 하여, 상위 위젯 트리에서 상태를 하위 위젯 트리에 전달할 수 있습니다.

사용법: provider 패키지를 사용하여 구현합니다.

예제:

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Provider Example')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Consumer<Counter>(
                builder: (context, counter, _) {
                  return Text(
                    '${counter.count}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Provider.of<Counter>(context, listen: false).increment();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

2. BLoC (Business Logic Component) 패턴

 

설명: BLoC 패턴은 비즈니스 로직을 UI에서 분리하여 유지보수성과 테스트 용이성을 높입니다. 이벤트와 상태 스트림을 사용하여 비동기 데이터 흐름을 관리합니다.

사용법: flutter_bloc 패키지를 사용하여 구현합니다.

예제:

class CounterEvent {}

class CounterState {
  final int counter;
  CounterState(this.counter);
}

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);
    }
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BLoC Example')),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Text(
              '${state.counter}',
              style: Theme.of(context).textTheme.headline4,
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          BlocProvider.of<CounterBloc>(context).add(IncrementEvent());
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

3. Redux 패턴

 

설명: 상태 관리 라이브러리로, 애플리케이션의 상태를 하나의 중앙 저장소에서 관리합니다. 액션을 통해 상태를 업데이트하고 리듀서를 통해 새로운 상태를 반환합니다.

사용법: flutter_redux 패키지를 사용하여 구현합니다.

예제:

// State
class AppState {
  final int counter;
  AppState(this.counter);
}

// Actions
class IncrementAction {}

// Reducer
AppState counterReducer(AppState state, action) {
  if (action is IncrementAction) {
    return AppState(state.counter + 1);
  }
  return state;
}

// Store
final store = Store<AppState>(
  counterReducer,
  initialState: AppState(0),
);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Redux Example')),
      body: Center(
        child: StoreConnector<AppState, String>(
          converter: (store) => store.state.counter.toString(),
          builder: (context, counter) {
            return Text(
              counter,
              style: Theme.of(context).textTheme.headline4,
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          StoreProvider.of<AppState>(context).dispatch(IncrementAction());
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

4. MVVM (Model-View-ViewModel) 패턴

 

설명: 애플리케이션의 UI를 모델과 분리하여 유지보수성과 테스트 용이성을 높입니다. ViewModel은 모델 데이터를 처리하고 이를 뷰에 바인딩합니다.

사용법: providerChangeNotifier를 사용하여 구현합니다.

예제:

class CounterViewModel extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterViewModel(),
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MVVM Example')),
      body: Center(
        child: Consumer<CounterViewModel>(
          builder: (context, viewModel, _) {
            return Text(
              '${viewModel.counter}',
              style: Theme.of(context).textTheme.headline4,
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<CounterViewModel>(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

참고 자료

 

Flutter Documentation - Provider

Flutter Documentation - BLoC

Redux in Flutter

MVVM in Flutter

반응형
반응형

Dart의 Futureasync/await는 비동기 프로그래밍을 지원하는 핵심 개념입니다.

이들 개념을 사용하면 비동기 작업을 더 쉽게 작성하고 관리할 수 있습니다.

 

Future

 

Future는 비동기 작업의 결과를 나타내는 객체입니다. 이 객체는 작업이 완료되었을 때 결과를 제공하거나 오류를 전달합니다. Future는 JavaScript의 Promise와 유사합니다.

 

Future 사용 예제

 

1. Future 생성

Future 사용 예제

 

1. Future 생성

Future<String> fetchUserOrder() {
  // 2초 후에 'Order Complete' 반환
  return Future.delayed(Duration(seconds: 2), () => 'Order Complete');
}

 

2. then() 사용

void main() {
  fetchUserOrder().then((order) {
    print(order); // 'Order Complete' 출력
  }).catchError((error) {
    print('Error: $error');
  });
}

async 및 await

 

asyncawait 키워드는 비동기 코드를 더 읽기 쉽게 작성할 수 있도록 도와줍니다. async 함수는 Future를 반환하며, await 키워드는 Future가 완료될 때까지 기다린 후 결과를 반환합니다.

 

async 및 await 사용 예제

 

1. async 함수 정의

 

Future<void> fetchUserOrder() async {
  try {
    String order = await Future.delayed(Duration(seconds: 2), () => 'Order Complete');
    print(order); // 'Order Complete' 출력
  } catch (e) {
    print('Error: $e');
  }
}

2. async 함수 호출

void main() {
  fetchUserOrder();
  print('Fetching user order...'); // 먼저 출력됨
}

 

자세한 설명

 

1. Future

비동기 작업을 표현: Future는 비동기 작업의 완료(성공/실패)를 표현합니다.

메서드:

then: Future가 완료될 때 호출될 콜백을 등록합니다.

catchError: Future가 실패할 때 호출될 콜백을 등록합니다.

whenComplete: Future가 완료되면 성공 여부와 상관없이 호출될 콜백을 등록합니다.

2. async

함수를 비동기로 표시: 함수에 async를 붙이면 해당 함수는 Future를 반환합니다.

비동기 코드 작성: async 키워드를 사용하면 코드가 비동기로 실행되며, 함수는 즉시 Future를 반환합니다.

3. await

Future가 완료될 때까지 기다림: await 키워드를 사용하면 Future가 완료될 때까지 기다리고, 완료 후 결과를 반환합니다.

try-catch와 함께 사용: 비동기 작업에서 발생하는 오류를 처리하기 위해 try-catch 블록과 함께 사용할 수 있습니다.

 

 

참고

https://dart-ko.dev/codelabs/async-await

반응형
반응형

 

Firestore를 사용하여 To-Do 앱을 만드는 방법을 설명하겠습니다. 이 예제에서는 Firebase Firestore를 사용하여 할 일 데이터를 저장하고, 읽고, 업데이트하고, 삭제하는 기능을 구현합니다.

 

1. firebase 내 데이터베이스 만들기

위와 같이 firestore 를 생성해줍니다. 

 

1. 프로젝트 설정

 

pubspec.yaml 파일 업데이트

 

최신 Firebase 패키지를 pubspec.yaml 파일에 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  firebase_core: 
  firebase_auth: 
  cloud_firestore:

2. Firebase 설정

 

google-services.json 파일을 다운로드하여 android/app 디렉토리에 복사합니다. Firebase 콘솔에서 프로젝트를 설정하고 앱을 등록한 후 이 파일을 받을 수 있습니다.

 

android/build.gradle 파일 업데이트

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.0.0'
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

android/app/build.gradle 파일 업데이트

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

android {
    compileSdkVersion 34

    defaultConfig {
        applicationId "com.example.flutter_todo"
        minSdkVersion 21
        targetSdkVersion 34
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation platform('com.google.firebase:firebase-bom:32.2.2')
}

apply plugin: 'com.google.gms.google-services'

 

3. Firebase 초기화

 

main.dart

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'screens/todo_list_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Firebase Todo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

 

4. Firestore 데이터 모델

 

models/todo.dart

class Todo {
  final String id;
  final String title;
  final bool isDone;

  Todo({
    required this.id,
    required this.title,
    this.isDone = false,
  });

  factory Todo.fromMap(Map<String, dynamic> data, String documentId) {
    return Todo(
      id: documentId,
      title: data['title'] ?? '',
      isDone: data['isDone'] ?? false,
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'title': title,
      'isDone': isDone,
    };
  }
}

5. Firestore 서비스

 

services/firestore_service.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/todo.dart';

class FirestoreService {
  final FirebaseFirestore _db = FirebaseFirestore.instance;

  Stream<List<Todo>> getTodos() {
    return _db.collection('todos').snapshots().map((snapshot) =>
        snapshot.docs.map((doc) => Todo.fromMap(doc.data(), doc.id)).toList());
  }

  Future<void> addTodo(Todo todo) {
    return _db.collection('todos').add(todo.toMap());
  }

  Future<void> updateTodo(Todo todo) {
    return _db.collection('todos').doc(todo.id).update(todo.toMap());
  }

  Future<void> deleteTodo(String id) {
    return _db.collection('todos').doc(id).delete();
  }
}

6. 프로바이더 설정

 

providers/todo_provider.dart

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/todo.dart';
import '../services/firestore_service.dart';

class TodoProvider with ChangeNotifier {
  final FirestoreService _firestoreService = FirestoreService();

  late Stream<List<Todo>> _todos;
  Stream<List<Todo>> get todos => _todos;

  TodoProvider() {
    _todos = _firestoreService.getTodos();
  }

  Future<void> addTodo(Todo todo) async {
    await _firestoreService.addTodo(todo);
  }

  Future<void> updateTodo(Todo todo) async {
    await _firestoreService.updateTodo(todo);
  }

  Future<void> deleteTodo(String id) async {
    await _firestoreService.deleteTodo(id);
  }
}

7. UI 작성

 

screens/todo_list_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../providers/todo_provider.dart';
import 'edit_todo_screen.dart';

class TodoListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('To-Do List'),
      ),
      body: StreamBuilder<List<Todo>>(
        stream: Provider.of<TodoProvider>(context).todos,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Center(child: CircularProgressIndicator());
          }
          final todos = snapshot.data!;
          return ListView.builder(
            itemCount: todos.length,
            itemBuilder: (ctx, i) => ListTile(
              title: Text(todos[i].title),
              trailing: Checkbox(
                value: todos[i].isDone,
                onChanged: (value) {
                  Provider.of<TodoProvider>(context, listen: false).updateTodo(
                    Todo(
                      id: todos[i].id,
                      title: todos[i].title,
                      isDone: value!,
                    ),
                  );
                },
              ),
              onLongPress: () {
                Provider.of<TodoProvider>(context, listen: false)
                    .deleteTodo(todos[i].id);
              },
              onTap: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => EditTodoScreen(todo: todos[i]),
                  ),
                );
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showDialog(
            context: context,
            builder: (ctx) {
              TextEditingController controller = TextEditingController();
              return AlertDialog(
                title: Text('Add Todo'),
                content: TextField(
                  controller: controller,
                  decoration: InputDecoration(labelText: 'Title'),
                ),
                actions: [
                  TextButton(
                    onPressed: () {
                      if (controller.text.isNotEmpty) {
                        final newTodo = Todo(
                          id: '',
                          title: controller.text,
                        );
                        Provider.of<TodoProvider>(context, listen: false)
                            .addTodo(newTodo);
                        Navigator.of(ctx).pop();
                      }
                    },
                    child: Text('Add'),
                  ),
                ],
              );
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

screens/edit_todo_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../providers/todo_provider.dart';

class EditTodoScreen extends StatelessWidget {
  final Todo todo;

  EditTodoScreen({required this.todo});

  @override
  Widget build(BuildContext context) {
    TextEditingController titleController = TextEditingController(text: todo.title);

    return Scaffold(
      appBar: AppBar(
        title: Text('Edit Todo'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: titleController,
              decoration: InputDecoration(labelText: 'Title'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (titleController.text.isNotEmpty) {
                  final updatedTodo = Todo(
                    id: todo.id,
                    title: titleController.text,
                    isDone: todo.isDone,
                  );
                  Provider.of<TodoProvider>(context, listen: false)
                      .updateTodo(updatedTodo);
                  Navigator.of(context).pop();
                }
              },
              child: Text('Update'),
            ),
          ],
        ),
      ),
    );
  }
}

요약

 

1. Firestore 데이터 모델(Todo)을 정의합니다.

2. Firestore 서비스(FirestoreService)를 작성하여 Firestore와 상호작용합니다.

3. 프로바이더(TodoProvider)를 작성하여 상태 관리를 합니다.

4. UI를 작성하여 할 일 목록을 표시하고 추가, 수정, 삭제 기능을 구현합니다.

5. Firebase를 초기화하고 앱을 실행합니다.

 

이제 Firestore를 사용하여 할 일 데이터를 관리하는 간단한 To-Do 앱이 완성되었습니다. 필요에 따라 추가 기능을 구현하거나 UI를 개선할 수 있습니다.

반응형
반응형

Flutter에서 바텀 네비게이션 바(Bottom Navigation Bar)는 주로 앱의 주요 네비게이션을 제공하는 데 사용됩니다. 사용자에게 앱의 주요 섹션을 탐색할 수 있는 인터페이스를 제공합니다. 아래는 바텀 네비게이션 바에 대한 주요 특징과 예제들을 정리한 것입니다.

 

주요 특징

 

1. 사용 용도:

앱의 주요 섹션 간 빠른 네비게이션을 제공.

일반적으로 3~5개의 탭을 포함.

각 탭에는 아이콘과 선택적으로 텍스트 레이블을 포함할 수 있음.

2. 구성 요소:

items: 네비게이션 항목 목록으로, 각각 BottomNavigationBarItem 객체를 포함.

currentIndex: 현재 선택된 탭의 인덱스.

onTap: 사용자가 탭을 선택할 때 호출되는 콜백 함수.

type: 네비게이션 바의 타입(fixed 또는 shifting).

 

예제 코드

 

아래는 Flutter에서 바텀 네비게이션 바를 구현하는 기본적인 예제입니다.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0;

  static const List<Widget> _widgetOptions = <Widget>[
    Text('Home Page'),
    Text('Search Page'),
    Text('Profile Page'),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bottom Navigation Bar Example'),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

주요 속성

 

1. items:

BottomNavigationBarItem 객체의 리스트로, 각 항목은 아이콘과 텍스트 레이블을 가짐.

예제:

items: const <BottomNavigationBarItem>[
  BottomNavigationBarItem(
    icon: Icon(Icons.home),
    label: 'Home',
  ),
  BottomNavigationBarItem(
    icon: Icon(Icons.search),
    label: 'Search',
  ),
  BottomNavigationBarItem(
    icon: Icon(Icons.person),
    label: 'Profile',
  ),
],

 

2. currentIndex:

현재 선택된 탭의 인덱스를 나타냄.

예제:

currentIndex: _selectedIndex,

 

3. onTap:

사용자가 탭을 선택할 때 호출되는 콜백 함수.

예제:

onTap: _onItemTapped,

4. type:

네비게이션 바의 타입을 설정. BottomNavigationBarType.fixed 또는 BottomNavigationBarType.shifting을 사용.

예제:

type: BottomNavigationBarType.fixed,

참고 자료

 

Flutter Documentation - BottomNavigationBar

Flutter Cookbook - BottomNavigationBar

반응형
반응형

Flutter에서는 다양한 레이아웃 위젯을 제공하여 UI 요소들을 배치하고 정렬할 수 있습니다. 레이아웃 위젯은 화면의 구조를 정의하고, 자식 위젯들을 어떻게 배치할지 결정하는 데 사용됩니다. 주요 레이아웃 위젯과 그 설명을 아래에 정리했습니다.

 

주요 레이아웃 위젯

 

1. Container

설명: 단일 자식을 포함할 수 있는 상자 위젯. 패딩, 마진, 경계선, 색상 등을 설정할 수 있습니다.

예제:

Container(
  padding: EdgeInsets.all(16.0),
  margin: EdgeInsets.all(8.0),
  decoration: BoxDecoration(
    border: Border.all(color: Colors.black),
  ),
  child: Text('Hello, Container!'),
);

2. Row

설명: 자식 위젯들을 가로로 나란히 배치합니다.

예제:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.star),
    Icon(Icons.star),
    Icon(Icons.star),
  ],
);

3. Column

설명: 자식 위젯들을 세로로 나란히 배치합니다.

예제:

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Text('First Line'),
    Text('Second Line'),
    Text('Third Line'),
  ],
);

4. Stack

설명: 자식 위젯들을 겹쳐서 배치합니다.

예제:

Stack(
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 50,
      height: 50,
      color: Colors.green,
    ),
  ],
);

5. Expanded

설명: Flex 위젯(Row, Column) 내에서 남은 공간을 차지하도록 자식 위젯을 확장합니다.

예제:

Row(
  children: <Widget>[
    Expanded(
      child: Container(
        color: Colors.red,
      ),
    ),
    Expanded(
      child: Container(
        color: Colors.green,
      ),
    ),
  ],
);

 

6. SizedBox

설명: 특정 크기의 상자를 생성합니다.

예제:

SizedBox(
  width: 100,
  height: 100,
  child: Container(
    color: Colors.blue,
  ),
);

 

7. Padding

설명: 자식 위젯 주위에 패딩을 추가합니다.

예제:

Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Padded Text'),
);

8. Align

설명: 자식 위젯을 부모 위젯 내에서 정렬합니다.

예제:

Align(
  alignment: Alignment.center,
  child: Text('Centered Text'),
);

9. Center

설명: 자식 위젯을 중앙에 배치합니다.

예제:

Center(
  child: Text('Centered Text'),
);

참고 자료

 

Flutter Documentation - Layout Widgets

Flutter Widget Catalog

반응형
반응형

Flutter에서 Controller는 다양한 위젯과 함께 사용되어 위젯의 상태나 동작을 제어하는 데 사용됩니다.

특히 스크롤, 페이지, 텍스트 입력 등을 다룰 때 유용합니다.

아래는 일반적으로 Controller가 사용되는 주요 위젯과 그 역할을 설명합니다.

 

1. ScrollController

 

사용 위젯: ListView, GridView, SingleChildScrollView 등

 

역할:

 

스크롤 위치를 제어하고 모니터링합니다.

특정 위치로 스크롤하거나, 사용자가 스크롤할 때 발생하는 이벤트를 처리할 수 있습니다.

 

예제:

ListView.builder(
  controller: ScrollController(),
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
);

 

class _ScrollControllerExampleState extends State<ScrollControllerExample> {
  final ScrollController _scrollController = ScrollController();
  double _scrollPosition = 0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      setState(() {
        _scrollPosition = _scrollController.position.pixels;
      });
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Scroll Position: $_scrollPosition'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: 100,
        itemBuilder: (context, index) {
          return ListTile(title: Text('Item $index'));
        },
      ),
    );
  }
}

 

2. PageController

 

사용 위젯: PageView, TabBarView 등

 

역할:

 

페이지 뷰어의 현재 페이지를 제어하고 모니터링합니다.

특정 페이지로 이동하거나, 페이지 변화 이벤트를 처리할 수 있습니다.

 

예제:

PageView(
  controller: PageController(initialPage: 0),
  children: <Widget>[
    Container(color: Colors.red),
    Container(color: Colors.green),
    Container(color: Colors.blue),
  ],
);
class _PageControllerExampleState extends State<PageControllerExample> {
  final PageController _pageController = PageController(initialPage: 0);
  int _currentPage = 0;

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  void _onPageChanged(int page) {
    setState(() {
      _currentPage = page;
    });
  }

  void _nextPage() {
    _pageController.nextPage(duration: Duration(milliseconds: 300), curve: Curves.easeIn);
  }

  void _previousPage() {
    _pageController.previousPage(duration: Duration(milliseconds: 300), curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page View Example')),
      body: Column(
        children: [
          Expanded(
            child: PageView(
              controller: _pageController,
              onPageChanged: _onPageChanged,
              children: <Widget>[
                Container(color: Colors.red),
                Container(color: Colors.green),
                Container(color: Colors.blue),
              ],
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.arrow_back),
                onPressed: _previousPage,
              ),
              Text('Page $_currentPage'),
              IconButton(
                icon: Icon(Icons.arrow_forward),
                onPressed: _nextPage,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

 

3. TextEditingController

 

사용 위젯: TextField, TextFormField

 

역할:

 

텍스트 입력 필드의 값을 제어하고 모니터링합니다.

초기 값을 설정하거나, 텍스트 변화 이벤트를 처리할 수 있습니다.

 

예제:

TextField(
  controller: TextEditingController(),
  decoration: InputDecoration(labelText: 'Enter text'),
);
class _TextEditingControllerExampleState extends State<TextEditingControllerExample> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _printLatestValue() {
    print('Second text field: ${_controller.text}');
  }

  @override
  void initState() {
    super.initState();
    _controller.addListener(_printLatestValue);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('TextEditingController Example')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: InputDecoration(labelText: 'Enter text'),
            ),
            ElevatedButton(
              onPressed: () {
                print('First text field: ${_controller.text}');
              },
              child: Text('Print Text'),
            ),
          ],
        ),
      ),
    );
  }
}

 

4. AnimationController

 

사용 위젯: 다양한 애니메이션 위젯

 

역할:

 

애니메이션을 제어하고 모니터링합니다.

애니메이션의 시작, 중지, 반복 등을 제어할 수 있습니다.

 

예제:

AnimationController(
  duration: const Duration(seconds: 2),
  vsync: this,
);
class _AnimationControllerExampleState extends State<AnimationControllerExample> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimationController Example')),
      body: Center(
        child: FadeTransition(
          opacity: _controller,
          child: FlutterLogo(size: 100.0),
        ),
      ),
    );
  }
}

 

5. TabController

 

사용 위젯: TabBar, TabBarView

 

역할:

 

탭 바의 현재 탭을 제어하고 모니터링합니다.

탭 변경을 제어하거나, 탭 변화 이벤트를 처리할 수 있습니다.

 

예제:

TabController(
  length: 3,
  vsync: this,
);
class _TabControllerExampleState extends State<TabControllerExample> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TabController Example'),
        bottom: TabBar(
          controller: _tabController,
          tabs: [
            Tab(icon: Icon(Icons.directions_car), text: 'Car'),
            Tab(icon: Icon(Icons.directions_transit), text: 'Transit'),
            Tab(icon: Icon(Icons.directions_bike), text: 'Bike'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          Center(child: Text('Car Tab')),
          Center(child: Text('Transit Tab')),
          Center(child: Text('Bike Tab')),
        ],
      ),
    );
  }
}

 

요약

 

ScrollController: 스크롤 위치를 제어 (ListView, GridView 등)

PageController: 페이지 뷰어의 현재 페이지를 제어 (PageView, TabBarView)

TextEditingController: 텍스트 입력 필드의 값을 제어 (TextField, TextFormField)

AnimationController: 애니메이션을 제어 (애니메이션 관련 위젯)

TabController: 탭 바의 현재 탭을 제어 (TabBar, TabBarView)

 

참고 자료

 

ScrollController 문서

PageController 문서

TextEditingController 문서

AnimationController 문서

TabController 문서

반응형
반응형

Flutter에서 Photo Gallery는 사용자가 여러 이미지를 탐색할 수 있게 해주는 기능을 의미합니다.

사용자는 이미지를 확대하거나 축소할 수 있고, 스와이프하여 다음 이미지로 넘어갈 수 있으며, 이미지에 대한 메타데이터를 볼 수도 있습니다.

Flutter에서 Photo Gallery를 구현하기 위해 다양한 위젯과 패키지를 사용할 수 있으며, 그 중 photo_view 패키지가 많이 사용됩니다.

 

Photo Gallery의 주요 구성 요소

 

1. 이미지 데이터 소스: 이미지를 저장하거나 가져오는 곳입니다. 로컬 디렉토리, 원격 서버, 또는 데이터베이스 등이 될 수 있습니다.

2. 이미지 로딩 및 캐싱: 대량의 이미지를 부드럽게 처리하기 위해 효율적인 로딩 및 캐싱 메커니즘이 필요합니다. cached_network_image 패키지와 같은 도구를 사용할 수 있습니다.

3. 이미지 표시: 이미지를 화면에 표시하기 위해 Flutter의 다양한 위젯을 사용합니다. 특히 photo_view 패키지는 이미지 확대/축소, 드래그, 회전 등의 기능을 제공합니다.

4. UI 및 사용자 상호작용: 사용자가 이미지를 탐색하고 상호작용할 수 있는 UI를 설계합니다. 예를 들어, 스와이프를 통한 이미지 전환, 버튼을 통한 이미지 삭제 등입니다.

 

주요 패키지

 

photo_view: 이미지를 확대/축소하고 드래그할 수 있는 기능을 제공하는 패키지입니다.

cached_network_image: 네트워크 이미지를 로드하고 캐시하는 기능을 제공하는 패키지입니다.

 

예제 코드

 

아래는 photo_view 패키지를 사용하여 Photo Gallery를 구현하는 예제 코드입니다.

 

PhotoViewPage.dart

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

class PhotoViewPage extends StatefulWidget {
  final List<String> imagePaths;
  final int currentIndex;

  PhotoViewPage({required this.imagePaths, required this.currentIndex});

  @override
  _PhotoViewPageState createState() => _PhotoViewPageState();
}

class _PhotoViewPageState extends State<PhotoViewPage> {
  late PageController _controller;
  late int currentPageIndex;

  @override
  void initState() {
    super.initState();
    _controller = PageController(initialPage: widget.currentIndex);
    currentPageIndex = widget.currentIndex;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          PhotoViewGallery.builder(
            itemCount: widget.imagePaths.length,
            pageController: _controller,
            onPageChanged: (index) {
              setState(() {
                currentPageIndex = index;
              });
            },
            builder: (context, index) {
              return PhotoViewGalleryPageOptions(
                imageProvider: NetworkImage(widget.imagePaths[index]),
                maxScale: PhotoViewComputedScale.covered * 3,
                minScale: PhotoViewComputedScale.contained,
              );
            },
            loadingBuilder: (context, event) {
              return Center(
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                ),
              );
            },
          ),
          Align(
            alignment: Alignment.topLeft,
            child: Container(
              margin: const EdgeInsets.only(left: 25, top: 25),
              child: IconButton(
                onPressed: () => Navigator.of(context).pop(),
                icon: const Icon(
                  Icons.close,
                  color: Colors.white,
                ),
              ),
            ),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              margin: const EdgeInsets.only(bottom: 20),
              child: Text(
                '${currentPageIndex + 1} / ${widget.imagePaths.length}',
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

사용법

 

위 코드를 사용하기 위해 pubspec.yaml 파일에 photo_view 패키지를 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  photo_view: ^0.13.0 # 최신 버전을 확인하세요

 

Main.dart

import 'package:flutter/material.dart';
import 'photo_view_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GalleryPage(),
    );
  }
}

class GalleryPage extends StatelessWidget {
  final List<String> imagePaths = [
    'https://example.com/image1.jpg',
    'https://example.com/image2.jpg',
    'https://example.com/image3.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Gallery')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => PhotoViewPage(
                  imagePaths: imagePaths,
                  currentIndex: 0,
                ),
              ),
            );
          },
          child: Text('Open PhotoView Gallery'),
        ),
      ),
    );
  }
}

 

참고 자료

 

photo_view 패키지 공식 문서

cached_network_image 패키지 공식 문서

 

결론

 

Flutter에서 Photo Gallery를 구현하는 것은 photo_view와 같은 패키지를 사용하면 매우 간단합니다. 이 패키지를 사용하면 사용자에게 고급 이미지 뷰어 기능을 제공할 수 있으며, 다양한 소스에서 이미지를 로드하고 캐시할 수 있습니다. 이로 인해 사용자 경험이 향상됩니다.

반응형
반응형

 

1. Firebase 프로젝트 설정

 

1.1 Firebase 콘솔에서 프로젝트 생성

 

1. Firebase 콘솔에 접속하여 새 프로젝트를 만듭니다.

2. 프로젝트 이름을 입력하고, 필요한 설정을 완료합니다.

 

 

1.2 앱에 Firebase 추가

 

1. Firebase 프로젝트의 대시보드에서 Android 또는 iOS 앱을 추가합니다.

 

firebase cli 를 먼저 설치를 해줍니다. 

 

https://firebase.google.com/docs/cli?hl=ko&authuser=0&_gl=1*i16l2u*_ga*MTY1Njg1OTM5My4xNzIxNzEyMzA2*_ga_CW55HF8NVT*MTcyMTcxNjg4NS4yLjEuMTcyMTcxNzYxMi4zNy4wLjA.#windows-standalone-binary

각 os 에 맞는 방식을 클릭 후 그대로 따라합니다.

 

firebase cli 를 설치했다면 

firebase login

 으로 구글 계정 로그인 하면 됩니다. 

그 후 로그인이 잘 되었는지 확인하기 위해 프로젝트 리스트를 조회합니다.

firebase projects:list

그 후 아래 명령러를 그대로 실행해 줍니다.

 

flutterfire ~~

flutterfire configure --project=todo-프로젝트아이디

명령어를 치면 아래와 같이 나오고 원하는 os 선택 후 설치 진행합니다.

 

해당 명령어가 잘 실행된다면 콘솔에는 아래와 같이 앱을 등록했다는 로그가 남습니다. 

그리고 파이어베이스 대시보드로 이동하면 등록된 앱이 뜨게 됩니다. 

해당 작업이 완료되면
lib 폴더에 firebase_options.dart 가 생성된다고 하는데, 필자는 뜨지 않아서 새로 수동으로 만들어주었습니다. 아래 적었습니다.

 

 

안드로이드 에 firebase 권한 추가 

google-services.json 파일을 다운 후 
android > app > 경로에 google-services.json 을 넣어줍니다. 

 

프로젝트에 firebase sdk 추가 

 

 

settings.gradle 의 plugins 내부에 아래 코드를 넣어줍니다. 가이드에는 루트프로젝트의 build.gradle 라고 나옴.

settings.gradle 에 수정 

id "com.google.gms.google-services" version "4.3.15" apply false

해당 과정에서 혹시 kotlin 버젼 이슈가 있다면 (아래 와 같이 버젼 이슈)

integ.client.measurement_api_measurement_api.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.

버젼을 settings.gradle 에서 올려주면 됩니다. 

id "org.jetbrains.kotlin.android" version "1.9.0" apply false

 

추가로 프로젝트내 build.gradle 에 firebae 라이브러리 추가

android > app > build.gradle

dependencies {
    implementation platform('com.google.firebase:firebase-bom:33.1.2')
    implementation 'com.google.firebase:firebase-analytics'
}

 

여기까지 추가 후 실행이 되면 firebase 사용준비는 완료되었습니다. 

 

 

Authentication 시작 

firebase 를 이용한 회원가입 및 로그인 기능을 두기 위해서는 firebase 에서 authentication 을 시작해주어야 합니다.

Authentication 설정: 좌측 메뉴에서 Authentication을 클릭합니다.

 

Sign-in Method: 상단 탭에서 Sign-in method를 선택합니다. (로그인 방법 클릭)

Email/Password: 이메일/비밀번호 인증이 활성화되어 있는지 확인합니다

활성화되지 않았다면 활성화합니다.

 

 

firebase_options.dart 수동생성

lib 폴더에 firebase_options.dart  파일을 생성합니다. 

// lib/firebase_options.dart

import 'package:firebase_core/firebase_core.dart';

class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    return const FirebaseOptions(
      apiKey: 'AIzaSyBbHR~~~~~~~~~~~~~',
      appId: '1:508050~~~~:android:7201919812b~~~~~~',
      messagingSenderId: '50805~~~~~~',
      projectId: 'todo-~~~~',
    );
  }
}

각 값에 맞는 값은 프로젝트 내 앱 설정에서 확인합니다. 

google-services.json 파일안에 해당 값들이 존재 합니다. 

 

 

회원가입 페이지
register_page.dart

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

import 'login_page.dart';

class RegisterPage extends StatefulWidget {
  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Future<void> _register() async {
    try {
      UserCredential userCredential = await _auth.createUserWithEmailAndPassword(
        email: _emailController.text,
        password: _passwordController.text,
      );
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Successfully registered!')),
      );
      // 회원가입 후 원하는 페이지로 이동
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => LoginPage()),
      );
    } on FirebaseAuthException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to register: ${e.message}')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Register')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _emailController,
              decoration: InputDecoration(labelText: 'Email'),
            ),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _register,
              child: Text('Register'),
            ),
          ],
        ),
      ),
    );
  }
}

 

회원가입이 완료되면 로그인 페이지로 리다이렉트 하게 했습니다.

 

로그인 페이지

login_page.dart

class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  Future<void> _login() async {
    try {
      UserCredential userCredential = await _auth.signInWithEmailAndPassword(
        email: _emailController.text,
        password: _passwordController.text,
      );
      // 로그인 성공 시 처리
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (context) => TodoListScreen()), // ListPage로 이동
      );
    } on FirebaseAuthException catch (e) {
      String message;
      switch (e.code) {
        case 'user-not-found':
          message = 'No user found for that email.';
          break;
        case 'wrong-password':
          message = 'Wrong password provided for that user.';
          break;
        default:
          message = 'An error occurred. Please try again.';
      }
      // 에러 메시지를 스낵바로 출력
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(message)),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('An error occurred. Please try again.')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _emailController,
              decoration: InputDecoration(labelText: 'Email'),
            ),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _login,
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

 

main 메소드 
firebase 메소드를 추가해줍니다. 

void main() async{

  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(
      child: MyApp(),
    ),
  );
}
반응형

+ Recent posts