동기식과 비동기식의 차이

송민경's avatar
Apr 24, 2024
동기식과 비동기식의 차이

1. 기본 디자인 만들기

import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Expanded( child: Center( child: Text( "제목1", style: TextStyle( fontSize: 30, ), ), ), ), Expanded( child: ListView.separated( itemCount: 5, separatorBuilder: (context, index) { return Divider( color: Colors.grey, height: 1, thickness: 1, ); }, itemBuilder: (context, index) { return ListTile( leading: Icon(Icons.ac_unit_outlined), title: Text("제목 ${index + 1}"), subtitle: Text("내용 ${index + 1}"), ); }, ), ) ], ), ), ); } }
notion image
 

2. 창고 만들기

/** * 1. API 요청 -> json -> Map으로 자동으로 받아줌 -> Map -> Dart Object 변환 * 2. API 요청 -> 통신 -> wait가 걸린다 -> block 당하지 않도록 비동기 통신을 한다.(단일스레드) */ class Post { int userId; int id; String title; String body; Post({ required this.userId, required this.id, required this.title, required this.body, }); } // Mock 데이터 Post p1 = Post(userId: 1, id: 1, title: "제목1", body: "내용1"); Post p2 = Post(userId: 2, id: 2, title: "제목2", body: "내용2"); Post p3 = Post(userId: 3, id: 3, title: "제목3", body: "내용3"); Post p4 = Post(userId: 1, id: 4, title: "제목4", body: "내용4"); Post p5 = Post(userId: 2, id: 5, title: "제목5", body: "내용5"); List<Post> postList = [p1, p2, p3, p4, p5];
api 요청시 서버는 제이슨을 준다.
통신 라이브러리를 쓸껀데 map으로 자동 받아준다.
통신하고 나면 맵으로 받은걸
다시
notion image
다트에서 맵 쌍따옴표 씀
모든 타입을 다받기위해 다이나믹을쓴다.
 
notion image
꺼내쓰려면 이렇게 쓰는데 객체연결 연산자를 쓰지 않는다.
그래서 위험하다.
notion image
요딴식으로 적으면 안된다.
실수한다.
 
그래서 오브젝트로 파싱해야함
전부 자기 오브젝트로 변환하는게 좋다.
통신을 하면 자바스크립트의 프로미스로 구현해야 함
 
단일 스레드이다. 비동기 기반
메모장에 적어 놓고 나중에 반드시 실행할꺼다 → 프로미스
/** * 1. API 요청 -> json -> Map으로 자동으로 받아줌 -> Map -> Dart Object 변환 * 2. API 요청 -> 통신 -> wait가 걸린다 -> block 당하지 않도록 비동기 통신을 한다.(단일스레드) */ class Post { int userId; int id; String title; String body; Post({ required this.userId, required this.id, required this.title, required this.body, }); }
 

더미 데이터 만들기

/** * 1. API 요청 -> json -> Map으로 자동으로 받아줌 -> Map -> Dart Object 변환 * 2. API 요청 -> 통신 -> wait가 걸린다 -> block 당하지 않도록 비동기 통신을 한다.(단일스레드) */ class Post { int userId; int id; String title; String body; Post({ required this.userId, required this.id, required this.title, required this.body, }); } // Mock 데이터 Post p1 = Post(userId: 1, id: 1, title: "제목1", body: "내용1"); Post p2 = Post(userId: 2, id: 2, title: "제목2", body: "내용2"); Post p3 = Post(userId: 3, id: 3, title: "제목3", body: "내용3"); Post p4 = Post(userId: 1, id: 4, title: "제목4", body: "내용4"); Post p5 = Post(userId: 2, id: 5, title: "제목5", body: "내용5"); List<Post> postList = [p1, p2, p3, p4, p5];
dao 데이터 엑세스 오브젝트
디비접근
 
