|
안녕하세요.
이번에는 Flutter Project 메뉴 구성하는 법에 대해 글을 써보게 되었습니다.
저는 다음과 같이 Flutter Project Menu 를 구성해보았습니다.
저는 GetX 에 대해 아직은 잘 몰라서 Provider 에 대해서만 File Directory 를 생성해보았습니다.
이번에는 각 File 및 File-Directory 별로 어떻게 구성이 되는지 보겠습니다.
main.dart
import 'package:flutter/material.dart';
import 'package:myTestApp_Test/model/model_home.dart';
import 'package:myTestApp_Test/provider/provider_home.dart';
import 'package:myTestApp_Test/shared/style/style.dart';
import 'package:myTestApp_Test/shared/style/text.dart';
import 'package:provider/provider.dart';
class ListTileHome extends StatelessWidget{
final ModelHome listHome;
ListTileHome({@required this.listHome});
Widget _buildImage(){
return Container(
child: Image.asset(
listHome.homeImage,
fit: BoxFit.fill
)
);
}
Widget _buildBody(BuildContext context){
return Container(
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Divider(height: 5, color: Colors.transparent),
TextDesign(
basicText: listHome.homeTitle,
textStyle: mainListSize
),
]
)
);
}
@override
Widget build(BuildContext context) {
return Container(
child: GestureDetector(
onTap: (){
Provider.of<ProviderHome>(context, listen: false).selectHomeMenu(listHome);
Navigator.pushNamed(context, "/home/${listHome.homeID}");
},
child: Card(
child: Column(
children: [
_buildImage(),
_buildBody(context),
],
)
)
),
);
}
}
이와 같이 Widget 을 구성해볼 수도 있을 것 같습니다.
Provider
Provider 는 Widget File Directory 의 구조와 다르고, app_screen File Directory 의 구조와 비슷합니다.
다만, Provider 는 특정 Page 에서 DB 에 있는 내용들을 어떻게 화면에 뿌려줄 것이냐에 대해 미리 정할 수 있습니다.
예를 들어서 Data 들의 집합을 List 로 뿌려서 전체를 한꺼번에 보여줄 것인지, 아니면 상세 Page 와 같이 하나씩 보여주고자 Object 형태로 뿌려서 보여줄 것인지 정할 수 있습니다.
import 'package:flutter/material.dart';
import 'package:flutter_app/data/games.dart';
import 'package:flutter_app/model/game/game.dart';
import 'package:flutter_app/shared/helpers/helpers.dart';
class GameProvider with ChangeNotifier{
List<Game> _gameItems = DUMMY_GAMES.toList();
List<Game> _userItems = [];
Game _selectedGame;
List<Game> get gameItems => [..._gameItems];
List<Game> get userItems => [..._userItems];
Game get selectedGame => _selectedGame != null ? Game.from(_selectedGame) : null;
void createNewGameHome(Map<String, dynamic> data){
data['id'] = getRandomString(2);
print(data['id']);
final Game newGame = Game.fromJSON(data);
_userItems.add(newGame);
notifyListeners();
}
void createNewGameDiscover(Map<String, dynamic> data){
data['id'] = getRandomString(2);
final Game newGame = Game.fromJSON(data);
_gameItems.add(newGame);
notifyListeners();
}
void editNewGameHome(Map<String, dynamic> data){
final Game updatedGame = Game.fromJSON(data);
final int index = _userItems.indexWhere((g) => g.id == data['id']);
if(index >= 0){
_selectedGame = updatedGame;
_userItems[index] = updatedGame;
}
notifyListeners();
}
void editNewGameDisover(Map<String, dynamic> data){
final Game updatedGame = Game.fromJSON(data);
final int index = _userItems.indexWhere((g) => g.id == data['id']);
if(index >= 0){
_selectedGame = updatedGame;
_userItems[index] = updatedGame;
}
notifyListeners();
}
void changeProgression(Game selectedGame, double progression){
final int index = _userItems.indexWhere((game) => game.id == selectedGame.id);
if(index != 1){
final Game newGame = Game.from(selectedGame);
newGame.progression = progression;
_userItems[index] = newGame;
}
notifyListeners();
}
void changeFavorite(bool isFavorite){_selectedGame.isFavorite = isFavorite; notifyListeners();}
void selectGame(Game game){_selectedGame = game; notifyListeners();}
void addGameList(Game game){_userItems.add(game); notifyListeners();}
}
Shared
다음은 공통 File 들을 관리해주는 Shared 입니다.
여기서는 대부분 또는 모든 Page 에서 공통적으로 쓰이는 TextColor, Widget Color, 그 외 UI Design 과 관련된 내용들을 Shared File Directory 에 담아줍니다.
Helpers
Helpers 에서는 API 를 호출할 때 사용할 수도 있는 함수들을 여기에 배치해보았습니다.
예를 들어서 String Type 의 Data 들을 무작위로 가지고 올 수 있는 Code 와 같이 자주 쓰이지는 않습니다.
그렇지만, Data 호출 또는 그와 관련된 API 를 사용 시, Data 변환을 위해 사용하는 경우도 많이 있습니다.
공통으로 사용되는 Style 들에 대해 나열한 Code 입니다.
import 'package:flutter/material.dart';
const double defaultPadding = 20;
const Color boxBackgroundColor = Color(0xff0f151b);
const Color appBarColor = Color(0xDD000000);
const Color backgroundColor = Color(0x1F000000);
const Color textAccentColor = Color(0xDD000000);
const Color textGreyColor = Color(0xff9b9d9e);
const Color linkColor = Color(0xff02a1ff);
const Color lineColor = Color(0xDD000000);
const Color textYellowColor = Color(0xffe1d819);
const Color textInfoColor = Color(0x42000000);
// const Color textInfoColor = Color.fromRGBO(255, 255, 255, 0.7);
// Home
const mainFont = TextStyle(
decoration: TextDecoration.none,
fontSize: 25.0,
fontFamily: "icomoon",
fontWeight: FontWeight.w700,
color: Colors.black87
);
const contextFont = TextStyle(
color: Colors.black,
fontFamily: "icomoon",
fontSize: 15,
);
const subcontextFont = TextStyle(
color: Colors.black26,
fontFamily: "icomoon",
fontSize: 12,
);
// Settings
const settingsMainFont = TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold
);
const settingsContextFont = TextStyle(
color: textAccentColor,
fontFamily: "icomoon",
fontWeight: FontWeight.w700,
);
const settingsSubContextFont = TextStyle(
color: textInfoColor,
fontFamily: "icomoon",
fontWeight: FontWeight.w700,
);
// Favorites
const favoritesFont = TextStyle(
color: Colors.black,
fontFamily: "icomoon",
fontSize: 8
);
Model
Model 은 API 를 호출하기 전, DB 에 저장된 Data 들을 Flutter Application 화면에 어떻게 띄우면 될까라는 생각에서 출발하였습니다.
이 방식은 Spring Boot 나 Django 에서 사용하는 model 이라는 기능과 거의 유사, 아니 똑같습니다.
DTO 라고 생각하면 좋을 것 같습니다.
DTO 란 Data Transfer Object 의 약자로, MVC (Model, View, Controller) Pattern, 즉 계층 간 데이터 교환을 하기 위해 사용되는 객체로, Logic 을 가지고 있지 않은 순수한 객체입니다.
다음과 같이 구현된 방식이 DTO 방식입니다.
이러한 방식을 Flutter 에서 사용될 경우, 하단의 Code 처럼 사용이 됩니다.
import 'package:flutter/foundation.dart';
// part "game.d.dart";
class Game {
final String id;
final String title;
List<String> images;
final List<String> platforms;
final List<String> genres;
final String publisher;
final String description;
final String releaseDate;
bool isFavorite;
double progression;
String videoUrl;
Game({
@required this.id,
@required this.title,
this.images,
@required this.platforms,
@required this.genres,
this.publisher,
this.description,
@required this.releaseDate,
@required this.isFavorite,
this.progression,
this.videoUrl,
});
// factory Game.fromJson(Map<String, dynamic> jsonData) => _$GameFromJson(jsonData);
// Map<String dynamic> toJsonData => _$GameToJson(this);
// Allocates Memory of Game Class by using 'factory'.
factory Game.from(Game game) {
return Game(
id: game.id ?? '',
title: game.title ?? '',
images: game.images ?? [],
platforms: game.platforms ?? [],
genres: game.genres ?? [],
publisher: game.publisher ?? [],
description: game.description ?? '',
releaseDate: game.releaseDate ?? '',
isFavorite: game.isFavorite ?? false,
progression: game.progression ?? 0.0,
videoUrl: game.videoUrl ?? ''
);
}
// Mapign Game form to modify or add new games into my DiscoverList.
factory Game.fromJSON(Map<String, dynamic> data){
return Game(
id: data['id'] ?? '',
title: data['title'] ?? '',
images: data['images'] ?? [],
platforms: data['platforms'] ?? [],
genres: data['genres'] ?? [],
publisher: data['publisher'] ?? '',
description: data['descrption'] ?? '',
releaseDate: data['releaseDate'] ?? '',
isFavorite: data['isFavorite'] ?? false,
progression: data['progression'] ?? 0.0,
videoUrl: data['videoURL'] ?? ''
);
}
// Initializing Data
factory Game.initialData(){
return Game(
id: '',
title: '',
images: [],
platforms: [],
genres: [],
publisher: '',
description: '',
releaseDate: '',
isFavorite: false,
progression: 0.0,
videoUrl: ''
);
}
}
Data
Data 는 보통 DB 에 연결하기 전, Flutter 가 정상적으로 Flutter Project 에 사용할 Dummy Data 들을 저장 및 관리하기 위해 사용합니다.
단, Data 는 추후 DB 에 Data 를 연결하여 API 를 통해 값을 불러올 경우, 필요없어 사용하지 않을 수도 있으니, 나중에 필요없을 경우 과감하게 지우셔도 무방한 File Directory 입니다.
그러나 가끔 DB 에 Data 를 실수로 넣지 않았거나 기능이 구현되지 않거나 아니면 처음 화면이 빈 화면으로 나올 수도 있으므로, Dummy Data 는 초기 Data 보여주기 용으로 사용하는 것을 추천드립니다.
예를 들어 이 App 의 Main Data 인 Game 에 관한 내용들을 담고 있는 Dummy List 입니다.
이 File 에는 Game 에 관련된 내용 뿐만 아니라 다양한 Data 들도 같이 담고 있는 File 입니다.
var DUMMY_GAMES = [
Game(
id: 'ga1',
title: 'Persona 5',
images: ['assets/img/games/persona5/persona5-2.jpg', 'assets/img/games/persona5/persona5.png'],
platforms: [ 'p1', 'p2' ],
genres: [ 'g1', 'g2' ],
publisher: 'e1',
description: "Persona 5 is a role-playing video game developed by Atlus. It is the sixth installment in the Persona series, which is part of the larger Megami Tensei franchise. Persona 5 takes place in modern-day Tokyo and follows a high school student known by the pseudonym Joker who transfers to a new school after being falsely accused of assault and put on probation. Over the course of a school year, he and other students awaken to a special power, becoming a group of secret vigilantes known as the Phantom Thieves of Hearts. They explore the Metaverse, a supernatural realm born from humanity's subconscious desires, to steal malevolent intent from the hearts of adults. As with previous games in the series, the party battles enemies known as Shadows using physical manifestations of their psyche known as their Personas. The game incorporates role-playing and dungeon crawling elements alongside social simulation scenarios.",
releaseDate: '2016/09/15',
isFavorite: false,
progression: 90,
videoUrl: " https://www.youtube.com/watch?v=YKfaPgl8N8E "
),
Game(
id: 'ga2',
title: 'The legend of Zelda: Breath of the Wild',
images: ['assets/img/games/zeldaBreathOfTheWild/zelda-breath-of-the-wild.jpeg', 'assets/img/games/zeldaBreathOfTheWild/SI_WiiU_TheLegendOfZeldaBreathOfTheWild_image1600w.jpg'],
platforms: [ 'p3', 'p4' ],
genres: [ 'g3', 'g4' ],
publisher: 'e2',
description: "The Legend of Zelda: Breath of the Wild is a 2017 action-adventure game developed and published by Nintendo for the Nintendo Switch and Wii U consoles. Breath of the Wild is part of the Legend of Zelda franchise and is set at the end of the Zelda timeline; the player controls Link, who awakens from a hundred-year slumber to defeat Calamity Ganon and save the kingdom of Hyrule.",
releaseDate: '2017/03/03',
isFavorite: false,
videoUrl: " https://www.youtube.com/watch?v=zw47_q9wbBE&ab_channel=Nintendo ",
progression: 10
),
Game(
id: 'ga3',
title: 'Fire Emblem Three Houses',
images: ['assets/img/games/fireEmblemThreeHouses/fire-emblem-three-houses.jpg', 'assets/img/games/fireEmblemThreeHouses/fire-emblem-three-houses-official-site-jp-may122019.jpg'],
platforms: [ 'p4' ],
genres: [ 'g1' ],
publisher: 'e2',
description: "Fire Emblem: Three Houses is a tactical role-playing game developed by Intelligent Systems and Koei Tecmo for the Nintendo Switch and published worldwide by Nintendo on July 26, 2019. It is an entry in the Fire Emblem series, and the first one for home consoles since Fire Emblem: Radiant Dawn, originally released in 2007. Three Houses is set on the continent of Fódlan, divided between three rival nations now at peace, connected through the Garreg Mach Monastery. Taking the role of a former mercenary and new tutor at Garreg Mach, the player must choose a nation to support and guide them through a series of battles. The game carries over the turn-based tactical gameplay of earlier Fire Emblem titles, while incorporating social simulation and time management elements.",
releaseDate: '2019/07/26',
isFavorite: false,
videoUrl: " https://www.youtube.com/watch?v=ADaRsEhTB70&ab_channel=Nintendo ",
progression: 100
),
Game(
id: 'ga4',
title: 'Super Smash Bros. Ultimate',
images: ['assets/img/games/superSmashBrosUltimate/super-smash-bros-ultimate-cover.jpg', 'assets/img/games/image.jpeg'],
platforms: [ 'p4' ],
genres: [ 'g5' ],
publisher: 'e2',
description: "Super Smash Bros. Ultimate is a 2018 crossover fighting game developed by Bandai Namco Studios and Sora Ltd. and published by Nintendo for the Nintendo Switch. It is the fifth installment in the Super Smash Bros. series, succeeding Super Smash Bros. for Nintendo 3DS and Wii U. The game follows the series' traditional style of gameplay: controlling one of the various characters, players must use differing attacks to weaken their opponents and knock them out of an arena. It features a wide variety of game modes, including a campaign for single-player and multiplayer versus modes. ",
releaseDate: '2018/12/07',
isFavorite: false,
videoUrl: " https://www.youtube.com/watch?v=WShCN-AYHqA&ab_channel=SuperSmashBros ",
progression: 50
)
];
App Screen
이 Project 에서 관리하고 있는 모든 대표 Page 들에 대한 내용들을 담고 있는 File Directory 입니다.
각 대표 Page 들은 각각의 기능과 Page 들을 보여주고 있으며, 다양한 내용들을 관리합니다.
예를 들어 대표 Page 들 중 하나인 Home Page 에서는 다음과 같이 화면을 보여줍니다.
import 'package:flutter/material.dart';
import 'package:flutter_app/provider/filter.dart';
import 'package:flutter_app/provider/games_provider.dart';
import 'package:flutter_app/widgets/Home/list_tiles_with_title.dart';
import 'package:flutter_app/widgets/expanded/divider.dart';
import 'package:flutter_app/model/game/game.dart';
import 'package:provider/provider.dart';
class Home extends StatefulWidget{
// final void Function(int) onBottomTapped;
// const Home({Key key, this.onBottomTapped}) : super(key: key);
@override
State<StatefulWidget> createState()
=> _BodyState();
}
class _BodyState extends State<Home>{
int pageIndex = 0;
// Function onBottomTapped;
List<Game> inProgress = [], completed = [], newGame = [];
List<String> titleList = <String>['IN PROCESS', 'Completed', 'New Game'];
Game deletedGame;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20),
color: Colors.black12,
child: SingleChildScrollView(
child: Consumer<GameProvider>(
builder: (ctx, gamesProduct, child){
final Map<String, dynamic> homeFilter = Provider.of<Filters>(context).homeFilters;
final List<Game> listGame = gamesProduct.userItems;
// final List<Game> listGame2 = gamesProduct.gameItems;
inProgress = listGame.where((game) => game.progression != 0 && game.progression != 100).toList();
completed = listGame.where((game) => game.progression == 100).toList();
newGame = listGame.where((game) => game.progression == 0).toList();
// inProgress = listGame.where((game) => game.progression != 0 && game.progression != 100 || isFilter(game, homeFilter)).toList();
// completed = listGame.where((game) => game.progression == 100 || isFilter(game, homeFilter)).toList();
// newGame = listGame.where((game) => game.progression == 0 && isFilter(game, homeFilter)).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTilesWithTitle(title: titleList[0], gameList: inProgress),
transparent_divider(),
ListTilesWithTitle(title: titleList[1], gameList: completed),
transparent_divider(),
ListTilesWithTitle(title: titleList[2], gameList: newGame),
],
);
}
)
),
);
}
}
이상으로 Flutter File 구성하는 법에 대해서 작성해보았습니다.
끝까지 봐주셔서 감사합니다.