반응형

 

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

반응형
반응형

 

MVVM(Model-View-ViewModel) 패턴은 UI와 비즈니스 로직을 분리하는 소프트웨어 아키텍처 패턴입니다. Flutter에서 MVVM 패턴을 사용하면 코드의 가독성과 유지보수성을 높이고, 테스트 용이성을 개선할 수 있습니다.

 

구성 요소

 

1. Model

데이터 구조와 비즈니스 로직을 포함합니다.

데이터 소스(예: 데이터베이스, API)와 상호작용합니다.

2. View

사용자 인터페이스를 담당합니다.

사용자 입력을 받습니다.

ViewModel에서 제공하는 데이터를 표시합니다.

3. ViewModel

View와 Model 간의 중개자 역할을 합니다.

Model에서 데이터를 가져와서 View에 전달합니다.

사용자 입력을 처리하고, Model을 업데이트합니다.

ChangeNotifier를 상속받아 데이터 변경 시 View에 알립니다.

 

MVVM 패턴의 장점

 

1. 유지보수성 향상: UI와 비즈니스 로직이 분리되어 코드를 더 쉽게 관리할 수 있습니다.

2. 재사용성: ViewModel과 Model은 특정 UI에 의존하지 않으므로 재사용이 가능합니다.

3. 테스트 용이성: UI와 독립적으로 ViewModel과 Model을 테스트할 수 있습니다.

4. 유연성: 다양한 플랫폼과 프레임워크에서 사용 가능합니다.

 

Flutter에서 MVVM 패턴 구현 예제

 

1. Model

 

Model 클래스는 데이터를 정의하고, 데이터와 상호작용하는 로직을 포함합니다.

class User {
  String username;
  String email;

  User({required this.username, required this.email});
}

2. ViewModel

 

ViewModel 클래스는 ChangeNotifier를 상속받아 상태 변화를 알립니다. View와 Model 간의 데이터 처리를 담당합니다.

import 'package:flutter/material.dart';
import 'model/user.dart';

class UserViewModel extends ChangeNotifier {
  User _user = User(username: '', email: '');

  String get username => _user.username;
  String get email => _user.email;

  void setUsername(String username) {
    _user.username = username;
    notifyListeners();
  }

  void setEmail(String email) {
    _user.email = email;
    notifyListeners();
  }
}

3. View

 

View 클래스는 사용자 인터페이스를 정의하고, ViewModel과 상호작용합니다.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'viewmodel/user_viewmodel.dart';

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MVVM Example')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              onChanged: (value) {
                context.read<UserViewModel>().setUsername(value);
              },
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              onChanged: (value) {
                context.read<UserViewModel>().setEmail(value);
              },
              decoration: InputDecoration(labelText: 'Email'),
            ),
            SizedBox(height: 20),
            Consumer<UserViewModel>(
              builder: (context, viewModel, child) {
                return Column(
                  children: [
                    Text('Username: ${viewModel.username}'),
                    Text('Email: ${viewModel.email}'),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => UserViewModel(),
      child: MaterialApp(home: MyHomePage()),
    ),
  );
}

요약

 

Model: 데이터 구조와 비즈니스 로직을 정의합니다.

View: 사용자 인터페이스를 담당하며, 사용자 입력을 처리합니다.

ViewModel: Model과 View 간의 중개자로서, 데이터를 처리하고 View에 전달합니다.


다른예시(실습)

 

파일목록
datasource 
- datasource.dart

repository

- album_datasource_repository.dart

viewModel

- album_view_model.dart

view

- album_mvvm_view.dart

 

datasource.dart

import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:flutter_lecture/model/album.dart';

class Datasource {

  Future<List<Album>> getAlbumList() async {
    final response = await http
        .get(Uri.parse("https://jsonplaceholder.typicode.com/albums"));

    return jsonDecode(response.body)
        .map<Album>((json) => Album.fromJson(json))
        .toList();
  }

}

 

album_datasource_repository.dart

import 'package:flutter_lecture/datasource/datasource.dart';

import '../model/album.dart';

class AlbumDatasourceRepository {
  final Datasource _datasource = Datasource();


  Future<List<Album>> getAlbumList() {
    return _datasource.getAlbumList();
  }
}

 

album_view_model.dart


import 'package:flutter/cupertino.dart';
import 'package:flutter_lecture/repository/album_datasource_repository.dart';

import '../model/album.dart';

class AlbumViewModel with ChangeNotifier {
  late final AlbumDatasourceRepository _albumRepository;
  List<Album> _albumList = List.empty(growable: true);
  List<Album> get albumList => _albumList;



  AlbumViewModel() {
    _albumRepository = AlbumDatasourceRepository();
    _getAlbumList();
  }

  Future<void> _getAlbumList() async{
    _albumList = await _albumRepository.getAlbumList();
    notifyListeners();
  }

}

 

album_mvvm_view.dart

import 'package:flutter/material.dart';
import 'package:flutter_lecture/model/albums.dart';
import 'package:flutter_lecture/bloc/album_bloc.dart';
import 'package:flutter_lecture/viewModel/album_view_model.dart';
import 'package:provider/provider.dart';

import '../model/album.dart';

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

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

class _MyHomePageState extends State<MyHomePage> {
  late List<Album> albumList;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<AlbumViewModel>(
        create: ((context) => AlbumViewModel()),
        child: Scaffold(
          appBar: AppBar(
            title: const Text("test title"),
          ),
          body: Consumer<AlbumViewModel>(
            builder: (context, provider, child) {
                albumList = provider.albumList;
                return ListView.builder(
                  itemCount: albumList.length,
                  itemBuilder: (context, index) {
                    return Container(
                      padding: const EdgeInsets.all(10),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text("ID: ${albumList[index].id.toString()}"),
                          Text("Title: ${albumList[index].title}" )
                        ],
                      ),
                    );
                  }
                );
              },
          ),
        ),
    );
  }
}

 

 

