2023.5.21.(일)

Flutter

Flutter란 무엇인가요?

위 링크에 들어가보면, Flutter는 Google에서 개발 및 지원하는 오픈 소스 프레임워크입니다. 프런트엔드 및 풀 스택 개발자는 Flutter를 사용해 다수의 플랫폼에 대한 애플리케이션의 사용자 인터페이스(UI)를 단일 코드 베이스로 구축합니다. 라고 되어 있다. iOS, Android, 웹, Windows, MacOS, Linux의 여섯 가지 플랫폼에 대한 애플리케이션 개발을 지원한다.

네이티브 앱 개발에 보다 크로스 플랫폼 앱 개발에 더가까움

iOS와 같은 특정 플랫폼을 위한 애플리케이션을 코딩하는 것을 네이티브 앱 개발이라고 합니다. 이와는 대조적으로, 크로스 플랫폼 앱 개발은 단일 코드베이스를 기반으로 여러 플랫폼을 위한 애플리케이션을 구축하는 것입니다.

네이티브 앱 개발

네이티브 앱 개발에서 개발자는 특정 플랫폼용으로 코드를 작성하므로, 네이티브 디바이스 기능에 대한 전체 액세스 권한을 가집니다. 이는 일반적으로 크로스 플랫폼 앱 개발에 비해 더 높은 성능과 속도로 이어집니다. 반면, 여러 플랫폼에서 애플리케이션을 실행하려는 경우, 네이티브 앱 개발 방식에서는 더 많은 코드와 개발자가 필요합니다.

크로스 플랫폼 앱 개발

크로스 플랫폼 앱 개발을 통해 개발자는 하나의 프로그래밍 언어와 하나의 코드베이스를 사용하여 여러 플랫폼용 애플리케이션을 구축할 수 있습니다. 여러 플랫폼용으로 애플리케이션을 출시하는 경우 크로스 플랫폼 앱 개발 방식이 네이티브 앱 개발 방식보다 비용과 시간이 적게 듭니다.

Flutter 의 이점

  • 네이티브에 가까운 성능: Flutter는 프로그래밍 언어로 Dart를 사용하고 기계 코드로 컴파일 합니다. 호스트 디바이스가 이 코드를 이해하므로 빠르고 효과적인 성능이 보장됩니다.
  • 빠르고 일관적이며 사용자 지정이 가능한 렌더링: Flutter는 플랫폼별 렌더링 도구를 사용하지 않고, Google의 오픈 소스 Skia 그래픽 라이브러리를 사용하여 UI를 렌더링합니다. 따라서 애플리케이션에 액세스하는 데 사용하는 플랫폼에 관계 없이 사용자에게 일관된 시각적 경험을 제공합니다.
  • 개발자에게 편리한 도구: Google은 사용 편의성에 중점을 두고 Flutter를 만들었습니다. 개발자는 핫 리로드와 같은 도구를 사용하여 상태를 바꾸지 않고 코드 변경 내용을 미리 볼 수 있습니다. 위젯 검사기와 같은 다른 도구를 사용하면 UI 레이아웃 문제를 손쉽게 시각화하고 해결할 수 있습니다.

Flutter의 위젯이란 무엇인가요?

Flutter에서 개발자는 위젯을 사용하여 UI 레이아웃을 만듭니다. 즉, 창과 패널부터 버튼과 텍스트에 이르기까지, 사용자가 화면에서 보는 모든 요소가 위젯으로 만들어집니다.
Flutter 위젯은 개발자가 손쉽게 사용자 지정할 수 있도록 설계되었습니다. Flutter는 구성 접근 방식을 통해 이를 실현합니다. 즉, 대부분의 위젯은 작은 위젯으로 구성되며, 가장 기본적인 위젯은 특정한 용도가 있습니다. 따라서 개발자가 위젯을 결합하거나 편집하여 새 위젯을 만들 수 있습니다.

