Yejun Cheon
Yejun Cheon
log
study
read
coffee
로그인
배운 것을 정리합니다.
🛕

Flutter 오답노트

예
예준천
카테고리
  1. flutter

ListTile의 Leading이 가운데 정렬되지 않는 문제

원하는거
현재상태
Vertically centering ListTile's leading Widget
This is just a short tip for something really simple. For some reason when you add a leading Widget to a ListTile in Flutter, an Icon for example, it doesn't seem to center vertically by default.
developermemos.com
이거로 해결되는줄 알았으나…
결론
In Flutter's ListTile, the leading and trailing doesn't span accross the full height of the ListTile when subtitle is used. Hence create your own Widget using row.
CustomListTile 위젯을 만들거나 Row로 해결하는 것이 바람직함.

flutter StatefulWidget의 LifeCycle

createState

State를 생성하고 StatefulWidget에서 state를 실행합니다.

initState

State를 초기화합니다. 단, initState는 StatefulWidget이 실행되면 단 한번 동작합니다. initState 내부를 수정하고 싶다면 rebuild, hot-reload를 해도 변화가 없으므로 반드시 종료 후 실행시켜야 합니다.
또한 반드시 super.initState()를 호출해야 합니다.

didChangeDependencies

•
위젯이 최초 생성될때 initState 다음에 호출됩니다.
•
위젯이 의존하는 객체가 호출될 때마다 실행됩니다.

build

•
화면의 Widget을 그립니다. 반드시 Widget을 리턴해야 합니다.
•
UI위젯을 랜더링 할 때 마다 호출됩니다.

didUpdateWidget

•
부모 위젯이 변경되어서 이 위젯 갱신이 필요할때 호출됩니다.

deactivate

•
거의 사용되지 않지만 tree에서 State가 제거될 때 호출됩니다.

dispose

•
화면이 종료될 때 호출되며 상태도 제거됩니다.
•
일반적으로 controller를 종료할 때 사용하기도 합니다.

Permission Granted

permission_handler | Flutter package
Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
pub.dev
dependencies:
  permission_handler: ^11.3.1
<key>NSCameraUsageDescription</key>
<string>이 앱은 카메라 접근 권한이 필요합니다.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>이 앱은 위치 정보를 사용하기 위해 권한이 필요합니다.</string>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

class PermissionExample extends StatelessWidget {
  Future<void> requestPermission(Permission permission) async {
    // 권한 상태 확인
    var status = await permission.status;

    if (status.isGranted) {
      print("${permission.toString()} 권한이 이미 허용되었습니다.");
    } else if (status.isDenied || status.isRestricted || status.isLimited) {
      // 권한 요청
      var result = await permission.request();
      if (result.isGranted) {
        print("${permission.toString()} 권한이 허용되었습니다.");
      } else {
        print("${permission.toString()} 권한이 거부되었습니다.");
      }
    } else if (status.isPermanentlyDenied) {
      // 사용자가 영구적으로 거부한 경우 설정 화면으로 이동
      print("${permission.toString()} 권한이 영구적으로 거부되었습니다.");
      openAppSettings();
    }
  }
  Future<bool> 

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Permission Example')),
        body: Center(
          child: ElevatedButton(
            onPressed: () => requestPermission(Permission.camera),
            child: Text('카메라 권한 요청'),
          ),
        ),
      ),
    );
  }
}

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

Provider 의 필요성과 사용법

