
* 겪고 있는 문제 상황을 최대한 자세하게 작성해주세요.
* 문제 해결을 위해 어떤 시도를 해보았는지 구체적으로 함께 알려주세요.
String 타입으로 제대로 지정하지 못해서 에러 나는 것 같은데 List<String>로 book.dart에서 지정해도 안되네요....
전체 화면 캡
보고 계신 화면

작성한 코드 및 에러 메세지
//main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:watcha/book.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'book_service.dart';
import 'book.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => BookService(),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var bottomNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
SearchPage(),
LikedPage(),
].elementAt(bottomNavIndex),
bottomNavigationBar: BottomNavigationBar(
onTap: (value) {
setState(() {
bottomNavIndex = value;
});
},
currentIndex: bottomNavIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: '좋아요',
),
],
selectedItemColor: Colors.black,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedFontSize: 12,
unselectedFontSize: 12,
),
);
}
}
class SearchPage extends StatelessWidget {
const SearchPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(builder: (context, bookService, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: SafeArea(
child: Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
),
child: Container(
padding: EdgeInsets.only(
left: 10,
right: 10,
),
width: MediaQuery.sizeOf(context).width,
decoration: BoxDecoration(
color: Color.fromARGB(255, 250, 246, 238),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.black),
),
child: Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: TextField(
onSubmitted: (value) {
bookService.search(value);
},
cursorColor: Color.fromARGB(255, 192, 192, 192),
decoration: InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: '작품명, 배우, 감독 등을 검색하세요',
hintStyle: TextStyle(
fontSize: 13,
),
),
),
),
),
),
),
),
body: ListView.separated(
itemCount: bookService.bookList.length,
itemBuilder: (context, index) {
if (bookService.bookList.isEmpty) return SizedBox();
Book book = bookService.bookList.elementAt(index);
return BookTile(book: book);
},
separatorBuilder: (context, index) {
return Divider();
},
),
);
});
}
}
class BookTile extends StatelessWidget {
const BookTile({
super.key,
required this.book,
});
final Book book;
@override
Widget build(BuildContext context) {
BookService bookService = context.read<BookService>();
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(
url: book.previewLink.replaceFirst("http", "https"),
),
),
);
},
leading: Image.network(
book.thumbnail,
fit: BoxFit.fitHeight,
),
title: Text(
book.title,
),
subtitle: Text(
"${book.authors.join(", ")}\n${book.publishedDate}",
),
trailing: IconButton(
onPressed: () {
bookService.toggleLikedBook(book: book);
},
icon: bookService.likedList.map((book) => book.id).contains(book.id)
? Icon(
Icons.favorite,
color: Colors.red[400],
)
: Icon(Icons.favorite_border),
),
);
}
}
class LikedPage extends StatelessWidget {
const LikedPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(builder: (context, BookService, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 25, right: 25),
child: Container(
padding: EdgeInsets.only(
left: 10,
right: 10,
),
width: MediaQuery.sizeOf(context).width,
decoration: BoxDecoration(
color: Color.fromARGB(255, 250, 246, 238),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.black),
),
child: Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: TextField(
onSubmitted: (value) {},
cursorColor: Color.fromARGB(255, 192, 192, 192),
decoration: InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: "'좋아요' 누른 작품들 중 검색할 수 있어요",
hintStyle: TextStyle(
fontSize: 13,
),
),
),
),
),
),
),
),
body: ListView.separated(
itemCount: BookService.likedList.length,
itemBuilder: (context, index) {
if (BookService.likedList.isEmpty) return SizedBox();
Book book = BookService.likedList.elementAt(index);
return BookTile(book: book);
},
separatorBuilder: (context, index) {
return Divider();
},
),
);
});
}
}
class WebViewPage extends StatelessWidget {
WebViewPage({super.key, required this.url});
String url;
@override
Widget build(BuildContext context) {
return Scaffold(
body: WebView(initialUrl: url),
);
}
}
..
//book.dart
import 'package:flutter/material.dart';
class Book {
String id;
String title;
String subtitle;
List authors;
String publishedDate;
String thumbnail;
String previewLink;
Book({
required this.id,
required this.title,
required this.subtitle,
required this.authors,
required this.publishedDate,
required this.thumbnail,
required this.previewLink,
});
}
..
//book_service.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'book.dart';
class BookService extends ChangeNotifier {
List<Book> bookList = [];
List<Book> likedList = [];
Future<void> loadLikedBooks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String>? likedBooks = prefs.getStringList('likedBooks');
if (likedBooks != null) {
likedList = likedList
.map((id) => likedList.firstWhere((book) => book.id == id))
.toList();
}
}
void toggleLikedBook({required Book book}) {
String bookId = book.id;
if (likedList.map((book) => book.id).contains(bookId)) {
likedList.removeWhere((book) => book.id == bookId);
} else {
likedList.add(book);
}
notifyListeners();
}
Future<void> saveLikedBooks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> likedBooksIds = likedList.map((book) => book.id).toList();
await prefs.setStringList('likedBooks', likedBooksIds);
}
void search(String q) async {
bookList.clear();
if (q.isNotEmpty) {
Response res = await Dio().get(
"https://www.googleapis.com/books/v1/volumes?q=dog&startIndex=0&maxResults=40",
);
List items = res.data["items"];
for (Map<String, dynamic> item in items) {
Book book = Book(
id: item['id'],
title: item['volumeInfo']['title'] ?? "",
subtitle: item['volumeInfo']['subtitle'] ?? "",
authors: item['volumeInfo']['authors'] ?? [],
publishedDate: item['volumeInfo']['publishedDate'] ?? "",
thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
"https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
previewLink: item['volumeInfo']['previewLink'] ?? "",
);
bookList.add(book);
}
}
notifyListeners();
}
}