Flutter는 플랫폼의 기본 제공 위젯을 사용하는 것이 아니라, 자체 그래픽 엔진을 사용하여 위젯을 렌더링합니다. 덕분에 사용자는 플랫폼 전체에 걸쳐 Flutter 애플리케이션에서 유사한 모양과 느낌을 경험할 수 있습니다. 일부 Flutter 위젯은 플랫폼별 위젯에서는 수행할 수 없는 기능을 수행할 수 있으므로, 이러한 접근 방식은 개발자에게 유연성을 제공합니다.

Flutter 위젯의 유형

Flutter는 다운로드 할 때부터 광범위한 위젯 카탈로그와 함께 제공됩니다. 카탈로그에는 스타일링, Cupertino(iOS 스타일 위젯) 및 Material Components(Google의 재료 설계 지침을 따르는 위젯)를 비롯한 14개의 범주가 있습니다.

Flutter에는 레이아웃과 테마도 포함되어 있어 개발자가 빠르게 구축하는 데 도움이 됩니다.

AWS는 Flutter를 어떻게 지원하나요?

Flutter는 애플리케이션에서 사용자에게 표시되는 부분을 만드는 데 도움이 됩니다. 하지만 애플리케이션을 개발하는 데에는 인증, 파일 스토리지, 분석 등 사용자에게 보이지 않는 많은 기능이 필요합니다. AWS Amplify와 Amplify Flutter는 바로 여기에 사용됩니다.

AWS Amplify는 안전하고 확장 가능한 모바일 및 웹 애플리케이션을 구축할 수 있는 프레임워크입니다. iOS, Android, 웹, React Native 및 Flutter 를 지원하는 AWS Amplify 를 사용하면 AWS 기반의 애플리케이션을 쉽고 빠르게 구축할 수 있습니다.

Amplify Flutter는 Flutter 애플리케이션의 백엔드를 프로비저닝, 빌드 및 배포할 수 있는 도구와 라이브러리의 집합입니다. Amplify Flutter를 사용하여 Flutter 애플리케이션을 AWS에 연결하고 일반적인 백엔드 요구 사항을 해결할 수 있습니다.

Amplify Flutter를 백엔드 솔루션으로 사용

  • 분석: Amazon Pinpoint에서 사용자에 대한 추적 데이터를 수집할 수 있습니다. 이벤트를 손쉽게 기록하고 필요에 따라 지표와 속성을 사용자 지정할 수 있습니다.

  • API: GraphQL API는 백엔드의 데이터를 검색하는 데 유용하며 AWS AppSync에서 지원됩니다.

  • 인증: 사용자를 인증하고 등록 및 로그인 양식뿐만 아니라 다중 인증도 구현할 수 있습니다. 백그라운드에서 다른 Amplify 범주에 필요한 권한을 제공합니다. 사용을 시작하는 순간부터 Cognito 사용자 풀과 ID 풀을 지원합니다.

  • 데이터 스토어: 오프라인 및 온라인 시나리오를 위한 추가 코드를 작성하지 않고도 분산 공유 데이터를 사용할 수 있습니다. 따라서 분산된 사용자 간 데이터에 대해서도 로컬 전용 데이터 만큼 간편하게 작업에 사용할 수 있습니다. Amplify DataStore는 데이터를 자동으로 버저닝하고 AppSync를 사용하여 클라우드에서 충돌 감지 및 해결 기능을 구현합니다.

  • 스토리지: Amplify Flutter를 사용하면 스토리지의 객체를 업로드하고, 다운로드하고, 삭제할 수 있습니다.

외부 데이터를 fetch 할 때.


import 'package:best_friend/api/data_of_articles.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:html/parser.dart' as parser;
import 'package:best_friend/api/article_model.dart';
import 'package:url_launcher/url_launcher.dart';

class Articles extends StatefulWidget {
  final String link, queryString;
  const Articles({super.key, required this.link, required this.queryString});

  @override
  State<Articles> createState() => _ArticlesState();
}

