자체 기획앱
아래는 공개된 API의 리스트를 공유하는 Github 문서이다.
아래의 링크에 들어가서, 공개되어있는 API중 하나를 분석하고 직접 기획하여 본인이 기획한 어플을 제작하시오.
- https://github.com/public-apis/public-apis기획 과제내용을 간단히 2-3줄로 설명하여 제출하시오.
이 때 해결 과정을 함께 포함하여 정리하시오.
(어려웠던 내용과 헤맸던 내용이 있다면 반드시 기재할 것)다음의 조건을 반드시 만족할 것.
- 페이지는 두 페이지 이상이어야 하며 네비게이션을 활용하여 페이지 이동이 포함될 것
- 클래스를 작성하여 Serialization이 적용될 수 있도록 할 것
- 적절한 애니메이션 효과를 포함할 것
미국의 맥주 양조장을 조회할 수 있는 앱
도시별, 타입별 맥주 양조장을 조회하고, 양조장을 검색할 수 있다. 양조장 상세 페이지를 조회하면 조회 기록이 저장되고, history 페이지에서 조회 내역을 확인할 수 있다.
https://www.openbrewerydb.org/documentation
[ { "id": "5128df48-79fc-4f0f-8b52-d06be54d0cec", "name": "(405) Brewing Co", "brewery_type": "micro", "address_1": "1716 Topeka St", "address_2": null, "address_3": null, "city": "Norman", "state_province": "Oklahoma", "postal_code": "73069-8224", "country": "United States", "longitude": "-97.46818222", "latitude": "35.25738891", "phone": "4058160490", "website_url": "http://www.405brewing.com", "state": "Oklahoma", "street": "1716 Topeka St" }, ... ]
Figma : Brewery
import 'package:flutter/material.dart';
import 'page/main_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: MainPage(),
);
}
}
class Brewery {
String id;
String name;
String breweryType;
String? address1;
String? address2;
String? address3;
String city;
String stateProvince;
String postalCode;
String country;
String? longitude;
String? latitude;
String? phone;
String? websiteUrl;
String state;
String? street;
String imgUrl;
Brewery({
required this.id,
required this.name,
required this.breweryType,
required this.address1,
required this.address2,
required this.address3,
required this.city,
required this.stateProvince,
required this.postalCode,
required this.country,
required this.longitude,
required this.latitude,
required this.phone,
required this.websiteUrl,
required this.state,
required this.street,
this.imgUrl = 'https://picsum.photos/100/100',
});
factory Brewery.fromMap(Map<String, dynamic> map) {
return Brewery(
id: map['id'],
name: map['name'],
breweryType: map['brewery_type'],
address1: map['address_1'],
address2: map['address_2'],
address3: map['address_3'],
city: map['city'],
stateProvince: map['state_province'],
postalCode: map['postal_code'],
country: map['country'],
longitude: map['longitude'],
latitude: map['latitude'],
phone: map['phone'],
websiteUrl: map['website_url'],
state: map['state'],
street: map['street'],
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'brewery_type': breweryType,
'address_1': address1,
'address_2': address2,
'address_3': address3,
'city': city,
'state_province': stateProvince,
'postal_code': postalCode,
'country': country,
'longitude': longitude,
'latitude': latitude,
'phone': phone,
'website_url': websiteUrl,
'state': state,
'street': street,
};
}
String toString() => 'Brewery($name), $breweryType';
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../model/brewery.dart';
import '../widget/floating_button.dart';
import 'brewery_page.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<String> cityList = []; // 전체 도시 리스트
List<Brewery> breweries = []; // 양조장 리스트
Future<List<Brewery>> getData() async {
Dio dio = Dio();
String url = 'https://api.openbrewerydb.org/v1/breweries';
var res = await dio.get(url);
if (res.statusCode == 200) {
var data = List<Map<String, dynamic>>.from(res.data);
breweries = data.map((e) => Brewery.fromMap(e)).toList();
// breweries의 breweryType들을 Set 자료형으로 중복제거 후 리스트로 변환
cityList = breweries.map((e) => e.city).toSet().toList();
// cityList 정렬
cityList.sort((a, b) => a.compareTo(b));
return breweries;
}
return [];
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Brewery'),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
),
body: FutureBuilder(
future: getData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
return ListView.separated(
itemCount: cityList.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Text('city', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
Divider(),
],
),
);
}
var city = cityList[index-1];
return ListTile(
title: Text(city, style: TextStyle(fontSize: 20)),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BreweryPage(city: city)),
);
},
);
},
separatorBuilder: (context, index) => Divider()
);
} else {
return Center(child: Text('null'));
}
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingButton(context, breweries),
);
}
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:first_app/homework/week6/week6_brewery/widget/brewery_tile.dart';
import '../model/brewery.dart';
import 'package:flutter/material.dart';
class BreweryPage extends StatefulWidget {
const BreweryPage({Key? key, required this.city}) : super(key: key);
final String city;
State<BreweryPage> createState() => _BreweryPageState();
}
class _BreweryPageState extends State<BreweryPage> {
List<String> typeList = []; // 양조장 타입 리스트
int selectedTabIdx = 0; // 선택한 양조장 타입 버튼의 인덱스
// 데이터 불러오기
Future<List<Brewery>> getData() async {
Dio dio = Dio();
var url = 'https://api.openbrewerydb.org/v1/breweries?by_city=${widget.city}';
var res = await dio.get(url);
if (res.statusCode == 200) {
var data = List<Map<String, dynamic>>.from(res.data);
var breweries = data.map((e) => Brewery.fromMap(e)).toList();
// breweries의 breweryType들을 Set 자료형으로 중복제거 후 리스트로 변환
typeList = breweries.map((e) => e.breweryType).toSet().toList();
log('$typeList');
return breweries;
}
return [];
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.city),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
body: FutureBuilder(
future: getData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
var breweries = snapshot.data!;
// 양조장 타입 버튼으로 필터링된 양조장 리스트
List<Brewery> filteredBreweries = [];
for (var brewery in breweries) {
if (selectedTabIdx == 0) {
filteredBreweries.add(brewery);
} else {
if (brewery.breweryType == typeList[selectedTabIdx-1]) {
filteredBreweries.add(brewery);
}
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 50,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: typeList.length + 1,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ElevatedButton(
// 선택된 인덱스의 버튼만 색 변경
style: ElevatedButton.styleFrom(
foregroundColor: selectedTabIdx == index ? Colors.white : Colors.black,
backgroundColor: selectedTabIdx == index ? Colors.black : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
)
),
onPressed: (){
setState(() {
// 선택된 인덱스 업데이트
selectedTabIdx = index;
});
},
child: Text(index == 0 ? 'all' : typeList[index-1])
),
);
}
),
),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: filteredBreweries.length,
itemBuilder: (context, index) {
var brewery = filteredBreweries[index];
return BreweryTile(brewery: brewery);
}
),
),
],
);
}
return Container();
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
import 'package:url_launcher/url_launcher_string.dart';
import '../model/brewery.dart';
import 'package:flutter/material.dart';
class BreweryDetailPage extends StatelessWidget {
const BreweryDetailPage({Key? key, required this.brewery}) : super(key: key);
final Brewery brewery;
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Container(
width: 200,
child: Text(brewery.name, overflow: TextOverflow.ellipsis)
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Container(
width: double.infinity,
height: 250,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(brewery.imgUrl),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.black54, BlendMode.darken
),
),
),
),
Positioned(
right: 0,
bottom: 0,
child: Row(
children: [
brewery.websiteUrl != null
? ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
)
),
onPressed: (){
launchUrlString(brewery.websiteUrl!);
},
child: Text('website'),
)
: Container(),
SizedBox(width: 8),
brewery.phone != null
? ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
)
),
onPressed: (){
launchUrlString('sms:${brewery.phone!}');
},
child: Icon(Icons.call, color: Colors.white),
)
: Container(),
SizedBox(width: 8),
],
),
)
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(brewery.name, style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),),
SizedBox(height: 16),
Row(
children: [
Container(
padding: EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.0),
border: Border.all(color: Colors.white),
color: Colors.transparent,
),
child: Text(brewery.breweryType, style: TextStyle(fontSize: 16, color: Colors.white)),
),
SizedBox(width: 16),
brewery.phone != null
? Text('phone: ${brewery.phone}', style: TextStyle(fontSize: 16))
: Container()
],
),
SizedBox(height: 20),
brewery.street != null
? Text(brewery.street!, style: TextStyle(fontSize: 16))
: Container(),
SizedBox(height: 8),
Text('${brewery.city}, ${brewery.state} ${brewery.postalCode}', style: TextStyle(fontSize: 16))
],
),
),
)
],
),
);
}
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:first_app/homework/week6/week6_brewery/widget/brewery_tile.dart';
import 'package:flutter/material.dart';
import '../model/brewery.dart';
class SearchPage extends StatefulWidget {
const SearchPage({Key? key, required this.breweries}) : super(key: key);
final List<Brewery> breweries;
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
List<Brewery> searchResult = []; // 검색 결과
// 데이터 불러오기
getData(String keyword) async {
Dio dio = Dio();
String url = 'https://api.openbrewerydb.org/v1/breweries/search?query=$keyword';
try {
var res = await dio.get(url);
if (res.statusCode == 200) {
var data = List<Map<String, dynamic>>.from(res.data);
searchResult = data.map((e) => Brewery.fromMap(e)).toList();
}
} on DioError { // 검색 결과 없거나 에러 발생시 빈 리스트로 초기화
searchResult = [];
}
log('$searchResult');
setState(() { });
}
void initState() {
super.initState();
// 검색 전에는 전체 리스트 보여주기
searchResult = widget.breweries;
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search'),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search",
suffixIcon: Icon(Icons.search),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
onSubmitted: (value) {
getData(value);
},
),
),
Expanded(
child: ListView.builder(
itemCount: searchResult.length,
itemBuilder: (context, index) {
var brewery = searchResult[index];
return BreweryTile(brewery: brewery);
}
),
),
],
),
);
}
}
import 'dart:convert';
import 'package:first_app/homework/week6/week6_brewery/widget/brewery_tile.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/brewery.dart';
class HistoryPage extends StatefulWidget {
const HistoryPage({Key? key, required this.breweries}) : super(key: key);
final List<Brewery> breweries;
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
List<Brewery> _history = [];
void initState() {
super.initState();
_loadHistory();
}
Future<void> _loadHistory() async {
final prefs = await SharedPreferences.getInstance();
final history = prefs.getStringList('history') ?? [];
setState(() {
_history = history.map((json) => Brewery.fromMap(jsonDecode(json))).toList();
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('History'),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
body: ListView.builder(
itemCount: _history.length,
itemBuilder: (context, index) {
// _history 리스트의 끝에서부터 역순으로 접근
final brewery = _history[_history.length - 1 - index];
return BreweryTile(brewery: brewery);
},
),
);
}
}
// floating button
import 'package:flutter/material.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import '../page/history_page.dart';
import '../page/search_page.dart';
Widget FloatingButton(context, breweries) {
return SpeedDial(
overlayOpacity: 0.3,
overlayColor: Colors.black.withOpacity(0.5),
animatedIcon: AnimatedIcons.menu_close,
visible: true,
curve: Curves.bounceIn,
children: [
SpeedDialChild(
backgroundColor: Colors.white,
child: Icon(Icons.search, color: Colors.black),
labelWidget: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text('search', style: TextStyle(fontSize: 16, color: Colors.white)),
),
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SearchPage(breweries: breweries)),
);
},
),
SpeedDialChild(
backgroundColor: Colors.white,
child: Icon(Icons.list, color: Colors.black),
labelWidget: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text('history', style: TextStyle(fontSize: 16, color: Colors.white)),
),
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HistoryPage(breweries: breweries)),
);
},
),
],
);
}
import 'dart:convert';
import 'dart:developer';
import 'package:first_app/homework/week6/week6_brewery/page/brewery_detail_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/brewery.dart';
import 'package:flutter/material.dart';
class BreweryTile extends StatelessWidget {
const BreweryTile({Key? key, required this.brewery}) : super(key: key);
final Brewery brewery;
Widget build(BuildContext context) {
void _onListItemTap() async {
final prefs = await SharedPreferences.getInstance();
// 방문 기록을 가져오고 저장된 데이터가 없으면 빈 리스트를 생성
final history = prefs.getStringList('history') ?? [];
log('$history');
// brewery 객체를 json 문자열로 변환
final breweryJson = jsonEncode(brewery.toMap());
// 방문 기록에 json 문자열을 추가
history.add(breweryJson);
// prefs 에 방문 기록을 저장
await prefs.setStringList('history', history);
}
return GestureDetector(
child: SizedBox(
height: 100,
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(backgroundImage: NetworkImage(brewery.imgUrl), radius: 48),
),
Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 210,
child: Text(
brewery.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 8),
Text(brewery.breweryType),
Text('${brewery.city}, ${brewery.state}'),
],
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Icon(Icons.arrow_forward_ios),
)
],
),
],
),
),
onTap: () {
_onListItemTap();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BreweryDetailPage(brewery: brewery)),
);
},
);
}
}
이번 과제에서는 주어진 예시에 따라 코드만 작성하는 것이 아니라 직접 기획과 디자인까지 하고 코드를 작성해야 했는데, 처음엔 api 고르기도 막막했지만 과제를 끝내고 보니까 복습도 아주 잘 된것 같고 ... 무엇보다 내가 잘 이해한 부분과 조금 부족한 부분을 확실하게 구분하는 데에 많은 도움이 된 것 같습니다!
다음 월말평가랑 팀프로젝트가 기대되네요~~~