Repository
모든 핸드폰 메모장에는 에스큐엘 라이트라는게있다
Repository 목적은 데이터접근이다.
import 'package:future_app_01/post.dart'; /** * 1. DAO -> 디비 접근 * 2. Repository -> 디비 or 다른API or 파일 */ // 가짜 통신 class PostRepository { Post findById(int id) { return postList[id - 1]; } List<Post> findAll() { return postList; } }
 

3.?

json 을 map 타입으로 바꿔줌
프로미스는 작업리스트에 적어놓고 그 코드로 돌아와서 작업을 완료하겠다는것
웨이트가 걸릴때 블락이 당하지 않도록 비동기 통신을 해야 함
여러가지에 붙을 수 있는게 레파지토리
레파지토리의 책임 → 데이터 접근
그래서 레파지토리에 통신코드를 짜야 함
뷰에서 데이터가 필요할 경우 바로 레파지토리를 때림
notion image
  • 싱글톤 or const 사용하는 방법 후에 배울 예정
???
import 'package:flutter/material.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { // 실제 통신이면 null -> nullPointException 터짐 Post post = PostRepository().findById(2); // 실제 통신이면 null -> nullPointException 터짐 List<Post> postList = PostRepository().findAll(); // 미리 만들어놓고 써도 됨 return Scaffold( body: SafeArea( child: Column( children: [ Expanded( child: Center( child: Text( "${post.title}", style: TextStyle( fontSize: 30, ), ), ), ), Expanded( child: ListView.separated( // 구분선 itemCount: 5, separatorBuilder: (context, index) { return Divider( color: Colors.grey, height: 1, thickness: 1, ); }, itemBuilder: (context, index) { return ListTile( leading: Icon(Icons.ac_unit_outlined), title: Text("${postList[index].title}"), subtitle: Text("${postList[index].body}"), ); }, ), ) ], ), ), ); } }
 
  • 실행하고 바로 밑으로 내려오기에 약속을 리턴해줘야 함
  • 실제 5가 아닌 나중에 5줄께를 리턴해야 함
notion image
  • then 문법
Future<int> download(){ Future<int> future = Future.delayed( Duration(seconds: 3), () => 5 ); return future; } void main() { Future<int> future = download(); future.then((res)=>{ print("통신 끝 : ${res}") }); }
notion image
  • 데이터를 화면에 뿌릴때 future.then을 사용해야 함
 

바로 숫자(int)로 받기

  • async 걸어서 await가 걸려 빠져나오면 main이 종료되서 하면 안됨
notion image
  • test라는 함수 만들어서 시도하기
  • async가 걸리면 무조건 await를 걸어야 함
notion image
  • main 실행 → test 호출 → 다운로드 시작(3초 걸림) → await 부분을 캡쳐(기억) 해놓고 빠져나감
함수 내부는 동기적으로 실행 → print 실행 → 3초후에 숫자가 찍히고 종료됨
Future<int> download(){ Future<int> future = Future.delayed( Duration(seconds: 3), () => 5 ); return future; } void test() async { int result = await download(); print(result); } void main() { test(); print("그림그리기 완료"); }
 
통신은 딜레이가 걸리니까 Future를 리턴함
바로 응답이 안될 수도 있음
하드디스크에 기록하는 것은 오래 걸림 → 메모리 관련은 다 오래 걸림
자바에서는 async를 안쓴 이유는 내부적으로 스레드가 돌고 있기에 새로운 스레드가 기록하기 때문
dart는 단일 스레드이기 때문에 동기적으로 실행하면 안녕이 3초 뒤에 뜨니까 블락이 걸려서 그림을 먼저 그리고 오래 걸리는 일은 await를 걸어서 다하고나서 main이 할일이 없을때 안한 일 목록을 확인하고 돌아와서 다됐는지 확인하고 팬딩하니까 안되면 다시 갔다가 돌아와서 확인하고 다 되면 찍히는 것이 프로미스임!
동기적으로 걸 애들은 await 바로 밑에 두면 됨
async가 걸리면 캡처링해서 빠져나옴
 
