Comparison of Flutter and SwiftUi Declarative UI Implementation — A tribute to a famous Japanese news app

final version

Flutter on the left and SwiftUi on the right

The Foundation of the UI

This section compares the foundation needed to make up each UI component. There is a specific way to write both of them.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Name',
theme: LightTheme.pattern(context),
darkTheme: DarkTheme.pattern(context),
home: TopScreen(),
);
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let topView = TopView()

if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: topView)
self.window = window
window.makeKeyAndVisible()
}
}
}
class TopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _LayoutWidget();
}
}

class _LayoutWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: HomeScreen(),
bottomNavigationBar: _bottomNavigationBar(context));
}

void _tap(BuildContext context, StatelessWidget screen) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return screen;
}));
}

AppBar _appBar() {
return AppBar(
titleSpacing: 5.0,
elevation: 0.0,
title: SearchBar(),
actions: [
IconButton(
icon: Icon(Icons.filter_none, color: Colors.grey),
onPressed: () => null),
],
backgroundColor: Colors.white,
bottom: PreferredSize(
child: Container(
color: Color(0xffC0C0C0),
height: 0.4,
),
preferredSize: Size.fromHeight(0.4)));
}

Widget _bottomNavigationBar(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.transparent, boxShadow: [
BoxShadow(
color: Colors.transparent, spreadRadius: 0, blurRadius: 0)
]),
child: SizedBox(
height: 100,
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: 30,
selectedItemColor: Colors.red,
unselectedItemColor: Colors.grey,
onTap: (int index) {
switch (index) {
case 0:
_tap(context, HomeScreen());
break;
case 1:
_tap(context, ToolScreen());
break;
case 2:
_tap(context, InformationScreen());
break;
case 3:
_tap(context, OtherScreen());
break;
}
},
items: <BottomNavigationBarItem>[
_bottomNavigationBarItem(Icons.home, 'ホーム'),
_bottomNavigationBarItem(Icons.view_module, 'ツール'),
_bottomNavigationBarItem(Icons.notifications, 'お知らせ'),
_bottomNavigationBarItem(Icons.more_horiz, 'その他'),
])));
}

BottomNavigationBarItem _bottomNavigationBarItem(
IconData icon, String label) {
return BottomNavigationBarItem(
icon: Stack(children: [
Padding(
padding: EdgeInsets.only(left: 4, right: 4),
child: Icon(icon),
),
Positioned(
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(4)),
constraints: BoxConstraints(minHeight: 8, minWidth: 8),
child: Container()))
]),
title: Text(label, style: TextStyle(fontSize: 10)),
);
}
}
struct TopView: View {
var body: some View {
TabView{
HomeScreen()
.tabItem {
VStack {
Image(systemName: "house.fill")
Text("ホーム")
}
}.tag(1)
ToolScreen()
.tabItem {
VStack {
Image(systemName: "square.grid.2x2.fill")
Text("ツール")
}
}.tag(2)
InformationScreen()
.tabItem {
VStack {
Image(systemName: "bell.fill")
Text("お知らせ")
}
}.tag(3)
OtherScreen()
.tabItem {
VStack {
Image(systemName: "ellipsis")
Text("その他")
}
}.tag(4)
}
}
}

struct TopView_Previews: PreviewProvider {
static var previews: some View {
TopView()
}
}

SearchBar

class SearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 40,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Color(0xff0066FF), width: 2),
borderRadius: BorderRadius.circular(5)),
child: Row(
children: [
Expanded(flex: 1, child: Icon(Icons.search, color: Colors.grey)),
Expanded(
flex: 6,
child: Text("xxxxxxx検索",
style: TextStyle(color: Colors.grey, fontSize: 14))),
Expanded(flex: 1, child: Icon(Icons.mic, color: Colors.grey)),
Expanded(
flex: 2,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0.0))),
color: Color(0xff0066FF),
child: Text("検索",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
onPressed: () => {}))
],
),
);
}
}
struct SearchBar: View {
fileprivate func searchBar() -> some View {
return HStack(spacing: 0) {
HStack(spacing: 0) {
Spacer().frame(width:10)
Image(systemName:"magnifyingglass")
.resizable()
.frame(width: 15, height: 15)
.foregroundColor(.gray)
Spacer().frame(width:10)
Text("xxxxxxx検索")
.foregroundColor(Color.gray)
.font(.system(size: 12))
Spacer()
Image(systemName:"mic.fill" ).foregroundColor(.gray)
Spacer().frame(width:10)
Button(action: {}) {
Text("検索")
.foregroundColor(Color.white)
.font(.system(size: 15))
.fontWeight(.bold)
}
.padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10))
.background(Color.blue)
}
.frame(maxWidth: .infinity)
.padding(EdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1))
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(Color.blue, lineWidth: 2)
)
Spacer().frame(width: 10)
Image(systemName:"square.on.square" ).foregroundColor(.gray)
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}

