flutter webView app 만들기 url_list_screen 구현 편
완성본 스크린샷 및 기능 설명
1. 주소 리스트를 shared_prefeences로 로컬에 저장하고, 보여 줍니다.
2. 주소는 추가 할 수 있습니다.
3. 삭제도 할 수 있습니다.
4. url 클릭시 해당 주소로 이동할수 있으면 목표 달성
구현 하기
1. pubspec.yaml 패키지 추가
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 동작 원리 간단하게 알고 넘어가기.
- initState는 Widget의 LifeCycle동안 오직 1번만 수행됩니다.
- initState의 수행이 완료되지 않았더라도 build 함수가 호출 될 수 있습니다.
- initState 자체는 async 함수가 될 수 없습니다.
- 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,
),
지정된 번호 순으로 함수가 동작합니다. 부가설명을 하자면 아래와 같습니다.
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' 카테고리의 다른 글
flutter 간단한 WebView app 만들기 (home_widget 후편) (0) | 2023.09.26 |
---|---|
flutter 간단한 WebView app 만들기 (home_widget 전편) (0) | 2023.09.25 |
Flutter Android App Icon 변경 (초간단) (0) | 2023.09.21 |
Flutter ios app icon 변경 (초 간단) (0) | 2023.09.21 |
"Error 'DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead' in Xcode 15 beta 5" (0) | 2023.09.20 |
댓글