import 'package:future_app_01/post.dart'; /** * 1. DAO -> 디비 접근 * 2. Repository -> 디비 or 다른API or 파일 */ // 가짜 통신 class PostRepository { Future<Post> findById(int id) { // 바로 리턴됨 return Future.delayed(Duration(seconds: 3), () => postList[id - 1]); } Future<List<Post>> findAll() { return Future.delayed(Duration(seconds: 3), () => postList); } }
  • Future니까 main.dart에 오류남
  • 기다렸다가 그림을 그려야되서 문제가 됨 -> FutureBuilder 를 사용함
 
  • 화면에 통신을 해서 통신 데이터를 기반으로 그림을 그릴 것
  • api 요청(레포가 요청 ) → 3초전 : 프로미스/약속=일 데이터 → 3초후 : 응답
 
  • 그냥 그릴 수 있는 나무가 있고 데이터를 뿌려야 하는 자리에 통신 데이터 1를 넣어야함
  • 데이터를 받아 FUTURE를 그리면 비어있어서 나무는 그릴 수 있는데 1을 넣을때 터짐
  • 한번에 그리면 3초동안 나무도 안그려지고 화면이 비어있으니 클라이언트는 화면이 고장난지 알았는데 3초뒤에 그려져서 아닌지 알았음
  • 나무가 그려지고 끝나고 3초뒤에 FUTURE를 다시 그리는 2번 그리는 방법이 최고임 → UX가 좋음
  • 나무를 RETURN하고 FUTURE BUILDER를 사용해서 3초 뒤에 1을 그리면 됨
 
  • snapshot : 다운받은 데이터= 결과 값 (post) : 선물박스
  • builder : 이벤트 다됐는지 팬딩중인지 확인하고 끝나면 확인됨
  • 팬딩중엔 false라 동그라미가 그려지고 반복되다가 완료되면 데이터가 그려짐
  • 날아갔다가 다시 그리는 것이나 빠르기 때문에 우리 눈에는 계속 그려지는 것처럼 보임
  • builder의 역할
  • 이벤트루프에 가서 팬딩중인지 확인하는것
  • 완료되면 그림을 그리는것
  • 데이터를 꺼내서 담아서 뿌리는 것
  • while로 만들어져있음
import 'package:flutter/material.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { Future<Post> postFT = PostRepository().findById(2); Future<List<Post>> postListFT = PostRepository().findAll(); return Scaffold( body: SafeArea( child: Column( children: [ Expanded( child: FutureBuilder( future: PostRepository().findById(3), builder: (context, snapshot) { print(11111111111111111); if (snapshot.hasData) { Post post = snapshot.data!; return Center( child: Text( "${post.title}", style: TextStyle( fontSize: 30, ), ), ); } else { return CircularProgressIndicator(); } }, ), ), Expanded( child: ListView.separated( itemCount: postList.length, separatorBuilder: (context, index) { return Divider( color: Colors.grey, height: 1, thickness: 1, ); }, itemBuilder: (context, index) { return ListTile( leading: Icon(Icons.ac_unit_outlined), title: Text("${postList[index].title}"), subtitle: Text("${postList[index].body}"), ); }, ), ) ], ), ), ); } }
  • 4번 그리는 것이 비효율 적임
notion image
notion image
 

상태값을 바꾸면서그림 2번 그리기

  • statenotify로 그림 그리면 처음에 1번 변경될때 1번 2번만에 그림을 그릴 수 있음
  • future를 리턴하는 함수가 repository함수를 때리고 선물박스를 리턴
  • 처음 상태 : null → 끝날 때까지 유지
  • 상태를 그림 → if면 프로그레스 받으면 됨
  • 3초 뒤에 future를 리턴해서 선물이 들어있음
  • 상태 : null → post 로 갱신
  • null이 다시 그려짐
  • watch를 써서 상태를 보고 있어야 함
notion image
 