var body: some View {
VStack(spacing: 0.0) {
searchBar()
}
}
}

Login and ID Acquisition

class Login extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(width: 0.4, color: Color(0xffC0C0C0))),
),
child: Row(children: [
Expanded(
flex: 1,
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(),
color: Color(0xffF5F5F5),
child: Text("ログイン",
style: TextStyle(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.bold)),
onPressed: () => {})),
Container(height: 20.0, width: 0.4, color: Colors.grey),
Expanded(
flex: 1,
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(),
color: Color(0xffF5F5F5),
child: Text("ID新規取得",
style: TextStyle(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.bold)),
onPressed: () => {}))
]));
}
}
struct Login: View {
fileprivate func login() -> some View {
return HStack {
Button(action: {}){
Text("ログイン")
.foregroundColor(Color.black)
.font(.system(size: 12))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
Button(action: {}){
Text("ID新規取得")
.foregroundColor(Color.black)
.font(.system(size: 12))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0))
.frame(height: 30.0, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color.init(hex: "eee"))
.border(Color.init(hex: "bbb"), width: 0.5)
}

var body: some View {
VStack(spacing: 0.0) {
login()
}
}
}

Tile menu : Mail, weather, fortune telling, etc.

class Menu1 extends StatelessWidget {
final List<Map<String, dynamic>> menuData1 = [
{"icon": Icons.mail_outline, "text": "メール"},
{"icon": Icons.wb_sunny, "text": "天気"},
{"icon": Icons.stars, "text": "占い"},
{"icon": Icons.star_border, "text": "お気に入り"},
{"icon": Icons.add_shopping_cart, "text": "ショッピング"}
];

final List<Map<String, dynamic>> menuData2 = [
{"icon": Icons.accessibility, "text": "スポーツナビ"},
{"icon": Icons.account_balance, "text": "オク"},
{"icon": Icons.train, "text": "路線情報"},
{"icon": Icons.account_balance_wallet, "text": "スロットくじ"},
{"icon": Icons.all_inclusive, "text": "すべて"}
];

@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: List.generate(
menuData1.length,
(index) => Expanded(
child: _menu(
icon: menuData1[index]["icon"],
text: menuData1[index]["text"])))),
Container(
color: Color(0xffEEEEEE),
padding: EdgeInsets.only(bottom: 6.0),
child: Row(
children: List.generate(
menuData2.length,
(index) => Expanded(
child: _menu(
icon: menuData2[index]["icon"],
text: menuData2[index]["text"])))))
],
);
}

Widget _menu({IconData icon, String text}) {
return text != "すべて"
? GestureDetector(
onTap: () => {},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(
right: BorderSide(width: 0.4, color: Colors.black12),
bottom: BorderSide(width: 0.4, color: Colors.black12))),
padding: EdgeInsets.only(top: 7, bottom: 7),
child: Column(children: [
Icon(icon),
Text(text, style: TextStyle(fontSize: 10))
])))
: Container(
decoration: BoxDecoration(
color: Color(0xffEEEEEE),
border: Border(
right: BorderSide(width: 0.4, color: Colors.black12),
bottom: BorderSide(width: 0.4, color: Colors.black12))),
child: SizedBox(
height: 53.0,
child: Padding(
padding: EdgeInsets.only(top: 18.0),
child: Text("すべて",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black54),
textAlign: TextAlign.center))));
}
}
struct Menu1: View {
@State private var isMailScreenPresented = false
@State private var isWeatherScreenPresented = false

fileprivate func menu1() -> some View {

return VStack(spacing: 0) {
Divider()
HStack(spacing: 0) {
tile(imageName: "envelope",
label: "メール",
isPresented: $isMailScreenPresented,
tapGesture: {self.isMailScreenPresented.toggle()},
screen: MailScreen(isMailScreenPresented: self.$isMailScreenPresented))
Divider()
tile(imageName: "sun.max",
label: "天気",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "star.circle",
label: "占い",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "star",
label: "お気に入り",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "cart",
label: "ショッピング",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
}
Divider()
HStack(spacing: 0) {
tile(imageName: "sportscourt",
label: "スポーツナビ",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "hammer",
label: "オク!",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "tram.fill",
label: "経路情報",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "cube.box",
label: "スロットくじ",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
Text("すべて").font(.system(size: 12))
.frame(height: 52, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color.init(hex: "eee"))
.onTapGesture {}
}
Divider()
.padding(EdgeInsets(top: 0, leading: 0, bottom: 7, trailing: 0))
.background(Color(red: 230/255, green: 230/255, blue: 230/255))
}
}

fileprivate func tile<T:View>(imageName: String,
label: String,
isPresented: Binding<Bool>,
tapGesture: @escaping () -> Void,
screen: T) -> some View {
return VStack(spacing: 0) {
Image(systemName:imageName ).foregroundColor(.gray)
.padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
Text(label).font(.system(size: 8))
}
.frame(height: 48, alignment: .center)
.frame(maxWidth: .infinity)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0))
.onTapGesture {tapGesture()}
.sheet(isPresented: isPresented) {
screen
}
}

var body: some View {
VStack(spacing: 0.0) {
menu1()
}
}
}

