23/03/21(flutter)

조영문·2023년 3월 21일
0

flutter

목록 보기
4/9

안드로이드 스튜디오 버전 맞추기

SDK 연결

플러터sdk경로\flutter\bin\cache\dart-sdk



플러터 간단한 애니메이션

  1. 클래스에 SingleTickerProviderStateMixin
  2. 애니메이션 / 애니메이션 컨트롤러 정의
  3. 애니메이션을 이용하는 위젯으로 애니메이션 사용

flutter_animation_1.dart

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

void main() {
  runApp(const 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: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AnimatedScreen1(),
    );
  }
}

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

  @override
  State<AnimatedScreen1> createState() => _AnimatedScreen1State();
}

class _AnimatedScreen1State extends State<AnimatedScreen1>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  //late는 선언할 때가 아닌 나중에 값을 넣을테니까 지금은 일단 만들어달라는 의미
  late Animation<double> _animation;

  @override
  void initState() { //TODO ?
    super.initState();
    //_animationController는 시간을 정해준다?
    _animationController =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    //_animation는 값을 바꿔준다.
    _animation =
        Tween<double>(begin: 0, end: 1.5).animate(_animationController);
    _animationController.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _animationController.reset();
          _animationController.forward();
        },
        child: const Icon(Icons.play_arrow),
      ),
      appBar: AppBar(title: const Text('Animated Screen')),
      body: Center(
        child: ScaleTransition(
          scale: _animation,
          child: const Text(
            'Hello, world!',
            style: TextStyle(fontSize: 50),
          ),
        ),
      ),
    );
  }
}

// // 훅스 버전
// class AnimatedScreen extends HookWidget {
//   const AnimatedScreen({super.key});

//   @override
//   Widget build(BuildContext context) {
//     final animationController =
//         useAnimationController(duration: const Duration(seconds: 2));
//     final animation = useAnimation(
//         Tween<double>(begin: 0, end: 1.5).animate(animationController));

//     useEffect(() {
//       animationController.forward();
//     });

//     return Scaffold(
//       floatingActionButton: FloatingActionButton(
//         onPressed: () {
//           animationController.reset();
//           animationController.forward();
//         },
//         child: const Icon(Icons.play_arrow),
//       ),
//       appBar: AppBar(title: const Text('Animated Screen')),
//       body: Center(
//         child: Transform.scale(
//           scale: animation,
//           child: const Text(
//             'Hello, world!',
//             style: TextStyle(fontSize: 50),
//           ),
//         ),
//       ),
//     );
//   }
// }

SingleTickerProviderStateMixin

위젯에 애니메이션 1개일 경우

TickerProviderStateMixin

위젯에 애니메이션 1개 이상일 경우
플러터는 프레임 60 목표로 만들어진 프레임워크다. TickerProviderStateMixin을 사용하면 프레임마다 화면을 갱신하도록 만들 수 있다.

페이지 이동 심화

flutter_navigator

push - 현재페이지 위에 새페이지를 올림
pushReplacement

  • 현재페이지 없애고 새페이지를 올림
    pushAndRemoveUntil
  • 새페이지 올리고, 나머지 것들 중에서 조건에 맞는 페이지를 삭제
    페이지 없애기
    pop - 현재 페이지 삭제
    popUntil - 원하는 페이지 나올 때 까지 페이지 삭제

First Page

Second Page

Third Page

Forth Page

first_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_navigator/view/second_page.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: const Text('두번째 화면 열기\n(현재페이지 위로 열기)'),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const SecondPage()),
                );
              },
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              child: const Text('두번째 화면 열기\n(현재페이지를 교체해서 열기 )'),
              onPressed: () {
                Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(builder: (context) => const SecondPage()),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

second_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_navigator/view/third_page.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const ThirdPage()),
                );
              },
              child: const Text('세번째 화면 열기\n(현재페이지 위로 열기)'),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                if(!Navigator.canPop(context)) {
                  showToast(context, "현재 페이지가 마지막 남은 페이지 입니다.");
                  return;
                }
                Navigator.pop(context);
              },
              child: const Text('첫번째 화면 돌아가기\n(현재페이지 없애기)'),
            ),
          ],
        ),
      ),
    );
  }
}

void showToast(BuildContext context, String message) {
  final scaffold = ScaffoldMessenger.of(context);
  scaffold.showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

third_page.dart

import 'package:flutter/material.dart';
import 'first_page.dart';
import 'fourth_page.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Third Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const FourthPage(),
                  ),
                );
              },
              child: const Text('네번째 화면 열기\n(현재페이지 위로 열기)'),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const FirstPage(),
                  ),
                );
              },
              child: const Text('첫번째 화면 열기\n(현재페이지 위로 열기)'),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.pushAndRemoveUntil(
                  context,
                  MaterialPageRoute(builder: (context) => const FirstPage()),
                  (Route<dynamic> route) => false,
                );
              },
              child: const Text('첫번째 화면 열기\n(나머지 페이지 다 지우기)'),
            ),
          ],
        ),
      ),
    );
  }
}