vue의 props drilling 문제와 동일. 결국 값이 변화를 감지해서 화면을 다시 그려야하거나 값의 재평가가 필요할 때 사용한다.
Provider.of<타입>(context) ⇒ 주어진 컨텍스트를 거슬러 올라가면서 가장 가까이에 있는 원하는 타입의 인스턴스를 찾아서 반환한다.
변경감지는 어떻게 하나?
ChangeNotifier → 변경을 구독한다. vue의 watch 와 비슷한듯.
class FishModel with ChangeNotifier{

}
changeNotifier의 단점.
변경감지를 원하는 애를 addListner에 등록해야함.
removeListner도 해야함.
UI 리빌드도 알아서 해야함.
UI를 리빌드(rebuild) 해 줄 수 있는 ChangeNotifierProvider와 MultiProvider(멀티 프로바이더) 사용!
1. Provider
•특징
•Flutter에서 상태 관리의 기본 패키지.
•의존성 주입과 상태 관리 용도로 사용됨.
•BuildContext를 통해 데이터를 손쉽게 상위 위젯에서 하위 위젯으로 전달.
•원리
•위젯 트리에서 데이터를 공유하도록 하는 InheritedWidget 기반.
•상태가 변경될 때 하위 위젯 트리 전체를 다시 빌드하지 않고, 필요한 부분만 리빌드.
2. ChangeNotifier
•특징
•Provider 패키지에서 상태 관리 목적으로 많이 사용됨.
•notifyListeners()를 호출하여 상태 변화 알림.
•가벼운 상태 관리에 적합.
•원리
•ChangeNotifier는 Listenable을 구현하여 상태 변경 이벤트를 리스너들에게 전달.
3. ChangeNotifierProvider
•특징
•ChangeNotifier를 사용하여 상태를 관리할 수 있도록 하는 Provider.
•위젯 트리에서 ChangeNotifier 객체를 생성하고 제공.
•상태 변경 시 리빌드가 필요한 위젯들만 업데이트.
•원리
•ChangeNotifierProvider는 ChangeNotifier 객체를 생성해 위젯 트리에서 사용할 수 있도록 제공.
4. MultiProvider
•특징
•여러 개의 Provider를 한 번에 제공.
•코드 중복을 줄이고, Provider를 관리하기 쉽게 함.
•원리
•MultiProvider는 여러 Provider를 리스트로 받아 위젯 트리에서 사용 가능하도록 설정.

sizedbox vs container

공간만 차지 하기 위한? ⇒ SizedBox
배경색과 같은 데코리이션 지정 해야하는 ⇒ Container 단, 더 리소스를 많이 잡아먹음

TextEditingController

TextField 위젯에는 controller라는게 있는데, 이게 왜 필요한걸까?
1.텍스트 읽기 및 설정
TextEditingController controller = TextEditingController();

// 텍스트 읽기
String text = controller.text;

// 텍스트 설정
controller.text = "Hello, Flutter!";
2.커서 위치 관리
controller.selection = TextSelection(
  baseOffset: 0, 
  extentOffset: controller.text.length,
); // 텍스트 전체 선택
3.텍스트 변경 감지
controller.addListener(() {
  print("텍스트가 변경되었습니다: ${controller.text}");
});
4.초기값 설정
이런게 된다고 하네요~~. 이름이 컨트롤러라 헷갈렸음.

스플래시 화면

1.
진입화면으로서의 역할(대표 이미지 또는 로딩 바)
2.
어플 필수 권한받기
3.
유저 검증(기존 로그인 정보)
class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();
    _permissionHandler();
    _navigateToNextScreen();
  }

  void _permissionHandler() async {
    if (await Permission.notification.isDenied) {
      await Permission.notification.request();
    }
  }

  void _navigateToNextScreen() async {
    await Future.delayed(const Duration(seconds: 1));
    User? user = FirebaseAuth.instance.currentUser;
    if (user == null) {
      Navigator.pushReplacementNamed(context, '/sign_in_screen');
      return;
    }
    Navigator.pushReplacementNamed(context, '/main_screen');
  }
}
•
Permission.notification.request() 으로 간단하게 권한 요청 팝업 띄우기
•
유저 정보 유무에 따라 다음 화면 분기.

FirebaseAuth.instance.currentUser가 이전 로그인 정보를 유지하는 방법

1.
영구 저장소 사용:
•
Firebase Auth는 iOS의 Keychain과 Android의 SharedPreferences를 사용하여 인증 토큰을 안전하게 저장합니다
•
앱을 종료하고 다시 실행해도 이 토큰이 유효하면 자동으로 로그인 상태가 유지됩니다
1.
토큰 관리:
// Firebase Auth는 내부적으로 이런 방식으로 동작합니다
class FirebaseAuth {
  // 액세스 토큰과 리프레시 토큰을 관리
  String? _accessToken;
  String? _refreshToken;
  