Scroll through the menu: StayHome, News, Coupons, Entertainment, etc.

class Menu2 extends StatelessWidget {
final List<String> menuData = [
"すべて",
"ニュース",
"新型コロナ",
"クーポン",
"芸能",
"スポーツ",
"話題",
"フォロー",
"東京五輪"
];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
menuData.length, (index) => _tabMenu(menuData[index]))));
}

Widget _tabMenu(String label) {
return GestureDetector(
onTap: () => {},
child: Container(
color: Color(0xffF5F5F5),
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 9.0),
child: Row(children: [
Text(label,
style: TextStyle(
color: Color(0xff333333),
fontSize: 12,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
label != "東京五輪"
? Padding(
padding: EdgeInsets.only(left: 15.0),
child: Container(
height: 20.0, width: 0.4, color: Colors.grey))
: Container()
])));
}
}
struct Menu2: View {
@Binding var selectedPage: Int

let menu2s = ["すべて","StayHome","ニュース","クーポン","芸能","スポーツ","話題","フォロー","東京五輪"]

var body: some View {
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(0..<9) { index in
Button(
action: {
self.selectedPage = index
})
{
Text(self.menu2s[index])
.foregroundColor(self.selectedPage == index ? .white : .gray)
.font(.system(size: 13))
.fontWeight(.bold)

}
.padding(EdgeInsets(top: 12, leading: 10, bottom: 12, trailing: 10))
.background(self.selectedPage == index ? Color.init(hex: "ed615b") : Color.init(hex: "eee"))
Divider().padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
}
}
}
Divider()
}
}
}

News List 1: Latest News

class NewsList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
child: Row(children: [
Expanded(
flex: 1,
child: SizedBox(
width: 50.0,
height: 50.0,
child: Image.asset("assets/images/150x150.png"),
)),
SizedBox(width: 10),
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("東京の感染者数 経路不明5割弱",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Row(children: [
Icon(Icons.chat_bubble_outline,
size: 12, color: Colors.pinkAccent),
SizedBox(width: 3),
Text("47",
style: TextStyle(
fontSize: 12.0, color: Colors.pinkAccent)),
SizedBox(width: 10),
Text("7/12(日) 0:24",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
SizedBox(width: 10),
Container(
padding: EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
color: Colors.orangeAccent),
child: Text("NEW",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 10.0,
color: Colors.white,
letterSpacing: -0.5)),
)
])
]))
]));
}
}
struct NewsList: View {
fileprivate func newsDetail() -> some View {
return HStack(spacing: 0.0){
Image("150x150").resizable().frame(width: 50.0, height: 50.0)
Spacer().frame(width:10)
VStack(alignment: .leading){
Text("東京の感染者数 経路不明5割弱")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
HStack{
Image(systemName: "bubble.right")
.resizable()
.frame(width: 12.0, height: 12.0, alignment: .center)
.foregroundColor(Color.init(hex: "FF4081"))
Spacer().frame(width:1)
Text("47")
.font(.system(size: 12))
.foregroundColor(Color.init(hex: "FF4081"))
Text("7/12(日) 0:24")
.font(.system(size: 12))
.foregroundColor(Color.init(hex: "9E9E9E"))
Text("NEW")
.padding(EdgeInsets.init(top: 2.0, leading: 4.0, bottom: 2.0, trailing: 4.0))
.font(.system(size: 10))
.foregroundColor(.white)
.background(Color.init(hex: "FFAB40"))
.cornerRadius(30)
}
Divider()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))

}

var body: some View {
VStack(spacing: 0.0){
newsDetail()
}
}
}

News list 2: The biggest news of all

class BigNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Image.asset("assets/images/collecting.jpeg"),
Text("女性から寄生虫、刺身食べて侵入か? 日本",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("NN.co.jp",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]),
);
}
}
struct BigNews: View {
fileprivate func newsDetail() -> some View {
return VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 200)
Text("女性から寄生虫、刺身食べて侵入か? 日本")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("News.co.jp")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}

