반응형

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에서 State는 위젯의 상태를 관리하는 데 사용됩니다. State는 두 가지 주요 유형으로 나뉩니다: StatelessWidgetStatefulWidget. 이 두 가지는 서로 다른 방식으로 위젯의 상태를 처리합니다.

 

1. StatelessWidget

 

StatelessWidget은 변경되지 않는 상태를 가지는 위젯입니다. 즉, 한 번 생성되면 상태가 변하지 않습니다. 이러한 위젯은 상태를 가지지 않고, 빌드 시 제공된 데이터로만 렌더링됩니다.

 

예제

import 'package:flutter/material.dart';

class MyStatelessWidget extends StatelessWidget {
  final String title;

  MyStatelessWidget({required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text('Hello, $title!'),
      ),
    );
  }
}

void main() => runApp(MaterialApp(home: MyStatelessWidget(title: 'Stateless Widget')));

2. StatefulWidget

 

StatefulWidget은 변경 가능한 상태를 가지는 위젯입니다.

StatefulWidget은 자체적으로 상태를 관리하며, 상태가 변경될 때마다 다시 빌드됩니다.

StatefulWidget은 두 개의 클래스가 함께 작동합니다:

StatefulWidget 클래스State 클래스.

import 'package:flutter/material.dart';

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stateful Widget'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

void main() => runApp(MaterialApp(home: MyStatefulWidget()));

StatefulWidget의 동작 방식

 

1. createState():

StatefulWidget은 createState 메서드를 통해 State 객체를 생성합니다. 이 객체는 위젯의 상태를 관리합니다.

2. State 객체:

State 객체는 위젯의 상태를 포함하며, 상태가 변경될 때 setState 메서드를 호출하여 UI를 업데이트합니다.

setState는 상태를 변경하고, 변경된 상태를 기반으로 위젯을 다시 빌드합니다.

3. initState():

initState는 State 객체가 처음 생성될 때 호출되며, 초기화 작업을 수행합니다.

4. dispose():

dispose는 State 객체가 소멸될 때 호출되며, 리소스를 정리하는 작업을 수행합니다.

 

State의 생명주기

 

initState(): State 객체가 처음 생성될 때 호출됩니다.

build(): 상태가 변경될 때마다 호출되어 위젯을 다시 빌드합니다.

didUpdateWidget(): 위젯이 업데이트될 때 호출됩니다.

dispose(): State 객체가 소멸될 때 호출됩니다.

 

예제

 

아래는 위젯의 상태를 변경하여 UI를 업데이트하는 예제입니다.

import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Button pressed $_count times',
              style: Theme.of(context).textTheme.headline4,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

void main() => runApp(MaterialApp(home: CounterWidget()));

참고 자료

 

Flutter Documentation - State

Flutter Documentation - StatefulWidget

Flutter Documentation - StatelessWidget

반응형
반응형

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와 같은 패키지를 사용하면 매우 간단합니다. 이 패키지를 사용하면 사용자에게 고급 이미지 뷰어 기능을 제공할 수 있으며, 다양한 소스에서 이미지를 로드하고 캐시할 수 있습니다. 이로 인해 사용자 경험이 향상됩니다.

반응형
반응형

 

Flutter에서 UI를 구성하는 주요 요소인 StatelessWidgetStatefulWidget에 대해 설명하고, 이들이 상태(state)를 어떻게 관리하는지 정리해 보겠습니다.

 

1. StatelessWidget

 

**StatelessWidget**은 불변의 상태를 가지며, 빌드할 때마다 동일한 UI를 렌더링합니다. 상태가 변하지 않기 때문에 한 번 빌드된 후에는 UI가 변경되지 않습니다. 이는 주로 정적인 UI 요소를 렌더링할 때 사용됩니다.

 

특징:

상태가 변하지 않음.

생성자에서 필요한 값을 받아 빌드 메서드에서 UI를 렌더링.

상태를 가지지 않는 간단한 위젯에 적합.

예제:

import 'package:flutter/material.dart';

class MyStatelessWidget extends StatelessWidget {
  final String title;

  MyStatelessWidget({required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Text('This is a stateless widget'),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: MyStatelessWidget(title: 'Stateless Example'),
  ));
}

 

 

2. StatefulWidget

 

**StatefulWidget**은 동적인 상태를 가지며, 상태가 변할 때마다 UI를 재빌드할 수 있습니다. 상태는 위젯 자체가 아닌 State 객체에서 관리됩니다. State 객체는 StatefulWidget과 연결되어 있으며, 상태 변경 시 setState 메서드를 호출하여 UI를 업데이트합니다.

 