참고 자료

 

Flutter 공식 문서

MVVM 패턴

 

이 패턴을 사용하면 Flutter 애플리케이션의 구조를 더 명확하게 만들고, 유지보수와 테스트를 더 쉽게 할 수 있습니다.

반응형
반응형

Flutter에서 Form 데이터를 저장하고 성공 페이지로 이동하는 과정은 다음과 같습니다.

이를 위해 Form, TextFormField, Navigator 등을 사용합니다.

 

단계별 설명

 

1. Form 생성: 사용자로부터 입력을 받을 Form을 생성합니다.

2. Form 데이터 저장: 사용자가 입력한 데이터를 저장합니다.

3. 성공 페이지로 이동: 데이터가 성공적으로 저장되면 성공 페이지로 이동합니다.

 

전체 코드 예제

 

1. main.dart 파일

 

main.dart 파일에서는 초기 화면을 설정하고, 라우트를 정의합니다.

 

import 'package:flutter/material.dart';
import 'form_page.dart';
import 'success_page.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => MyHomePage(),
        '/success': (context) => SuccessPage(),
      },
    );
  }
}

2. form_page.dart 파일

 

form_page.dart 파일에서는 Form을 정의하고, 데이터를 저장한 후 성공 페이지로 이동합니다.

import 'package:flutter/material.dart';

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

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

