Comparison of Flutter and SwiftUi Declarative UI Implementation — A tribute to a famous sports video streaming app.
This is the second installment of my comparison of the declarative UI implementations of Flutter and SwiftUi. This time, I used a certain sports-only video streaming application as the subject. There are no difficult parts at all, so I think even a beginner can easily implement it.
final version
Flutter on the left and SwiftUi on the right.
Now let’s compare the implementations in order.
The Foundation of the UI
First of all, let’s look at Flutter.
Set the view to be displayed when the tab is tapped in the TopView class, which is the class for switching tabs. This time, we will only create the HomeView class. In the HomeView class, we will use the CupertinoPageScaffold widget to create the header, and the SingleChildScrollView widget to make sure that the content can be scrolled to be viewed on devices with small screen sizes. The TopView class and HomeView class are used to create the header.
The TopView class and HomeView class inherit from the StatefulWidget class. This is better if there is a possibility of changing the UI only through screen operations.
class TopView extends StatefulWidget {
TopView({Key key}) : super(key: key);
@override
_TopViewState createState() => _TopViewState();
}
class _TopViewState extends State<TopView> {
@override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
activeColor: Colors.white,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'ホーム'),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today), label: '番組表'),
BottomNavigationBarItem(
icon: Icon(Icons.sports_soccer), label: 'スポーツ一覧'),
BottomNavigationBarItem(icon: Icon(Icons.more_horiz), label: 'その他'),
],
),
tabBuilder: (context, index) {
return CupertinoTabView(
builder: (context) {
switch (index) {
case 0:
return HomeView();
break;
case 1:
return Container();
break;
case 2:
return Container();
break;
case 3:
return Container();
break;
default:
return Container();
}
},
);
},
);
}
}
class HomeView extends StatefulWidget {
final ValueChanged<String> onChangedTitle;
HomeView({this.onChangedTitle});
@override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
@override
Widget build(BuildContext context) {
//widget.onChangedTitle(value.toString());
return CupertinoPageScaffold(
backgroundColor: Colors.black.withOpacity(0.7),
navigationBar: CupertinoNavigationBar(
backgroundColor: HexColor.fromHex(baseBackgroundColor),
leading: Icon(CupertinoIcons.search, size: 20.0),
middle: TextHeader(text: 'ホーム')),
child: SingleChildScrollView(
child: Container(
padding: EdgeInsets.only(
top: 10.0, right: 10.0, left: 10.0, bottom: 50.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(children: [BigMovieCell(), BigMovieCellOverlay()]),
SizedBox(height: 10),
Text16(text: '配信中'),
SizedBox(height: 10),
Broadcasting(),
SizedBox(height: 30),
Text16(text: 'XXXリーグ'),
SizedBox(height: 10),
League(),
],
))));
}
}
Next, let’s look at SwiftUi.
Set the ScreenBase struct, which is a header display struct, to each tab of the TopScreen struct, which is a tab switching struct.
In the TopScreen struct, the background color of the tabBar is black, and the icon is white.
struct TopScreen: View {
init() {
UITabBar.appearance().backgroundColor = .black
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().barTintColor = .white
}
var body: some View {
TabView{
ScreenBase(child: HomeScreen())
.tabItem {
VStack {
Image(systemName: "house.fill")
Text("ホーム")
}
}.tag(1)
ScreenBase(child: HomeScreen())
.tabItem {
VStack {
Image(systemName: "square.grid.2x2.fill")
Text("番組表")
}
}.tag(2)
ScreenBase(child: HomeScreen())
.tabItem {
VStack {
Image(systemName: "bell.fill")
Text("スポーツ一覧")
}
}.tag(3)
ScreenBase(child: HomeScreen())
.tabItem {
VStack {
Image(systemName: "ellipsis")
Text("その他")
}
}.tag(4)
}.accentColor(.white)
}
}
The ScreenBase struct is a template for displaying the header. In this example, the search icon and screen title are displayed.
struct ScreenBase<T: View>: View {
let child: T
init(child: T) {
self.child = child
let coloredNavAppearance = UINavigationBarAppearance()
coloredNavAppearance.configureWithOpaqueBackground()
coloredNavAppearance.backgroundColor = UIColor(hex: headerBackgroundColor)
coloredNavAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
coloredNavAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
UINavigationBar.appearance().standardAppearance = coloredNavAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredNavAppearance
}
var body: some View {
NavigationView {
ScrollView {
child
}
.padding(EdgeInsets.init(top: 10.0, leading: 15.0, bottom: 0.0, trailing: 15.0))
.background(Color.init(hex: baseBackgroundColor))
.navigationBarTitle("ホーム", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {}) {
Image(systemName: "magnifyingglass")
}
)
}
}
}
The HomeScreen struct is the main body of the home screen. This is where the video is played and the thumbnail list is displayed.
struct HomeScreen: View {
@ObservedObject var homeViewModel = HomeViewModel()
@State var isShowMovie = false
@State var movieHight = 0.0
@State var isRecord = false
@State var isWatching = false
@State var isWatchingId = 0
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if isShowMovie {
VStack {
ZStack {
BigMovieCell(movieHight: movieHight, isRecord: isRecord).frame(height: CGFloat(movieHight) + (isRecord ? CGFloat(recordHeight) : 0.0))
BigMovieCellOverlay(movieHight: movieHight + (isRecord ? recordHeight : 0.0))
}
Spacer()
}
}
Broadcasting(isShowMovie: $isShowMovie, movieHight: $movieHight, isRecord: $isRecord, isWatchingId: $isWatchingId)
Spacer1()
League(isShowMovie: $isShowMovie, movieHight: $movieHight, isRecord: $isRecord, isWatchingId: $isWatchingId)
}
}
}
Video play
First, let’s look at the Flutter.
Each element is placed in the appropriate position using the MainAxisAlignment of the Column widget and Row widget. As a point of interest, the Slider must be the CupertinoSlider widget if you are using the CupertinoTabScaffold widget. For a normal Scaffold, the Slider widget is fine.
class BigMovieCellOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(10.0),
height: 200.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Icon(Icons.fullscreen),
SizedBox(width: 10),
Icon(Icons.menu),
SizedBox(width: 10),
Icon(Icons.clear),
]),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.replay_30, size: 50.0, color: Colors.white24),
SizedBox(width: 10),
Icon(Icons.pause, size: 50.0, color: Colors.white24),
SizedBox(width: 10),
Icon(Icons.forward_30, size: 50.0, color: Colors.white24),
]),
SizedBox(
width: double.infinity,
child: CupertinoSlider(
min: 0,
max: 100,
divisions: 10,
value: 0,
onChanged: (d) => {}),
),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text10(text: '00:22', fontWeight: FontWeight.normal),
Row(
children: [
Icon(Icons.screen_share),
SizedBox(width: 10),
Icon(Icons.arrow_circle_up),
SizedBox(width: 10),
Text10(text: '35:22', fontWeight: FontWeight.normal),
],
)
]),
],
));
}
}
Next, let’s look at SwiftUi.
The components for manipulating the playing movie are placed in the BigMovieCellOverlay struct, which can be easily implemented using VStack and HStack. The indicator for the progress of the video playback is a Rectangle in the ZStack with knobs and bars.
struct BigMovieCellOverlay: View {
var movieHight = 0.0
@State private var currentValue: Double = 50
init(movieHight: Double) {
self.movieHight = movieHight
}
var body: some View {
VStack(spacing: 0.0) {
HStack(spacing: 10.0) {
Spacer()
Image(systemName: "arrow.up.left.and.arrow.down.right").foregroundColor(Color.init(hex: "ffffff"))
Image(systemName: "line.horizontal.3").foregroundColor(Color.init(hex: "ffffff"))
Image(systemName: "multiply").foregroundColor(Color.init(hex: "ffffff"))
}
SpacerH(height: 10.0)
HStack {
Spacer()
Image(systemName: "gobackward.30")
.resizable()
.frame(width: 40.0, height: 40.0, alignment: .center)
.foregroundColor(Color.init(hex: "444444"))
SpacerW(width: 20.0)
Image(systemName: "pause")
.resizable()
.frame(width: 40.0, height: 40.0, alignment: .center)
.foregroundColor(Color.init(hex: "444444"))
SpacerW(width: 20.0)
Image(systemName: "goforward.30")
.resizable()
.frame(width: 40.0, height: 40.0, alignment: .center)
.foregroundColor(Color.init(hex: "444444"))
Spacer()
}
SpacerH(height: 40.0)
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.gray).frame(height: 4.0).cornerRadius(12)
Rectangle()
.fill(Color.white)
.frame(width:12, height: 12)
.rotationEffect(Angle(degrees: 45))
}
SpacerH(height: 30.0)
HStack {
Text3(text: "00:22").foregroundColor(Color.init(hex: "ffffff"))
Spacer()
Image(systemName: "tv").foregroundColor(Color.init(hex: "ffffff"))
Image(systemName: "person.fill.viewfinder").foregroundColor(Color.init(hex: "ffffff"))
Text3(text: "35:26").foregroundColor(Color.init(hex: "ffffff"))
}
}
.padding()
.frame(height: CGFloat(movieHight), alignment: .topLeading)
.background(Color.clear)
}
}
Distribution List
The list of currently distributed videos is simply a horizontal scrolling thumbnail arrangement. The thumbnail of the currently selected video is marked with a yellow frame.
First, let’s look at the Flutter.
The ListView widget in the Broadcasting class is set to Axis.horizontal to achieve a horizontal scroll list of thumbnails.” ●Live” is superimposed on the thumbnails using the Stack widget. Each thumbnail is implemented in the BigPicCell class described below.
class Broadcasting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Stack(children: [
BigPicCell(
isStreaming: true,
movie: Movie(
id: 1,
title: 'AAAA X BBBB',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
Positioned(top: 15, left: 15, child: LiveLabel())
]),
SizedBox(width: 10),
Stack(children: [
BigPicCell(
movie: Movie(
id: 2,
title: 'AAAA X BBBB',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
Positioned(top: 15, left: 15, child: LiveLabel())
]),
SizedBox(width: 10),
Stack(children: [
BigPicCell(
movie: Movie(
id: 3,
title: 'AAAA X BBBB',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
Positioned(top: 15, left: 15, child: LiveLabel())
]),
SizedBox(width: 10),
Stack(children: [
BigPicCell(
movie: Movie(
id: 4,
title: 'AAAA X BBBB',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
Positioned(top: 15, left: 15, child: LiveLabel())
]),
SizedBox(width: 10),
Stack(children: [
BigPicCell(
movie: Movie(
id: 5,
title: 'AAAA X BBBB',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
Positioned(top: 15, left: 15, child: LiveLabel())
]),
],
),
);
}
}
Next, let’s look at SwiftUi.
The ScrollView is used to allow the thumbnails to be scrolled horizontally. Each thumbnail is implemented in the BigPicCell struct described below.
struct Broadcasting: View {
@Binding var isShowMovie: Bool
@Binding var movieHight: Double
@Binding var isRecord: Bool
@Binding var isWatchingId: Int
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text1(text: "配信中")
Spacer3()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
BigPicCell(id: 1, title: "AAAA X BBBB", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: false)
.onTapGesture {
withAnimation(.easeOut(duration: 0.2)) {
isWatchingId = 1
isRecord = false
isShowMovie = false
isShowMovie = true
if isShowMovie {
movieHight = 200.0
} else {
movieHight = 0.0
}
}
}
Spacer()
BigPicCell(id: 2, title: "AAAA X BBBB", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: false)
Spacer()
BigPicCell(id: 3, title: "AAAA X BBBB", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: false)
Spacer()
BigPicCell(id: 4, title: "AAAA X BBBB", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: false)
Spacer()
BigPicCell(id: 5, title: "AAAA X BBBB", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: false)
}
}
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
).background(Color.init(hex: baseBackgroundColor))
}
}
League List
The league list is simply a horizontal scrolling thumbnail similar to the list being distributed, but the thumbnail of the currently selected video is marked with a yellow frame.
First, let’s look at the Flutter.
The League class is implemented almost identically to the Broadcasting class. Each thumbnail is implemented in the BigPicCell class described below.
class League extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
BigPicCell(
movie: Movie(
id: 1,
title: 'AAAA X BBBB : 第5節',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
SizedBox(width: 10),
BigPicCell(
movie: Movie(
id: 2,
title: 'AAAA X BBBB : 第5節',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
SizedBox(width: 10),
BigPicCell(
movie: Movie(
id: 3,
title: 'AAAA X BBBB : 第5節',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
SizedBox(width: 10),
BigPicCell(
movie: Movie(
id: 4,
title: 'AAAA X BBBB : 第5節',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
SizedBox(width: 10),
BigPicCell(
movie: Movie(
id: 5,
title: 'AAAA X BBBB : 第5節',
imageName: 'bigpic',
leagueName: 'T league',
dateTime: '9/12 sun 18:00-')),
],
),
);
}
}
The BigPicCell class implements the display of thumbnails and additional information at the bottom.
The Column widget contains a thumbnail and the Text widget for the description.
The thumbnail uses the BoxDecoration widget to create a yellow border to indicate that it is selected.
class BigPicCell extends StatelessWidget {
final Movie movie;
final bool isStreaming;
BigPicCell({this.movie, this.isStreaming = false});
@override
Widget build(BuildContext context) {
return Container(
height: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.yellow, width: isStreaming ? 2.0 : 0.0)),
child: Image.asset('assets/images/bigpic.jpg', width: 230),
),
Text16(text: movie.title),
Text10(text: '${movie.leagueName} | ${movie.dateTime}'),
],
),
);
}
}
Next, let’s look at SwiftUi.
The League struct is implemented almost identically to the Broadcasting struct. Each thumbnail is implemented in the BigPicCell struct described below.
struct League: View {
@Binding var isShowMovie: Bool
@Binding var movieHight: Double
@Binding var isRecord: Bool
@Binding var isWatchingId: Int
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text1(text: "XXXリーグ")
Spacer3()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
BigPicCell(id: 11, title: "AAAA X BBBB : 第5節", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: true)
.onTapGesture {
withAnimation(.easeOut(duration: 0.2)) {
isWatchingId = 11
isRecord = true
isShowMovie = false
isShowMovie = true
if isShowMovie {
movieHight = 200.0
} else {
movieHight = 0.0
}
}
}
Spacer()
BigPicCell(id: 12, title: "AAAA X BBBB : 第5節", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: true)
Spacer()
BigPicCell(id: 13, title: "AAAA X BBBB : 第5節", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: true)
Spacer()
BigPicCell(id: 14, title: "AAAA X BBBB : 第5節", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: true)
Spacer()
BigPicCell(id: 15, title: "AAAA X BBBB : 第5節", imageName: "bigpic", leagueName: "T league", dateTime: "9/12 sun 18:00-", isWatchingId: isWatchingId, isRecord: true)
}
}
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
).background(Color.init(hex: baseBackgroundColor))
}
}
The BigPicCell struct implements the display of thumbnails and additional information at the bottom.
Set the ZStack to border(Color.yellow, width: isWatchingId == id ? 2 : 0) to display a yellow border when a thumbnail is tapped. At the same time, when a thumbnail is tapped, the video for that thumbnail will be played, and Text(“● Live”) will be displayed on the thumbnail. This is the same as Group {isRecord ? nil : LiveLabel()}
.padding(EdgeInsets.init(top: 15.0, leading: 15.0, bottom: 0.0, trailing: 0.0)).
struct BigPicCell: View {
let id: Int
let title: String
let imageName: String
let leagueName: String
let dateTime: String
let isWatchingId: Int
let isRecord: Bool
init(id: Int, title: String, imageName: String, leagueName: String, dateTime: String, isWatchingId: Int, isRecord: Bool) {
self.id = id
self.title = title
self.imageName = imageName
self.leagueName = leagueName
self.dateTime = dateTime
self.isWatchingId = isWatchingId
self.isRecord = isRecord
}
fileprivate func bigPicCell() -> some View {
return VStack(alignment: .leading, spacing: 0.0) {
ZStack(alignment: .topLeading) {
Image(imageName)
.resizable()
.frame(height: 140)
Group {isRecord ? nil : LiveLabel()}
.padding(EdgeInsets.init(top: 15.0, leading: 15.0, bottom: 0.0, trailing: 0.0))
}.border(Color.yellow, width: isWatchingId == id ? 2 : 0)
Spacer3()
Text2(text: title)
Text3(text: "\(leagueName) | \(dateTime)")
}
.frame(width: 250)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0) {
bigPicCell()
}
}
}
summary
In this article, I compared Flutter and SwiftUi with reference to my favorite sports-only video streaming application. Flutter is easier for creating headers, while SwiftUi is very confusing for decorating navigation and tab. The way to write tabs including page transitions is more concise in SwiftUi, where you can write the tap element and the transition destination as a set. However, I think there are people who prefer to use Flutter, which is written separately. This is a matter of preference.
However, in SwiftUi, you have to set spacing: 0.0 every time you want to remove the default space of VStack and HStack.