null일떄 프로그래스 인디게이터를 돌림
watch하고 있다가 상태값이 달라지면 자동으로 다시 그려줌
통신 끝나고 상태값을 바꾸는 코드는 내가 짜야함
notion image
 
커스터마이징해서 디자인 수정하기
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post_list_page.dart'; void main() { runApp(ProviderScope(child: const MyApp())); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: PostListPage(), ); } }
notion image
  • post_vm.dart 만들기
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; //1. 창고 데이터 : 상태값 class PostListModel { List<Post> postList; PostListModel(this.postList); } //2. 창고 : 상태 변경(메서드), VIEW에 필요한 데이터 커스터마이징 class PostListVM extends StateNotifier<PostListModel?> { // 처음 상태값이 null PostListVM(super.state); } //3. 창고 관리자 : 창고에 IO(접근)하게 해줌 final postListProvider = StateNotifierProvider<PostListVM, PostListModel?>((ref) { return PostListVM(null); });
  • 레파지토리 호출하는 함수 notifyInit() 만들기
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; //1. 창고 데이터 : 상태값 class PostListModel { List<Post> postList; PostListModel(this.postList); } //2. 창고 : 상태 변경(메서드), VIEW에 필요한 데이터 커스터마이징 class PostListVM extends StateNotifier<PostListModel?> { // 처음 상태값이 null PostListVM(super.state); void notifyInit() async{ // 레파지토리를 호출하는 함수 List<Post> postList = await PostRepository().findAll(); state = PostListModel(postList); // 변경할 상태값 } // 화면에서 때리면 됨 } //3. 창고 관리자 : 창고에 IO(접근)하게 해줌 final postListProvider = StateNotifierProvider<PostListVM, PostListModel?>((ref) { return PostListVM(null); // 창고 데이터 });
notion image
  • watch가 돌면서 창고관리자가 실행됨
  • 이때 상태값은 null → 프로그레스 인디케이터가 실행됨
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_list_vm.dart'; class PostListPage extends StatelessWidget { const PostListPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: PostList(), ), ); } } class PostList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { PostListModel? model = ref.watch(postListProvider); if (model == null) { return Center(child: CircularProgressIndicator()); } else { List<Post> posts = model.postList; return Column( children: [ ElevatedButton( onPressed: () { ref.read(postListProvider.notifier).notifyDelete(); }, child: Text("3번 삭제")), Expanded( child: ListView.separated( itemCount: posts.length, separatorBuilder: (context, index) { return Divider( color: Colors.grey, height: 1, thickness: 1, ); }, itemBuilder: (context, index) { return ListTile( leading: Icon(Icons.ac_unit_outlined), title: Text("${posts[index].title}"), subtitle: Text("${posts[index].body}"), ); }, ), ), ], ); } } }
notion image