class _MyHomePageState extends State<MyHomePage> {
  final _key = GlobalKey<FormState>();
  late String _username, _email;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Form Page"),
      ),
      body: Container(
        padding: const EdgeInsets.all(15),
        child: Form(
          key: _key,
          child: Column(
            children: [
              usernameInput(),
              const SizedBox(height: 15),
              emailInput(),
              const SizedBox(height: 15),
              submitButton()
            ],
          ),
        ),
      ),
    );
  }

  Widget usernameInput() {
    return TextFormField(
      autofocus: true,
      validator: (val) {
        if (val == null || val.isEmpty) {
          return 'The input is empty';
        } else {
          return null;
        }
      },
      onSaved: (username) => _username = username ?? '',
      decoration: const InputDecoration(
        border: OutlineInputBorder(),
        hintText: 'Input your username',
        labelText: 'username',
        labelStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget emailInput() {
    return TextFormField(
      autofocus: true,
      validator: (val) {
        if (val == null || val.isEmpty) {
          return 'The input is empty';
        } else {
          return null;
        }
      },
      onSaved: (email) => _email = email ?? '',
      decoration: const InputDecoration(
        border: OutlineInputBorder(),
        hintText: 'Input your email address',
        labelText: 'email address',
        labelStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget submitButton() {
    return ElevatedButton(
      onPressed: () {
        if (_key.currentState!.validate()) {
          _key.currentState!.save();
          Navigator.pushNamed(
            context,
            '/success',
            arguments: {'username': _username, 'email': _email},
          );
        }
      },
      child: Container(
        padding: const EdgeInsets.all(15),
        child: const Text('Submit'),
      ),
    );
  }
}

3. success_page.dart 파일

 

success_page.dart 파일에서는 성공적으로 데이터를 저장한 후 이동하는 페이지를 정의합니다.

import 'package:flutter/material.dart';

class SuccessPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Map<String, String> args =
        ModalRoute.of(context)!.settings.arguments as Map<String, String>;

    return Scaffold(
      appBar: AppBar(
        title: const Text("Success Page"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Username: ${args['username']}"),
            Text("Email: ${args['email']}"),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Back to Home'),
            ),
          ],
        ),
      ),
    );
  }
}

주요 단계 설명

 

1. Form 위젯 사용: Form 위젯을 사용하여 사용자 입력을 받습니다. GlobalKey<FormState>를 사용하여 Form의 상태를 관리합니다.

2. Validator 및 onSaved 사용: TextFormField 위젯의 validator 속성을 사용하여 입력 검증을 수행하고, onSaved 속성을 사용하여 데이터를 저장합니다.

3. Navigator 사용: Navigator.pushNamed를 사용하여 성공 페이지로 이동합니다. 이때 arguments를 사용하여 입력된 데이터를 성공 페이지로 전달합니다.

4. 성공 페이지에서 데이터 표시: 성공 페이지에서 ModalRoute.of(context)!.settings.arguments를 사용하여 전달된 데이터를 받아 표시합니다.

 

요약

 

이 과정은 Flutter에서 Form 데이터를 저장하고, 성공 페이지로 이동하는 일반적인 방법을 설명합니다. 이를 통해 사용자로부터 입력을 받고, 입력된 데이터를 다음 페이지로 전달하여 표시할 수 있습니다.

반응형

'프론트엔드 > Flutter' 카테고리의 다른 글

flutter scrollview pagination 예제 및 정리  (2) 2024.07.22
flutter mvvm 패턴 정리 및 예제  (0) 2024.07.22
flutter webview 사용방법  (0) 2024.07.22
flutter local notifications - 알림  (0) 2024.07.22
flutter get_it 패턴  (1) 2024.07.22
반응형

Flutter에서 WebView를 사용하는 방법을 정리하겠습니다.

WebView는 애플리케이션 내에서 웹 콘텐츠를 표시할 수 있게 해주는 위젯입니다. 이를 통해 Flutter 앱에서 웹 페이지를 렌더링할 수 있습니다.

 

1. WebView 패키지 추가

 

먼저 webview_flutter 패키지를 pubspec.yaml 파일에 추가합니다.

dependencies:
  webview_flutter: ^4.0.0  # 최신 버전 확인 필요

 

1.2 플랫폼별 설정

iOS (ios/Runner/Info.plist):

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

Android (android/app/build.gradle):

android {
    defaultConfig {
        minSdkVersion 19
    }
}

 

2. 기본 사용법

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

class WebViewExample extends StatefulWidget {
  @override
  _WebViewExampleState createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse('https://flutter.dev'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('WebView Example')),
      body: WebViewWidget(controller: controller),
    );
  }
}

3. 주요 기능

3.1 JavaScript 활성화/비활성화

controller.setJavaScriptMode(JavaScriptMode.unrestricted);

3.2 로딩 프로그레스 모니터링

NavigationDelegate(
  onProgress: (int progress) {
    print('WebView is loading (progress : $progress%)');
  },
)

3.3 페이지 로딩 이벤트

NavigationDelegate(
  onPageStarted: (String url) { print('Page started loading: $url'); },
  onPageFinished: (String url) { print('Page finished loading: $url'); },
)

3.4 에러 처리

NavigationDelegate(
  onWebResourceError: (WebResourceError error) {
    print('Error: ${error.description}');
  },
)

3.5 쿠키 관리

final cookieManager = WebViewCookieManager();
await cookieManager.setCookie(WebViewCookie(name: 'foo', value: 'bar', domain: 'example.com'));

4. 고급 기능

4.1 JavaScript과 Flutter 간 통신

controller.addJavaScriptChannel(
  'Toaster',
  onMessageReceived: (JavaScriptMessage message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message.message)),
    );
  },
);

4.2 사용자 지정 사용자 에이전트

controller.setUserAgent('My Custom User Agent');


NavigationDelegate 예시

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

class WebViewExample extends StatefulWidget {
  const WebViewExample({Key? key}) : super(key: key);