특징:

상태가 변할 수 있음.

State 객체에서 상태를 관리.

UI가 상태에 따라 동적으로 변경될 필요가 있는 경우 사용.

예제:

import 'package:flutter/material.dart';

class MyStatefulWidget extends StatefulWidget {
  final String title;

  MyStatefulWidget({required this.title});

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

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: MyStatefulWidget(title: 'Stateful Example'),
  ));
}

 

3. State

 

State 클래스는 StatefulWidget과 연결된 상태를 정의합니다. State 객체는 StatefulWidget의 생명주기 동안 유지되며, 상태가 변경될 때마다 setState 메서드를 호출하여 UI를 재빌드합니다.

 

생명주기:

initState(): 처음으로 상태를 초기화할 때 호출.

didChangeDependencies(): 종속성 또는 상위 객체가 변경될 때 호출.

build(): UI를 렌더링할 때 호출.

setState(): 상태를 변경하고 UI를 재빌드할 때 호출.

dispose(): 위젯이 소멸될 때 호출.

예제 (위에서 사용한 예제에 포함됨):

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    // 초기화 작업 수행
  }

  @override
  void dispose() {
    // 정리 작업 수행
    super.dispose();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

 

 

 

 

요약

 

StatelessWidget: 불변의 상태를 가지며, 정적인 UI를 렌더링하는 데 사용됩니다.

StatefulWidget: 동적인 상태를 가지며, 상태 변경에 따라 UI를 재빌드할 수 있습니다.

State: StatefulWidget과 연결된 상태를 관리하며, 상태 변경 시 UI를 업데이트합니다.

반응형
반응형

 

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(),
    ),
  );
}
반응형
반응형

이 앱은 “할 일 목록 (To-Do List)” 앱으로, 기본적인 CRUD(생성, 읽기, 업데이트, 삭제) 작업을 포함할 것입니다.

 

샘플 앱: To-Do List

 

1. 프로젝트 생성

 

Flutter 프로젝트를 생성합니다.

flutter create todo_app
cd todo_app

 

2. 주요 패키지 추가

 

pubspec.yaml 파일에 필요한 패키지를 추가합니다. 이 예제에서는 provider를 상태 관리를 위해 사용합니다.

dev_dependencies:
  flutter_test:
    sdk: flutter
  provider:
  uuid:

이후 flutter pub get 명령어를 실행해 패키지를 설치합니다.

 

3. 모델 정의

 

할 일 항목의 모델 클래스를 정의합니다.

// lib/models/todo.dart
class Todo {
  String id;
  String title;
  bool isDone;

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

4. 상태 관리 설정

 

할 일 목록을 관리하는 ChangeNotifier 클래스를 정의합니다.

import 'package:flutter/foundation.dart';
import '../model/todo.dart';

// 할 일 목록을 관리하는 ChangeNotifier 클래스
class TodoProvider with ChangeNotifier {
  List<Todo> _todos = [];

  List<Todo> get todos => _todos;

  // 할 일 항목을 추가하는 메서드
  void addTodo(Todo todo) {
    _todos.add(todo);
    notifyListeners();
  }

  // 할 일 항목의 완료 상태를 토글하는 메서드
  void toggleTodoStatus(String id) {
    final index = _todos.indexWhere((todo) => todo.id == id);
    if (index != -1) {
      _todos[index].isDone = !_todos[index].isDone;
      notifyListeners();
    }
  }