class _ArticlesState extends State<Articles> {
  List<Article> articles = [];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getWebsiteData();
  }

  Future<void> getWebsiteData() async {
    print(widget.link);
    final url = Uri.parse(widget.link);
    final response = await http.get(url);
    final document = parser.parse(response.body);
    //dom.Document html = dom.Document.html(response.body);

    final mainTitles = document
        .querySelectorAll(widget.queryString)
        .map((element) => element.innerHtml.trim())
        .toList();

    final links = document.querySelectorAll('td.td_subject.text-left > a');

    List<String> titles = [];
    List<String> detailLink = [];

    print('Count: ${mainTitles.length}');
    for (var title in mainTitles) {
      var doc = parser.parseFragment(title);
      doc.querySelectorAll('span').forEach((span) => span.remove());
      String docString = doc.text!.replaceAll('[]', '');

      titles.add(docString);
    }

    for (var link in links) {
      detailLink.add(link.attributes['href']!);
      print(link.attributes['href']);
    }

    setState(() {
      articles = List.generate(
        titles.length,
        (index) =>
            Article(url: detailLink[index], title: titles[index], urlImage: ''),
      );

      DataOfArticles list = DataOfArticles();
      DataOfArticles.listArticles = articles;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 150,
      child: ListView.separated(
          scrollDirection: Axis.horizontal,
          itemCount: articles.length,
          separatorBuilder: (context, index) {
            return const SizedBox(
              width: 15,
            );
          },
          itemBuilder: (context, index) {
            final article = articles[index];
            return GestureDetector(
              onTap: () => launchUrl(Uri.parse(articles[index].url)),
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                child: Container(
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(10),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.grey.withOpacity(0.5),
                        spreadRadius: 2,
                        blurRadius: 10,
                        offset: const Offset(0, 3),
                      ),
                    ],
                  ),
                  width: 150,
                  child: ListTile(
                    title: Text(
                      article.title,
                      style: const TextStyle(
                          fontSize: 15, fontWeight: FontWeight.bold),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
              ),
            );
          }),
    );
  }
}


로그인할 때


import 'package:flutter/material.dart';
import 'package:best_friend/main_screens/main_community.dart';

class AuthPage extends StatelessWidget {
  AuthPage({Key? key}) : super(key: key);
  final GlobalKey<FormState> _formkey = GlobalKey<FormState>();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    return Scaffold(
        appBar: AppBar(
          title: const Text(
            "로그인",
            style: TextStyle(
              fontSize: 27,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
          leading: IconButton(
            icon: const Icon(Icons.arrow_back_ios),
            onPressed: () {},
          ),
        ),
        body: Stack(alignment: Alignment.center, children: <Widget>[
          Container(color: Colors.white),
          Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
              Container(
                  width: 200, height: 200, color: Colors.blue), // 로고 넣을 자리
              SizedBox(
                width: 850,
                child: Stack(
                  children: <Widget>[
                    _inputForm(size),
                    _authButton(size, context),
                  ],
                ),
              ),
              Container(
                height: size.height * 0.1,
              ),
              const Text("회원가입 버튼 text는 추후 수정"),
              Container(
                height: size.height * 0.05,
              ),
            ],
          ),
        ]));
  }

  Widget _authButton(Size size, BuildContext context) {
    return Positioned(
      left: 100,
      right: 100,
      bottom: 0,
      child: SizedBox(
        height: 50,
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(25),
            ),
          ),
          onPressed: () {
            if (_formkey.currentState?.validate() != null) {
              //
              Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) => const CommunityScreen()),
              );
            }
          },
          child: const Text(
            "로그인",
            style: TextStyle(fontSize: 20, color: Colors.white),
          ),
        ),
      ),
    );
  }

  Widget _inputForm(Size size) {
    return Padding(
      padding: EdgeInsets.all(size.width * 0.05),
      child: Card(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        elevation: 6,
        child: Padding(
          padding:
              const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 32),
          child: Form(
              key: _formkey,
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    TextFormField(
                      controller: _emailController,
                      decoration: const InputDecoration(
                        icon: Icon(Icons.account_circle),
                        labelText: "Email",
                      ),
                      validator: (String? value) {
                        if (value == null) {
                          return "이메일 주소를 입력해주세요";
                        }
                        return null;
                      },
                    ),
                    TextFormField(
                      obscureText: true,
                      controller: _passwordController,
                      decoration: const InputDecoration(
                        icon: Icon(Icons.vpn_key),
                        labelText: "Password",
                      ),
                      validator: (String? value) {
                        if (value == null) {
                          return "비밀번호를 입력해주세요";
                        }
                        return null;
                      },
                    ),
                    Container(
                      height: 8,
                    ),
                    const Text("비밀번호를 잊으셨나요? 비밀번호 찾기")
                  ])),
        ),
      ),
    );
  }
}