  @override
  State<WebViewExample> createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            // 웹 페이지 로딩 진행률 업데이트
            print('WebView is loading (progress : $progress%)');
          },
          onPageStarted: (String url) {
            // 페이지 로딩 시작
            print('Page started loading: $url');
          },
          onPageFinished: (String url) {
            // 페이지 로딩 완료
            print('Page finished loading: $url');
          },
          onWebResourceError: (WebResourceError error) {
            // 웹 리소스 로딩 중 오류 발생
            print('Error: ${error.description}');
          },
          onNavigationRequest: (NavigationRequest request) {
            // 특정 도메인으로의 이동을 막고 싶을 때 사용
            if (request.url.startsWith('https://www.youtube.com/')) {
              print('Blocking navigation to ${request.url}');
              return NavigationDecision.prevent;
            }
            print('Allowing navigation to ${request.url}');
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse('https://flutter.dev'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WebView Example'),
      ),
      body: WebViewWidget(controller: controller),
    );
  }
}

 

NavigationDelegate는 WebViewController를 설정할 때 setNavigationDelegate 메서드를 통해 설정합니다. 일반적으로 WebViewController를 초기화하는 부분에 넣습니다. 주로 initState 메서드 내에서 이루어집니다.

다음은 NavigationDelegate를 올바르게 설정하는 방법을 보여주는 예제입니다:

 
WebView NavigationDelegate 예제
Click to open code

이 예제에서 주목해야 할 점들은 다음과 같습니다:

  1. NavigationDelegate는 WebViewController의 setNavigationDelegate 메서드를 통해 설정됩니다.
  2. initState 메서드 내에서 WebViewController를 초기화하고 설정합니다.
  3. NavigationDelegate는 여러 콜백 함수를 포함할 수 있습니다:
    • onProgress: 페이지 로딩 진행률을 추적합니다.
    • onPageStarted: 페이지 로딩이 시작될 때 호출됩니다.
    • onPageFinished: 페이지 로딩이 완료될 때 호출됩니다.
    • onWebResourceError: 웹 리소스 로딩 중 오류가 발생했을 때 호출됩니다.
    • onNavigationRequest: 새로운 URL로 이동하기 전에 호출됩니다. 여기서 특정 URL로의 이동을 차단할 수 있습니다.
  4. 각 콜백 함수 내에서 원하는 로직을 구현할 수 있습니다. 예를 들어, 로딩 진행률을 UI에 표시하거나, 특정 URL로의 이동을 차단하는 등의 작업을 수행할 수 있습니다.

이렇게 설정된 NavigationDelegate는 WebView의 다양한 이벤트를 처리하고 사용자 경험을 향상시키는 데 도움이 됩니다. 특정 요구사항에 따라 각 콜백 함수의 내용을 커스터마이즈할 수 있습니다.



5. 보안 고려사항

  • HTTPS 사용을 권장합니다.
  • 신뢰할 수 있는 웹사이트만 로드하세요.
  • 필요한 경우에만 JavaScript를 활성화하세요.

6. 성능 최적화

  • 웹뷰는 네이티브 위젯보다 성능이 떨어질 수 있으므로 필요한 경우에만 사용하세요.
  • 복잡한 웹 콘텐츠를 로드할 때는 로딩 인디케이터를 표시하세요.

7. 사용자 경험 개선

  • 뒤로 가기 기능 구현
  • 새로고침 버튼 추가
  • 로딩 진행률 표시

8. 플랫폼별 고려사항

  • iOS와 Android에서 웹뷰의 동작이 약간 다를 수 있으므로 두 플랫폼에서 모두 테스트하세요.
  • 플랫폼별 특정 기능이 필요한 경우 조건부 코드를 사용하세요.
반응형

'프론트엔드 > Flutter' 카테고리의 다른 글

flutter mvvm 패턴 정리 및 예제  (0) 2024.07.22
flutter form 제출 및 라우터 이동  (0) 2024.07.22
flutter local notifications - 알림  (0) 2024.07.22
flutter get_it 패턴  (1) 2024.07.22
flutter provider 패턴  (1) 2024.07.22
반응형

Flutter에서 로컬 알림(Local Notifications)을 사용하면 앱이 백그라운드에 있거나 사용자가 앱을 사용하고 있지 않은 경우에도 사용자에게 알림을 보낼 수 있습니다. 이를 위해 flutter_local_notifications 패키지를 사용할 수 있습니다. 이 패키지는 Android와 iOS 모두에서 로컬 알림을 지원합니다.

 

1. 패키지 설치

 

먼저 pubspec.yaml 파일에 flutter_local_notifications 패키지를 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^12.0.3

그런 다음 flutter pub get 명령을 실행하여 패키지를 설치합니다.

 

2. Android 설정

 

Android에서 로컬 알림을 사용하려면 AndroidManifest.xml 파일에 필요한 권한과 리시버(receiver)를 추가해야 합니다.

 

