본문 바로가기
flutter

flutter 간단한 WebView app 만들기 (url_list_screen 구현편)

by Rogan_Kim 2023. 9. 23.
728x90

flutter webView app 만들기  url_list_screen 구현 편

 

완성본 스크린샷 및 기능 설명

1. 주소 리스트를 shared_prefeences로 로컬에 저장하고, 보여 줍니다.

2. 주소는 추가 할 수 있습니다.

 

3. 삭제도 할 수 있습니다.

 

 

4. url 클릭시 해당 주소로 이동할수 있으면 목표 달성

 

구현 하기

1. pubspec.yaml 패키지 추가

shared_prefereces

shared_preferences: ^2.2.0

2. main.dart 초기화

import 'package:flutter/material.dart';
import 'package:hybrid_app/url_list_screen.dart';

void main() async {
  // main() 함수에서 await 키워드를 사용하여 비동기 작업을 수행해야 하는 경우 사용해야함.
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MaterialApp(home: UrlListScreen()));
}

 

3. url_list_screen.dart  StatefullWidget으로생성 ( 작명 센스 꽝 )

import 'package:flutter/material.dart';

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

  @override
  State<UrlListScreen> createState() => _UrlListScreenState();
}

class _UrlListScreenState extends State<UrlListScreen> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

 

step 1.  List UI 구현 및 필요한 변수 선언

import 'package:flutter/material.dart';

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

  @override
  State<UrlListScreen> createState() => _UrlListScreenState();
}

class _UrlListScreenState extends State<UrlListScreen> {
  List<String> _itemList = []; // 1. urlList가 담길 배열


  @override
  Widget build(BuildContext context) {
    returnScaffold(
      appBar: AppBar(
        title: const Text('링크 설정'),
      ),
      body: ListView.builder(  // 2. _itemList 길이만큼 Widget 반복 생성
        itemCount: _itemList.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_itemList[index]),
          );
        },
      ),
      floatingActionButton: IconButton(  // 3. _itemList를 추가할  + 버튼
      	iconSize: 52,
        icon: const Icon(
          Icons.add_circle_outline_rounded,
        ),
        onPressed: (){}, // step.3에서 함수 추가할 예정
      ),
    );
  }
}

 urlList가 담길 배열을 구현하고 이를 배열의 길이만큼 Widget을 생성해줍니다.
반복된 Widget을 생성할대는 ListView.builder도 있지만 for문 혹은 map으로도 생성가능합니다. ( 그 외의 방법도 더 더 있음)

map으로 만들경우 예

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('링크 설정'),
      ),
      body: ListView(
        children: [
          ..._itemList
              .map(
                (item) => ListTile(
                  title: Text(item),
                ),
              )
              .toList()
        ],
      ),
    );
  }

for문으로 만들경우 

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('링크 설정'),
      ),
      body: ListView(
        children: [
          for (var item in _itemList)
            ListTile(
              title: Text(item),
            ),
        ],
      ),
    );
  }

 

step 2.  SharedPreferences에서 UrlList가져오기

  final String _localKey = 'webViewLink';   // localKey 변수로 선언해서 오타 방지
  
  @override
  void initState() { // 함수가 초기화 될때 한번만 동작한다.
    _getUrlList();
    super.initState();
  }

  Future<void> _getUrlList() async { // 여러번 쓸 것을 대비하여 함수로 구현
    final preferences = await SharedPreferences.getInstance();
    setState(() {
      final localUrlList = preferences.getStringList(_localKey);
      if (localUrlList != null) {
        _itemList = localUrlList;
      }
    });
  }

initState 동작 원리 간단하게 알고 넘어가기.

  1. initState는 Widget의 LifeCycle동안 오직 1번만 수행됩니다.
  2. initState의 수행이 완료되지 않았더라도 build 함수가 호출 될 수 있습니다.
  3. initState 자체는 async 함수가 될 수 없습니다.
  4. initState 에서도 BuildContext를 사용할 수 있습니다.

preferenecs는 다양한 메서드가 있습니다.

문서를 보고 사용법을 알 수도 있지만, 플루터는 너무 친절해서 메서드명과 친절한 설명을 보고 유추해보고 써보면 대부분 다 맞음.

( 사실 다른 프레임워크도 마찬가지 )

 