  User? get currentUser {
    // 저장된 토큰이 유효한지 확인
    if (_accessToken != null && !_isTokenExpired()) {
      return _getUserFromToken(_accessToken!);
    }
    // 리프레시 토큰으로 새로운 액세스 토큰 발급
    else if (_refreshToken != null) {
      _refreshAccessToken();
      return _getUserFromToken(_accessToken!);
    }
    return null;
  }
}
2.
token 갱신
•
액세스 토큰이 만료되면 리프레시 토큰을 사용해 자동으로 새로운 액세스 토큰을 발급받습니다
•
리프레시 토큰도 만료되면 사용자는 다시 로그인해야 합니다
1.
로그아웃 처리
await FirebaseAuth.instance.signOut();
1.
보안:
•
토큰들은 암호화되어 저장됩니다
•
iOS의 Keychain과 Android의 암호화된 SharedPreferences를 사용하여 안전하게 보관됩니다
이러한 메커니즘으로 인해:
•
앱을 다시 실행해도 자동 로그인이 가능합니다
•
토큰이 만료되면 자동으로 갱신됩니다
•
보안적으로 안전한 방식으로 인증 상태가 유지됩니다

RiverPod

Provider : 불변 전역 변수

final nameProvider = Provider<String>(
  (ref){ return //something };
);
변화 감지 기능 x. just 전역에 뿌리기용도.

StateProvider : 원시 타입 전역 상태관리

final countProvider = StateProvider<int>((ref) => return 0;)

ChangeNotifier : class 를 전역상태관리하기(가변 객체)

import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends ChangeNotifier {
  int count;
  CounterNotifier({this.count = 0}) : super();

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

  void decrement() {
    count--;
    notifyListeners();
  }
}

final counterProvider = ChangeNotifierProvider<CounterNotifier>(
  (ref) => CounterNotifier(),
);
1.
ChangeNotifier를 전역 상태관리 하고 싶은 객체에 상속시킨다.
2.
변경이 일어나는 메서드에서는 구독하고 있는 watch 들에게 알려주기 위해서 notifyListner를 호출한다.
3.
provider를 하나 만들어 놓는다. 어디든 상관없다.
class _MyHomePageState extends ConsumerState<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
         Text(
            ref.watch(counterProvider).count.toString(),
            style: Theme.of(context).textTheme.headlineMedium,
         ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
1.
ConsumerState, ConsumerStatefulWidget, ConsumerWidget을 상속받는다.
2.
ref.watch( provider ) 로 변경 추적 읽기. ref.read( provider ) 로 단순 읽기.
3.
ref.read( provider.notifier ) 로 변경시키기.

StateNotifier : class 를 전역 상태관리(불변 객체)

import 'package:flutter_riverpod/flutter_riverpod.dart';

class Counter {
  final int count;
  Counter({this.count = 0});

  Counter copyWith({int? count}) {
    return Counter(
      count: count ?? this.count,
    );
  }
}

class CounterNotifier extends StateNotifier<Counter> {
  CounterNotifier(super.state);
  void increment() {
    state = state.copyWith(count: state.count + 1);
  }

  void decrement() {
    state = state.copyWith(count: state.count - 1);
  }
}

final counterProvier = StateNotifierProvider<CounterNotifier, Counter>(
  (ref) => CounterNotifier(Counter()),
);
1.
원래 사용하던 클래스
2.
StateNotifier 상속받기. 변경은 여기서만 이루어지기
3.
StateNotifierProvider로 provider 만들어두기.
vue에서 ref는 객체의 깊은 변화를 탐지해주지만 간혹 헷갈리는 부분이 꽤나 있었다.
가변 객체에서는changeNotifier에서는 변화가 일어나는 곳에서 명시적으로 notifyListner를 호출하고,
불변객체는 내부 변화가 일어날일이 없고, 참조하는 객체가 변하니까 변화 탐지가 용이한것 같다.

FutureProvider : 전역 비동기 상태관리

final fetchUserProvider = FutureProvider<User>((ref){
	const url = '';
	return http.get().then(value => User.fromJson(value.body));
})
@override
  Widget build(BuildContext context, WidgetRef ref) {
    var user = ref.watch(fetchUserData);
    // 정확히는 riverpod 전용 타입 AsyncValue<User> 타입임.
    return Scaffold(
      appBar: AppBar(
	      actions: [
	        IconButton(
	          icon: Icon(Icons.refresh),
	          onPressed: () {
	            // fetchUserData 갱신
	            ref.refresh(fetchUserData);
	          },
	        )
	      ],
	    ),
      body:user.when(
      data:(data)=>Column(children: [
        Text(data.name.toString()),          
        Text(data.email.toString()),
        ],), 
      error: (error,stackTrace)=>Center(child:Text(error.toString())), 
      loading:() =>CircularProgressIndicator()),
      
    );
  }
}
ref.refresh 로 데이터 갱신 가능.
ref.watch(provider).when으로 진행상태에 따른 분기처리 가능.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

// User 모델 정의
class User {
  final String name;
  final String email;

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

  factory User.fromJson(String jsonString) {
    final jsonData = json.decode(jsonString);
    return User(
      name: jsonData['name'],
      email: jsonData['email'],
    );
  }
}

// userId를 관리하는 StateProvider
final userIdProvider = StateProvider<int>((ref) => 1);

// fetchUserData를 정의하는 FutureProvider.family
final fetchUserData = FutureProvider.family<User, int>((ref, userId) {
  final url = 'https://jsonplaceholder.typicode.com/users/$userId';
  return http.get(Uri.parse(url)).then((response) {
    if (response.statusCode == 200) {
      return User.fromJson(response.body);
    } else {
      throw Exception('Failed to load user');
    }
  });
});

// 메인 위젯
void main() {
  runApp(ProviderScope(child: MyApp()));
}

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

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // StateProvider로부터 현재 userId를 가져오기
    final userId = ref.watch(userIdProvider);
    // userId에 따라 fetchUserData 호출
    final user = ref.watch(fetchUserData(userId));

    return Scaffold(
      appBar: AppBar(
        title: Text('User Info'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 비동기 상태 처리
          user.when(
            data: (data) => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Name: ${data.name}'),
                Text('Email: ${data.email}'),
              ],
            ),
            error: (error, stackTrace) =>
                Center(child: Text('Error: $error')),
            loading: () => Center(child: CircularProgressIndicator()),
          ),
          const SizedBox(height: 20),
          // ID 증가 버튼
          ElevatedButton(
            onPressed: () {
              // userId 증가
              ref.read(userIdProvider.notifier).state++;
            },
            child: Text('Next User'),
          ),
        ],
      ),
    );
  }
}

