android - 영단어 플래시 카드 + papago
inspiration
초, 중, 고 과정의 영단어를 교육부 홈페이지에서 찾아보니.. 3000천여개가 되는 것 같다.
간단한 플래시 카드를 만들어 본다.
execution
영단어가 나오고, pass, del, hint 버튼이 있다.
✶ 계속 외워야할 필요가 있는 단어는 두고, pass를 누르면 다음 단어로 변경,
✶ 외운 단어는 del을 누르면 리스트에 삭제, 삭제된 단어는 별도 페이지에서 관리,
✶ hint 버튼을 누르면, 해당단어에 대한 papago 해석 페이지가 하단 웹뷰로 노출.
papago의 해석은 별로이지만, 하단에 사전 기능과 듣기 기능이 있으므로, 정확한 해석과 발음을 보기에 나쁘지 않다. 따로 사전 부분만 api로 떼어오려고 했는데, api 서비스가 종료되었다고 한다.
→ 사용된 패키지는 webview_flutter, shared_preference 이다.
→ 영단어가 추가되지 않는다는 전제로 별도 DB작업은 하지 않았으며,
→ 마음같아서는 인터넷 연결없이 실행되도록 하고 싶었는데, 역량의 한계이다.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:math';
import 'word.dart'; // 여기에 wordslist가 정의되어 있어야 합니다.
void main() {
WidgetsFlutterBinding.ensureInitialized(); // WebView 초기화
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '영단어 퀴즈',
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: WordQuizPage(),
);
}
}
class WordQuizPage extends StatefulWidget {
@override
_WordQuizPageState createState() => _WordQuizPageState();
}
class _WordQuizPageState extends State<WordQuizPage> {
int wordlistcount = 0;
int currentIndex = 0;
bool showHint = false;
late final WebViewController _controller;
List<String> words = [];
List<String> del_words = [];
Future<void> _loadList() async {
final prefs = await SharedPreferences.getInstance();
final newWords =
prefs.getStringList('wordslist') ?? List<String>.from(wordslist);
final newDelWords = prefs.getStringList('del_wordslist') ?? [];
final currentWord = words.isNotEmpty ? words[currentIndex] : null;
setState(() {
words = newWords;
del_words = newDelWords;
wordlistcount = words.length;
if (currentWord != null && words.contains(currentWord)) {
currentIndex = words.indexOf(currentWord); // 현재 단어 유지
} else {
currentIndex = words.isNotEmpty ? Random().nextInt(words.length) : 0;
}
});
}
Future<void> _listChange(int idx) async {
final prefs = await SharedPreferences.getInstance();
setState(() {
del_words.add(words[idx]);
words.removeAt(idx);
wordlistcount = words.length;
});
await prefs.setStringList('wordslist', words);
await prefs.setStringList('del_wordslist', del_words);
if (words.isNotEmpty) {
setState(() {
currentIndex = Random().nextInt(wordlistcount);
showHint = false;
});
} else {
setState(() {
currentIndex = 0;
showHint = false;
});
}
}
@override
void initState() {
super.initState();
_loadList();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('https://papago.naver.com'));
}
void _showHint() {
if (words.isEmpty) return;
final word = words[currentIndex];
final url = 'https://papago.naver.com/?sk=auto&tk=en&st=$word';
_controller.loadRequest(Uri.parse(url));
setState(() => showHint = true);
}
void _nextWord() {
if (words.isEmpty) return;
setState(() {
currentIndex = Random().nextInt(words.length);
showHint = false;
});
}
@override
Widget build(BuildContext context) {
final currentWord = words.isNotEmpty ? words[currentIndex] : "단어 없음";
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Text('? 영단어 퀴즈'),
SizedBox(width: 12),
Text(
'(${words.length}/${wordslist.length})',
style: TextStyle(fontSize: 16, color: Colors.grey[300]),
),
],
),
actions: [
IconButton(
onPressed: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DelWords(dataList: del_words),
),
);
_loadList();
},
icon: Icon(Icons.list_alt),
),
],
),
body: Column(
children: [
SizedBox(height: 30),
Center(
child: Text(
currentWord,
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
),
SizedBox(height: 10),
if (words.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(onPressed: _nextWord, child: Text('PASS')),
ElevatedButton(
onPressed: () => _listChange(currentIndex),
child: Text('DEL'),
),
ElevatedButton(onPressed: _showHint, child: Text('HINT')),
],
)
else
Padding(
padding: const EdgeInsets.all(20.0),
child: Text('? 모든 단어를 완료했습니다!', style: TextStyle(fontSize: 20)),
),
Divider(color: Colors.white, thickness: 0.5),
if (showHint && words.isNotEmpty)
Expanded(child: WebViewWidget(controller: _controller)),
],
),
);
}
}
class DelWords extends StatefulWidget {
final List<String> dataList;
const DelWords({super.key, required this.dataList});
@override
State<DelWords> createState() => _DelWordsState();
}
class _DelWordsState extends State<DelWords> {
late List<String> delWords;
@override
void initState() {
super.initState();
delWords = List<String>.from(widget.dataList); // 복사본 생성
}
Future<void> _restoreWord(String word) async {
final prefs = await SharedPreferences.getInstance();
List<String> words = prefs.getStringList('wordslist') ?? [];
List<String> deleted = prefs.getStringList('del_wordslist') ?? [];
if (deleted.contains(word)) {
deleted.remove(word);
words.add(word);
await prefs.setStringList('wordslist', words);
await prefs.setStringList('del_wordslist', deleted);
setState(() {
delWords.remove(word); // UI에서 제거
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('삭제된 단어 목록')),
body: delWords.isEmpty
? Center(child: Text('삭제된 단어가 없습니다.'))
: ListView.builder(
itemCount: delWords.length,
itemBuilder: (context, index) {
final word = delWords[index];
return ListTile(
title: Text(word),
trailing: IconButton(
icon: Icon(Icons.restore),
onPressed: () => _restoreWord(word),
),
);
},
),
);
}
}
영어 단어 리스트는 words.dart 파일에 wordslist 변수로 넣어줘야 한다.
[pjt]\android\app\src\main\AndroidManifest.xml 안에 인터넷 사용 권한 permission을 넣어주는 것을 잊지 말아야 한다.
<uses-permission android:name=“android.permission.INTERNET” />
이유는 모르겠는데, 최근 ndk 버전을 지정하라는 에러가 계속 뜨니,
[pjt]\android\app\build.gradle.kts 파일안에 ndk 버전을 에러문구와 같이 적어버린다. 나중에 무슨 문제가 생길지는 모른다.
ndkVersion = “27.0.12077973”
대략 이런 모습이 되겠다.


conclusion
내가 사용자가 아니기 때문에, 예상되는 불편한 점을 예상할 수는 없다.
시간이 될 때, 조금씩 업데이트를 해보자.
끝.