  // 할 일 항목을 제거하는 메서드
  void removeTodo(String id) {
    _todos.removeWhere((todo) => todo.id == id);
    notifyListeners();
  }
// 업데이트
  void updateTodo(Todo updatedTodo) {
    final index = _todos.indexWhere((todo) => todo.id == updatedTodo.id);
    if (index != -1) {
      _todos[index] = updatedTodo;
      notifyListeners();
    }
  }
}

참고 if 문에 -1 확인 이유

f 문을 사용하여 index-1이 아닌지 확인하는 이유는 indexWhere 메서드가 일치하는 항목을 찾지 못할 때 -1을 반환하기 때문입니다. index-1이면 _todos[index]는 유효하지 않으며, 이 경우 배열에 접근하려고 하면 오류가 발생합니다. 따라서 if 문은 안전한 코드를 작성하는 데 중요합니다.

 

그럼에도 불구하고, indexWhere가 항상 올바른 값을 반환한다고 확신할 수 있다면 이 조건문을 생략할 수 있지만, 이는 코드의 안정성과 유연성을 감소시킬 수 있습니다. 일반적으로는 if 문을 포함하는 것이 좋습니다.

 

5. 메인 파일 설정

 

프로바이더를 설정하고 메인 파일을 구성합니다.

import 'package:flutter/material.dart';
import 'package:flutter_todo/screen/todo_list_screen.dart';
import 'package:provider/provider.dart';
import 'package:flutter_todo/providers/todo_provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        // TodoProvider를 ChangeNotifierProvider로 제공
        ChangeNotifierProvider(create: (_) => TodoProvider()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

6. UI 작성

 

할 일 목록 화면을 작성합니다.

// lib/screens/todo_list_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_todo/screen/todo_edit_screen.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';

import '../model/todo.dart';
import '../providers/todo_provider.dart';

class TodoListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('To-Do List'),
      ),
      body: ListView.builder(
        itemCount: todoProvider.todos.length, // 할 일 목록의 개수
        itemBuilder: (ctx, i) => ListTile(
          title: Text(todoProvider.todos[i].title), // 할 일 제목
          trailing: Checkbox(
            value: todoProvider.todos[i].isDone, // 완료 상태
            onChanged: (_) {
              todoProvider.toggleTodoStatus(todoProvider.todos[i].id); // 완료 상태 토글
            },
          ),
          onTap: () {
            // 수정 페이지로 이동
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => EditTodoScreen(todo: todoProvider.todos[i]),
              ),
            );
          },
          onLongPress: () {
            todoProvider.removeTodo(todoProvider.todos[i].id); // 할 일 항목 제거
          },
        ),
      ),
      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: Uuid().v4(),
                          title: controller.text,
                        );
                        todoProvider.addTodo(newTodo);
                        Navigator.of(ctx).pop();
                      }
                    },
                    child: Text('Add'),
                  ),
                ],
              );
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

7. 수정 페이지 생성

 

새로운 파일 edit_todo_screen.dart를 생성합니다.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model/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'),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

반응형
반응형

Flutter에서 스크롤 뷰를 사용한 페이지네이션은 앱이 많은 양의 데이터를 표시할 때 유용한 패턴입니다.

이 패턴은 사용자가 스크롤할 때 새로운 데이터를 동적으로 로드하여 사용자 경험을 향상시킵니다.

이를 구현하는 데 필요한 주요 단계와 예제를 정리해보겠습니다.

 

주요 개념

 

1. ScrollController: 스크롤 위치를 추적하고 스크롤 이벤트를 처리합니다.

2. ListView.builder: 동적으로 리스트 아이템을 생성하여 성능을 최적화합니다.

3. HTTP 요청: 데이터를 페이징하여 서버에서 가져옵니다.

4. 상태 관리: 현재 페이지, 로딩 상태, 데이터 리스트 등을 관리합니다.

 

단계별 구현

 

1. 프로젝트 설정:

필요한 패키지 추가: http, provider (옵션)

2. ScrollController 설정:

스크롤 이벤트를 감지하고, 사용자가 리스트 끝에 도달하면 새로운 데이터를 로드합니다.

3. ListView.builder 사용**:

리스트 아이템을 동적으로 생성하여 메모리 사용을 최적화합니다.

4. 데이터 로드:

서버에서 데이터를 페이징하여 가져옵니다.

5. 상태 관리:

StatefulWidget 또는 상태 관리 패키지(예: provider)를 사용하여 현재 페이지, 로딩 상태, 데이터 리스트 등을 관리합니다.

 

코드 예제

 

아래는 위 개념을 바탕으로 한 간단한 코드 예제입니다.

 

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

class _MyHomePageState extends State<MyHomePage> {
  final ScrollController _scrollController = ScrollController();
  List _data = [];
  int _page = 1;
  bool _isLoading = false;
  bool _hasMoreData = true;

  @override
  void initState() {
    super.initState();
    _fetchData();
    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter < 300 && !_isLoading && _hasMoreData) {
        _fetchData();
      }
    });
  }

  Future<void> _fetchData() async {
    setState(() {
      _isLoading = true;
    });

    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums?_page=$_page&_limit=10'));
    if (response.statusCode == 200) {
      List newData = json.decode(response.body);
      setState(() {
        _data.addAll(newData);
        _isLoading = false;
        if (newData.length < 10) {
          _hasMoreData = false;
        } else {
          _page++;
        }
      });
    } else {
      setState(() {
        _isLoading = false;
        _hasMoreData = false;
      });
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pagination Example'),
      ),
      body: _data.isEmpty && _isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
              controller: _scrollController,
              itemCount: _data.length + (_hasMoreData ? 1 : 0),
              itemBuilder: (context, index) {
                if (index == _data.length) {
                  return Center(child: CircularProgressIndicator());
                }
                return ListTile(
                  title: Text('Item ${_data[index]['id']}'),
                  subtitle: Text(_data[index]['title']),
                );
              },
            ),
    );
  }
}