var body: some View {
VStack(spacing: 0.0) {
newsDetail()
}
}
}

News list 3: The Second Biggest News

class MiddleNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Row(children: [
Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.only(right: 5.0),
width: (MediaQuery.of(context).size.width / 2) - 10.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset("assets/images/collecting.jpeg"),
Text("「100万円たまる貯金箱」を6年かけて満杯に 2000日の苦労を本人い聞いた",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("タウンネット",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
])),
Container(
padding: EdgeInsets.only(left: 5.0),
width: (MediaQuery.of(context).size.width / 2) - 10.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset("assets/images/collecting.jpeg"),
Text("「当時は睡眠2時間」、再結成、引退。。。紆余曲折から。。。",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("スポーツニュース",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
])),
]));
}
}
struct MiddleNews: View {
fileprivate func newsDetail() -> some View {
return HStack {
VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 150)
Text("「100万円たまる貯金箱」を6年かけて満杯に 2000日の苦労を本人い聞いた")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("タウンネット")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 150)
Text("「当時は睡眠2時間」解散、再結成、引退。。。紆余曲折から。。。")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("スポーツニュース")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
}
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}

var body: some View {
VStack(spacing: 0.0) {
newsDetail()
}
}
}

News list 4: News with the simplest UI

class SmallNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
child: Row(children: [
Expanded(
flex: 2,
child: Image.asset("assets/images/150x150.png"),
),
SizedBox(width: 10),
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("映画4作品の再上映。異例のヒット",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("シネマシネマ",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]))
]));
}
}
struct SmallNews: View {
fileprivate func newsDetail() -> some View {
return HStack(spacing: 0.0){
Image("150x150").resizable().frame(width: 120.0, height: 120.0)
Spacer().frame(width:10)
VStack(alignment: .leading){
Text("映画4作品の再上映。異例のヒット")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("タウンネット")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))

}

var body: some View {
VStack(spacing: 0.0){
newsDetail()
}
}
}

Video News

class BigMovie extends StatefulWidget {
@override
_BigMovie createState() => _BigMovie();
}

class _BigMovie extends State<BigMovie> {
VideoPlayerController _controller;

@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4')
..initialize().then((_) {
_controller.setLooping(true);
_controller.play();
setState(() {});
});
}

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller))
: Container(),
Text("女性から寄生虫、刺身食べて侵入か? 日本",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("NN.co.jp",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]),
);
}
}
struct BigMovie: View {
fileprivate func movieDetail() -> some View {
return VStack(alignment: .leading, spacing: 0.0) {
PlayerView().frame(height: 200, alignment: .center)
Text("女性から寄生虫、刺身食べて侵入か? 日本")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("NN.co.jp")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}

var body: some View {
VStack(spacing: 0.0) {
movieDetail()
}
}
}

scroll

Scrolling up locks the search bar, the scrolling menu to the top, and the rest of the content disappears to the top of the screen.
Scrolling down brings up the content that has disappeared to the top.

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
backgroundColor: Color(0xffFFFAFA),
expandedHeight: 242,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Column(children: [Login(), PickUpNews(), Menu1()]))),
bottom: PreferredSize(
preferredSize: Size.fromHeight(-6), child: Menu2())),
SliverToBoxAdapter(
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_newsList(),
_bigNewsList(),
_middleNewsList(),
_smallNewsList(),
_bigMovieList(),
]))
],
);
}
}
var body: some View {
VStack(spacing: 0) {
SearchBar()
List {
VStack(spacing: 0) {
Login()
PickupNews()
Menu1()
}
.background(Color.white)
.listRowInsets(EdgeInsets())
Section(header: Menu2(selectedPage: $selectedPage)) {
self.minNewsList()
self.minNewsList()
self.bigNewsList()
self.middleNewsList()
self.smallNewsList()
self.bigMovieList()
}
.listRowInsets(EdgeInsets())
}
}
}

summary

This time, I built a UI for a famous Japanese news app in Flutter and SwiftUi, but I found that it took time and effort to make the font family, font size, padding, margin, etc. exactly the same for both Flutter and SwiftUi. For the implementation of declarative UI, I didn’t notice any significant differences in terms of ease of writing or source visibility. (Although Flutter was a little easier to write due to familiarity)
For those of us who have been using storyboard in iOS development, SwiftUi may have some resistance, but once you get used to it, it’s faster, lighter and more like programming.
I’d like to implement more screen transitions and behaviors of the widgets in the screen, but I’ve decided to implement the static part. I’d also like to compare the ui rebuild. I am curious about it.

--

--

I am a mobile app developer.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store