forth_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_navigator/view/first_page.dart';
class FourthPage extends StatelessWidget {
  const FourthPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Fourth Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const FirstPage(),
                  ),
                );
              },
              child: const Text('첫번째 화면 열기\n(현재페이지 위로 열기)'),
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.popUntil(
                  context,
                  (Route<dynamic> route) => route.isFirst,
                );
              },
              child: const Text('두번째 화면 돌아가기\n(나머지 없애기)'),
            ),
          ],
        ),
      ),
    );
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_navigator/view/first_page.dart';

void main() {
  runApp(const 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: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirstPage(),
    );
  }
}

통신

흐름 :
1. 상태를 만든다.

  • 통신 전 : 데이터가 없다(null) (스켈레톤ui)
  • 통신 후 데이터가 있을 수도 있고 없을 수도 있다.
  1. 페이지가 빌드된다.
  • 통신을 시작한다.
  • 통신이 완료되면 상태를 업데이트 한다.)
  1. 페이지가 재빌드된다.

useEffect();

++useEffect(
	첫번째 매개변수 : 사용할 함수
    두번째 매개변수 : 관찰할 상태 리스트
);
// 특징 : 화면이 빌드 된 후 작동한다.
// 보통 통신을 받기 위해서 사용한다.
// 두번째 매개변수가 빈 리스트면 페이지에서 딱 한번 실행된다.
// 두번째 매개변수에 상태들을 넣으면 해당 상태가 바뀌면 재실행된다.
// 첫번째 매개변수로 받은 함수는 종료함수를 리턴해야한다.

flutter_http


dto/post_req_dto.dart

// 요청시 body에 담을 객체

dto/post_res_dto.dart

// 다트는 이너클래스가 없음

// 작명
// 테이블명 + 요청/응답 + DTO + 용도
// Post + Req/Res + DTO + Table/Detail
class PostResDTOTable {
  // 값 대입 후 바뀔 필요가 없으면 final
  final int userId;
  final int id;
  final String title;

  // 생성자
  PostResDTOTable({
    required this.userId,
    required this.id,
    required this.title,
  });

  // 팩토리 생성자
  // 팩토리 생성자는 해당 클래스 타입의 객체를 리턴해줄 때
  // 여러가지 방식을 사용할 수 있도록 해준다.
  // 아래는 map데이터를 받아서 객체를 생성.
  factory PostResDTOTable.fromMap(Map<String, dynamic> map) =>
      PostResDTOTable(
        userId: map["userId"],
        id: map["id"],
        title: map["title"],
      );
}

class PostResDTODetail {
  final int userId;
  final int id;
  final String title;
  final String body;

  PostResDTODetail({
    required this.userId,
    required this.id,
    required this.title,
    required this.body,
  });

  factory PostResDTODetail.fromMap(Map<String, dynamic> map) =>
      PostResDTODetail(
        userId: map["userId"],
        id: map["id"],
        title: map["title"],
        body: map["body"],
      );
}

repository/post_repository.dart

import 'dart:convert';

import 'package:flutter_http/post/model/dto/post_res_dto.dart';
import 'package:http/http.dart' as http;

class PostRepository {
  // 싱글톤
  // 스태틱 변수 선언
  static PostRepository? _instance;

  // private 생성자를 만들어서
  // 기본 public 생성자를 사용하지 못하게 함
  PostRepository._();

  // getter로 인스턴스를 반환
  // _instance가 null 일 경우 _instance에 객체를 생성해서 대입한 후 리턴
  // 변수 ??= 대입값 / 변수가 대입된 후 변수를 리턴
  static PostRepository get getInstance => _instance ??= PostRepository._();

  // 통신 결과는 언제 올지 모르기 때문에
  // 비동기 통신인 Future를 사용한다.
  //  Future<들어갈변수>
  Future<List<PostResDTOTable>?> getPostTableDTOList() async {
    // url 주소설정
    const String url = "https://jsonplaceholder.typicode.com/posts";
    // 통신하여 받아옴
    // http.get(url) ->  데이터를 가져올 때(서버에 보낼 데이터가 주소에 보임)
    // http.post(url) -> 데이터를 서버에 보낼 때(로그인 등) / (서버에 보낼 데이터가 body에 담김)
    // http.put(url) -> 서버의 데이터를 업데이트 할 때(개인정보수정 등) / 데이터 전체
    // http.delete(url) -> 서버의 데이터를 삭제할 때(댓글 삭제)
    // http.patch(url) -> put이랑 비슷 / 데이터 일부만 바꿀 때
    // JSP에서는 get/post 두가지를 주로 쓴다.
    // post가 put과 delete 역할을 동시에 한다.
    // patch는 put으로 대체해서 쓰는 경우가 많다.

    final response = await http.get(Uri.parse(url));

    // 응답이 200이면
    if (response.statusCode == 200) {
      // // dto를 리턴 (Future로)
      // final List jsonList = response.body as List;
      // final List<PostResDTOTable> postResDTOTableList = jsonList.map((e) {
      //   final Map<String, dynamic> map = e as Map<String, dynamic>;
      //   return PostResDTOTable.fromMap(map);
      // }).toList();
      
      // 위 코드를 압축하면 아래와 같음
      return (jsonDecode(response.body) as List).map((e) => PostResDTOTable.fromMap(e)).toList();
    } else {
      return null;
    }
  }