상태관리

  • 창고가 만들어질 때 노티파이 인잇 때리면 그때 상태는 널
  • 상태값이 바뀌면 상태값만 그림이 다시 그려짐
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; //1. 창고 데이터 : 상태값 class PostListModel { List<Post> postList; PostListModel(this.postList); } //2. 창고 : 상태 변경(메서드), VIEW에 필요한 데이터 커스터마이징 class PostListVM extends StateNotifier<PostListModel?> { // 처음 상태값이 null PostListVM(super.state); void notifyInit() async{ // 레파지토리를 호출하는 함수 List<Post> postList = await PostRepository().findAll(); state = PostListModel(postList); // 변경할 상태값 } // 화면에서 때리면 됨 } //3. 창고 관리자 : 창고에 IO(접근)하게 해줌 final postListProvider = StateNotifierProvider<PostListVM, PostListModel?>((ref) { PostListVM vm = PostListVM(null); vm.notifyInit(); // null이다가 3초후에 상태값이 변경됨 return vm; // 창고 데이터 });
 
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; //1. 창고 데이터 : 상태값 class PostListModel { List<Post> postList; PostListModel(this.postList); } //2. 창고 : 상태 변경(메서드), VIEW에 필요한 데이터 커스터마이징 class PostListVM extends StateNotifier<PostListModel?> { // 처음 상태값이 null PostListVM(super.state); void notifyInit() async{ // 레파지토리를 호출하는 함수 List<Post> postList = await PostRepository().findAll(); state = PostListModel(postList); // 변경할 상태값 } // 화면에서 때리면 됨 } //3. 창고 관리자 : 창고에 IO(접근)하게 해줌 final postListProvider = StateNotifierProvider<PostListVM, PostListModel?>((ref) { return PostListVM(null)..notifyInit(); // 창고 데이터 }); // 실행하면서 때림
notion image
  • 구조는 그려놔야 함
  • 화면은 vm에 의존하게 변경되었음
  • 상태를 가지고 잇는것을 리턴할 필요도 없음
  • 레파지토리에서 리턴받을 필요가 없음
  • 노티파이인잇이 리턴을 안함 → 호출한 놈은 결과가 아니라 상태값만 바라보고 있음
  • 상태값이 바뀌면 그림이 다시 그려지니까 리턴할 필요가 없음
 
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:future_app_01/post.dart'; import 'package:future_app_01/post_repository.dart'; // 1. 창고 데이터 class PostListModel { List<Post> postList; PostListModel(this.postList); } // 2. 창고 class PostListVM extends StateNotifier<PostListModel?> { PostListVM(super.state); void notifyInit() async { List<Post> postList = await PostRepository().findAll(); state = PostListModel(postList); } void notifyDelete() async { String result = await PostRepository().deleteById(3); print("result : ${result}"); if (result == "ok") { PostListModel model = state!; List<Post> postList = model.postList; List<Post> newPostList = postList.where((p) => p.id != 3).toList(); print("newPostList 크기 : ${newPostList.length}"); state = PostListModel(newPostList); } else { print("삭제 실패"); } } } // 3. 창고 관리자 final postListProvider = StateNotifierProvider<PostListVM, PostListModel?>((ref) { PostListVM vm = PostListVM(null); vm.notifyInit(); return vm; });
 
  • 값 삭제하기
  • 컴포넌트 분리해놓고 if 모델에서 통신코드 추가하면 끝남
  • 컴포넌트 분리안하면 통신할때 다 꼬임
  • 통신은 화면과 상관없이 통신 코드를 짜면 됨
  • 리턴이 무조건 future임
  • 뷰 모델 3개를 만들기
  • 창고 만들 데이터를 컴포지션해서 만들어 넣으면 됨
notion image
  • 데이터를 추가하려면 결국 class를 추가해야함 // DTO와 마찬가지
notion image
 

삭제하기

notion image
import 'package:future_app_01/post.dart'; /** * 1. DAO -> 디비 접근 * 2. Repository -> 디비 or 다른API or 파일 */ // 가짜 통신 class PostRepository { Future<Post> findById(int id) { // 바로 리턴됨 return Future.delayed(Duration(seconds: 3), () => postList[id - 1]); } Future<List<Post>> findAll() { return Future.delayed(Duration(seconds: 3), () => postList); } Future<String> deleteById(int id) { return Future.delayed(Duration(seconds: 3), () => "ok"); } }
notion image
 

이벤트 루프를 통한 비동기 프로그램

notion image
main을 그리러 감 → scaffold를 그림 → body에 postList가 그려짐
→ build가 실행됨 → ref.watch가 실행 → 창고 관리자 호출
 
다른 페이지에서도 watch하고 프로바이저를 호충하면 싱글톤으로 관리 가능함
watch 100번 해도 창고는 1개
 