주요 포인트

 

1. ScrollController 설정:

스크롤 이벤트를 감지하고, 사용자가 리스트 끝에 도달하면 _fetchData 함수를 호출하여 다음 페이지 데이터를 로드합니다.

2. ListView.builder:

itemCount를 설정하여 로딩 중인 상태 표시를 위한 추가 아이템을 처리합니다.

itemBuilder에서 로딩 인디케이터를 표시하여 더 많은 데이터를 로드 중임을 사용자에게 알립니다.

3. 데이터 로드 및 상태 관리:

데이터를 로드하는 동안 _isLoading 상태를 업데이트하여 UI에서 로딩 인디케이터를 표시합니다.

새로운 데이터를 가져와서 _data 리스트에 추가하고, 페이지 수를 증가시킵니다.

더 이상 가져올 데이터가 없을 경우 _hasMoreDatafalse로 설정합니다.

 

다른 예제(실습)

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>  {

  final _url = 'https://jsonplaceholder.typicode.com/albums';
  int _page = 1;
  final int _limit = 20;
  bool _hasNextPage = true; // 다음 페이지가 있는지 여부
  bool _isFirstLoadRunning = false; // 첫번째 페이지 로딩중
  bool _isLoadMoreRunning = false; // 다음페이지 로딩중
  List _albumList = [];
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();
    initLoad();
    _controller = ScrollController()..addListener(_nextLoad);
  }


  void initLoad() async {
    setState(() {
      _isFirstLoadRunning = true;
    });
    try {
      final res = await http.get(Uri.parse("$_url?_page=$_page&_limit=$_limit"));
      setState(() {
        _albumList = jsonDecode(res.body);
      });

    } catch(e) {
      print(e.toString());
    }

    setState(() {
      _isFirstLoadRunning = false;
    });
  }

  void _nextLoad() async {
    print("nextLoad");
    if(_hasNextPage && !_isFirstLoadRunning && !_isLoadMoreRunning
      && _controller.position.extentAfter < 100) {
      setState(() {
        _isLoadMoreRunning = true;
      });
      _page += 1;
      try {
        final res = await http.get(Uri.parse("$_url?_page=$_page&_list=$_limit"));
        final List fetchedAlbums = json.decode(res.body);
        if(fetchedAlbums.isNotEmpty) {
          setState(() {
            _albumList.addAll(fetchedAlbums);
          });
        } else { // 데이터가 비어있는 경우
          setState(() {
            _hasNextPage = false;
          });
        }
      } catch(e) {
        print(e.toString());
      }

      setState(() {
        _isLoadMoreRunning = false;
      });

    }
  }

  // 페이지 종료 시 종료해 주는 기능
  @override
  void dispose() {
    super.dispose();
    _controller.removeListener(_nextLoad);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("test title"),
        ),
        body: _isFirstLoadRunning
        ? const Center(
          child: CircularProgressIndicator(),
        )
        : Column(
          children: [
            Expanded(
                child: ListView.builder(
                  controller: _controller,
                  itemCount: _albumList.length,
                  itemBuilder: (context, index) => Card(
                    margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
                    child: ListTile(
                      title: Text(_albumList[index]["id"].toString()),
                      subtitle: Text(_albumList[index]["title"]),
                    ),
                  )
                )
            ),
            if (_isLoadMoreRunning == true)
              Container(
                padding: const EdgeInsets.all(30),
                child: const Center(
                  child: CircularProgressIndicator(),
                )
              ),
            if (_hasNextPage == false)
              Container(
                  padding: const EdgeInsets.all(20),
                  child: const Center(
                    child: Text(
                      "No more data to be fetched",
                      style: TextStyle(
                        color: Colors.white
                      )
                    ),
                  )
              )
          ],
        )
    );
  }
}

 

 

참고 자료

 

Flutter Documentation - ListView

Flutter Documentation - ScrollController

Flutter HTTP Package

반응형

+ Recent posts