  Future<PostResDTODetail> getPostDTODetail(int id) async {
    final String url = "https://jsonplaceholder.typicode.com/posts/$id";
    final response = await http.get(Uri.parse(url));
    return PostResDTODetail.fromMap(jsonDecode(response.body));
  }
}

view/post_detail_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_http/post/model/dto/post_res_dto.dart';
import 'package:flutter_http/post/model/repository/post_repository.dart';

class PostDetailPage extends HookWidget {
  const PostDetailPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    // 이전 페이지에서 보낸 값을 받는다.
    var args = ModalRoute.of(context)?.settings.arguments;

    final postResDTODetailState = useState<PostResDTODetail?>(null);

    useEffect(() {
      // 이전페이지에서 값을 보내지 않았거나
      // 잘못된 접근을 하였을 경우
      if (args == null) return;
      final argsMap = args as Map;

      PostRepository.getInstance.getPostDTODetail(argsMap["id"]).then((value) {
        postResDTODetailState.value = value;
      });
      return null;
    }, []);

    final Widget childWidget;

    if (args == null) {
      childWidget = const Center(
          child: Text("관리자에게 문의 하세요.", style: TextStyle(fontSize: 50)));
    } else if (postResDTODetailState.value == null) {
      childWidget =
          const Center(child: Text("로딩 중...", style: TextStyle(fontSize: 50)));
    } else {
      final e = postResDTODetailState.value!;

      childWidget = Center(
        child: Column(
          children: [
            Text("글번호 : ${e.id} /  유저번호 : ${e.userId} "),
            const Divider(),
            Text("제목 : ${e.title}"),
            const Divider(),
            Text("내용 : ${e.body}"),
          ],
        ),
      );
    }

    return WillPopScope(
      onWillPop: () {
        Navigator.pop(context, "테이블로 보낼 데이터");
        return Future.value(true);
      },
      child: Scaffold(
        appBar: AppBar(),
        body: SafeArea(
          child: childWidget,
        ),
      ),
    );
  }
}

view/post_list_page.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_http/post/model/dto/post_res_dto.dart';
import 'package:flutter_http/post/model/repository/post_repository.dart';

// 페이지 내부 상태 관리를 위한 HookWidget
class PostListPage extends HookWidget {
  const PostListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 상태 - 통신 데이터 유무 판단.
    // 데이터를 외부 페이지에서도 사용할 경우
    // Provider 등으로 전역관리한다.
    final postResDTOTableListState = useState<List<PostResDTOTable>?>(null);

    // 페이지가 빌드 된 이후 작동
    useEffect(() {
      // 통신해서 데이터를 가져온다.
      PostRepository.getInstance.getPostTableDTOList().then((value) {
        postResDTOTableListState.value = value;
      });
      // useEffect 종료시 작동할 함수를 정의한다.
      return null;
    }, []); // 대괄호에는 모니터링할 상태를 적는다.

    // 호출 확인용
    // 대괄호에 상태를 넣어서 비교해본다.
    useEffect(() {
      print("useEffect호출됨 ${Random().nextInt(100)}");
    }, [postResDTOTableListState.value]);

    // 상태에 따라 보여줄 위젯을 선언
    final Widget childWidget;

    if (postResDTOTableListState.value == null) {
      // 상태값이 null이면
      childWidget =
          const Center(child: Text("로딩 중...", style: TextStyle(fontSize: 50)));
    } else {
      // 값이 있으면
      childWidget = ListView(
        children: postResDTOTableListState.value!.map((e) {
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: Container(
              decoration: BoxDecoration(
                borderRadius: const BorderRadius.all(Radius.circular(20)),
                border: Border.all(
                  color: Colors.blue,
                ),
              ),
              child: InkWell(
                onTap: () async {
                  // 페이지 이동시 값을 넘겨준다. arguments
                  // .then을 이용해서 페이지가 꺼지는 타이밍을 기다리거나
                  // 값을 반환 받을 수 있다.
                  Navigator.pushNamed(context, '/detail',
                      arguments: {"id": e.id}).then((value) {
                    print('Returned from new page');
                    print(value);
                  });
                },
                child: Column(
                  children: [
                    Text("글번호 : ${e.id} /  유저번호 : ${e.userId} "),
                    const Divider(),
                    Text("제목 : ${e.title}"),
                  ],
                ),
              ),
            ),
          );
        }).toList(),
      );
    }

    return Scaffold(
      appBar: AppBar(),
      body: SafeArea(
        child: childWidget,
      ),
    );
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_http/post/view/post_detail_page.dart';
import 'package:flutter_http/post/view/post_list_page.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => const PostListPage(),
        '/detail': (context) => const PostDetailPage(),
      },
      initialRoute: '/',
    );
  }
}

참고

https://velog.io/@jaybon/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98

0개의 댓글