step 3.  TextField을 활용한 _itemList 추가

    // 컨트롤러
    final TextEditingController _textEditingController = TextEditingController();
    // TextFiedl style로 2번 반복되어서 변수로 선언
    final TextStyle _textStyle = const TextStyle(color: Colors.white);

    // 1. page route
    void _addUrlList() {
      Navigator.of(context).push(
        PageRouteBuilder(
          opaque: false,
          pageBuilder: (BuildContext context, _, __) => _fullTextFieldScreen(),
        ),
      );
    }
    
   // 4. submit 이벤트 함수
    Future<void> _textFiledSubmitted(String value) async {
      final preferences = await SharedPreferences.getInstance();
      _itemList.add(value);
      preferences.setStringList(_localKey, _itemList);
      setState(() {});
    }

  
   // 2. textFieldScreen  
    Widget _fullTextFieldScreen() {
    return Scaffold(
      backgroundColor: Colors.black.withOpacity(
        0.45,
      ),
      appBar: AppBar(),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: TextField(
            onSubmitted: (value) async { // keyboard의 '완료/ done'을 누르면 일어나는 이벤트
              if (await _checkValidUrl(value)) {
                await _textFiledSubmitted(value);
                if (mounted) {
                  _textEditingController.text = '';
                  Navigator.of(context).pop();
                }
              }
            },
            controller: _textEditingController,
            style: _textStyle,
            textAlign: TextAlign.center,
            decoration: InputDecoration(
              hintText: 'https://example.com',
              hintStyle: _textStyle,
              border: const UnderlineInputBorder(
                borderSide: BorderSide(
                  color: Colors.white,
                ),
              ),
              enabledBorder: const UnderlineInputBorder(
                borderSide: BorderSide(
                  color: Colors.white,
                ),
              ),
              focusedBorder: const UnderlineInputBorder(
                borderSide: BorderSide(
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ),
      ),
    );

   // 3. 올바른 주소인지 확인하는 함수
   Future<bool> _checkValidUrl(String value) async {
    if (isURL(value)) {
      return true;
    } else {
      await showDialog(
        context: context,
        builder: (BuildContext context) => AlertDialog(
          title: const Text(
            '경고',
            textAlign: TextAlign.center,
          ),
          content: const Text('http, https로 시작하는 url이 아닙니다.'),
          actions: [
            TextButton(
              child: const Text('확인'),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      );
      return false;
    }
  }
  
  
  =======  utils/is_url.dart ==========
  bool isURL(String input) {
  final pattern = RegExp(r'^(https?://)');
  return pattern.hasMatch(input);
}

flutter에서 TextField의 값을 컨트롤할려면 'TextEdittingConroller'를 사용한다.

사용방법은 TextField Widget의 controller에 값만 지정해주면 된다.

 

그리고 Build 메서드의  IConButton을 찾아 onPress에 함수 지정해주기

      floatingActionButton: IconButton(
        iconSize: 52,
        icon: const Icon(
          Icons.add_circle_outline_rounded,
        ),
        onPressed: _addUrlList,
      ),

 

지정된 번호 순으로 함수가 동작합니다. 부가설명을 하자면 아래와 같습니다.

1. Navigator.of(context).push

 

Navigator class - widgets library - Dart API

A widget that manages a set of child widgets with a stack discipline. Many apps have a navigator near the top of their widget hierarchy in order to display their logical history using an Overlay with the most recently visited pages visually on top of the o

api.flutter.dev

flutter에서 page navigate를 구현하는데 사용합니다.

push로 페이지를 구성하면 history가 쌓여서 'pop'메서드로 뒤로가리를 실행할 수 있습니다.

뒤로가기를 할때는 아래의 함수를 사용합니다.

Navigator.of(context).pop();

2. textFieldScreen

 app에서 인풋창을 사용할때는 keyboard가 하단에서 올라오는거를 고려해줘야합니다.

(예시로 하단에서 keyboard가 올라오면 height값이 좁아지면서 overflow error를 마주 칠 수도 있고,

혹은 input이 keyboard에 가려질 수도 있습니다.)

저는 카카오톡 프로필 메세지 인풋창에 영감을 받아 fullScreen으로 구현해 봤습니다.

 

3. input에 입력된 url이 올바른 패턴인지 확인해주는 함수입니다. 그리고 올바르지 않으면 alert으로 경고를 줍니다.

 

4. _itemList에 url을 추가해주고 setState로 hook을 일으킵니다.

(react의 setState와 동작이 유사합니다. page를 rebuild 해줍니다.)

 

 

 

step 4. 삭제 구현

 

listTile에 있는 메서드중에 onLongPress에 삭제를 구현해보왔습니다.

class _UrlListScreenState extends State<UrlListScreen> {

  /// 3. url 삭제 
  Future<void> _deleteSelectedUrl(String url) async {
    final preferences = await SharedPreferences.getInstance();
    _itemList.remove(url);
    preferences.setStringList(_localKey, _itemList);
    setState(() {});
  }

  /// 2. url 삭제 시도 확인하는 dialog
  Future<void> deleteDialog(String url) async {
    await showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Text(
          '삭제 하기',
          textAlign: TextAlign.center,
        ),
        actions: [
          TextButton(
            child: const Text('확인'),
            onTap: () { // step 5.에서 구현
              _navigateToWebView(_itemList[index]);
            },
            onPressed: () async {
              await _deleteSelectedUrl(url);
              if (mounted) {
                Navigator.pop(context);
              }
            },
          ),
          TextButton(
            child: const Text('취소'),
            onPressed: () => Navigator.pop(context),
          ),
        ],
      ),
    );
  }


  @override
  Widget build(BuildContext context) {
    returnScaffold(
      appBar: AppBar(
        title: const Text('링크 설정'),
      ),
      body: ListView.builder(  
        itemCount: _itemList.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_itemList[index]),
            onLongPress: () async { // 1. 길게 누르면 이벤트 발생
             await deleteDialog(_itemList[index]);
           },
          );
        },
      ),
    );
  }
}

 

step 5. webView로 이동

  // 어떤 url로 이동했는지 넘겨주기
  void _navigateToWebView(String url) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) {
          return WebViewScreen(
            url: url,
          );
        },
      ),
    );
  }

 

 

이상으로 url_list_screen 구현은 마무리 하였습니다.

 

 

 

해당코드 구현은 https://github.com/kimjuno97/hybrid_app 에서 확인할 수 있습니다.

 

추가로 참고할만한 블로그 
https://kimjunho97.tistory.com/30

 

flutter webView 세팅 및 데이터 주고 받기

시작하기 사용할 도메인 https://www.chaam.co.kr/login 사용할 패키지 https://pub.dev/packages/webview_flutter project 생성 및 패키지 설치 flutter create chaam_webview dependencies에 추가 dependencies: webview_flutter: ^4.2.4 웹

kimjunho97.tistory.com

 

728x90

댓글