이때 postListVm에 창고 관리자가 만들어짐
알림을 주는 프로바이저 → vm(창고) 객체가 만들어짐
창고가 관리하는 state가 만들어짐\
postListModel 타입을 관리해서 이 타입만 들어올 수 있음 || 현재 null이 들어옴
→ 창고에 있는 행위 메서드 → Init(0 실행 - 레파지토리를 호출하는 비동기 수로 3초 있다가 뜸 → 레파지토리가 만들어짐(통신 코드:http 요청해서 future를 리턴만 해주면 됨) / 책임이 분리되어있음
→ 리턴 vm : 리턴되는 순간 창고 관리자가 창고를 관리할 수 있게 선이 연결됨 / 리턴 안하면 관리를 할 수 없음
뷰로 돌아옴
→ null 기반으로 그림을 그려서 프로그레스 인디케이터로 그림을 그리고 있음 // CPU할일 끝
NOTIFY선이 생겨서 선이 생겨서 유지됨 - READ는 응답하고 끈김
백그라운드로 통신은 돌고 있음(메모리가 할 일)
실행되는 순간 통신 코드를 쏜 것
통신 코드가 실행되면서 이벤트 루프(큐)가 만들어져 있고 INIT이 들어옴 / 모든 io는 등록됨
등록 안하면 블락 당함
INIT안에 있는 FIND.ALL()코드가 실행되서 맨 처음에는 팬딩 상태임
처음에는 통신 코드가 팬딩 상태인 1개만 담겨있음
 
cpu가 할일 다하고(리턴되고) 확인하러 가서 팬딩이면 다른거 하러 감
cpu가 할일 다하고(리턴되고) 확인하러 가서 팬딩이 완료로 바뀌면 INIT 메서드로 콜백됨(돌아감!)
await가 걸려있는 캡쳐한 코드로 돌아감/ 위치를 다 기억하고 있는 것
 
팬딩이 완료되어서 실제 값이 들어오면 상태NULL의 값을 바꿔줘야 함 - 우리가 할 일!!
SCAFFOLD 화면이 창고 관리자를 WATCH하고 있음
→ 창고의 상태가 POSTList로 변경되면 창고 관리자가 알려줌 → 다시 그려짐
 
삭제 코드 → delete가 실행 → 팬딩 상태로 대기상태 → 나머지 것들 그리기
팬딩하고 체크하고 하는 것은 cpu가 아닌 다른 것이 하는 것
삭제 버튼을 누르고 나서 그림 그리는 ui가 할 일이 없으니 가만히 있으면 됨
 
만약 스크롤이 있다면 그림 그리는 ui가 할 일이 없어야 스크롤도 되는 것
 
화면이 안 뻗기 위해서 오래걸리는 일은 메모리에 맡기고 화면에서 리스닝하고 있음 / main 스레드 일
main 스레드가 할일이 없지만 리스닝은 하고 있음
그래서 간헐적으로 확인해야 함 // 다른 이벤트들을 리스닝하고 반응해야 함
UI 그리는 것이 main임
이벤트를 다 짧게 만들어야 함
 
4가지 방법!!!!!!
프로바이더 : 한번만 그리고 상태 값이 바껴도 다시 그려지지 않음
스테이트 노티파이어 : 초기 + 통신후 총 2번만 그리는 것
퓨처 프로바이더 : 통신을 해서 기다렸다가 화면에 그림
어떤 변수의 값이 통신을 통해 오다보니 바로 못그려서 만든 것
스트림 프로바이더 : 응답이 통신이 끝날때까지 계속 오는 것 EX) 통신
상태가 외부 통신이 소켓이라 계속 들어와서 들어올때마다 갱신하는 것
데이터가 계속 갱신되면서 지속적으로 바뀌는 데이터를 계속 화면에 갱신하기 위해 만든 것
 
퓨퍼, 스트링 둘다 스테이트 노티파이어로 다 할 수 있음 / 상태 바뀌면 갈아끼울 수 있음
 
class PostRepository { static final PostRepository _instance = PostRepository._single(); // (1) _instance factory PostRepository() { // (2) PostRepository return _instance; } PostRepository._single(); // (3) PostRepository._single()
Share article

vosw1