Flutter Hooks

HookWidget을 상속받고, useState(), useEffect()처럼 쓰면 됨.
customHook을 만들어서 useXXX도 만들 수 있음.

useState

shallow comparison 임. list 요소 추가나 객체 내부 속성 변경 같은 깊은 감지는 할 수 없음.
final a = useState( initial value )
a.value++;

useEffect

1.
Initializing data and dispose data
useEffect((){
	// initializing code
	return () {
		//dispose code
	}
},[])
1.
react to dependency change
useEffect((){
	// 변경될 때마다 새로 실행될 로직
	return () {
		//dispose code
	}
},[변경 감지할 dependency])

Widget : ListView

1.
The default constructor takes an explicit List<Widget> of children. This constructor is appropriate for list views with a small number of children because constructing the List requires doing work for every child that could possibly be displayed in the list view instead of just those children that are actually visible.
2.
The ListView.builder constructor takes an IndexedWidgetBuilder, which builds the children on demand. This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.
3.
The ListView.separated constructor takes two IndexedWidgetBuilders: itemBuilder builds child items on demand, and separatorBuilder similarly builds separator children which appear in between the child items. This constructor is appropriate for list views with a fixed number of children.
4.
The ListView.custom constructor takes a SliverChildDelegate, which provides the ability to customize additional aspects of the child model. For example, a SliverChildDelegate can control the algorithm used to estimate the size of children that are not actually visible.
이 중 ListView.builder는 한번에 렌더링이 아니라 필요시 on demand 로 아이템을 그려낸다.
ListView.builder(
	itemCount: list.length,
	itemBuilder: (ctx,index) => Text(list[index]),
)

펼쳐진 프로젝트를 구조화 하기

Setstate

StatefulWidget의 상태를 변경한 후, 해당 위젯 트리의 빌드 메서드를 다시 호출함.
그래서 아무 상태를 안바꿔도 위젯트리를 리빌드 하는 효과를 가짐.
상태변경만 여기서 할 것. 불필요한 setState 는 성능문제 발생시킴.