구글 맵 API 가져오기


import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:best_friend/appBar/BFAppBar.dart';
//https://kitty-geno.tistory.com/46

// api key:

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

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  final String apiKey = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const BFAppBar(
        appBarFunction: 3,
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 30),
        child: Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(10),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.5),
                spreadRadius: 2,
                blurRadius: 5,
                offset: const Offset(0, 3),
              ),
            ],
          ),
          child: SizedBox(
            width: 620,
            height: 850,
            child: InAppWebView(
              initialUrlRequest: URLRequest(
                url: Uri.parse('https://maps.google.com/maps?key=$apiKey'),
              ),
            ),
          ),
        ),
      ),
    );
  }
}



특정 키워드로 검색하기


import 'package:best_friend/api/article_model.dart';
import 'package:best_friend/api/data_of_articles.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

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

  @override
  State<Search> createState() => _SearchState();
}

class _SearchState extends State<Search> {
  String _searchText = "";
  final List<Article> dataList = DataOfArticles.listArticles;
  List<Article> filteredList = [];

  void _filterList() {
    filteredList = dataList
        .where((article) =>
            article.title.toLowerCase().contains(_searchText.toLowerCase()))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          '검색',
          style: TextStyle(
            fontSize: 27,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
        backgroundColor: Colors.blue,
        actions: const [],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                onChanged: (value) {
                  setState(() {
                    _searchText = value;
                    _filterList();
                  });
                },
                decoration: const InputDecoration(
                  labelText: "Search",
                  prefixIcon: Icon(Icons.search),
                ),
              ),
            ),
            if (_searchText.isNotEmpty) ...[
              const Padding(
                padding: EdgeInsets.all(8.0),
                child: Text(
                  '검색 결과...',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
              ),
              const SizedBox(height: 10),
              for (var i = 0; i < filteredList.length; i++) ...[
                GestureDetector(
                  onTap: () => launchUrl(Uri.parse(filteredList[i].url)),
                  child: Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Container(
                      width: 450,
                      height: 60,
                      decoration: BoxDecoration(
                        color: const Color.fromARGB(255, 236, 247, 254),
                        borderRadius: BorderRadius.circular(5),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.grey.withOpacity(0.5),
                            spreadRadius: 2,
                            blurRadius: 10,
                            offset: const Offset(0, 3),
                          ),
                        ],
                      ),
                      // Your container code...
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: ListTile(
                          title: Container(
                            child: Text(
                              '${i + 1}. ${filteredList[i].title}',
                              style: const TextStyle(
                                fontSize: 15,
                              ),
                              textAlign: TextAlign.start,
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 20), // Increased spacing between items
              ],
            ],
          ],
        ),
      ),
    );
  }
}


TextField. 입력을 받을 때


import 'package:flutter/material.dart';

class Input extends StatelessWidget {
  final double sizeOfBox;
  final String category;
  const Input({super.key, this.sizeOfBox = 130, required this.category});

  // 이 함수에서 입력된 값을 handling 함.
  void textChanged(String value) {
    print(value);
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text(
          category,
          style: const TextStyle(
              color: Colors.black, fontSize: 30, fontWeight: FontWeight.bold),
        ),
        const SizedBox(
          width: 70,
        ),
        // TexField 를 클래스로 만들 것.
        SizedBox(
          width: sizeOfBox,
          child: TextField(
            onChanged: textChanged,
            decoration: InputDecoration(
              labelText: '$category를 입력하세요',
              hintText: ':',
            ),
          ),
        ),
        //BFInputField(nameInputField: '나이를 입력하세요'),
      ],
    );
  }
}


프론트엔드는 거의 완성 되었다. 백엔드가 남았는데, DB, 서버 등등 관련 지식이 거의 없어서 이것도 이번주에 열심히 공부해 보아야 겠다.