android/app/src/main/AndroidManifest.xml 파일을 다음과 같이 수정합니다:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.your_app">

    <!-- 알림 관련 권한 -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
        android:label="flutter_lecture"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <!-- 알림 관련 서비스 -->
        <receiver
            android:name="com.dexterous.flutterlocalnotifications.receivers.NotificationBroadcastReceiver"
            android:exported="true"/>
        <receiver
            android:name="com.dexterous.flutterlocalnotifications.receivers.ActionReceiver"
            android:exported="true"/>
        <receiver
            android:name="com.dexterous.flutterlocalnotifications.receivers.DismissedReceiver"
            android:exported="true"/>
        <receiver
            android:name="com.dexterous.flutterlocalnotifications.receivers.ScheduledNotificationBootReceiver"
            android:permission="android.permission.RECEIVE_BOOT_COMPLETED"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
            </intent-filter>
        </receiver>

        <!-- 기타 설정들 -->
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize"
            android:showWhenLocked="true"
            android:turnScreenOn="true">
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"/>
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2"/>
    </application>

    <!-- 패키지 가시성 설정 -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

3. iOS 설정

 

iOS에서 로컬 알림을 사용하려면 추가적인 설정이 필요합니다.

 

ios/Runner/Info.plist 파일을 다음과 같이 수정합니다:

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>remote-notification</string>
</array>

4. FlutterLocalNotification 클래스

 

Flutter에서 로컬 알림을 관리할 클래스를 정의합니다. 이 클래스는 알림을 초기화하고, 알림 권한을 요청하며, 알림을 보내는 기능을 포함합니다.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class FlutterLocalNotification {
  FlutterLocalNotification._();

  static final FlutterLocalNotificationsPlugin flutterLocalNotificationPlugin = FlutterLocalNotificationsPlugin();

  static Future<void> init() async {
    const AndroidInitializationSettings androidInitializationSettings =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    const DarwinInitializationSettings iosInitializationSettings =
        DarwinInitializationSettings(
            requestAlertPermission: false,
            requestBadgePermission: false,
            requestSoundPermission: false
        );

    final InitializationSettings initializationSettings = InitializationSettings(
      android: androidInitializationSettings,
      iOS: iosInitializationSettings,
    );

    await flutterLocalNotificationPlugin.initialize(
      initializationSettings,
      onSelectNotification: (String? payload) async {
        // 알림을 클릭했을 때의 동작 정의
      },
    );
  }

  static void requestNotificationPermission() {
    flutterLocalNotificationPlugin
        .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(alert: true, badge: true, sound: true);
  }

  static Future<void> showNotification() async {
    const AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
            'channel id',
            'channel name',
            channelDescription: 'channel description',
            importance: Importance.max,
            priority: Priority.high,
            showWhen: false
        );

    const NotificationDetails notificationDetails = NotificationDetails(
      android: androidNotificationDetails,
      iOS: DarwinNotificationDetails(badgeNumber: 1),
    );

    await flutterLocalNotificationPlugin.show(
      0,
      'test title',
      'test body',
      notificationDetails,
    );
  }
}

 

5. main.dart 파일

 

앱을 초기화하고 로컬 알림을 설정하는 코드를 추가합니다.

import 'package:flutter/material.dart';
import 'package:flutter_lecture/notification.dart'; // 경로를 실제 경로로 변경하세요.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterLocalNotification.init();  // 플러그인 초기화
  runApp(MyApp());
}

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

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

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    initializeNotifications();
  }

  Future<void> initializeNotifications() async {
    await FlutterLocalNotification.init();
    Future.delayed(
      const Duration(seconds: 3),
      () => FlutterLocalNotification.requestNotificationPermission()
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("알림 목록"),
      ),
      body: Center(
        child: TextButton(
          onPressed: () => FlutterLocalNotification.showNotification(),
          child: const Text("알림보내기")
        )
      ),
    );
  }
}

6. 프로젝트 정리 및 빌드

 

마지막으로, 프로젝트를 정리하고 다시 빌드합니다.

flutter clean
flutter pub get
flutter run

 

반응형

'프론트엔드 > Flutter' 카테고리의 다른 글

flutter form 제출 및 라우터 이동  (0) 2024.07.22
flutter webview 사용방법  (0) 2024.07.22
flutter get_it 패턴  (1) 2024.07.22
flutter provider 패턴  (1) 2024.07.22
flutter bloc 패턴 적용  (0) 2024.07.22

+ Recent posts