stream

•**스트림(Stream)**은 비동기 데이터를 전달하는 파이프라인입니다.
•데이터를 한 번에 하나씩 전달하며, 데이터가 도착하면 이를 처리하는 리스너가 반응합니다.
•ViewModel -> View로 데이터(이벤트)를 전달할 때, 상태를 관리하지 않고도 일회성 이벤트를 처리할 수 있습니다.
      StreamBuilder<bool>(
        stream: viewModel.deleteResultStream,
        builder: (context, snapshot) {

MVVM 적용기

Stateful widget 으로

class ListPageState extends State<ListPage> {
  ListViewModel vm = ListViewModel();
 
  @override
  void initState() {
    super.initState();
    // 뷰모델을 listen => onUpdate 를 호출하면 아래 build를 다시 호출하는 역할을 하게됨.
    vm.onUpdated = () => setState(() {}); 
    vm.onAlert = (msg) => context.showAlert(msg);
  }
 
  @override
  Widget build(BuildContext context) {
    return Frame(
      layer: vm.loading ? LoadingView() : null, // View ← ViewModel.State
      children: [
        // -------- UI counter --------
        CounterButton(
          text: Text('CLICK COUNT : ${vm.clickCount}'), // View ← ViewModel.State
          onTapCount: () => vm.addClickCount(), // 뷰모델의 액션 실행
          onTapOpen: () => context.pushCountView(),
        ),
 
        // -------- UI remove button --------
        RemoveButton(
          text: Text('REMOVE : ${vm.list.checkedCount}'), // View ← ViewModel.State
          onPressed: () => vm.removeChecked(), // 뷰모델의 액션 실행
        ),
 
        // -------- UI list --------
        ArticleListView(
          layer: vm.listLoading ? ListLoadingView() : null,
          itemBuilder: (context, index) {
            return ListItem(
              article: vm.list[index], // View ← ViewModel.State
              onTap: () => vm.toggleCheck(vm.list[index]), // 뷰모델의 액션 실행
            );
          },
        ),
      ],
    ); // Frame
  }
}
여기서 Stateful widget 을 사용하는 이유는 상태가 변화하는 viewModel 을 감지하기 위한것이다.
데이터를 stateful widget 에서 관리하는게 아니다! 데이터 관리는 viewModel 에서, 그리고 변화된 상태를 반영하기 위해 listView 에서 statefulwidget의 setState 메서드를 활용하는 것이다.
→ 단점 : 전체 리빌드 된다.
→ 해결 : 위젯을 세분화하고, 콜백함수를 나누고, 파라미터도 늘리면 된다.
→ 단점 : 너무 복잡함.

Get X

class ListViewModel extends GetxController {
 
  // 필드 하나 하나를 Rx로 선언해 Obx 위젯이 변경 내역을 통보받을 수 있게 함
  RxInt clickCount = 0.obs;
  RxBool listLoading = false.obs;
  Rx<List<Article>> list = [].obs;
  ...
 
  addClickCount() {
    clickCount.value = clickCount.value + 1;
  }
  ...
→ GetxController를 상속받고, Rx 로 래핑된 타입으로 ViewModel 내부의 필드들을 정의
→ 필드에 접근시에 .vlaue로 접근하여 데이터 조회 및 수정.
  final ListController controller = Get.put(ListController()); // Dependency Injection
      
  ...
  
  return Obx(() {
    if (controller.isLoading.value) {
      return Center(child: CircularProgressIndicator());
    }

    return Column(
      children: [
        // Counter Section
        CounterView(
          clickCount: controller.clickCount.value,
→ Obx로 감싸진 스코프 내에서 controller(우리한테는 viewModel)에 접근하면 됨.
→ 값이 바뀌어도 그 부분만 업데이트 됨.
→ 사용하는 클래스는 statelessWidget이어도 됨.
함수설명
Get.put()객체를 즉시 등록 및 싱글톤으로 관리.
Get.lazyPut()객체를 지연 등록. 사용될 때 인스턴스 생성.
Get.find<T>()등록된 객체를 검색 및 반환.
Get.delete()등록된 객체를 삭제.
Get.putAsync()비동기 작업으로 객체를 생성 및 등록.
→ Get.put으로 등록하고 ( 보통 루트에서 ), Get.find<가져올 컨트롤러 타입>()으로 가져오기가 일반적인듯.
Provider나 BLoC 와 다르게 BuildContext 를 알고 있지 않아도 됨.
→ 편하긴 한데 나름대로 단점이 있음.
context를 활용한 의존성 관리 및 생명 주기 관리가 힘듦.

Provider

// 뷰모델은 ChangeNotifier를 상속받아야 함
class ListViewModel extends ChangeNotifier {
  int clickCount = 0;
  ...
 
  addClickCount() {
    clickCount += 1;
    notifyListeners(); // 변경 내역을 뷰에 통보하기 위해 notifyListeners() 호출
  }
  ...
→ 뷰모델은 ChangeNotifier를 상속받고 상태를 변화시키면 notifyListners() 메서드를 호출
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => ArticleViewModel()),
      ],
      child: MaterialApp(
        home: ArticleListView(),
      ),
    );
→ ChangeNotifierProvider 로 감싸서 생성한 뷰 모델을 주입해줌. 이때 context 도 의존성을 주입함.
class ArticleListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<ArticleViewModel>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('MVVM with Provider'),
      ),
      body: viewModel.isLoading
          ? Center(child: CircularProgressIndicator())
          : Column(
              children: [
                // Checked count
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    'Selected Articles: ${viewModel.checkedCount}',
→ Provider에서 원하는 타입을 꺼내다가 잘 쓰면 됨.
아직 이해 덜된 부분
Service를 provider에 주입해서 하위 위젯들이 이러쿵 저러쿵 쓰게 만드는 사용방법
→ 단점 : 뷰모델 스트림만 리슨 할수 있어 일회성 이벤트 지원이 되지 않음.
ex. 내가 이런거까지 상태관리 해야함??
class ListViewModel extends ChangeNotifier {
  bool deleteSuccess = false;

  Future<void> removeChecked() async {
    deleteSuccess = true; // 삭제 성공
    notifyListeners();
  }
}
이 상태에서 view 에서 deletSuccess 상태를 구독하고 있다고 가정하면, 만약 다른 이유로 리빌드가 일어나면 원치 않는 이유로 중복으로 모달이 뜰 수 있다.
→ 이벤트를 트리거한 후 상태를 초기화해야 하므로 코드가 복잡해지고, View가 상태를 계속 구독하면, 같은 상태로 인해 이벤트가 반복 실행될 수 있음.
→ 대안 중에 viewModel에서 stream 으로 가지고 있고, view에서 stream 읽는 방법 있긴 한데 복잡함. (스트림(Stream)을 활용한 방식은 같은 프론트엔드 내부에서 ViewModel ↔ View 간 통신을 비동기 방식으로 처리하는 방법입니다. 이는 마치 프론트엔드와 백엔드 간 비동기 통신처럼, 신호(이벤트)를 한 번 보내고 결과를 수신하는 구조로 동작합니다.)
→ 그래서 MVVM 깨고 이렇게 사용
class _View extends StatelessWidget {
  ...
  
  // 현재 선택한 List 항목을 삭제하고 결과를 경고 창으로 보여줌
  remove(BuildContext context) async {
    final result = await context.listVm.removeChecked(); // 규칙 위반, 뷰에서 뷰모델 데이터가 아닌 함수 호출 결과 사용
    if (!mounted) return;
    if (result) {
      context.showAlert('SUCCESS');
    } else {
      context.showAlert('ERROR');
    }
  }

BLoC

→ 1개의 Cubit(viewModel)에는 1개의 상태만을 가질 수 있음.
→ 서로 다른 상태의 와리가리가 필요하면 RepositoryProvider로 묶음.
return RepositoryProvider(
  create: (context) => LocalService(),
  child: HomePage(),
  
  
// 상태 클래스
class ClickCountState {
  final int count;

  ClickCountState(this.count);
}

// Cubit 클래스
class ClickCountCubit extends Cubit<ClickCountState> {
  // 초기 상태를 0으로 설정
  ClickCountCubit() : super(ClickCountState(0));

  // 클릭 횟수를 증가시키는 메서드
  void addClickCount() {
    final newCount = state.count + 1; // 현재 상태의 count 값에 1을 추가
    emit(ClickCountState(newCount)); // 새로운 상태를 방출
  }
}


home: BlocProvider(
  create: (context) => ClickCountCubit(),
  child: HomePage(),
),

...
BlocBuilder<ClickCountCubit, ClickCountState>(
  builder: (context, state) {
    return Text(
      '${state.count}', // 현재 상태에서 count 값 표시
      style: TextStyle(fontSize: 40),
    );
  },
),

RiverPod

Provider : 불변 전역 변수

final nameProvider = Provider<String>(
  (ref){ return //something };
);
변화 감지 기능 x. just 전역에 뿌리기용도.

StateProvider : 원시 타입 전역 상태관리

final countProvider = StateProvider<int>((ref) => return 0;)

ChangeNotifier : class 를 전역상태관리하기(가변 객체)

import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends ChangeNotifier {
  int count;
  CounterNotifier({this.count = 0}) : super();

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

  void decrement() {
    count--;
    notifyListeners();
  }
}

final counterProvider = ChangeNotifierProvider<CounterNotifier>(
  (ref) => CounterNotifier(),
);
1.
ChangeNotifier를 전역 상태관리 하고 싶은 객체에 상속시킨다.
2.
변경이 일어나는 메서드에서는 구독하고 있는 watch 들에게 알려주기 위해서 notifyListner를 호출한다.
3.
provider를 하나 만들어 놓는다. 어디든 상관없다.
class _MyHomePageState extends ConsumerState<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
         Text(
            ref.watch(counterProvider).count.toString(),
            style: Theme.of(context).textTheme.headlineMedium,
         ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
1.
ConsumerState, ConsumerStatefulWidget, ConsumerWidget을 상속받는다.
2.
ref.watch( provider ) 로 변경 추적 읽기. ref.read( provider ) 로 단순 읽기.
3.
ref.read( provider.notifier ) 로 변경시키기.

StateNotifier : class 를 전역 상태관리(불변 객체)

import 'package:flutter_riverpod/flutter_riverpod.dart';

class Counter {
  final int count;
  Counter({this.count = 0});

  Counter copyWith({int? count}) {
    return Counter(
      count: count ?? this.count,
    );
  }
}

class CounterNotifier extends StateNotifier<Counter> {
  CounterNotifier(super.state);
  void increment() {
    state = state.copyWith(count: state.count + 1);
  }

  void decrement() {
    state = state.copyWith(count: state.count - 1);
  }
}

final counterProvier = StateNotifierProvider<CounterNotifier, Counter>(
  (ref) => CounterNotifier(Counter()),
);
1.
원래 사용하던 클래스
2.
StateNotifier 상속받기. 변경은 여기서만 이루어지기
3.
StateNotifierProvider로 provider 만들어두기.
vue에서 ref는 객체의 깊은 변화를 탐지해주지만 간혹 헷갈리는 부분이 꽤나 있었다.
가변 객체에서는changeNotifier에서는 변화가 일어나는 곳에서 명시적으로 notifyListner를 호출하고,
불변객체는 내부 변화가 일어날일이 없고, 참조하는 객체가 변하니까 변화 탐지가 용이한것 같다.

FutureProvider : 전역 비동기 상태관리

final fetchUserProvider = FutureProvider<User>((ref){
	const url = '';
	return http.get().then(value => User.fromJson(value.body));
})
@override
  Widget build(BuildContext context, WidgetRef ref) {
    var user = ref.watch(fetchUserData);
    // 정확히는 riverpod 전용 타입 AsyncValue<User> 타입임.
    return Scaffold(
      appBar: AppBar(
	      actions: [
	        IconButton(
	          icon: Icon(Icons.refresh),
	          onPressed: () {
	            // fetchUserData 갱신
	            ref.refresh(fetchUserData);
	          },
	        )
	      ],
	    ),
      body:user.when(
      data:(data)=>Column(children: [
        Text(data.name.toString()),          
        Text(data.email.toString()),
        ],), 
      error: (error,stackTrace)=>Center(child:Text(error.toString())), 
      loading:() =>CircularProgressIndicator()),
      
    );
  }
}
ref.refresh 로 데이터 갱신 가능.
ref.watch(provider).when으로 진행상태에 따른 분기처리 가능.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

// User 모델 정의
class User {
  final String name;
  final String email;

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

  factory User.fromJson(String jsonString) {
    final jsonData = json.decode(jsonString);
    return User(
      name: jsonData['name'],
      email: jsonData['email'],
    );
  }
}

// userId를 관리하는 StateProvider
final userIdProvider = StateProvider<int>((ref) => 1);

// fetchUserData를 정의하는 FutureProvider.family
final fetchUserData = FutureProvider.family<User, int>((ref, userId) {
  final url = 'https://jsonplaceholder.typicode.com/users/$userId';
  return http.get(Uri.parse(url)).then((response) {
    if (response.statusCode == 200) {
      return User.fromJson(response.body);
    } else {
      throw Exception('Failed to load user');
    }
  });
});

// 메인 위젯
void main() {
  runApp(ProviderScope(child: MyApp()));
}

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

class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // StateProvider로부터 현재 userId를 가져오기
    final userId = ref.watch(userIdProvider);
    // userId에 따라 fetchUserData 호출
    final user = ref.watch(fetchUserData(userId));

    return Scaffold(
      appBar: AppBar(
        title: Text('User Info'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 비동기 상태 처리
          user.when(
            data: (data) => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Name: ${data.name}'),
                Text('Email: ${data.email}'),
              ],
            ),
            error: (error, stackTrace) =>
                Center(child: Text('Error: $error')),
            loading: () => Center(child: CircularProgressIndicator()),
          ),
          const SizedBox(height: 20),
          // ID 증가 버튼
          ElevatedButton(
            onPressed: () {
              // userId 증가
              ref.read(userIdProvider.notifier).state++;
            },
            child: Text('Next User'),
          ),
        ],
      ),
    );
  }
}

@Freezed 패키지

class User {
  final int id;
  final String name;

  // 생성자
  const User({required this.id, required this.name});

  // copyWith 메서드
  User copyWith({int? id, String? name}) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
    );
  }

  // deep equality를 위한 == 연산자 재정의
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other.runtimeType != runtimeType) return false;

    return other is User && other.id == id && other.name == name;
  }

  // hashCode 재정의
  @override
  int get hashCode => Object.hash(id, name);

  // JSON 직렬화
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
    };
  }

  // JSON 역직렬화
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
    );
  }
}

