Flutter 오답노트
예
예준천

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());class FishModel with ChangeNotifier{
}TextEditingController controller = TextEditingController();
// 텍스트 읽기
String text = controller.text;
// 텍스트 설정
controller.text = "Hello, Flutter!";controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
); // 텍스트 전체 선택controller.addListener(() {
print("텍스트가 변경되었습니다: ${controller.text}");
});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');
}
}// 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;
}
}await FirebaseAuth.instance.signOut();final nameProvider = Provider<String>(
(ref){ return //something };
);final countProvider = StateProvider<int>((ref) => return 0;)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(),
);
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),
),
);
}
}
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()),
);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()),
);
}
}
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'),
),
],
),
);
}
}useEffect((){
// initializing code
return () {
//dispose code
}
},[])useEffect((){
// 변경될 때마다 새로 실행될 로직
return () {
//dispose code
}
},[변경 감지할 dependency])ListView.builder(
itemCount: list.length,
itemBuilder: (ctx,index) => Text(list[index]),
) StreamBuilder<bool>(
stream: viewModel.deleteResultStream,
builder: (context, snapshot) {
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
}
}
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;
}
...
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,
// 뷰모델은 ChangeNotifier를 상속받아야 함
class ListViewModel extends ChangeNotifier {
int clickCount = 0;
...
addClickCount() {
clickCount += 1;
notifyListeners(); // 변경 내역을 뷰에 통보하기 위해 notifyListeners() 호출
}
...
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ArticleViewModel()),
],
child: MaterialApp(
home: ArticleListView(),
),
);
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}',
class ListViewModel extends ChangeNotifier {
bool deleteSuccess = false;
Future<void> removeChecked() async {
deleteSuccess = true; // 삭제 성공
notifyListeners();
}
}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');
}
}
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),
);
},
),
final nameProvider = Provider<String>(
(ref){ return //something };
);final countProvider = StateProvider<int>((ref) => return 0;)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(),
);
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),
),
);
}
}
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()),
);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()),
);
}
}
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'),
),
],
),
);
}
}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
}