void main() {
  // User 객체 생성
  final user = User(id: 1, name: 'Alice');

  // copyWith 사용
  final updatedUser = user.copyWith(name: 'Bob');
  print(updatedUser.name); // Bob

  // JSON 직렬화
  final json = user.toJson();
  print(json); // {id: 1, name: Alice}

  // JSON 역직렬화
  final newUser = User.fromJson({'id': 2, 'name': 'Charlie'});
  print(newUser.name); // Charlie

  // 객체 비교
  print(user == updatedUser); // false
}
→ 불변패키지를 쉽게 만들어줌.

커스텀 FAB 만들기

Scaffold의 자체 기능을 사용하지 않음
→ stack으로 감쌈.
→ scaffold 외부에서는 material 이 적용되지 않기에, 다시 material 로 감싸야 오류가 나지 않을 수 있음
→ 상태관리를 위해 providerscope로 감싸기

꿀팁

Placeholder 위젯을 쓰면 영역을 볼수 있다.

레이아웃 위젯 정리

•
Container
◦
child가 없다면 가능한 모든 영역을 차지함.
◦
child가 존재한다면, child 영역만큼을 차지.
•
Column / Row
◦
가능한 모든 세로 / 가로 영역을 차지.
•
Expanded
◦
column / row 의 child로 사용하여, flex 속성을 지정하면 웹개발의 flex와 유사하게 사용가능
◦
가능한 모든 영역차지.
•
Stack
◦
위젯위에 위젯 표시 가능
Yejun Cheon
'Yejun Cheon' 구독하기
사이트를 구독하면 새 포스트 등 최신 업데이트를 알림과 메일로 가장 먼저 받아보실 수 있습니다.
Slashpage에 가입하고 'Yejun Cheon'을 구독하세요!
구독
👍